From 0263deb5ab8dce46b1056f5baa2c7c141fad9471 Mon Sep 17 00:00:00 2001 From: Mark Towers Date: Wed, 25 May 2022 15:28:19 +0100 Subject: [PATCH] Add support for python 3.6 (#2836) * Add support for python 3.6 * Add support for python 3.6 * Added check for python 3.6 to not install mujoco as no version exists * Fixed the install groups for python 3.6 * Re-added python 3.6 support for gym * black * Added support for dataclasses through dataclasses module in setup that backports the module * Fixed install requirements * Re-added dummy env spec with dataclasses * Changed type for compatability for python 3.6 * Added a python 3.6 warning * Fixed python 3.6 typing issue * Removed __future__ import annotation for python 3.6 support * Fixed python 3.6 typing --- .github/workflows/build.yml | 2 +- .pre-commit-config.yaml | 2 +- gym/core.py | 28 ++++++++------ gym/envs/registration.py | 46 +++++++++++------------ gym/envs/toy_text/frozen_lake.py | 6 +-- gym/spaces/box.py | 16 ++++---- gym/spaces/dict.py | 14 +++---- gym/spaces/discrete.py | 6 +-- gym/spaces/multi_binary.py | 8 ++-- gym/spaces/multi_discrete.py | 10 ++--- gym/spaces/space.py | 24 ++++++++---- gym/spaces/tuple.py | 8 ++-- gym/spaces/utils.py | 6 +-- gym/utils/play.py | 14 +++---- gym/utils/seeding.py | 10 ++--- gym/vector/__init__.py | 6 +-- gym/vector/async_vector_env.py | 10 ++--- gym/vector/sync_vector_env.py | 6 +-- gym/vector/vector_env.py | 14 +++---- gym/wrappers/monitoring/video_recorder.py | 6 +-- gym/wrappers/pixel_observation.py | 8 ++-- py.Dockerfile | 3 +- pyproject.toml | 2 +- setup.py | 6 ++- test_requirements.txt | 1 - tests/envs/spec_list.py | 13 ++++++- tests/envs/test_action_dim_check.py | 27 +++++++------ 27 files changed, 148 insertions(+), 154 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c376ad0c..b670b2c76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 - run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89a02ac19..1359be7a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: hooks: - id: pyupgrade # TODO: remove `--keep-runtime-typing` option - args: ["--py37-plus", "--keep-runtime-typing"] + args: ["--py36-plus", "--keep-runtime-typing"] - repo: local hooks: - id: pyright diff --git a/gym/core.py b/gym/core.py index e8dd4ed87..1356aebf8 100644 --- a/gym/core.py +++ b/gym/core.py @@ -1,13 +1,17 @@ """Core API for Environment, Wrapper, ActionWrapper, RewardWrapper and ObservationWrapper.""" -from __future__ import annotations - -from typing import Generic, Optional, SupportsFloat, TypeVar, Union +import sys +from typing import Generic, Optional, SupportsFloat, Tuple, TypeVar, Union from gym import spaces -from gym.logger import deprecation +from gym.logger import deprecation, warn from gym.utils import seeding from gym.utils.seeding import RandomNumberGenerator +if sys.version_info == (3, 6): + warn( + "Gym minimally supports python 3.6 as the python foundation not longer supports the version, please update your version to 3.7+" + ) + ObsType = TypeVar("ObsType") ActType = TypeVar("ActType") @@ -62,7 +66,7 @@ def np_random(self) -> RandomNumberGenerator: def np_random(self, value: RandomNumberGenerator): self._np_random = value - def step(self, action: ActType) -> tuple[ObsType, float, bool, dict]: + def step(self, action: ActType) -> Tuple[ObsType, float, bool, dict]: """Run one timestep of the environment's dynamics. When end of episode is reached, you are responsible for calling :meth:`reset` to reset this environment's state. @@ -92,7 +96,7 @@ def reset( seed: Optional[int] = None, return_info: bool = False, options: Optional[dict] = None, - ) -> Union[ObsType, tuple[ObsType, dict]]: + ) -> Union[ObsType, Tuple[ObsType, dict]]: """Resets the environment to an initial state and returns the initial observation. This method can reset the environment's random number generator(s) if ``seed`` is an integer or @@ -201,7 +205,7 @@ def seed(self, seed=None): return [seed] @property - def unwrapped(self) -> Env: + def unwrapped(self) -> "Env": """Returns the base non-wrapped environment. Returns: @@ -248,7 +252,7 @@ def __init__(self, env: Env): self._action_space: Optional[spaces.Space] = None self._observation_space: Optional[spaces.Space] = None - self._reward_range: Optional[tuple[SupportsFloat, SupportsFloat]] = None + self._reward_range: Optional[Tuple[SupportsFloat, SupportsFloat]] = None self._metadata: Optional[dict] = None def __getattr__(self, name): @@ -290,14 +294,14 @@ def observation_space(self, space: spaces.Space): self._observation_space = space @property - def reward_range(self) -> tuple[SupportsFloat, SupportsFloat]: + def reward_range(self) -> Tuple[SupportsFloat, SupportsFloat]: """Return the reward range of the environment.""" if self._reward_range is None: return self.env.reward_range return self._reward_range @reward_range.setter - def reward_range(self, value: tuple[SupportsFloat, SupportsFloat]): + def reward_range(self, value: Tuple[SupportsFloat, SupportsFloat]): self._reward_range = value @property @@ -311,11 +315,11 @@ def metadata(self) -> dict: def metadata(self, value): self._metadata = value - def step(self, action: ActType) -> tuple[ObsType, float, bool, dict]: + def step(self, action: ActType) -> Tuple[ObsType, float, bool, dict]: """Steps through the environment with action.""" return self.env.step(action) - def reset(self, **kwargs) -> Union[ObsType, tuple[ObsType, dict]]: + def reset(self, **kwargs) -> Union[ObsType, Tuple[ObsType, dict]]: """Resets the environment with kwargs.""" return self.env.reset(**kwargs) diff --git a/gym/envs/registration.py b/gym/envs/registration.py index 7aca0fa62..506960898 100644 --- a/gym/envs/registration.py +++ b/gym/envs/registration.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import contextlib import copy import difflib @@ -10,12 +8,14 @@ import warnings from dataclasses import dataclass, field from typing import ( - Any, Callable, + Dict, Iterable, + List, Optional, Sequence, SupportsFloat, + Tuple, Union, overload, ) @@ -34,15 +34,11 @@ if sys.version_info >= (3, 8): from typing import Literal else: - - class Literal(str): - def __class_getitem__(cls, item): - return Any - + from typing_extensions import Literal from gym import Env, error, logger -ENV_ID_RE: re.Pattern = re.compile( +ENV_ID_RE = re.compile( r"^(?:(?P[\w:-]+)\/)?(?:(?P[\w:.-]+?))(?:-v(?P\d+))?$" ) @@ -54,7 +50,7 @@ def load(name: str) -> type: return fn -def parse_env_id(id: str) -> tuple[Optional[str], str, Optional[int]]: +def parse_env_id(id: str) -> Tuple[Optional[str], str, Optional[int]]: """Parse environment ID string format. This format is true today, but it's *not* an official spec. @@ -241,7 +237,7 @@ def _check_version_exists(ns: Optional[str], name: str, version: Optional[int]): def find_highest_version(ns: Optional[str], name: str) -> Optional[int]: - version: list[int] = [ + version: List[int] = [ spec_.version for spec_ in registry.values() if spec_.namespace == ns and spec_.name == name and spec_.version is not None @@ -302,39 +298,39 @@ def load_env_plugins(entry_point: str = "gym.envs") -> None: @overload -def make(id: Literal["CartPole-v1"], **kwargs) -> Env[np.ndarray, np.ndarray | int]: ... +def make(id: Literal["CartPole-v0", "CartPole-v1"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, int]]: ... @overload -def make(id: Literal["MountainCar-v0"], **kwargs) -> Env[np.ndarray, np.ndarray | int]: ... +def make(id: Literal["MountainCar-v0"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, int]]: ... @overload -def make(id: Literal["MountainCarContinuous-v0"], **kwargs) -> Env[np.ndarray, np.ndarray | Sequence[SupportsFloat]]: ... +def make(id: Literal["MountainCarContinuous-v0"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, Sequence[SupportsFloat]]]: ... @overload -def make(id: Literal["Pendulum-v1"], **kwargs) -> Env[np.ndarray, np.ndarray | Sequence[SupportsFloat]]: ... +def make(id: Literal["Pendulum-v1"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, Sequence[SupportsFloat]]]: ... @overload -def make(id: Literal["Acrobot-v1"], **kwargs) -> Env[np.ndarray, np.ndarray | int]: ... +def make(id: Literal["Acrobot-v1"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, int]]: ... # Box2d # ---------------------------------------- @overload -def make(id: Literal["LunarLander-v2", "LunarLanderContinuous-v2"], **kwargs) -> Env[np.ndarray, np.ndarray | int]: ... +def make(id: Literal["LunarLander-v2", "LunarLanderContinuous-v2"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, int]]: ... @overload -def make(id: Literal["BipedalWalker-v3", "BipedalWalkerHardcore-v3"], **kwargs) -> Env[np.ndarray, np.ndarray | Sequence[SupportsFloat]]: ... +def make(id: Literal["BipedalWalker-v3", "BipedalWalkerHardcore-v3"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, Sequence[SupportsFloat]]]: ... @overload -def make(id: Literal["CarRacing-v1", "CarRacingDomainRandomize-v1"], **kwargs) -> Env[np.ndarray, np.ndarray | Sequence[SupportsFloat]]: ... +def make(id: Literal["CarRacing-v1", "CarRacingDomainRandomize-v1"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, Sequence[SupportsFloat]]]: ... # Toy Text # ---------------------------------------- @overload -def make(id: Literal["Blackjack-v1"], **kwargs) -> Env[np.ndarray, np.ndarray | int]: ... +def make(id: Literal["Blackjack-v1"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, int]]: ... @overload -def make(id: Literal["FrozenLake-v1", "FrozenLake8x8-v1"], **kwargs) -> Env[np.ndarray, np.ndarray | int]: ... +def make(id: Literal["FrozenLake-v1", "FrozenLake8x8-v1"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, int]]: ... @overload -def make(id: Literal["CliffWalking-v0"], **kwargs) -> Env[np.ndarray, np.ndarray | int]: ... +def make(id: Literal["CliffWalking-v0"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, int]]: ... @overload -def make(id: Literal["Taxi-v3"], **kwargs) -> Env[np.ndarray, np.ndarray | int]: ... +def make(id: Literal["Taxi-v3"], **kwargs) -> Env[np.ndarray, Union[np.ndarray, int]]: ... # Mujoco # ---------------------------------------- @@ -415,7 +411,7 @@ def env_specs(self): # Global registry of environments. Meant to be accessed through `register` and `make` -registry: dict[str, EnvSpec] = EnvRegistry() +registry: Dict[str, EnvSpec] = EnvRegistry() current_namespace: Optional[str] = None @@ -522,7 +518,7 @@ def register(id: str, **kwargs): def make( - id: str | EnvSpec, + id: Union[str, EnvSpec], max_episode_steps: Optional[int] = None, autoreset: bool = False, disable_env_checker: bool = False, diff --git a/gym/envs/toy_text/frozen_lake.py b/gym/envs/toy_text/frozen_lake.py index 512ff781d..1c87dfa5e 100644 --- a/gym/envs/toy_text/frozen_lake.py +++ b/gym/envs/toy_text/frozen_lake.py @@ -1,9 +1,7 @@ -from __future__ import annotations - from contextlib import closing from io import StringIO from os import path -from typing import Optional +from typing import List, Optional import numpy as np @@ -31,7 +29,7 @@ } -def generate_random_map(size: int = 8, p: float = 0.8) -> list[str]: +def generate_random_map(size: int = 8, p: float = 0.8) -> List[str]: """Generates a random valid map (one that has a path from start to goal) Args: diff --git a/gym/spaces/box.py b/gym/spaces/box.py index a86b4011a..012eb5aaf 100644 --- a/gym/spaces/box.py +++ b/gym/spaces/box.py @@ -1,7 +1,5 @@ """Implementation of a space that represents closed boxes in euclidean space.""" -from __future__ import annotations - -from typing import Optional, Sequence, SupportsFloat, Union +from typing import List, Optional, Sequence, SupportsFloat, Tuple, Type, Union import numpy as np @@ -52,8 +50,8 @@ def __init__( low: Union[SupportsFloat, np.ndarray], high: Union[SupportsFloat, np.ndarray], shape: Optional[Sequence[int]] = None, - dtype: type = np.float32, - seed: Optional[int | seeding.RandomNumberGenerator] = None, + dtype: Type = np.float32, + seed: Optional[Union[int, seeding.RandomNumberGenerator]] = None, ): r"""Constructor of :class:`Box`. @@ -105,7 +103,7 @@ def __init__( assert isinstance(high, np.ndarray) assert high.shape == shape, "high.shape doesn't match provided shape" - self._shape: tuple[int, ...] = shape + self._shape: Tuple[int, ...] = shape low_precision = get_precision(low.dtype) high_precision = get_precision(high.dtype) @@ -121,7 +119,7 @@ def __init__( super().__init__(self.shape, self.dtype, seed) @property - def shape(self) -> tuple[int, ...]: + def shape(self) -> Tuple[int, ...]: """Has stricter type than gym.Space - never None.""" return self._shape @@ -210,7 +208,7 @@ def to_jsonable(self, sample_n): """Convert a batch of samples from this space to a JSONable data type.""" return np.array(sample_n).tolist() - def from_jsonable(self, sample_n: Sequence[SupportsFloat]) -> list[np.ndarray]: + def from_jsonable(self, sample_n: Sequence[SupportsFloat]) -> List[np.ndarray]: """Convert a JSONable data type to a batch of samples from this space.""" return [np.asarray(sample) for sample in sample_n] @@ -278,7 +276,7 @@ def get_precision(dtype) -> SupportsFloat: def _broadcast( value: Union[SupportsFloat, np.ndarray], dtype, - shape: tuple[int, ...], + shape: Tuple[int, ...], inf_sign: str, ) -> np.ndarray: """Handle infinite bounds and broadcast at the same time if needed.""" diff --git a/gym/spaces/dict.py b/gym/spaces/dict.py index 20d1e90be..1aec7b0b0 100644 --- a/gym/spaces/dict.py +++ b/gym/spaces/dict.py @@ -1,10 +1,8 @@ """Implementation of a space that represents the cartesian product of other spaces as a dictionary.""" -from __future__ import annotations - from collections import OrderedDict from collections.abc import Mapping, Sequence from typing import Dict as TypingDict -from typing import Optional +from typing import Optional, Union import numpy as np @@ -53,8 +51,8 @@ class Dict(Space[TypingDict[str, Space]], Mapping): def __init__( self, - spaces: Optional[dict[str, Space]] = None, - seed: Optional[dict | int | seeding.RandomNumberGenerator] = None, + spaces: Optional[TypingDict[str, Space]] = None, + seed: Optional[Union[dict, int, seeding.RandomNumberGenerator]] = None, **spaces_kwargs: Space, ): """Constructor of :class:`Dict` space. @@ -101,7 +99,7 @@ def __init__( None, None, seed # type: ignore ) # None for shape and dtype, since it'll require special handling - def seed(self, seed: Optional[dict | int] = None) -> list: + def seed(self, seed: Optional[Union[dict, int]] = None) -> list: """Seed the PRNG of this space and all subspaces.""" seeds = [] if isinstance(seed, dict): @@ -188,9 +186,9 @@ def to_jsonable(self, sample_n: list) -> dict: for key, space in self.spaces.items() } - def from_jsonable(self, sample_n: dict[str, list]) -> list: + def from_jsonable(self, sample_n: TypingDict[str, list]) -> list: """Convert a JSONable data type to a batch of samples from this space.""" - dict_of_list: dict[str, list] = {} + dict_of_list: TypingDict[str, list] = {} for key, space in self.spaces.items(): dict_of_list[key] = space.from_jsonable(sample_n[key]) ret = [] diff --git a/gym/spaces/discrete.py b/gym/spaces/discrete.py index 8a10115f6..28cf84714 100644 --- a/gym/spaces/discrete.py +++ b/gym/spaces/discrete.py @@ -1,7 +1,5 @@ """Implementation of a space consisting of finitely many elements.""" -from __future__ import annotations - -from typing import Optional +from typing import Optional, Union import numpy as np @@ -23,7 +21,7 @@ class Discrete(Space[int]): def __init__( self, n: int, - seed: Optional[int | seeding.RandomNumberGenerator] = None, + seed: Optional[Union[int, seeding.RandomNumberGenerator]] = None, start: int = 0, ): r"""Constructor of :class:`Discrete` space. diff --git a/gym/spaces/multi_binary.py b/gym/spaces/multi_binary.py index 67ed6a4a7..143953601 100644 --- a/gym/spaces/multi_binary.py +++ b/gym/spaces/multi_binary.py @@ -1,7 +1,5 @@ """Implementation of a space that consists of binary np.ndarrays of a fixed shape.""" -from __future__ import annotations - -from typing import Optional, Sequence, Union +from typing import Optional, Sequence, Tuple, Union import numpy as np @@ -29,7 +27,7 @@ class MultiBinary(Space[np.ndarray]): def __init__( self, n: Union[np.ndarray, Sequence[int], int], - seed: Optional[int | seeding.RandomNumberGenerator] = None, + seed: Optional[Union[int, seeding.RandomNumberGenerator]] = None, ): """Constructor of :class:`MultiBinary` space. @@ -49,7 +47,7 @@ def __init__( super().__init__(input_n, np.int8, seed) @property - def shape(self) -> tuple[int, ...]: + def shape(self) -> Tuple[int, ...]: """Has stricter type than gym.Space - never None.""" return self._shape # type: ignore diff --git a/gym/spaces/multi_discrete.py b/gym/spaces/multi_discrete.py index 8ebca413a..cb4342015 100644 --- a/gym/spaces/multi_discrete.py +++ b/gym/spaces/multi_discrete.py @@ -1,7 +1,5 @@ """Implementation of a space that represents the cartesian product of `Discrete` spaces.""" -from __future__ import annotations - -from typing import Iterable, Optional, Sequence, Union +from typing import Iterable, List, Optional, Sequence, Tuple, Union import numpy as np @@ -31,9 +29,9 @@ class MultiDiscrete(Space[np.ndarray]): def __init__( self, - nvec: Union[np.ndarray, list[int]], + nvec: Union[np.ndarray, List[int]], dtype=np.int64, - seed: Optional[int | seeding.RandomNumberGenerator] = None, + seed: Optional[Union[int, seeding.RandomNumberGenerator]] = None, ): """Constructor of :class:`MultiDiscrete` space. @@ -61,7 +59,7 @@ def __init__( super().__init__(self.nvec.shape, dtype, seed) @property - def shape(self) -> tuple[int, ...]: + def shape(self) -> Tuple[int, ...]: """Has stricter type than :class:`gym.Space` - never None.""" return self._shape # type: ignore diff --git a/gym/spaces/space.py b/gym/spaces/space.py index 4bda3ce22..204a2dd44 100644 --- a/gym/spaces/space.py +++ b/gym/spaces/space.py @@ -1,7 +1,17 @@ """Implementation of the `Space` metaclass.""" -from __future__ import annotations -from typing import Generic, Iterable, Mapping, Optional, Sequence, TypeVar +from typing import ( + Generic, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, +) import numpy as np @@ -39,8 +49,8 @@ class Space(Generic[T_cov]): def __init__( self, shape: Optional[Sequence[int]] = None, - dtype: Optional[type | str | np.dtype] = None, - seed: Optional[int | seeding.RandomNumberGenerator] = None, + dtype: Optional[Union[Type, str, np.dtype]] = None, + seed: Optional[Union[int, seeding.RandomNumberGenerator]] = None, ): """Constructor of :class:`Space`. @@ -67,7 +77,7 @@ def np_random(self) -> seeding.RandomNumberGenerator: return self._np_random # type: ignore ## self.seed() call guarantees right type. @property - def shape(self) -> Optional[tuple[int, ...]]: + def shape(self) -> Optional[Tuple[int, ...]]: """Return the shape of the space as an immutable property.""" return self._shape @@ -88,7 +98,7 @@ def __contains__(self, x) -> bool: """Return boolean specifying if x is a valid member of this space.""" return self.contains(x) - def __setstate__(self, state: Iterable | Mapping): + def __setstate__(self, state: Union[Iterable, Mapping]): """Used when loading a pickled space. This method was implemented explicitly to allow for loading of legacy states. @@ -119,7 +129,7 @@ def to_jsonable(self, sample_n: Sequence[T_cov]) -> list: # By default, assume identity is JSONable return list(sample_n) - def from_jsonable(self, sample_n: list) -> list[T_cov]: + def from_jsonable(self, sample_n: list) -> List[T_cov]: """Convert a JSONable data type to a batch of samples from this space.""" # By default, assume identity is JSONable return sample_n diff --git a/gym/spaces/tuple.py b/gym/spaces/tuple.py index 89889b413..443de6c67 100644 --- a/gym/spaces/tuple.py +++ b/gym/spaces/tuple.py @@ -1,7 +1,5 @@ """Implementation of a space that represents the cartesian product of other spaces.""" -from __future__ import annotations - -from typing import Iterable, Optional, Sequence +from typing import Iterable, List, Optional, Sequence, Union import numpy as np @@ -25,7 +23,7 @@ class Tuple(Space[tuple], Sequence): def __init__( self, spaces: Iterable[Space], - seed: Optional[int | list[int] | seeding.RandomNumberGenerator] = None, + seed: Optional[Union[int, List[int], seeding.RandomNumberGenerator]] = None, ): r"""Constructor of :class:`Tuple` space. @@ -43,7 +41,7 @@ def __init__( ), "Elements of the tuple must be instances of gym.Space" super().__init__(None, None, seed) # type: ignore - def seed(self, seed: Optional[int | list[int]] = None) -> list: + def seed(self, seed: Optional[Union[int, List[int]]] = None) -> list: """Seed the PRNG of this space and all subspaces.""" seeds = [] diff --git a/gym/spaces/utils.py b/gym/spaces/utils.py index c364a37f4..2830c8d46 100644 --- a/gym/spaces/utils.py +++ b/gym/spaces/utils.py @@ -3,8 +3,6 @@ These functions mostly take care of flattening and unflattening elements of spaces to facilitate their usage in learning code. """ -from __future__ import annotations - import operator as op from collections import OrderedDict from functools import reduce, singledispatch @@ -142,7 +140,9 @@ def unflatten(space: Space[T], x: np.ndarray) -> T: @unflatten.register(Box) @unflatten.register(MultiBinary) -def _unflatten_box_multibinary(space: Box | MultiBinary, x: np.ndarray) -> np.ndarray: +def _unflatten_box_multibinary( + space: Union[Box, MultiBinary], x: np.ndarray +) -> np.ndarray: return np.asarray(x, dtype=space.dtype).reshape(space.shape) diff --git a/gym/utils/play.py b/gym/utils/play.py index c3ee40a88..c96769f29 100644 --- a/gym/utils/play.py +++ b/gym/utils/play.py @@ -1,8 +1,6 @@ """Utilities of visualising an environment.""" -from __future__ import annotations - from collections import deque -from typing import Callable, Dict, Optional, Tuple, Union +from typing import Callable, Dict, List, Optional, Tuple, Union import numpy as np import pygame @@ -35,7 +33,7 @@ class PlayableGame: def __init__( self, env: Env, - keys_to_action: Optional[dict[tuple[int], int]] = None, + keys_to_action: Optional[Dict[Tuple[int], int]] = None, zoom: Optional[float] = None, ): """Wraps an environment with a dictionary of keyboard buttons to action and if to zoom in on the environment. @@ -53,7 +51,7 @@ def __init__( self.running = True def _get_relevant_keys( - self, keys_to_action: Optional[dict[tuple[int], int]] = None + self, keys_to_action: Optional[Dict[Tuple[int], int]] = None ) -> set: if keys_to_action is None: if hasattr(self.env, "get_keys_to_action"): @@ -68,7 +66,7 @@ def _get_relevant_keys( relevant_keys = set(sum((list(k) for k in keys_to_action.keys()), [])) return relevant_keys - def _get_video_size(self, zoom: Optional[float] = None) -> tuple[int, int]: + def _get_video_size(self, zoom: Optional[float] = None) -> Tuple[int, int]: # TODO: this needs to be updated when the render API change goes through rendered = self.env.render(mode="rgb_array") video_size = [rendered.shape[1], rendered.shape[0]] @@ -103,7 +101,7 @@ def process_event(self, event: Event): def display_arr( - screen: Surface, arr: np.ndarray, video_size: tuple[int, int], transpose: bool + screen: Surface, arr: np.ndarray, video_size: Tuple[int, int], transpose: bool ): """Displays a numpy array on screen. @@ -273,7 +271,7 @@ class PlayPlot: """ def __init__( - self, callback: callable, horizon_timesteps: int, plot_names: list[str] + self, callback: callable, horizon_timesteps: int, plot_names: List[str] ): """Constructor of :class:`PlayPlot`. diff --git a/gym/utils/seeding.py b/gym/utils/seeding.py index 899493a0e..56a05e3c5 100644 --- a/gym/utils/seeding.py +++ b/gym/utils/seeding.py @@ -1,10 +1,8 @@ """Set of random number generator functions: seeding, generator, hashing seeds.""" -from __future__ import annotations - import hashlib import os import struct -from typing import Any, Optional, Union +from typing import Any, List, Optional, Tuple, Union import numpy as np @@ -12,7 +10,7 @@ from gym.logger import deprecation -def np_random(seed: Optional[int] = None) -> tuple[RandomNumberGenerator, Any]: +def np_random(seed: Optional[int] = None) -> Tuple["RandomNumberGenerator", Any]: """Generates a random number generator from the seed and returns the Generator and seed. Args: @@ -216,7 +214,7 @@ def _bigint_from_bytes(bt: bytes) -> int: return accum -def _int_list_from_bigint(bigint: int) -> list[int]: +def _int_list_from_bigint(bigint: int) -> List[int]: deprecation( "Function `_int_list_from_bigint` is marked as deprecated and will be removed in the future. " ) @@ -226,7 +224,7 @@ def _int_list_from_bigint(bigint: int) -> list[int]: elif bigint == 0: return [0] - ints: list[int] = [] + ints: List[int] = [] while bigint > 0: bigint, mod = divmod(bigint, 2**32) ints.append(mod) diff --git a/gym/vector/__init__.py b/gym/vector/__init__.py index 45b0be061..5aa5634bd 100644 --- a/gym/vector/__init__.py +++ b/gym/vector/__init__.py @@ -1,7 +1,5 @@ """Module for vector environments.""" -from __future__ import annotations - -from typing import Iterable, Optional, Union +from typing import Iterable, List, Optional, Union from gym.vector.async_vector_env import AsyncVectorEnv from gym.vector.sync_vector_env import SyncVectorEnv @@ -14,7 +12,7 @@ def make( id: str, num_envs: int = 1, asynchronous: bool = True, - wrappers: Optional[Union[callable, list[callable]]] = None, + wrappers: Optional[Union[callable, List[callable]]] = None, **kwargs, ) -> VectorEnv: """Create a vectorized environment from multiple copies of an environment, from its id. diff --git a/gym/vector/async_vector_env.py b/gym/vector/async_vector_env.py index 28bcc67a8..44aa73f55 100644 --- a/gym/vector/async_vector_env.py +++ b/gym/vector/async_vector_env.py @@ -1,12 +1,10 @@ """An async vector environment.""" -from __future__ import annotations - import multiprocessing as mp import sys import time from copy import deepcopy from enum import Enum -from typing import Optional, Sequence, Union +from typing import List, Optional, Sequence, Tuple, Union import numpy as np @@ -199,7 +197,7 @@ def seed(self, seed=None): def reset_async( self, - seed: Optional[Union[int, list[int]]] = None, + seed: Optional[Union[int, List[int]]] = None, return_info: bool = False, options: Optional[dict] = None, ): @@ -250,7 +248,7 @@ def reset_wait( seed: Optional[int] = None, return_info: bool = False, options: Optional[dict] = None, - ) -> Union[ObsType, tuple[ObsType, list[dict]]]: + ) -> Union[ObsType, Tuple[ObsType, List[dict]]]: """Waits for the calls triggered by :meth:`reset_async` to finish and returns the results. Args: @@ -333,7 +331,7 @@ def step_async(self, actions: np.ndarray): def step_wait( self, timeout: Optional[Union[int, float]] = None - ) -> tuple[np.ndarray, np.ndarray, np.ndarray, list[dict]]: + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, List[dict]]: """Wait for the calls to :obj:`step` in each sub-environment to finish. Args: diff --git a/gym/vector/sync_vector_env.py b/gym/vector/sync_vector_env.py index 7c1715200..9045a8eb6 100644 --- a/gym/vector/sync_vector_env.py +++ b/gym/vector/sync_vector_env.py @@ -1,8 +1,6 @@ """A synchronous vector environment.""" -from __future__ import annotations - from copy import deepcopy -from typing import Any, Iterator, Optional, Sequence, Union +from typing import Any, Iterator, List, Optional, Sequence, Union import numpy as np @@ -89,7 +87,7 @@ def seed(self, seed: Optional[Union[int, Sequence[int]]] = None): def reset_wait( self, - seed: Optional[Union[int, list[int]]] = None, + seed: Optional[Union[int, List[int]]] = None, return_info: bool = False, options: Optional[dict] = None, ): diff --git a/gym/vector/vector_env.py b/gym/vector/vector_env.py index a3b5ce02a..2cb9ee804 100644 --- a/gym/vector/vector_env.py +++ b/gym/vector/vector_env.py @@ -1,7 +1,5 @@ """Base class for vectorized environments.""" -from __future__ import annotations - -from typing import Any, Optional, Union +from typing import Any, List, Optional, Union import numpy as np @@ -50,7 +48,7 @@ def __init__( def reset_async( self, - seed: Optional[Union[int, list[int]]] = None, + seed: Optional[Union[int, List[int]]] = None, return_info: bool = False, options: Optional[dict] = None, ): @@ -68,7 +66,7 @@ def reset_async( def reset_wait( self, - seed: Optional[Union[int, list[int]]] = None, + seed: Optional[Union[int, List[int]]] = None, return_info: bool = False, options: Optional[dict] = None, ): @@ -91,7 +89,7 @@ def reset_wait( def reset( self, *, - seed: Optional[Union[int, list[int]]] = None, + seed: Optional[Union[int, List[int]]] = None, return_info: bool = False, options: Optional[dict] = None, ): @@ -144,10 +142,10 @@ def step(self, actions): def call_async(self, name, *args, **kwargs): """Calls a method name for each parallel environment asynchronously.""" - def call_wait(self, **kwargs) -> list[Any]: + def call_wait(self, **kwargs) -> List[Any]: """After calling a method in :meth:`call_async`, this function collects the results.""" - def call(self, name: str, *args, **kwargs) -> list[Any]: + def call(self, name: str, *args, **kwargs) -> List[Any]: """Call a method, or get a property, from each parallel environment. Args: diff --git a/gym/wrappers/monitoring/video_recorder.py b/gym/wrappers/monitoring/video_recorder.py index 8190fa3c8..75b52a956 100644 --- a/gym/wrappers/monitoring/video_recorder.py +++ b/gym/wrappers/monitoring/video_recorder.py @@ -1,6 +1,4 @@ """A wrapper for video recording environments by rolling it out, frame by frame.""" -from __future__ import annotations - import json import os import os.path @@ -9,7 +7,7 @@ import subprocess import tempfile from io import StringIO -from typing import Optional, Union +from typing import Optional, Tuple, Union import numpy as np @@ -362,7 +360,7 @@ class ImageEncoder: def __init__( self, output_path: str, - frame_shape: tuple[int, int, int], + frame_shape: Tuple[int, int, int], frames_per_sec: int, output_frames_per_sec: int, ): diff --git a/gym/wrappers/pixel_observation.py b/gym/wrappers/pixel_observation.py index 66af70a6a..8953d08b8 100644 --- a/gym/wrappers/pixel_observation.py +++ b/gym/wrappers/pixel_observation.py @@ -1,10 +1,8 @@ """Wrapper for augmenting observations by pixel values.""" -from __future__ import annotations - import collections import copy from collections.abc import MutableMapping -from typing import Any, Optional +from typing import Any, Dict, Optional, Tuple import numpy as np @@ -52,8 +50,8 @@ def __init__( self, env: gym.Env, pixels_only: bool = True, - render_kwargs: Optional[dict[str, dict[str, Any]]] = None, - pixel_keys: tuple[str, ...] = ("pixels",), + render_kwargs: Optional[Dict[str, Dict[str, Any]]] = None, + pixel_keys: Tuple[str, ...] = ("pixels",), ): """Initializes a new pixel Wrapper. diff --git a/py.Dockerfile b/py.Dockerfile index ce20b8430..882fd34e0 100644 --- a/py.Dockerfile +++ b/py.Dockerfile @@ -14,6 +14,7 @@ ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/.mujoco/mujoco210/bin COPY . /usr/local/gym/ WORKDIR /usr/local/gym/ -RUN pip install .[noatari] && pip install -r test_requirements.txt +RUN if [ python:$PYTHON_VERSION = "python:3.6.15" ] ; then pip install .[box2d,classic_control,toy_text,other] ; else pip install .[noatari] ; fi +RUN pip install -r test_requirements.txt ENTRYPOINT ["/usr/local/gym/bin/docker_entrypoint"] diff --git a/pyproject.toml b/pyproject.toml index 1522621bf..b886e94b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ strict = [ ] typeCheckingMode = "basic" -pythonVersion = "3.7" +pythonVersion = "3.6" typeshedPath = "typeshed" enableTypeIgnoreComments = true diff --git a/setup.py b/setup.py index d3c99ac78..ea1592b8b 100644 --- a/setup.py +++ b/setup.py @@ -55,8 +55,9 @@ install_requires=[ "numpy>=1.18.0", "cloudpickle>=1.2.0", - "importlib_metadata>=4.10.0; python_version < '3.10'", + "importlib_metadata>=4.8.0; python_version < '3.10'", "gym_notices>=0.0.4", + "dataclasses==0.8; python_version == '3.6'", ], extras_require=extras, package_data={ @@ -69,9 +70,10 @@ ] }, tests_require=["pytest", "mock"], - python_requires=">=3.7", + python_requires=">=3.6", classifiers=[ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/test_requirements.txt b/test_requirements.txt index 7f59ce32d..9c7e6956d 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,3 +1,2 @@ lz4~=3.1 pytest~=6.2 -pytest-forked~=1.3 diff --git a/tests/envs/spec_list.py b/tests/envs/spec_list.py index f7a1a2777..b96a57ead 100644 --- a/tests/envs/spec_list.py +++ b/tests/envs/spec_list.py @@ -1,7 +1,10 @@ from gym import envs, logger SKIP_MUJOCO_V3_WARNING_MESSAGE = ( - "Cannot run mujoco test because mujoco-py is not installed" + "Cannot run mujoco test because `mujoco-py` is not installed" +) +SKIP_MUJOCO_V4_WARNING_MESSAGE = ( + "Cannot run mujoco test because `mujoco` is not installed" ) skip_mujoco_v3 = False @@ -10,13 +13,19 @@ except ImportError: skip_mujoco_v3 = True +skip_mujoco_v4 = False +try: + import mujoco # noqa:F401 +except ImportError: + skip_mujoco_v4 = True + def should_skip_env_spec_for_tests(spec): # We skip tests for envs that require dependencies or are otherwise # troublesome to run frequently ep = spec.entry_point # Skip mujoco tests for pull request CI - if skip_mujoco_v3 and ep.startswith("gym.envs.mujoco"): + if (skip_mujoco_v3 or skip_mujoco_v4) and ep.startswith("gym.envs.mujoco"): return True try: import gym.envs.atari # noqa:F401 diff --git a/tests/envs/test_action_dim_check.py b/tests/envs/test_action_dim_check.py index 8289673c4..6b5032258 100644 --- a/tests/envs/test_action_dim_check.py +++ b/tests/envs/test_action_dim_check.py @@ -3,11 +3,11 @@ import numpy as np import pytest -from gym import envs +import gym +from gym import Env from gym.envs.registration import EnvSpec from gym.spaces.box import Box from gym.spaces.discrete import Discrete -from gym.spaces.space import Space from tests.envs.spec_list import ( SKIP_MUJOCO_V3_WARNING_MESSAGE, skip_mujoco_v3, @@ -17,17 +17,20 @@ ENVIRONMENT_IDS = ("HalfCheetah-v2",) -def make_envs_by_action_space_type(spec_list: List[EnvSpec], action_space: Space): +def filters_envs_action_space_type( + env_spec_list: List[EnvSpec], action_space: type +) -> List[Env]: """Make environments of specific action_space type. - This function returns a filtered list of environment from the - spec_list that matches the action_space type. + + This function returns a filtered list of environment from the spec_list that matches the action_space type. + Args: - spec_list (list): list of registered environments' specification + env_spec_list (list): list of registered environments' specification action_space (gym.spaces.Space): action_space type """ filtered_envs = [] - for spec in spec_list: - env = envs.make(spec.id) + for spec in env_spec_list: + env = gym.make(spec.id) if isinstance(env.action_space, action_space): filtered_envs.append(env) return filtered_envs @@ -36,7 +39,7 @@ def make_envs_by_action_space_type(spec_list: List[EnvSpec], action_space: Space @pytest.mark.skipif(skip_mujoco_v3, reason=SKIP_MUJOCO_V3_WARNING_MESSAGE) @pytest.mark.parametrize("environment_id", ENVIRONMENT_IDS) def test_serialize_deserialize(environment_id): - env = envs.make(environment_id) + env = gym.make(environment_id) env.reset() with pytest.raises(ValueError, match="Action dimension mismatch"): @@ -46,7 +49,7 @@ def test_serialize_deserialize(environment_id): env.step(0.1) -@pytest.mark.parametrize("env", make_envs_by_action_space_type(spec_list, Discrete)) +@pytest.mark.parametrize("env", filters_envs_action_space_type(spec_list, Discrete)) def test_discrete_actions_out_of_bound(env): """Test out of bound actions in Discrete action_space. In discrete action_space environments, `out-of-bound` @@ -65,7 +68,7 @@ def test_discrete_actions_out_of_bound(env): @pytest.mark.parametrize( ("env", "seed"), - [(env, 42) for env in make_envs_by_action_space_type(spec_list, Box)], + [(env, 42) for env in filters_envs_action_space_type(spec_list, Box)], ) def test_box_actions_out_of_bound(env, seed): """Test out of bound actions in Box action_space. @@ -80,7 +83,7 @@ def test_box_actions_out_of_bound(env, seed): env.reset(seed=seed) - oob_env = envs.make(env.spec.id) + oob_env = gym.make(env.spec.id) oob_env.reset(seed=seed) dtype = env.action_space.dtype