2022-12-10 22:04:14 +00:00
""" Functions for registering environments within gymnasium using public functions ``make``, ``register`` and ``spec``. """
from __future__ import annotations
2022-03-31 12:50:38 -07:00
import contextlib
2020-04-24 23:49:41 +02:00
import copy
2023-02-24 11:34:20 +00:00
import dataclasses
2022-01-19 13:50:25 -05:00
import difflib
2019-03-08 14:50:32 -08:00
import importlib
2021-12-22 18:51:33 -05:00
import importlib . util
2023-02-24 11:34:20 +00:00
import json
2022-03-31 12:50:38 -07:00
import re
import sys
2022-11-16 12:59:42 +00:00
from collections import defaultdict
2022-04-21 20:41:15 +02:00
from dataclasses import dataclass , field
2023-07-14 16:03:20 +01:00
from types import ModuleType
2023-02-12 07:49:37 -05:00
from typing import Any , Callable , Iterable , Sequence
2022-04-21 20:41:15 +02:00
2023-06-21 17:04:11 +01:00
import gymnasium as gym
2023-02-12 07:49:37 -05:00
from gymnasium import Env , Wrapper , error , logger
2022-04-21 20:41:15 +02:00
2022-12-04 22:24:02 +08:00
2021-12-22 13:54:20 -05:00
if sys . version_info < ( 3 , 10 ) :
2021-12-22 19:12:57 +01:00
import importlib_metadata as metadata # type: ignore
2021-09-14 20:14:05 -06:00
else :
import importlib . metadata as metadata
2023-07-03 17:28:18 +03:00
from typing import Protocol
2022-04-21 20:41:15 +02:00
2022-12-04 22:24:02 +08:00
2022-05-25 15:28:19 +01:00
ENV_ID_RE = re . compile (
2022-01-19 13:50:25 -05:00
r " ^(?:(?P<namespace>[ \ w:-]+) \ /)?(?:(?P<name>[ \ w:.-]+?))(?:-v(?P<version> \ d+))?$ "
)
2021-09-14 20:14:05 -06:00
2019-03-08 14:50:32 -08:00
2023-02-05 00:05:59 +00:00
__all__ = [
" registry " ,
" current_namespace " ,
2023-02-24 11:34:20 +00:00
" EnvSpec " ,
" WrapperSpec " ,
# Functions
2023-02-05 00:05:59 +00:00
" register " ,
" make " ,
2023-02-24 11:34:20 +00:00
" make_vec " ,
2023-02-05 00:05:59 +00:00
" spec " ,
" pprint_registry " ,
2023-07-14 16:03:20 +01:00
" register_envs " ,
2023-02-05 00:05:59 +00:00
]
2022-06-06 16:21:45 +01:00
2023-02-05 00:05:59 +00:00
class EnvCreator ( Protocol ) :
""" Function type expected for an environment. """
def __call__ ( self , * * kwargs : Any ) - > Env :
. . .
2023-02-12 07:49:37 -05:00
class VectorEnvCreator ( Protocol ) :
""" Function type expected for an environment. """
2023-06-21 17:04:11 +01:00
def __call__ ( self , * * kwargs : Any ) - > gym . experimental . vector . VectorEnv :
2023-02-12 07:49:37 -05:00
. . .
2023-02-24 11:34:20 +00:00
@dataclass
class WrapperSpec :
""" A specification for recording wrapper configs.
* name : The name of the wrapper .
* entry_point : The location of the wrapper to create from .
* kwargs : Additional keyword arguments passed to the wrapper . If the wrapper doesn ' t inherit from EzPickle then this is ``None``
"""
name : str
entry_point : str
kwargs : dict [ str , Any ] | None
2023-02-05 00:05:59 +00:00
@dataclass
class EnvSpec :
""" A specification for creating environments with :meth:`gymnasium.make`.
* * * id * * : The string used to create the environment with : meth : ` gymnasium . make `
* * * entry_point * * : A string for the environment location , ` ` ( import path ) : ( environment name ) ` ` or a function that creates the environment .
* * * reward_threshold * * : The reward threshold for completing the environment .
* * * nondeterministic * * : If the observation of an environment cannot be repeated with the same initial state , random number generator state and actions .
* * * max_episode_steps * * : The max number of steps that the environment can take before truncation
* * * order_enforce * * : If to enforce the order of : meth : ` gymnasium . Env . reset ` before : meth : ` gymnasium . Env . step ` and : meth : ` gymnasium . Env . render ` functions
* * * disable_env_checker * * : If to disable the environment checker wrapper in : meth : ` gymnasium . make ` , by default False ( runs the environment checker )
* * * kwargs * * : Additional keyword arguments passed to the environment during initialisation
2023-03-08 14:07:09 +00:00
* * * additional_wrappers * * : A tuple of additional wrappers applied to the environment ( WrapperSpec )
2023-02-12 07:49:37 -05:00
* * * vector_entry_point * * : The location of the vectorized environment to create from
2023-11-07 13:27:25 +00:00
Changelogs :
v1 .0 .0 - Autoreset attribute removed
2022-06-06 16:21:45 +01:00
"""
2019-03-08 14:50:32 -08:00
2023-02-05 00:05:59 +00:00
id : str
2023-02-12 07:49:37 -05:00
entry_point : EnvCreator | str | None = field ( default = None )
2023-02-05 00:05:59 +00:00
# Environment attributes
reward_threshold : float | None = field ( default = None )
nondeterministic : bool = field ( default = False )
# Wrappers
max_episode_steps : int | None = field ( default = None )
order_enforce : bool = field ( default = True )
disable_env_checker : bool = field ( default = False )
2023-02-24 11:34:20 +00:00
# Environment arguments
kwargs : dict = field ( default_factory = dict )
2023-02-05 00:05:59 +00:00
# post-init attributes
namespace : str | None = field ( init = False )
name : str = field ( init = False )
version : int | None = field ( init = False )
2016-04-27 08:00:58 -07:00
2023-02-24 11:34:20 +00:00
# applied wrappers
2023-03-08 14:07:09 +00:00
additional_wrappers : tuple [ WrapperSpec , . . . ] = field ( default_factory = tuple )
2022-01-19 13:50:25 -05:00
2023-02-24 11:34:20 +00:00
# Vectorized environment entry point
vector_entry_point : VectorEnvCreator | str | None = field ( default = None )
2023-02-12 07:49:37 -05:00
2023-02-05 00:05:59 +00:00
def __post_init__ ( self ) :
2023-02-24 11:34:20 +00:00
""" Calls after the spec is created to extract the namespace, name and version from the environment id. """
2023-02-05 00:05:59 +00:00
self . namespace , self . name , self . version = parse_env_id ( self . id )
2022-01-19 13:50:25 -05:00
2023-02-05 00:05:59 +00:00
def make ( self , * * kwargs : Any ) - > Env :
""" Calls ``make`` using the environment spec and any keyword arguments. """
return make ( self , * * kwargs )
2023-02-24 11:34:20 +00:00
def to_json ( self ) - > str :
""" Converts the environment spec into a json compatible string.
Returns :
A jsonifyied string for the environment spec
"""
env_spec_dict = dataclasses . asdict ( self )
# As the namespace, name and version are initialised after `init` then we remove the attributes
env_spec_dict . pop ( " namespace " )
env_spec_dict . pop ( " name " )
env_spec_dict . pop ( " version " )
# To check that the environment spec can be transformed to a json compatible type
self . _check_can_jsonify ( env_spec_dict )
return json . dumps ( env_spec_dict )
@staticmethod
def _check_can_jsonify ( env_spec : dict [ str , Any ] ) :
""" Warns the user about serialisation failing if the spec contains a callable.
Args :
env_spec : An environment or wrapper specification .
Returns : The specification with lambda functions converted to strings .
"""
spec_name = env_spec [ " name " ] if " name " in env_spec else env_spec [ " id " ]
for key , value in env_spec . items ( ) :
if callable ( value ) :
ValueError (
f " Callable found in { spec_name } for { key } attribute with value= { value } . Currently, Gymnasium does not support serialising callables. "
)
@staticmethod
def from_json ( json_env_spec : str ) - > EnvSpec :
""" Converts a JSON string into a specification stack.
Args :
json_env_spec : A JSON string representing the env specification .
Returns :
An environment spec
"""
parsed_env_spec = json . loads ( json_env_spec )
applied_wrapper_specs : list [ WrapperSpec ] = [ ]
2023-03-08 14:07:09 +00:00
for wrapper_spec_json in parsed_env_spec . pop ( " additional_wrappers " ) :
2023-02-24 11:34:20 +00:00
try :
applied_wrapper_specs . append ( WrapperSpec ( * * wrapper_spec_json ) )
except Exception as e :
raise ValueError (
f " An issue occurred when trying to make { wrapper_spec_json } a WrapperSpec "
) from e
try :
env_spec = EnvSpec ( * * parsed_env_spec )
2023-03-08 14:07:09 +00:00
env_spec . additional_wrappers = tuple ( applied_wrapper_specs )
2023-02-24 11:34:20 +00:00
except Exception as e :
raise ValueError (
f " An issue occurred when trying to make { parsed_env_spec } an EnvSpec "
) from e
return env_spec
def pprint (
self ,
disable_print : bool = False ,
include_entry_points : bool = False ,
print_all : bool = False ,
) - > str | None :
""" Pretty prints the environment spec.
Args :
disable_print : If to disable print and return the output
include_entry_points : If to include the entry_points in the output
print_all : If to print all information , including variables with default values
Returns :
If ` ` disable_print is True ` ` a string otherwise ` ` None ` `
"""
output = f " id= { self . id } "
if print_all or include_entry_points :
output + = f " \n entry_point= { self . entry_point } "
if print_all or self . reward_threshold is not None :
output + = f " \n reward_threshold= { self . reward_threshold } "
if print_all or self . nondeterministic is not False :
output + = f " \n nondeterministic= { self . nondeterministic } "
if print_all or self . max_episode_steps is not None :
output + = f " \n max_episode_steps= { self . max_episode_steps } "
if print_all or self . order_enforce is not True :
output + = f " \n order_enforce= { self . order_enforce } "
if print_all or self . disable_env_checker is not False :
output + = f " \n disable_env_checker= { self . disable_env_checker } "
2023-03-08 14:07:09 +00:00
if print_all or self . additional_wrappers :
2023-02-24 11:34:20 +00:00
wrapper_output : list [ str ] = [ ]
2023-03-08 14:07:09 +00:00
for wrapper_spec in self . additional_wrappers :
2023-02-24 11:34:20 +00:00
if include_entry_points :
wrapper_output . append (
f " \n \t name= { wrapper_spec . name } , entry_point= { wrapper_spec . entry_point } , kwargs= { wrapper_spec . kwargs } "
)
else :
wrapper_output . append (
f " \n \t name= { wrapper_spec . name } , kwargs= { wrapper_spec . kwargs } "
)
if len ( wrapper_output ) == 0 :
2023-03-08 14:07:09 +00:00
output + = " \n additional_wrappers=[] "
2023-02-24 11:34:20 +00:00
else :
2023-03-08 14:07:09 +00:00
output + = f " \n additional_wrappers=[ { ' , ' . join ( wrapper_output ) } \n ] "
2023-02-24 11:34:20 +00:00
if disable_print :
return output
else :
print ( output )
2023-02-05 00:05:59 +00:00
# Global registry of environments. Meant to be accessed through `register` and `make`
registry : dict [ str , EnvSpec ] = { }
current_namespace : str | None = None
def parse_env_id ( env_id : str ) - > tuple [ str | None , str , int | None ] :
""" Parse environment ID string format - ``[namespace/](env-name)[-v(version)]`` where the namespace and version are optional.
2022-05-25 14:46:41 +01:00
Args :
2023-02-05 00:05:59 +00:00
env_id : The environment id to parse
2022-05-25 14:46:41 +01:00
Returns :
A tuple of environment namespace , environment name and version number
Raises :
2023-02-05 00:05:59 +00:00
Error : If the environment id is not valid environment regex
2022-01-19 13:50:25 -05:00
"""
2023-02-05 00:05:59 +00:00
match = ENV_ID_RE . fullmatch ( env_id )
2022-01-19 13:50:25 -05:00
if not match :
raise error . Error (
2023-02-05 00:05:59 +00:00
f " Malformed environment ID: { env_id } . (Currently all IDs must be of the form [namespace/](env-name)-v(version). (namespace is optional)) "
2022-01-19 13:50:25 -05:00
)
2023-02-05 00:05:59 +00:00
ns , name , version = match . group ( " namespace " , " name " , " version " )
2022-01-19 13:50:25 -05:00
if version is not None :
version = int ( version )
2023-02-05 00:05:59 +00:00
return ns , name , version
2022-01-19 13:50:25 -05:00
2022-12-10 22:04:14 +00:00
def get_env_id ( ns : str | None , name : str , version : int | None ) - > str :
2022-05-25 14:46:41 +01:00
""" Get the full env ID given a name and (optional) version and namespace. Inverse of :meth:`parse_env_id`.
Args :
ns : The environment namespace
name : The environment name
version : The environment version
Returns :
The environment id
"""
2022-04-21 20:41:15 +02:00
full_name = name
if ns is not None :
2023-02-05 00:05:59 +00:00
full_name = f " { ns } / { name } "
if version is not None :
full_name = f " { full_name } -v { version } "
2022-07-11 02:45:24 +01:00
2023-02-05 00:05:59 +00:00
return full_name
2022-07-11 02:45:24 +01:00
2022-04-21 20:41:15 +02:00
2023-02-05 00:05:59 +00:00
def find_highest_version ( ns : str | None , name : str ) - > int | None :
""" Finds the highest registered version of the environment given the namespace and name in the registry.
2022-01-19 13:50:25 -05:00
2023-02-05 00:05:59 +00:00
Args :
ns : The environment namespace
name : The environment name ( id )
2016-04-27 08:00:58 -07:00
2023-02-05 00:05:59 +00:00
Returns :
The highest version of an environment with matching namespace and name , otherwise ` ` None ` ` is returned .
"""
version : list [ int ] = [
env_spec . version
for env_spec in registry . values ( )
if env_spec . namespace == ns
and env_spec . name == name
and env_spec . version is not None
]
return max ( version , default = None )
2016-04-27 08:00:58 -07:00
2022-04-08 09:54:49 -05:00
2022-12-10 22:04:14 +00:00
def _check_namespace_exists ( ns : str | None ) :
2022-04-21 20:41:15 +02:00
""" Check if a namespace exists. If it doesn ' t, print a helpful error message. """
2023-02-05 00:05:59 +00:00
# If the namespace is none, then the namespace does exist
2022-04-21 20:41:15 +02:00
if ns is None :
return
2023-02-05 00:05:59 +00:00
# Check if the namespace exists in one of the registry's specs
namespaces : set [ str ] = {
env_spec . namespace
for env_spec in registry . values ( )
if env_spec . namespace is not None
2022-04-21 20:41:15 +02:00
}
if ns in namespaces :
return
2022-01-13 15:59:55 -05:00
2023-02-05 00:05:59 +00:00
# Otherwise, the namespace doesn't exist and raise a helpful message
2022-04-21 20:41:15 +02:00
suggestion = (
difflib . get_close_matches ( ns , namespaces , n = 1 ) if len ( namespaces ) > 0 else None
)
2023-02-05 00:05:59 +00:00
if suggestion :
suggestion_msg = f " Did you mean: ` { suggestion [ 0 ] } `? "
else :
suggestion_msg = f " Have you installed the proper package for { ns } ? "
2022-04-08 09:54:49 -05:00
2022-04-21 20:41:15 +02:00
raise error . NamespaceNotFound ( f " Namespace { ns } not found. { suggestion_msg } " )
2017-02-01 13:10:59 -08:00
2022-04-08 09:54:49 -05:00
2022-12-10 22:04:14 +00:00
def _check_name_exists ( ns : str | None , name : str ) :
2022-04-21 20:41:15 +02:00
""" Check if an env exists in a namespace. If it doesn ' t, print a helpful error message. """
2023-02-05 00:05:59 +00:00
# First check if the namespace exists
2022-04-21 20:41:15 +02:00
_check_namespace_exists ( ns )
2022-04-08 09:54:49 -05:00
2023-02-05 00:05:59 +00:00
# Then check if the name exists
names : set [ str ] = {
env_spec . name for env_spec in registry . values ( ) if env_spec . namespace == ns
}
2022-04-21 20:41:15 +02:00
if name in names :
return
2022-04-08 09:54:49 -05:00
2023-02-05 00:05:59 +00:00
# Otherwise, raise a helpful error to the user
2022-04-21 20:41:15 +02:00
suggestion = difflib . get_close_matches ( name , names , n = 1 )
namespace_msg = f " in namespace { ns } " if ns else " "
2023-02-05 00:05:59 +00:00
suggestion_msg = f " Did you mean: ` { suggestion [ 0 ] } `? " if suggestion else " "
2021-11-20 10:43:36 -05:00
2022-04-21 20:41:15 +02:00
raise error . NameNotFound (
2023-02-05 00:05:59 +00:00
f " Environment ` { name } ` doesn ' t exist { namespace_msg } . { suggestion_msg } "
2022-04-21 20:41:15 +02:00
)
2021-11-20 10:43:36 -05:00
2022-12-10 22:04:14 +00:00
def _check_version_exists ( ns : str | None , name : str , version : int | None ) :
2022-04-21 20:41:15 +02:00
""" Check if an env version exists in a namespace. If it doesn ' t, print a helpful error message.
2022-12-10 22:04:14 +00:00
2022-05-25 14:46:41 +01:00
This is a complete test whether an environment identifier is valid , and will provide the best available hints .
Args :
ns : The environment namespace
name : The environment space
version : The environment version
Raises :
DeprecatedEnv : The environment doesn ' t exist but a default version does
VersionNotFound : The ` ` version ` ` used doesn ' t exist
DeprecatedEnv : Environment version is deprecated
"""
2022-04-21 20:41:15 +02:00
if get_env_id ( ns , name , version ) in registry :
return
2022-01-19 13:50:25 -05:00
2022-04-21 20:41:15 +02:00
_check_name_exists ( ns , name )
if version is None :
return
2022-01-19 13:50:25 -05:00
2022-04-21 20:41:15 +02:00
message = f " Environment version `v { version } ` for environment ` { get_env_id ( ns , name , None ) } ` doesn ' t exist. "
2022-01-19 13:50:25 -05:00
2022-04-21 20:41:15 +02:00
env_specs = [
2023-02-05 00:05:59 +00:00
env_spec
for env_spec in registry . values ( )
if env_spec . namespace == ns and env_spec . name == name
2022-04-21 20:41:15 +02:00
]
2023-02-05 00:05:59 +00:00
env_specs = sorted ( env_specs , key = lambda env_spec : int ( env_spec . version or - 1 ) )
2022-01-19 13:50:25 -05:00
2023-02-05 00:05:59 +00:00
default_spec = [ env_spec for env_spec in env_specs if env_spec . version is None ]
2022-01-19 13:50:25 -05:00
2022-04-21 20:41:15 +02:00
if default_spec :
2023-02-05 00:05:59 +00:00
message + = f " It provides the default version ` { default_spec [ 0 ] . id } `. "
2022-04-21 20:41:15 +02:00
if len ( env_specs ) == 1 :
2022-01-19 13:50:25 -05:00
raise error . DeprecatedEnv ( message )
2016-04-27 08:00:58 -07:00
2022-04-21 20:41:15 +02:00
# Process possible versioned environments
2021-11-20 10:43:36 -05:00
2023-02-05 00:05:59 +00:00
versioned_specs = [
env_spec for env_spec in env_specs if env_spec . version is not None
]
2017-02-01 13:10:59 -08:00
2023-02-05 00:05:59 +00:00
latest_spec = max ( versioned_specs , key = lambda env_spec : env_spec . version , default = None ) # type: ignore
2022-04-21 20:41:15 +02:00
if latest_spec is not None and version > latest_spec . version :
2023-02-05 00:05:59 +00:00
version_list_msg = " , " . join ( f " `v { env_spec . version } ` " for env_spec in env_specs )
2022-04-21 20:41:15 +02:00
message + = f " It provides versioned environments: [ { version_list_msg } ]. "
2016-04-27 08:00:58 -07:00
2022-04-21 20:41:15 +02:00
raise error . VersionNotFound ( message )
2019-03-08 14:50:32 -08:00
2022-04-21 20:41:15 +02:00
if latest_spec is not None and version < latest_spec . version :
raise error . DeprecatedEnv (
f " Environment version v { version } for ` { get_env_id ( ns , name , None ) } ` is deprecated. "
f " Please use ` { latest_spec . id } ` instead. "
)
2016-04-27 08:00:58 -07:00
2022-01-19 13:50:25 -05:00
2023-02-05 00:05:59 +00:00
def _check_spec_register ( testing_spec : EnvSpec ) :
""" Checks whether the spec is valid to be registered. Helper function for `register`. """
latest_versioned_spec = max (
(
env_spec
for env_spec in registry . values ( )
if env_spec . namespace == testing_spec . namespace
and env_spec . name == testing_spec . name
and env_spec . version is not None
) ,
key = lambda spec_ : int ( spec_ . version ) , # type: ignore
default = None ,
)
unversioned_spec = next (
(
env_spec
for env_spec in registry . values ( )
if env_spec . namespace == testing_spec . namespace
and env_spec . name == testing_spec . name
and env_spec . version is None
) ,
None ,
)
if unversioned_spec is not None and testing_spec . version is not None :
raise error . RegistrationError (
" Can ' t register the versioned environment "
f " ` { testing_spec . id } ` when the unversioned environment "
f " ` { unversioned_spec . id } ` of the same name already exists. "
)
elif latest_versioned_spec is not None and testing_spec . version is None :
raise error . RegistrationError (
f " Can ' t register the unversioned environment ` { testing_spec . id } ` when the versioned environment "
f " ` { latest_versioned_spec . id } ` of the same name already exists. Note: the default behavior is "
" that `gym.make` with the unversioned environment will return the latest versioned environment "
)
def _check_metadata ( testing_metadata : dict [ str , Any ] ) :
""" Check the metadata of an environment. """
if not isinstance ( testing_metadata , dict ) :
raise error . InvalidMetadata (
f " Expect the environment metadata to be dict, actual type: { type ( metadata ) } "
)
render_modes = testing_metadata . get ( " render_modes " )
if render_modes is None :
logger . warn (
f " The environment creator metadata doesn ' t include `render_modes`, contains: { list ( testing_metadata . keys ( ) ) } "
)
elif not isinstance ( render_modes , Iterable ) :
logger . warn (
f " Expects the environment metadata render_modes to be a Iterable, actual type: { type ( render_modes ) } "
)
2023-02-24 11:34:20 +00:00
def _find_spec ( env_id : str ) - > EnvSpec :
# For string id's, load the environment spec from the registry then make the environment spec
assert isinstance ( env_id , str )
# The environment name can include an unloaded module in "module:env_name" style
module , env_name = ( None , env_id ) if " : " not in env_id else env_id . split ( " : " )
2023-02-12 07:49:37 -05:00
if module is not None :
try :
importlib . import_module ( module )
except ModuleNotFoundError as e :
raise ModuleNotFoundError (
f " { e } . Environment registration via importing a module failed. "
f " Check whether ' { module } ' contains env registration and can be imported. "
) from e
# load the env spec from the registry
env_spec = registry . get ( env_name )
# update env spec is not version provided, raise warning if out of date
ns , name , version = parse_env_id ( env_name )
latest_version = find_highest_version ( ns , name )
if version is not None and latest_version is not None and latest_version > version :
2023-03-13 12:10:28 +01:00
logger . deprecation (
2023-02-12 07:49:37 -05:00
f " The environment { env_name } is out of date. You should consider "
f " upgrading to version `v { latest_version } `. "
)
if version is None and latest_version is not None :
version = latest_version
new_env_id = get_env_id ( ns , name , version )
env_spec = registry . get ( new_env_id )
logger . warn (
f " Using the latest versioned environment ` { new_env_id } ` "
f " instead of the unversioned environment ` { env_name } `. "
)
if env_spec is None :
_check_version_exists ( ns , name , version )
2023-07-14 16:03:20 +01:00
raise error . Error (
f " No registered env with id: { env_name } . Did you register it, or import the package that registers it? Use `gymnasium.pprint_registry()` to see all of the registered environments. "
)
2023-02-12 07:49:37 -05:00
return env_spec
2023-02-24 11:34:20 +00:00
def load_env_creator ( name : str ) - > EnvCreator | VectorEnvCreator :
2023-02-05 00:05:59 +00:00
""" Loads an environment with name of style `` " (import path):(environment name) " `` and returns the environment creation function, normally the environment class type.
Args :
name : The environment name
Returns :
The environment constructor for the given environment name .
"""
mod_name , attr_name = name . split ( " : " )
mod = importlib . import_module ( mod_name )
fn = getattr ( mod , attr_name )
return fn
2022-01-19 13:50:25 -05:00
2021-11-20 10:43:36 -05:00
2023-07-14 16:03:20 +01:00
def register_envs ( env_module : ModuleType ) :
""" A No-op function such that it can appear to IDEs that a module is used. """
pass
2021-09-16 08:23:32 -06:00
@contextlib.contextmanager
2021-12-22 19:12:57 +01:00
def namespace ( ns : str ) :
2022-12-10 22:04:14 +00:00
""" Context manager for modifying the current namespace. """
2022-04-21 20:41:15 +02:00
global current_namespace
old_namespace = current_namespace
current_namespace = ns
yield
current_namespace = old_namespace
2021-09-14 20:14:05 -06:00
2022-09-01 16:02:31 +01:00
def register (
id : str ,
2023-02-12 07:49:37 -05:00
entry_point : EnvCreator | str | None = None ,
2022-12-10 22:04:14 +00:00
reward_threshold : float | None = None ,
2022-09-01 16:02:31 +01:00
nondeterministic : bool = False ,
2022-12-10 22:04:14 +00:00
max_episode_steps : int | None = None ,
2022-09-01 16:02:31 +01:00
order_enforce : bool = True ,
disable_env_checker : bool = False ,
2023-03-08 14:07:09 +00:00
additional_wrappers : tuple [ WrapperSpec , . . . ] = ( ) ,
2023-02-12 07:49:37 -05:00
vector_entry_point : VectorEnvCreator | str | None = None ,
2023-02-05 00:05:59 +00:00
* * kwargs : Any ,
2022-09-01 16:02:31 +01:00
) :
2023-02-05 00:05:59 +00:00
""" Registers an environment in gymnasium with an ``id`` to use with :meth:`gymnasium.make` with the ``entry_point`` being a string or callable for creating the environment.
2022-05-25 14:46:41 +01:00
2023-02-05 00:05:59 +00:00
The ` ` id ` ` parameter corresponds to the name of the environment , with the syntax as follows :
` ` [ namespace / ] ( env_name ) [ - v ( version ) ] ` ` where ` ` namespace ` ` and ` ` - v ( version ) ` ` is optional .
2021-09-16 08:23:32 -06:00
2023-02-05 00:05:59 +00:00
It takes arbitrary keyword arguments , which are passed to the : class : ` EnvSpec ` ` ` kwargs ` ` parameter .
2022-05-25 14:46:41 +01:00
Args :
id : The environment id
2022-09-01 16:02:31 +01:00
entry_point : The entry point for creating the environment
2023-02-05 00:05:59 +00:00
reward_threshold : The reward threshold considered for an agent to have learnt the environment
nondeterministic : If the environment is nondeterministic ( even with knowledge of the initial seed and all actions , the same state cannot be reached )
max_episode_steps : The maximum number of episodes steps before truncation . Used by the : class : ` gymnasium . wrappers . TimeLimit ` wrapper if not ` ` None ` ` .
order_enforce : If to enable the order enforcer wrapper to ensure users run functions in the correct order .
If ` ` True ` ` , then the : class : ` gymnasium . wrappers . OrderEnforcing ` is applied to the environment .
disable_env_checker : If to disable the : class : ` gymnasium . wrappers . PassiveEnvChecker ` to the environment .
2023-03-08 14:07:09 +00:00
additional_wrappers : Additional wrappers to apply the environment .
2023-02-12 07:49:37 -05:00
vector_entry_point : The entry point for creating the vector environment
2023-02-05 00:05:59 +00:00
* * kwargs : arbitrary keyword arguments which are passed to the environment constructor on initialisation .
2023-11-07 13:27:25 +00:00
Changelogs :
v1 .0 .0 - ` autoreset ` and ` apply_api_compatibility ` parameter was removed
2022-04-21 20:41:15 +02:00
"""
2023-02-12 07:49:37 -05:00
assert (
entry_point is not None or vector_entry_point is not None
) , " Either `entry_point` or `vector_entry_point` (or both) must be provided "
2022-04-21 20:41:15 +02:00
global registry , current_namespace
2022-05-05 15:43:53 +02:00
ns , name , version = parse_env_id ( id )
if current_namespace is not None :
2022-05-30 16:38:20 +02:00
if (
kwargs . get ( " namespace " ) is not None
and kwargs . get ( " namespace " ) != current_namespace
) :
2022-05-05 15:43:53 +02:00
logger . warn (
2022-09-01 16:02:31 +01:00
f " Custom namespace ` { kwargs . get ( ' namespace ' ) } ` is being overridden by namespace ` { current_namespace } `. "
f " If you are developing a plugin you shouldn ' t specify a namespace in `register` calls. "
" The namespace is specified through the entry point package metadata. "
2022-05-05 15:43:53 +02:00
)
ns_id = current_namespace
else :
ns_id = ns
2023-02-05 00:05:59 +00:00
full_env_id = get_env_id ( ns_id , name , version )
2022-05-05 15:43:53 +02:00
2022-09-01 16:02:31 +01:00
new_spec = EnvSpec (
2023-02-05 00:05:59 +00:00
id = full_env_id ,
2022-09-01 16:02:31 +01:00
entry_point = entry_point ,
reward_threshold = reward_threshold ,
nondeterministic = nondeterministic ,
max_episode_steps = max_episode_steps ,
order_enforce = order_enforce ,
disable_env_checker = disable_env_checker ,
2023-03-08 14:07:09 +00:00
* * kwargs ,
additional_wrappers = additional_wrappers ,
2023-02-12 07:49:37 -05:00
vector_entry_point = vector_entry_point ,
2022-09-01 16:02:31 +01:00
)
_check_spec_register ( new_spec )
2023-02-05 00:05:59 +00:00
2022-09-01 16:02:31 +01:00
if new_spec . id in registry :
logger . warn ( f " Overriding environment { new_spec . id } already in registry. " )
registry [ new_spec . id ] = new_spec
2022-04-21 20:41:15 +02:00
def make (
2022-12-10 22:04:14 +00:00
id : str | EnvSpec ,
max_episode_steps : int | None = None ,
disable_env_checker : bool | None = None ,
2023-02-05 00:05:59 +00:00
* * kwargs : Any ,
2022-04-21 20:41:15 +02:00
) - > Env :
2023-02-05 00:05:59 +00:00
""" Creates an environment previously registered with :meth:`gymnasium.register` or a :class:`EnvSpec`.
2021-09-16 08:23:32 -06:00
2023-02-05 00:05:59 +00:00
To find all available environments use ` ` gymnasium . envs . registry . keys ( ) ` ` for all valid ids .
2022-09-01 16:02:31 +01:00
2022-04-21 20:41:15 +02:00
Args :
2023-02-05 00:05:59 +00:00
id : A string for the environment id or a : class : ` EnvSpec ` . Optionally if using a string , a module to import can be included , e . g . ` ` ' module:Env-v0 ' ` ` .
This is equivalent to importing the module first to register the environment followed by making the environment .
2023-11-07 13:27:25 +00:00
max_episode_steps : Maximum length of an episode , can override the registered : class : ` EnvSpec ` ` ` max_episode_steps ` `
with the value being passed to : class : ` gymnasium . wrappers . TimeLimit ` .
Using ` ` max_episode_steps = - 1 ` ` will not apply the wrapper to the environment .
2023-02-05 00:05:59 +00:00
disable_env_checker : If to add : class : ` gymnasium . wrappers . PassiveEnvChecker ` , ` ` None ` ` will default to the
: class : ` EnvSpec ` ` ` disable_env_checker ` ` value otherwise use this value will be used .
2022-04-21 20:41:15 +02:00
kwargs : Additional arguments to pass to the environment constructor .
2022-05-25 14:46:41 +01:00
2022-04-21 20:41:15 +02:00
Returns :
2023-02-05 00:05:59 +00:00
An instance of the environment with wrappers applied .
2022-05-25 14:46:41 +01:00
Raises :
2023-02-05 00:05:59 +00:00
Error : If the ` ` id ` ` doesn ' t exist in the :attr:`registry`
2023-11-07 13:27:25 +00:00
Changelogs :
v1 .0 .0 - ` autoreset ` and ` apply_api_compatibility ` was removed
2022-04-21 20:41:15 +02:00
"""
if isinstance ( id , EnvSpec ) :
2023-03-08 14:07:09 +00:00
env_spec = id
if not hasattr ( env_spec , " additional_wrappers " ) :
logger . warn (
f " The env spec passed to `make` does not have a `additional_wrappers`, set it to an empty tuple. Env_spec= { env_spec } "
2022-06-24 22:25:58 +02:00
)
2023-03-08 14:07:09 +00:00
env_spec . additional_wrappers = ( )
2022-09-06 17:20:04 +02:00
else :
2023-02-24 11:34:20 +00:00
# For string id's, load the environment spec from the registry then make the environment spec
assert isinstance ( id , str )
2022-09-06 17:20:04 +02:00
2023-02-24 11:34:20 +00:00
# The environment name can include an unloaded module in "module:env_name" style
env_spec = _find_spec ( id )
2022-07-11 02:45:24 +01:00
2023-03-08 14:07:09 +00:00
assert isinstance ( env_spec , EnvSpec )
# Update the env spec kwargs with the `make` kwargs
env_spec_kwargs = copy . deepcopy ( env_spec . kwargs )
env_spec_kwargs . update ( kwargs )
# Load the environment creator
if env_spec . entry_point is None :
raise error . Error ( f " { env_spec . id } registered but entry_point is not specified " )
elif callable ( env_spec . entry_point ) :
env_creator = env_spec . entry_point
else :
# Assume it's a string
env_creator = load_env_creator ( env_spec . entry_point )
# Determine if to use the rendering
render_modes : list [ str ] | None = None
if hasattr ( env_creator , " metadata " ) :
_check_metadata ( env_creator . metadata )
render_modes = env_creator . metadata . get ( " render_modes " )
render_mode = env_spec_kwargs . get ( " render_mode " )
apply_human_rendering = False
apply_render_collection = False
# If mode is not valid, try applying HumanRendering/RenderCollection wrappers
if (
render_mode is not None
and render_modes is not None
and render_mode not in render_modes
) :
displayable_modes = { " rgb_array " , " rgb_array_list " } . intersection ( render_modes )
if render_mode == " human " and len ( displayable_modes ) > 0 :
logger . warn (
" You are trying to use ' human ' rendering for an environment that doesn ' t natively support it. "
" The HumanRendering wrapper is being applied to your environment. "
)
env_spec_kwargs [ " render_mode " ] = displayable_modes . pop ( )
apply_human_rendering = True
elif (
render_mode . endswith ( " _list " )
and render_mode [ : - len ( " _list " ) ] in render_modes
) :
env_spec_kwargs [ " render_mode " ] = render_mode [ : - len ( " _list " ) ]
apply_render_collection = True
else :
logger . warn (
f " The environment is being initialised with render_mode= { render_mode !r} "
f " that is not in the possible render_modes ( { render_modes } ). "
)
try :
env = env_creator ( * * env_spec_kwargs )
except TypeError as e :
if (
str ( e ) . find ( " got an unexpected keyword argument ' render_mode ' " ) > = 0
and apply_human_rendering
) :
raise error . Error (
f " You passed render_mode= ' human ' although { env_spec . id } doesn ' t implement human-rendering natively. "
" Gym tried to apply the HumanRendering wrapper but it looks like your environment is using the old "
" rendering API, which is not supported by the HumanRendering wrapper. "
) from e
else :
2023-06-21 15:32:12 +01:00
raise type ( e ) (
f " { e } was raised from the environment creator for { env_spec . id } with kwargs ( { env_spec_kwargs } ) "
)
2023-03-08 14:07:09 +00:00
# Set the minimal env spec for the environment.
env . unwrapped . spec = EnvSpec (
id = env_spec . id ,
entry_point = env_spec . entry_point ,
reward_threshold = env_spec . reward_threshold ,
nondeterministic = env_spec . nondeterministic ,
max_episode_steps = None ,
order_enforce = False ,
disable_env_checker = True ,
kwargs = env_spec_kwargs ,
additional_wrappers = ( ) ,
vector_entry_point = env_spec . vector_entry_point ,
)
# Check if pre-wrapped wrappers
assert env . spec is not None
num_prior_wrappers = len ( env . spec . additional_wrappers )
if (
env_spec . additional_wrappers [ : num_prior_wrappers ]
!= env . spec . additional_wrappers
) :
for env_spec_wrapper_spec , recreated_wrapper_spec in zip (
env_spec . additional_wrappers , env . spec . additional_wrappers
) :
raise ValueError (
f " The environment ' s wrapper spec { recreated_wrapper_spec } is different from the saved `EnvSpec` additional wrapper { env_spec_wrapper_spec } "
)
# Run the environment checker as the lowest level wrapper
if disable_env_checker is False or (
disable_env_checker is None and env_spec . disable_env_checker is False
) :
2023-06-21 17:04:11 +01:00
env = gym . wrappers . PassiveEnvChecker ( env )
2023-03-08 14:07:09 +00:00
# Add the order enforcing wrapper
if env_spec . order_enforce :
2023-06-21 17:04:11 +01:00
env = gym . wrappers . OrderEnforcing ( env )
2023-03-08 14:07:09 +00:00
# Add the time limit wrapper
2023-11-07 13:27:25 +00:00
if max_episode_steps != - 1 :
if max_episode_steps is not None :
env = gym . wrappers . TimeLimit ( env , max_episode_steps )
elif env_spec . max_episode_steps is not None :
env = gym . wrappers . TimeLimit ( env , env_spec . max_episode_steps )
2023-07-14 16:03:20 +01:00
2023-03-08 14:07:09 +00:00
for wrapper_spec in env_spec . additional_wrappers [ num_prior_wrappers : ] :
if wrapper_spec . kwargs is None :
raise ValueError (
f " { wrapper_spec . name } wrapper does not inherit from `gymnasium.utils.RecordConstructorArgs`, therefore, the wrapper cannot be recreated. "
)
env = load_env_creator ( wrapper_spec . entry_point ) ( env = env , * * wrapper_spec . kwargs )
# Add human rendering wrapper
if apply_human_rendering :
2023-06-21 17:04:11 +01:00
env = gym . wrappers . HumanRendering ( env )
2023-03-08 14:07:09 +00:00
elif apply_render_collection :
2023-06-21 17:04:11 +01:00
env = gym . wrappers . RenderCollection ( env )
2023-03-08 14:07:09 +00:00
return env
2022-04-21 20:41:15 +02:00
2023-02-12 07:49:37 -05:00
def make_vec (
id : str | EnvSpec ,
num_envs : int = 1 ,
2023-11-07 13:27:25 +00:00
vectorization_mode : str | None = None ,
2023-02-12 07:49:37 -05:00
vector_kwargs : dict [ str , Any ] | None = None ,
wrappers : Sequence [ Callable [ [ Env ] , Wrapper ] ] | None = None ,
* * kwargs ,
2023-11-07 13:27:25 +00:00
) - > gym . vector . VectorEnv :
2023-02-12 14:17:57 -05:00
""" Create a vector environment according to the given ID.
2023-11-07 13:27:25 +00:00
To find all available environments use : func : ` gymnasium . pprint_registry ` or ` ` gymnasium . registry . keys ( ) ` ` for all valid ids .
We refer to the Vector environment as the vectorizor while the environment being vectorized is the base or vectorized environment ( ` ` vectorizor ( vectorized env ) ` ` ) .
2023-02-12 07:49:37 -05:00
Args :
id : Name of the environment . Optionally , a module to import can be included , eg . ' module:Env-v0 '
num_envs : Number of environments to create
2023-11-07 13:27:25 +00:00
vectorization_mode : The vectorization method used , defaults to ` ` None ` ` such that if a ` ` vector_entry_point ` ` exists ,
this is first used otherwise defaults to ` ` sync ` ` to use the : class : ` gymnasium . vector . SyncVectorEnv ` .
Valid modes are ` ` " async " ` ` , ` ` " sync " ` ` or ` ` " vector_entry_point " ` ` .
vector_kwargs : Additional arguments to pass to the vectorizor environment constructor , i . e . , ` ` SyncVectorEnv ( . . . , * * vector_kwargs ) ` ` .
wrappers : A sequence of wrapper functions to apply to the base environment . Can only be used in ` ` " sync " ` ` or ` ` " async " ` ` mode .
* * kwargs : Additional arguments passed to the base environment constructor .
2023-02-12 07:49:37 -05:00
Returns :
An instance of the environment .
Raises :
Error : If the ` ` id ` ` doesn ' t exist then an error is raised
"""
if vector_kwargs is None :
vector_kwargs = { }
if wrappers is None :
wrappers = [ ]
if isinstance ( id , EnvSpec ) :
2023-11-07 13:27:25 +00:00
id_env_spec = id
env_spec_kwargs = id_env_spec . kwargs . copy ( )
2023-02-12 07:49:37 -05:00
2023-11-07 13:27:25 +00:00
num_envs = env_spec_kwargs . pop ( " num_envs " , num_envs )
vectorization_mode = env_spec_kwargs . pop (
" vectorization_mode " , vectorization_mode
)
vector_kwargs = env_spec_kwargs . pop ( " vector_kwargs " , vector_kwargs )
wrappers = env_spec_kwargs . pop ( " wrappers " , wrappers )
2023-02-12 07:49:37 -05:00
else :
2023-11-07 13:27:25 +00:00
id_env_spec = _find_spec ( id )
env_spec_kwargs = id_env_spec . kwargs . copy ( )
2023-02-12 07:49:37 -05:00
2023-11-07 13:27:25 +00:00
env_spec_kwargs . update ( kwargs )
2023-02-12 07:49:37 -05:00
2023-11-07 13:27:25 +00:00
# Update the vectorization_mode if None
if vectorization_mode is None :
if id_env_spec . vector_entry_point is not None :
vectorization_mode = " vector_entry_point "
else :
vectorization_mode = " sync "
2023-02-12 07:49:37 -05:00
2023-11-07 13:27:25 +00:00
def create_single_env ( ) - > Env :
single_env = make ( id_env_spec . id , * * env_spec_kwargs . copy ( ) )
2023-02-12 07:49:37 -05:00
for wrapper in wrappers :
2023-11-07 13:27:25 +00:00
single_env = wrapper ( single_env )
return single_env
2023-02-12 07:49:37 -05:00
if vectorization_mode == " sync " :
2023-11-07 13:27:25 +00:00
if id_env_spec . entry_point is None :
raise error . Error (
f " Cannot create vectorized environment for { id_env_spec . id } because it doesn ' t have an entry point defined. "
)
env = gym . vector . SyncVectorEnv (
env_fns = ( create_single_env for _ in range ( num_envs ) ) ,
2023-02-12 07:49:37 -05:00
* * vector_kwargs ,
)
elif vectorization_mode == " async " :
2023-11-07 13:27:25 +00:00
if id_env_spec . entry_point is None :
raise error . Error (
f " Cannot create vectorized environment for { id_env_spec . id } because it doesn ' t have an entry point defined. "
)
env = gym . vector . AsyncVectorEnv (
env_fns = [ create_single_env for _ in range ( num_envs ) ] ,
2023-02-12 07:49:37 -05:00
* * vector_kwargs ,
)
2023-11-07 13:27:25 +00:00
elif vectorization_mode == " vector_entry_point " :
entry_point = id_env_spec . vector_entry_point
if entry_point is None :
raise error . Error (
f " Cannot create vectorized environment for { id } because it doesn ' t have a vector entry point defined. "
)
elif callable ( entry_point ) :
env_creator = entry_point
else : # Assume it's a string
env_creator = load_env_creator ( entry_point )
2023-02-12 07:49:37 -05:00
if len ( wrappers ) > 0 :
2023-11-07 13:27:25 +00:00
raise error . Error (
" Cannot use `vector_entry_point` vectorization mode with the wrappers argument. "
)
if " max_episode_steps " not in vector_kwargs :
vector_kwargs [ " max_episode_steps " ] = id_env_spec . max_episode_steps
2023-02-12 14:17:57 -05:00
env = env_creator ( num_envs = num_envs , * * vector_kwargs )
2023-02-12 07:49:37 -05:00
else :
raise error . Error ( f " Invalid vectorization mode: { vectorization_mode } " )
# Copies the environment creation specification and kwargs to add to the environment specification details
2023-11-07 13:27:25 +00:00
copied_id_spec = copy . deepcopy ( id_env_spec )
copied_id_spec . kwargs = env_spec_kwargs
if num_envs != 1 :
copied_id_spec . kwargs [ " num_envs " ] = num_envs
if vectorization_mode != " async " :
copied_id_spec . kwargs [ " vectorization_mode " ] = vectorization_mode
if vector_kwargs is not None :
copied_id_spec . kwargs [ " vector_kwargs " ] = vector_kwargs
if wrappers is not None :
copied_id_spec . kwargs [ " wrappers " ] = wrappers
env . unwrapped . spec = copied_id_spec
2023-02-12 07:49:37 -05:00
return env
2022-04-21 20:41:15 +02:00
def spec ( env_id : str ) - > EnvSpec :
2023-02-05 00:05:59 +00:00
""" Retrieve the :class:`EnvSpec` for the environment id from the :attr:`registry`.
Args :
env_id : The environment id with the expected format of ` ` [ ( namespace ) / ] id [ - v ( version ) ] ` `
Returns :
The environment spec if it exists
Raises :
Error : If the environment id doesn ' t exist
"""
env_spec = registry . get ( env_id )
if env_spec is None :
2022-04-21 20:41:15 +02:00
ns , name , version = parse_env_id ( env_id )
_check_version_exists ( ns , name , version )
raise error . Error ( f " No registered env with id: { env_id } " )
else :
2023-02-05 00:05:59 +00:00
assert isinstance (
env_spec , EnvSpec
) , f " Expected the registry for { env_id } to be an `EnvSpec`, actual type is { type ( env_spec ) } "
return env_spec
2022-11-16 12:59:42 +00:00
def pprint_registry (
2023-02-05 00:05:59 +00:00
print_registry : dict [ str , EnvSpec ] = registry ,
* ,
2022-11-22 23:43:41 +00:00
num_cols : int = 3 ,
2022-12-10 22:04:14 +00:00
exclude_namespaces : list [ str ] | None = None ,
2022-11-17 20:40:19 +00:00
disable_print : bool = False ,
2022-12-10 22:04:14 +00:00
) - > str | None :
2023-02-05 00:05:59 +00:00
""" Pretty prints all environments in the :attr:`registry`.
Note :
All arguments are keyword only
2022-11-17 20:40:19 +00:00
Args :
2023-02-05 00:05:59 +00:00
print_registry : Environment registry to be printed . By default , : attr : ` registry `
2022-11-22 23:43:41 +00:00
num_cols : Number of columns to arrange environments in , for display .
2023-02-05 00:05:59 +00:00
exclude_namespaces : A list of namespaces to be excluded from printing . Helpful if only ALE environments are wanted .
2022-11-17 20:40:19 +00:00
disable_print : Whether to return a string of all the namespaces and environment IDs
2023-02-05 00:05:59 +00:00
or to print the string to console .
2022-11-17 20:40:19 +00:00
"""
2023-02-05 00:05:59 +00:00
# Defaultdict to store environment ids according to namespace.
namespace_envs : dict [ str , list [ str ] ] = defaultdict ( lambda : [ ] )
2022-11-16 12:59:42 +00:00
max_justify = float ( " -inf " )
2023-02-05 00:05:59 +00:00
# Find the namespace associated with each environment spec
for env_spec in print_registry . values ( ) :
ns = env_spec . namespace
if ns is None and isinstance ( env_spec . entry_point , str ) :
# Use regex to obtain namespace from entrypoints.
env_entry_point = re . sub ( r " : \ w+ " , " " , env_spec . entry_point )
split_entry_point = env_entry_point . split ( " . " )
if len ( split_entry_point ) > = 3 :
# If namespace is of the format:
# - gymnasium.envs.mujoco.ant_v4:AntEnv
# - gymnasium.envs.mujoco:HumanoidEnv
ns = split_entry_point [ 2 ]
elif len ( split_entry_point ) > 1 :
2022-11-16 12:59:42 +00:00
# If namespace is of the format - shimmy.atari_env
2023-02-05 00:05:59 +00:00
ns = split_entry_point [ 1 ]
2022-11-16 12:59:42 +00:00
else :
2023-02-05 00:05:59 +00:00
# If namespace cannot be found, default to env name
ns = env_spec . name
namespace_envs [ ns ] . append ( env_spec . id )
max_justify = max ( max_justify , len ( env_spec . name ) )
# Iterate through each namespace and print environment alphabetically
output : list [ str ] = [ ]
for ns , env_ids in namespace_envs . items ( ) :
2022-11-16 12:59:42 +00:00
# Ignore namespaces to exclude.
2023-02-05 00:05:59 +00:00
if exclude_namespaces is not None and ns in exclude_namespaces :
2022-11-16 12:59:42 +00:00
continue
2023-02-05 00:05:59 +00:00
# Print the namespace
namespace_output = f " { ' = ' * 5 } { ns } { ' = ' * 5 } \n "
2022-11-16 12:59:42 +00:00
# Reference: https://stackoverflow.com/a/33464001
2023-02-05 00:05:59 +00:00
for count , env_id in enumerate ( sorted ( env_ids ) , 1 ) :
# Print column with justification.
namespace_output + = env_id . ljust ( max_justify ) + " "
2022-11-16 12:59:42 +00:00
# Once all rows printed, switch to new column.
2023-02-05 00:05:59 +00:00
if count % num_cols == 0 :
namespace_output = namespace_output . rstrip ( " " )
if count != len ( env_ids ) :
namespace_output + = " \n "
output . append ( namespace_output . rstrip ( " " ) )
2022-11-16 12:59:42 +00:00
2022-11-17 20:40:19 +00:00
if disable_print :
2023-02-05 00:05:59 +00:00
return " \n " . join ( output )
2022-11-17 20:40:19 +00:00
else :
2023-02-05 00:05:59 +00:00
print ( " \n " . join ( output ) )