diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..20d90d2c --- /dev/null +++ b/.flake8 @@ -0,0 +1,29 @@ +[flake8] +max-line-length = 120 +max-doc-length = 100 +select = B,C,E,F,W,Y,SIM +ignore = + # E203: whitespace before ':' + # W503: line break before binary operator + # W504: line break after binary operator + # format by black + E203,W503,W504, + # E501: line too long + # W505: doc line too long + # too long docstring due to long example blocks + E501,W505, +per-file-ignores = + # F401: module imported but unused + # intentionally unused imports + __init__.py: F401 +exclude = + .git, + .vscode, + venv, + third-party, + __pycache__, + docs/conf.py, + build, + dist, + tests, + examples diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e1d4333..4a137121 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,8 +3,8 @@ ci: skip: [pylint] autofix_prs: true - autofix_commit_msg: 'fix: [pre-commit.ci] auto fixes [...]' - autoupdate_commit_msg: 'chore(pre-commit): [pre-commit.ci] autoupdate' + autofix_commit_msg: "fix: [pre-commit.ci] auto fixes [...]" + autoupdate_commit_msg: "chore(pre-commit): [pre-commit.ci] autoupdate" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 @@ -33,6 +33,32 @@ repos: hooks: - id: black-jupyter stages: [commit, push, manual] + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: [--py38-plus] # sync with requires-python + stages: [commit, push, manual] + exclude: | + (?x)( + ^examples/ + ) + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-docstrings + - flake8-pyi + - flake8-simplify + exclude: | + (?x)( + ^examples/| + ^tests/| + ^docs/conf.py$ + ) - repo: local hooks: - id: pylint diff --git a/.pylintrc b/.pylintrc index 406901f1..3fa15658 100644 --- a/.pylintrc +++ b/.pylintrc @@ -322,8 +322,8 @@ min-public-methods=2 [EXCEPTIONS] # Exceptions that will emit a warning when caught. -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception [FORMAT] diff --git a/Makefile b/Makefile index 2583bf08..e1822640 100644 --- a/Makefile +++ b/Makefile @@ -38,14 +38,19 @@ check_pip_install_extra = $(PYTHON) -m pip show $(1) &>/dev/null || (cd && $(PYT pylint-install: $(call check_pip_install_extra,pylint,pylint[spelling]) + $(call check_pip_install,pyenchant) flake8-install: $(call check_pip_install,flake8) - $(call check_pip_install_extra,flake8-bugbear,flake8-bugbear) + $(call check_pip_install,flake8-bugbear) + $(call check_pip_install,flake8-comprehensions) + $(call check_pip_install,flake8-docstrings) + $(call check_pip_install,flake8-pyi) + $(call check_pip_install,flake8-simplify) py-format-install: $(call check_pip_install,isort) - $(call check_pip_install,black) + $(call check_pip_install_extra,black,black[jupyter]) mypy-install: $(call check_pip_install,mypy) @@ -55,15 +60,15 @@ pre-commit-install: $(PYTHON) -m pre_commit install --install-hooks docs-install: - $(call check_pip_install,pydocstyle) - $(call check_pip_install_extra,doc8,"doc8<1.0.0a0") + $(call check_pip_install_extra,pydocstyle,pydocstyle[toml]) + $(call check_pip_install,doc8) $(call check_pip_install,sphinx) $(call check_pip_install,sphinx-autoapi) $(call check_pip_install,sphinx-autobuild) $(call check_pip_install,sphinx-copybutton) $(call check_pip_install,sphinx-autodoc-typehints) $(call check_pip_install,sphinx-design) - $(call check_pip_install_extra,sphinxcontrib.spelling,sphinxcontrib.spelling pyenchant) + $(call check_pip_install_extra,sphinxcontrib-spelling,sphinxcontrib-spelling pyenchant) pytest-install: $(call check_pip_install,pytest) @@ -72,7 +77,7 @@ pytest-install: go-install: # requires go >= 1.16 - command -v go || (sudo apt-get install -y golang-1.16 && sudo ln -sf /usr/lib/go-1.16/bin/go /usr/bin/go) + command -v go || (sudo apt-get install -y golang && sudo ln -sf /usr/lib/go/bin/go /usr/bin/go) addlicense-install: go-install command -v addlicense || go install github.com/google/addlicense@latest @@ -93,7 +98,7 @@ pylint: pylint-install $(PYTHON) -m pylint $(PROJECT_PATH) flake8: flake8-install - $(PYTHON) -m flake8 $(PYTHON_FILES) --count --select=E9,F63,F7,F82,E225,E251 --show-source --statistics + $(PYTHON) -m flake8 --count --show-source --statistics py-format: py-format-install $(PYTHON) -m isort --project $(PROJECT_NAME) --check $(PYTHON_FILES) && \ @@ -108,7 +113,7 @@ pre-commit: pre-commit-install # Documentation addlicense: addlicense-install - addlicense -c $(COPYRIGHT) -ignore tests/coverage.xml -l apache -y 2022 -check $(SOURCE_FOLDERS) + addlicense -c $(COPYRIGHT) -ignore tests/coverage.xml -l apache -y 2022-$(shell date +"%Y") -check $(SOURCE_FOLDERS) docstyle: docs-install make -C docs clean diff --git a/pyproject.toml b/pyproject.toml index b8c4daf8..088bac51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,17 +43,21 @@ Documentation = "https://www.safety-gymnasium.com" [project.optional-dependencies] lint = [ - "isort == 5.12.0", - "black == 22.12.0", - "pylint == 2.15.10", + "isort >= 5.11.0", + "black >= 23.1.0", + "pylint[spelling] >= 2.15.0", "mypy >= 0.990", "flake8", "flake8-bugbear", - "doc8 < 1.0.0a0", + "flake8-comprehensions", + "flake8-docstrings", + "flake8-pyi", + "flake8-simplify", + "doc8", "pydocstyle", "pyenchant", "pre-commit", - "myst_parser", + "myst-parser", ] test = ['pytest', 'pytest-cov', 'pytest-xdist'] diff --git a/safety_gymnasium/assets/free_geoms/vases.py b/safety_gymnasium/assets/free_geoms/vases.py index bbf381b9..b8fc4c3b 100644 --- a/safety_gymnasium/assets/free_geoms/vases.py +++ b/safety_gymnasium/assets/free_geoms/vases.py @@ -76,10 +76,11 @@ def cal_cost(self): for contact in self.engine.data.contact[: self.engine.data.ncon]: geom_ids = [contact.geom1, contact.geom2] geom_names = sorted([self.engine.model.geom(g).name for g in geom_ids]) - if any(n.startswith('vase') for n in geom_names): - if any(n in self.agent.body_info.geom_names for n in geom_names): - # pylint: disable-next=no-member - cost['cost_vases_contact'] += self.contact_cost + if any(n.startswith('vase') for n in geom_names) and any( + n in self.agent.body_info.geom_names for n in geom_names + ): + # pylint: disable-next=no-member + cost['cost_vases_contact'] += self.contact_cost # Displacement processing if self.displace_cost: # pylint: disable=no-member diff --git a/safety_gymnasium/assets/geoms/buttons.py b/safety_gymnasium/assets/geoms/buttons.py index d6566375..107f6e7d 100644 --- a/safety_gymnasium/assets/geoms/buttons.py +++ b/safety_gymnasium/assets/geoms/buttons.py @@ -76,11 +76,13 @@ def cal_cost(self): for contact in self.engine.data.contact[: self.engine.data.ncon]: geom_ids = [contact.geom1, contact.geom2] geom_names = sorted([self.engine.model.geom(g).name for g in geom_ids]) - if any(n.startswith('button') for n in geom_names): - if any(n in self.agent.body_info.geom_names for n in geom_names): - if not any(n == f'button{self.goal_button}' for n in geom_names): - # pylint: disable-next=no-member - cost['cost_buttons'] += self.cost + if ( + any(n.startswith('button') for n in geom_names) + and any(n in self.agent.body_info.geom_names for n in geom_names) + and not any(n == f'button{self.goal_button}' for n in geom_names) + ): + # pylint: disable-next=no-member + cost['cost_buttons'] += self.cost return cost def timer_tick(self): diff --git a/safety_gymnasium/assets/geoms/pillars.py b/safety_gymnasium/assets/geoms/pillars.py index 80b4b45a..7ae81eec 100644 --- a/safety_gymnasium/assets/geoms/pillars.py +++ b/safety_gymnasium/assets/geoms/pillars.py @@ -64,10 +64,11 @@ def cal_cost(self): for contact in self.engine.data.contact[: self.engine.data.ncon]: geom_ids = [contact.geom1, contact.geom2] geom_names = sorted([self.engine.model.geom(g).name for g in geom_ids]) - if any(n.startswith('pillar') for n in geom_names): - if any(n in self.agent.body_info.geom_names for n in geom_names): - # pylint: disable-next=no-member - cost['cost_pillars'] += self.cost + if any(n.startswith('pillar') for n in geom_names) and any( + n in self.agent.body_info.geom_names for n in geom_names + ): + # pylint: disable-next=no-member + cost['cost_pillars'] += self.cost return cost diff --git a/safety_gymnasium/assets/mocaps/gremlins.py b/safety_gymnasium/assets/mocaps/gremlins.py index 87384b25..423de7c5 100644 --- a/safety_gymnasium/assets/mocaps/gremlins.py +++ b/safety_gymnasium/assets/mocaps/gremlins.py @@ -84,10 +84,11 @@ def cal_cost(self): for contact in self.engine.data.contact[: self.engine.data.ncon]: geom_ids = [contact.geom1, contact.geom2] geom_names = sorted([self.engine.model.geom(g).name for g in geom_ids]) - if any(n.startswith('gremlin') for n in geom_names): - if any(n in self.agent.body_info.geom_names for n in geom_names): - # pylint: disable-next=no-member - cost['cost_gremlins'] += self.contact_cost + if any(n.startswith('gremlin') for n in geom_names) and any( + n in self.agent.body_info.geom_names for n in geom_names + ): + # pylint: disable-next=no-member + cost['cost_gremlins'] += self.contact_cost return cost diff --git a/safety_gymnasium/bases/base_object.py b/safety_gymnasium/bases/base_object.py index 03921314..9be21029 100644 --- a/safety_gymnasium/bases/base_object.py +++ b/safety_gymnasium/bases/base_object.py @@ -120,7 +120,7 @@ def process_config(self, config: dict, layout: dict, rots: float) -> None: assert len(rots) == 1, 'The number of rotations should be 1.' config[self.type][self.name] = self.get_config(xy_pos=layout[self.name], rot=rots[0]) - def _specific_agent_config(self) -> None: + def _specific_agent_config(self) -> None: # noqa: B027 """Modify properties according to specific agent. Note: diff --git a/safety_gymnasium/bases/base_task.py b/safety_gymnasium/bases/base_task.py index 3984f221..48043c9b 100644 --- a/safety_gymnasium/bases/base_task.py +++ b/safety_gymnasium/bases/base_task.py @@ -265,13 +265,12 @@ def toggle_observation_space(self) -> None: def _build_world_config(self, layout: dict) -> dict: # pylint: disable=too-many-branches """Create a world_config from our own config.""" - world_config = {} - - world_config['floor_type'] = self.floor_conf.type - world_config['floor_size'] = self.floor_conf.size - - world_config['agent_base'] = self.agent.base - world_config['agent_xy'] = layout['agent'] + world_config = { + 'floor_type': self.floor_conf.type, + 'floor_size': self.floor_conf.size, + 'agent_base': self.agent.base, + 'agent_xy': layout['agent'], + } if self.agent.rot is None: world_config['agent_rot'] = self.random_generator.random_rot() else: @@ -305,9 +304,7 @@ def _build_static_geoms_config(self, geoms_config: dict) -> None: # load all config of meshes in specific environment from .yaml file base_dir = os.path.dirname(safety_gymnasium.__file__) - with open( - os.path.join(base_dir, f'configs/{config_name}.yaml'), 'r', encoding='utf-8' - ) as file: + with open(os.path.join(base_dir, f'configs/{config_name}.yaml'), encoding='utf-8') as file: meshes_config = yaml.load(file, Loader=yaml.FullLoader) for idx in range(level + 1): @@ -343,13 +340,13 @@ def _placements_dict_from_object(self, object_name: dict) -> dict: object_num = getattr(data_obj, 'num', None) object_locations = getattr(data_obj, 'locations', []) object_placements = getattr(data_obj, 'placements', None) - object_keepout = getattr(data_obj, 'keepout') + object_keepout = data_obj.keepout else: # Unique objects object_fmt = object_name object_num = 1 object_locations = getattr(data_obj, 'locations', []) object_placements = getattr(data_obj, 'placements', None) - object_keepout = getattr(data_obj, 'keepout') + object_keepout = data_obj.keepout for i in range(object_num): if i < len(object_locations): x, y = object_locations[i] # pylint: disable=invalid-name diff --git a/safety_gymnasium/builder.py b/safety_gymnasium/builder.py index 2d13a107..cd65615c 100644 --- a/safety_gymnasium/builder.py +++ b/safety_gymnasium/builder.py @@ -15,7 +15,7 @@ """Env builder.""" from dataclasses import asdict, dataclass -from typing import Dict, Tuple, Union +from typing import Dict, Optional, Tuple, Union import gymnasium import numpy as np @@ -281,7 +281,7 @@ def _cost(self) -> dict: return cost - def render(self) -> Union[None, np.ndarray]: + def render(self) -> Optional[np.ndarray]: """Call underlying :meth:`safety_gymnasium.bases.underlying.Underlying.render` directly. Width and height in parameters are constant defaults for rendering diff --git a/safety_gymnasium/tasks/button/button_level0.py b/safety_gymnasium/tasks/button/button_level0.py index 91ec7144..f2181664 100644 --- a/safety_gymnasium/tasks/button/button_level0.py +++ b/safety_gymnasium/tasks/button/button_level0.py @@ -109,7 +109,8 @@ def goal_achieved(self): geom_ids = [contact.geom1, contact.geom2] geom_names = sorted([self.model.geom(g).name for g in geom_ids]) # pylint: disable-next=no-member - if any(n == f'button{self.buttons.goal_button}' for n in geom_names): - if any(n in self.agent.body_info.geom_names for n in geom_names): - return True + if any(n == f'button{self.buttons.goal_button}' for n in geom_names) and any( + n in self.agent.body_info.geom_names for n in geom_names + ): + return True return False diff --git a/safety_gymnasium/utils/registration.py b/safety_gymnasium/utils/registration.py index e97afb1f..252c0dc1 100644 --- a/safety_gymnasium/utils/registration.py +++ b/safety_gymnasium/utils/registration.py @@ -20,17 +20,18 @@ from typing import Optional, Sequence, Union from gymnasium import Env, error, logger -from gymnasium.envs.registration import ( # pylint: disable=unused-import +from gymnasium.envs.registration import namespace # noqa: F401 # pylint: disable=unused-import +from gymnasium.envs.registration import spec # noqa: F401 # pylint: disable=unused-import +from gymnasium.envs.registration import ( EnvSpec, _check_version_exists, find_highest_version, get_env_id, load, - namespace, parse_env_id, ) from gymnasium.envs.registration import register as gymnasium_register -from gymnasium.envs.registration import registry, spec # pylint: disable=unused-import +from gymnasium.envs.registration import registry from gymnasium.wrappers import AutoResetWrapper, HumanRendering, OrderEnforcing, RenderCollection from gymnasium.wrappers.compatibility import EnvCompatibility diff --git a/safety_gymnasium/vector/async_vector_env.py b/safety_gymnasium/vector/async_vector_env.py index f19df170..eb7060e3 100644 --- a/safety_gymnasium/vector/async_vector_env.py +++ b/safety_gymnasium/vector/async_vector_env.py @@ -188,7 +188,7 @@ def _worker( env.seed(data) pipe.send((None, True)) elif command == 'render': - pipe.send((env.render())) + pipe.send(env.render()) elif command == 'close': pipe.send((None, True)) break @@ -267,7 +267,7 @@ def _worker_shared_memory( write_to_shared_memory(observation_space, index, observation, shared_memory) pipe.send(((None, reward, cost, terminated, truncated, info), True)) elif command == 'render': - pipe.send((env.render())) + pipe.send(env.render()) elif command == 'seed': env.seed(data) pipe.send((None, True)) diff --git a/safety_gymnasium/version.py b/safety_gymnasium/version.py index a03dab3b..eafa6590 100644 --- a/safety_gymnasium/version.py +++ b/safety_gymnasium/version.py @@ -29,7 +29,7 @@ ['git', 'describe', '--abbrev=7'], cwd=os.path.dirname(os.path.abspath(__file__)), stderr=subprocess.DEVNULL, - universal_newlines=True, + text=True, ) .strip() .lstrip('v') diff --git a/safety_gymnasium/world.py b/safety_gymnasium/world.py index d327c1ce..e736a563 100644 --- a/safety_gymnasium/world.py +++ b/safety_gymnasium/world.py @@ -163,7 +163,7 @@ def build(self): # pylint: disable=too-many-locals, too-many-branches, too-many mesh = self.xml['mujoco']['asset']['mesh'] # load all assets config from .yaml file - with open(os.path.join(BASE_DIR, 'configs/assets.yaml'), 'r', encoding='utf-8') as file: + with open(os.path.join(BASE_DIR, 'configs/assets.yaml'), encoding='utf-8') as file: assets_config = yaml.load(file, Loader=yaml.FullLoader) texture.append(assets_config['textures']['skybox']) @@ -180,19 +180,19 @@ def build(self): # pylint: disable=too-many-locals, too-many-branches, too-many selected_textures = {} selected_materials = {} selected_meshes = {} - for name, config in self.geoms.items(): # pylint: disable=no-member + for config in self.geoms.values(): # pylint: disable=no-member if config['type'] == 'mesh': mesh_name = config['mesh'] selected_textures[mesh_name] = assets_config['textures'][mesh_name] selected_materials[mesh_name] = assets_config['materials'][mesh_name] selected_meshes[mesh_name] = assets_config['meshes'][mesh_name] - for name, config in self.free_geoms.items(): # pylint: disable=no-member + for config in self.free_geoms.values(): # pylint: disable=no-member if config['type'] == 'mesh': mesh_name = config['mesh'] selected_textures[mesh_name] = assets_config['textures'][mesh_name] selected_materials[mesh_name] = assets_config['materials'][mesh_name] selected_meshes[mesh_name] = assets_config['meshes'][mesh_name] - for name, config in self.mocaps.items(): # pylint: disable=no-member + for config in self.mocaps.values(): # pylint: disable=no-member if config['type'] == 'mesh': mesh_name = config['mesh'] selected_textures[mesh_name] = assets_config['textures'][mesh_name] @@ -459,11 +459,11 @@ def body_vel(self, name): def get_state(self): """Returns a copy of the simulator state.""" - state = {} - - state['time'] = np.copy(self.data.time) - state['qpos'] = np.copy(self.data.qpos) - state['qvel'] = np.copy(self.data.qvel) + state = { + 'time': np.copy(self.data.time), + 'qpos': np.copy(self.data.qpos), + 'qvel': np.copy(self.data.qvel), + } if self.model.na == 0: state['act'] = None else: diff --git a/setup.py b/setup.py index b4f435d4..b0ec3792 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ VERSION_FILE.write_text( data=re.sub( r"""__version__\s*=\s*('[^']+'|"[^"]+")""", - r"__version__ = '{}'".format(version.__version__), + f'__version__ = {version.__version__!r}', string=VERSION_CONTENT, ), encoding='UTF-8',