From 7633de7f51c7ea978507fb1701412692976c2469 Mon Sep 17 00:00:00 2001 From: tatp-yf Date: Mon, 16 Mar 2026 18:50:57 +0800 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20upgrade=20dev=20tooling=20?= =?UTF-8?q?=E2=80=94=20add=20pyright,=20update=20CI=20and=20pre-commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pyproject.toml: add pyright>=1.1.408 to dev deps; add [tool.pyright] config (basic mode, targeted excludes for mujoco-uni/mlx stubs); add ruff src=["unilab"] for correct first-party import detection; tighten coverage omit to */rsl_rl/* glob - Makefile: `make type` now runs mypy + pyright in sequence - .pre-commit-config.yaml: ruff v0.8.4 → v0.14.14; replace mirrors-mypy hook with RobertCraigie/pyright-python v1.1.408 - .github/workflows/ci.yml: add global UV_FROZEN=1; add paths-ignore for docs/md/LICENSE changes; upgrade setup-uv v5→v6; pass python-version matrix param to setup-uv; typecheck job now runs both mypy and pyright - CLAUDE.md / CONTRIBUTING.md: sync make type description and CI table Type fixes to make pyright pass (0 errors, 7 warnings): - base/base.py: add abstract state/init_state/step/close to ABEnv interface - tests/: update _DummyEnv and _TestEnvA stubs to implement new ABEnv methods - algos/torch/appo/runner.py: assert observation/action shape not None - algos/torch/offpolicy/worker.py: assert observation/action shape not None - algos/torch/appo/learner.py: pyright ignore on vendored resolve_optimizer - algos/torch/fast_sac/learner.py: pyright ignore on torch.amp private API - base/backend/motrix_backend.py: pyright ignore on possibly-unbound mtx - utils/offpolicy_logger.py: type log_dict as dict[str, Any]; add Any import - utils/onpolicy_logger.py: type log_dict as dict[str, Any] - utils/torch_utils.py: pyright ignore on torch.from_dlpack private API --- .github/workflows/ci.yml | 28 +++++++++++++++---- .pre-commit-config.yaml | 10 +++---- CLAUDE.md | 3 +- CONTRIBUTING.md | 10 ++++--- Makefile | 1 + benchmark/benchmark_postprocess.py | 1 - pyproject.toml | 24 ++++++++++++++-- scripts/train_appo.py | 1 - scripts/train_mlx_ppo.py | 1 - scripts/train_offpolicy.py | 1 - scripts/train_rsl_rl.py | 2 -- scripts/visualization_env.py | 1 - tests/algos/test_mlx_ppo.py | 1 - tests/base/test_registry.py | 14 +++++++++- tests/config/test_locomotion_params.py | 1 - tests/conftest.py | 11 +++++++- tests/ipc/test_async_runner.py | 1 - tests/ipc/test_replay_buffer.py | 1 - tests/ipc/test_shared_obs_stats.py | 1 - tests/ipc/test_shared_onpolicy_storage.py | 1 - tests/ipc/test_shared_weight_sync.py | 1 - unilab/algos/torch/appo/learner.py | 2 +- unilab/algos/torch/appo/runner.py | 2 ++ unilab/algos/torch/fast_sac/learner.py | 12 ++++---- unilab/algos/torch/offpolicy/worker.py | 2 ++ unilab/base/backend/motrix_backend.py | 4 +-- unilab/base/base.py | 20 ++++++++++++- .../manipulation/inhand_rot_allegro/base.py | 1 - .../inhand_rot_allegro/rotation_sac.py | 5 ++-- unilab/utils/offpolicy_logger.py | 5 ++-- unilab/utils/onpolicy_logger.py | 2 +- unilab/utils/torch_utils.py | 2 +- uv.lock | 26 +++++++++++++++++ 33 files changed, 143 insertions(+), 55 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d375d9f7c..325f3b29f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,16 +2,29 @@ name: CI on: push: - branches: [ main ] + branches: [main] + paths-ignore: + - "*.md" + - "CONTRIBUTING.md" + - "CLAUDE.md" + - "LICENSE" pull_request: - branches: [ main ] + branches: [main] + paths-ignore: + - "*.md" + - "CONTRIBUTING.md" + - "CLAUDE.md" + - "LICENSE" + +env: + UV_FROZEN: "1" jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Lint with ruff @@ -23,13 +36,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v6 with: enable-cache: true - name: Install dependencies run: uv sync --extra dev - - name: Type check + - name: mypy run: uv run mypy unilab + - name: pyright + run: uv run pyright test: strategy: @@ -39,9 +54,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@v6 with: enable-cache: true + python-version: ${{ matrix.python-version }} - name: Install dependencies (Linux) if: runner.os == 'Linux' run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c702c7551..cddace5e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.4 + rev: v0.14.14 hooks: - id: ruff args: [--fix] - id: ruff-format - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.0 + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.408 hooks: - - id: mypy - additional_dependencies: [types-all] - args: [--ignore-missing-imports] + - id: pyright diff --git a/CLAUDE.md b/CLAUDE.md index 07bf77462..b5b1cad7b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ uv sync --extra dev --extra cu124 ```bash make format # Format and lint code (ruff format + ruff check --fix) -make type # Type check with mypy +make type # Type check with mypy + pyright make check # make format && make type make test # Run all non-slow tests (default) make test-cov # Run non-slow tests with coverage report @@ -47,6 +47,7 @@ uv run ruff check --fix # Type check uv run mypy unilab +uv run pyright # Test (non-slow) uv run pytest -m "not slow" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eace8c3cc..3f864a0e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,13 +9,13 @@ ## 开发规范 - **Always use `uv run`**,不要直接使用 `python` -- 提交前必须通过 `make check`(ruff lint + mypy) +- 提交前必须通过 `make check`(ruff lint + mypy + pyright) ## 常用命令 ```bash make format # ruff format + ruff check --fix -make type # mypy unilab +make type # mypy unilab + pyright make check # format + type(提交前必跑) make test # 非 slow 单元测试 make test-cov # 非 slow 测试 + 覆盖率报告 @@ -94,12 +94,14 @@ Push / PR 到 `main` 时自动触发三个 job: | Job | 内容 | 失败即阻断 | |-----|------|-----------| | `lint` | `ruff check` + `ruff format --check` | ✅ | -| `typecheck` | `mypy unilab` | ✅ | +| `typecheck` | `mypy unilab` + `pyright` | ✅ | | `test` | `pytest -m "not slow" --cov` (matrix: ubuntu+macos × py3.10/3.11/3.12) | ✅ | +纯文档改动(`*.md`、`LICENSE`)不触发 CI。 + ## Pull Request 流程 -1. 本地运行 `make check` 确保 lint/mypy 通过 +1. 本地运行 `make check` 确保 lint/mypy/pyright 通过 2. 本地运行 `make test` 确保单元测试通过 3. 若改动了 IPC / Runner / Config,补充或更新对应测试 4. 提交 PR 到 `main` 分支,等待 CI 全绿 diff --git a/Makefile b/Makefile index cc011ed8b..563c1d19c 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ format: .PHONY: type type: uv run mypy unilab + uv run pyright .PHONY: check check: format type diff --git a/benchmark/benchmark_postprocess.py b/benchmark/benchmark_postprocess.py index 30fedcbd6..c0d47e949 100644 --- a/benchmark/benchmark_postprocess.py +++ b/benchmark/benchmark_postprocess.py @@ -14,7 +14,6 @@ import mlx.core as mx import numpy as np import torch - from unilab.base import registry try: diff --git a/pyproject.toml b/pyproject.toml index 690699ca0..22b8a550a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,14 +28,14 @@ dependencies = [ ] [project.optional-dependencies] -dev = ["pytest", "pytest-cov", "ruff", "mypy"] +dev = ["pytest", "pytest-cov", "ruff", "mypy", "pyright>=1.1.408"] cu124 = [ # Keep compatibility with documented Linux CUDA installation command. ] motrix = ["motrixsim==0.6.0"] [dependency-groups] -dev = ["pytest", "pytest-cov", "ruff", "mypy"] +dev = ["pytest", "pytest-cov", "ruff", "mypy", "pyright>=1.1.408"] [[tool.uv.index]] name = "test-pypi" @@ -51,6 +51,7 @@ packages = ["unilab"] [tool.ruff] line-length = 100 target-version = "py310" +src = ["unilab"] [tool.ruff.lint] select = ["E", "F", "I", "N", "W"] @@ -83,7 +84,24 @@ addopts = "--tb=short" [tool.coverage.run] source = ["unilab"] -omit = ["unilab/algos/torch/rsl_rl/*"] +omit = ["*/rsl_rl/*"] [tool.coverage.report] exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"] + +[tool.pyright] +pythonVersion = "3.10" +venvPath = "." +venv = ".venv" +include = ["unilab"] +exclude = [ + "unilab/algos/torch/rsl_rl/", # vendored third-party + "unilab/algos/mlx/", # MLX stubs incomplete, macOS-only + "unilab/algos/torch/common/ane_*", # Apple Neural Engine, macOS-only + "unilab/base/backend/", # mujoco-uni stubs mismatch; optional backends + "unilab/envs/", # mujoco-uni internal API, stubs mismatch + "unilab/utils/render_many.py", # direct mujoco C bindings + "unilab/utils/hardware_monitor.py", # optional pynvml/psutil deps +] +reportMissingImports = "warning" +reportMissingModuleSource = "none" diff --git a/scripts/train_appo.py b/scripts/train_appo.py index d6650c241..c7965bf3c 100644 --- a/scripts/train_appo.py +++ b/scripts/train_appo.py @@ -48,7 +48,6 @@ def play_appo(args, rl_cfg): import numpy as np from rsl_rl.utils import resolve_callable from tensordict import TensorDict - from unilab.base import registry from unilab.utils import render_many from unilab.utils.rsl_rl_compat import convert_config_v3_to_v4, is_rsl_rl_v4, is_rsl_rl_v5 diff --git a/scripts/train_mlx_ppo.py b/scripts/train_mlx_ppo.py index 20c341dea..99a8f5d64 100644 --- a/scripts/train_mlx_ppo.py +++ b/scripts/train_mlx_ppo.py @@ -159,7 +159,6 @@ def play_mlx_ppo(args, cfg, dtype, use_fp16, resolved_sim_backend, task_log_root """Play mode for MLX PPO.""" import mlx.core as mx import numpy as np - from unilab.base import registry play_model_dtype = mx.float32 if use_fp16 else dtype diff --git a/scripts/train_offpolicy.py b/scripts/train_offpolicy.py index 9aad6684c..eba69ac25 100644 --- a/scripts/train_offpolicy.py +++ b/scripts/train_offpolicy.py @@ -212,7 +212,6 @@ def play_offpolicy(algo_name: str, args, cfg) -> None: import mediapy as media import numpy as np import torch - from unilab.base import registry from unilab.utils import render_many from unilab.utils.algo_utils import build_actor diff --git a/scripts/train_rsl_rl.py b/scripts/train_rsl_rl.py index f380ca86b..c5caf6e53 100644 --- a/scripts/train_rsl_rl.py +++ b/scripts/train_rsl_rl.py @@ -201,7 +201,6 @@ def step(self, actions): def play_rsl_rl(args, cfg, device): """Play mode for RSL-RL.""" import torch - from unilab.base import registry env = registry.make(args.task, num_envs=args.play_env_num, sim_backend=args.sim_backend) @@ -292,7 +291,6 @@ def play_rsl_rl(args, cfg, device): else: # MuJoCo backend: render to video import mediapy as media - from unilab.utils import render_many output_video = Path(load_path_dir) / "play_video.mp4" diff --git a/scripts/visualization_env.py b/scripts/visualization_env.py index e0817e330..ffe44f9ba 100644 --- a/scripts/visualization_env.py +++ b/scripts/visualization_env.py @@ -7,7 +7,6 @@ import time import numpy as np - from unilab.base import registry parser = argparse.ArgumentParser(description="Visualize robot tasks") diff --git a/tests/algos/test_mlx_ppo.py b/tests/algos/test_mlx_ppo.py index fac85415f..7f2dbb681 100644 --- a/tests/algos/test_mlx_ppo.py +++ b/tests/algos/test_mlx_ppo.py @@ -20,7 +20,6 @@ import mlx.core as mx import numpy as np - from unilab.algos.mlx.common import RolloutBuffer from unilab.algos.mlx.ppo import MLPActorCritic, PPOConfig, PPOTrainer diff --git a/tests/base/test_registry.py b/tests/base/test_registry.py index 865bb9743..0f56a8f9f 100644 --- a/tests/base/test_registry.py +++ b/tests/base/test_registry.py @@ -8,7 +8,6 @@ import gymnasium as gym import numpy as np import pytest - import unilab.base.registry as registry_mod from unilab.base.base import ABEnv, EnvCfg @@ -50,6 +49,19 @@ def observation_space(self): def action_space(self): return gym.spaces.Box(low=-1.0, high=1.0, shape=(2,), dtype=np.float32) + @property + def state(self): + return None + + def init_state(self): + return None + + def step(self, actions): + return None + + def close(self): + pass + # Register once at module level (idempotent guard) if not registry_mod.contains(_TEST_ENV_A): diff --git a/tests/config/test_locomotion_params.py b/tests/config/test_locomotion_params.py index 4bd7abe2a..c22f435db 100644 --- a/tests/config/test_locomotion_params.py +++ b/tests/config/test_locomotion_params.py @@ -5,7 +5,6 @@ from copy import deepcopy import pytest - from unilab.config.locomotion_params import appo_config, offpolicy_config, ppo_config # --------------------------------------------------------------------------- diff --git a/tests/conftest.py b/tests/conftest.py index 964415ff4..9f03a325c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,6 @@ import numpy as np import pytest import torch - from unilab.base import registry from unilab.base.base import ABEnv, EnvCfg from unilab.ipc.replay_buffer import ReplayBuffer @@ -61,6 +60,16 @@ def action_space(self) -> gym.Space: def close(self) -> None: pass + @property + def state(self): + return None + + def init_state(self): + return None + + def step(self, actions: np.ndarray): + return None + def _register_dummy_env() -> None: if not registry.contains(_DUMMY_ENV_NAME): diff --git a/tests/ipc/test_async_runner.py b/tests/ipc/test_async_runner.py index 838181ee7..ba82dbbe3 100644 --- a/tests/ipc/test_async_runner.py +++ b/tests/ipc/test_async_runner.py @@ -8,7 +8,6 @@ from unittest.mock import MagicMock import pytest - from unilab.ipc.async_runner import AsyncRunner _SPAWN_CTX = mp.get_context("spawn") diff --git a/tests/ipc/test_replay_buffer.py b/tests/ipc/test_replay_buffer.py index 8aacf1164..dabd0e5ff 100644 --- a/tests/ipc/test_replay_buffer.py +++ b/tests/ipc/test_replay_buffer.py @@ -7,7 +7,6 @@ import numpy as np import pytest import torch - from unilab.ipc.replay_buffer import ReplayBuffer _SPAWN_CTX = mp.get_context("spawn") diff --git a/tests/ipc/test_shared_obs_stats.py b/tests/ipc/test_shared_obs_stats.py index 64824cd73..5e86b6d46 100644 --- a/tests/ipc/test_shared_obs_stats.py +++ b/tests/ipc/test_shared_obs_stats.py @@ -5,7 +5,6 @@ import queue import numpy as np - from unilab.ipc.shared_obs_stats import SharedObsNormStats diff --git a/tests/ipc/test_shared_onpolicy_storage.py b/tests/ipc/test_shared_onpolicy_storage.py index 03f57a83e..b909ebcb5 100644 --- a/tests/ipc/test_shared_onpolicy_storage.py +++ b/tests/ipc/test_shared_onpolicy_storage.py @@ -5,7 +5,6 @@ import numpy as np import pytest import torch - from unilab.ipc.shared_onpolicy_storage import SharedOnPolicyStorage _NUM_ENVS = 4 diff --git a/tests/ipc/test_shared_weight_sync.py b/tests/ipc/test_shared_weight_sync.py index 73141da89..f619f0305 100644 --- a/tests/ipc/test_shared_weight_sync.py +++ b/tests/ipc/test_shared_weight_sync.py @@ -8,7 +8,6 @@ import numpy as np import pytest import torch - from unilab.ipc.weight_sync import SharedWeightSync _SPAWN_CTX = mp.get_context("spawn") diff --git a/unilab/algos/torch/appo/learner.py b/unilab/algos/torch/appo/learner.py index 79e90276e..cadb4e6a2 100644 --- a/unilab/algos/torch/appo/learner.py +++ b/unilab/algos/torch/appo/learner.py @@ -152,7 +152,7 @@ def __init__( self._update_counter = 0 # Optimizer - self.optimizer = resolve_optimizer(optimizer)( + self.optimizer = resolve_optimizer(optimizer)( # pyright: ignore[reportCallIssue] chain(self.actor.parameters(), self.critic.parameters()), lr=learning_rate ) diff --git a/unilab/algos/torch/appo/runner.py b/unilab/algos/torch/appo/runner.py index 433252486..af04213ec 100644 --- a/unilab/algos/torch/appo/runner.py +++ b/unilab/algos/torch/appo/runner.py @@ -86,6 +86,8 @@ def _detect_dims(self): ensure_registries() env = registry.make(self.env_name, num_envs=1, sim_backend="mujoco") + assert env.observation_space.shape is not None + assert env.action_space.shape is not None obs_dim = env.observation_space.shape[0] action_dim = env.action_space.shape[0] env.close() diff --git a/unilab/algos/torch/fast_sac/learner.py b/unilab/algos/torch/fast_sac/learner.py index d5b94dcc2..b3b1e652d 100644 --- a/unilab/algos/torch/fast_sac/learner.py +++ b/unilab/algos/torch/fast_sac/learner.py @@ -440,7 +440,7 @@ def __init__( self.update_count = 0 # AMP scaler for mixed precision - self.scaler = torch.amp.GradScaler("cuda") if self.use_amp else None + self.scaler = torch.amp.GradScaler("cuda") if self.use_amp else None # pyright: ignore[reportPrivateImportUsage] # Symmetry augmentation (G1 only) self.use_symmetry = ( @@ -480,20 +480,20 @@ def update_critic(self, batch: Dict[str, torch.Tensor]) -> Dict[str, float]: discount = torch.full_like(dones, self.gamma) with torch.no_grad(): - with torch.amp.autocast("cuda", enabled=self.use_amp): + with torch.amp.autocast("cuda", enabled=self.use_amp): # pyright: ignore[reportPrivateImportUsage] next_actions, next_log_probs, _ = self.actor.get_actions_and_log_probs(next_obs) adjusted_rewards = ( rewards - discount * bootstrap * self.log_alpha.exp() * next_log_probs ) - with torch.amp.autocast("cuda", enabled=self.use_amp): + with torch.amp.autocast("cuda", enabled=self.use_amp): # pyright: ignore[reportPrivateImportUsage] target_distributions = self.qnet_target.projection( next_obs, next_actions, adjusted_rewards, bootstrap, discount ) target_values = self.qnet_target.get_value(target_distributions) # Critic loss: cross-entropy with projected distributions - with torch.amp.autocast("cuda", enabled=self.use_amp): + with torch.amp.autocast("cuda", enabled=self.use_amp): # pyright: ignore[reportPrivateImportUsage] q_outputs = self.qnet(obs, actions) critic_log_probs = F.log_softmax(q_outputs, dim=-1).clamp(min=-30.0) critic_losses = -torch.sum(target_distributions * critic_log_probs, dim=-1) @@ -555,14 +555,14 @@ def update_actor(self, batch: Dict[str, torch.Tensor]) -> Dict[str, float]: if self.use_symmetry: obs = torch.cat([obs, self.symmetry.mirror_obs(obs)], dim=0) - with torch.amp.autocast("cuda", enabled=self.use_amp): + with torch.amp.autocast("cuda", enabled=self.use_amp): # pyright: ignore[reportPrivateImportUsage] actions, log_probs, log_std = self.actor.get_actions_and_log_probs(obs) with torch.no_grad(): action_std = log_std.exp().mean() policy_entropy = -log_probs.mean() - with torch.amp.autocast("cuda", enabled=self.use_amp): + with torch.amp.autocast("cuda", enabled=self.use_amp): # pyright: ignore[reportPrivateImportUsage] q_outputs = self.qnet(obs, actions) q_probs = F.softmax(q_outputs, dim=-1) q_values = self.qnet.get_value(q_probs) diff --git a/unilab/algos/torch/offpolicy/worker.py b/unilab/algos/torch/offpolicy/worker.py index 1e758157b..5a309c285 100644 --- a/unilab/algos/torch/offpolicy/worker.py +++ b/unilab/algos/torch/offpolicy/worker.py @@ -108,6 +108,8 @@ def _run_collector( ) # Build actor (always on CPU for env interaction) + assert env.observation_space.shape is not None + assert env.action_space.shape is not None obs_dim = env.observation_space.shape[0] action_dim = env.action_space.shape[0] actor = build_actor( diff --git a/unilab/base/backend/motrix_backend.py b/unilab/base/backend/motrix_backend.py index 929387840..e57a7a7d4 100644 --- a/unilab/base/backend/motrix_backend.py +++ b/unilab/base/backend/motrix_backend.py @@ -25,12 +25,12 @@ def __init__( if not MOTRIX_AVAILABLE: raise ImportError("motrixsim not available") - self._model = mtx.load_model(model_file) + self._model = mtx.load_model(model_file) # pyright: ignore[reportPossiblyUnbound] self._model.options.timestep = sim_dt self._num_envs = num_envs self._np_dtype = np_dtype - self._data = mtx.SceneData(self._model, batch=[num_envs]) + self._data = mtx.SceneData(self._model, batch=[num_envs]) # pyright: ignore[reportPossiblyUnbound] self._body = self._model.get_body(body_name) self._body_link = self._model.get_link(body_name) self._render_app: "RenderApp | None" = None diff --git a/unilab/base/base.py b/unilab/base/base.py index 760aa88fe..59ad799d7 100644 --- a/unilab/base/base.py +++ b/unilab/base/base.py @@ -1,8 +1,9 @@ import abc from dataclasses import dataclass -from typing import Optional +from typing import Any, Optional import gymnasium as gym +import numpy as np @dataclass @@ -66,3 +67,20 @@ def observation_space(self) -> gym.Space: @abc.abstractmethod def action_space(self) -> gym.Space: """Action space""" + + @property + @abc.abstractmethod + def state(self) -> Any: + """Current environment state (None before first reset)""" + + @abc.abstractmethod + def init_state(self) -> Any: + """Initialize environment and return initial state""" + + @abc.abstractmethod + def step(self, actions: np.ndarray) -> Any: + """Step the environment with given actions, return new state""" + + @abc.abstractmethod + def close(self) -> None: + """Clean up environment resources""" diff --git a/unilab/envs/manipulation/inhand_rot_allegro/base.py b/unilab/envs/manipulation/inhand_rot_allegro/base.py index 21283a3c7..a6eae5a5b 100644 --- a/unilab/envs/manipulation/inhand_rot_allegro/base.py +++ b/unilab/envs/manipulation/inhand_rot_allegro/base.py @@ -5,7 +5,6 @@ import gymnasium as gym import mujoco import numpy as np - from unilab.base.backend import SimBackend from unilab.base.base import EnvCfg from unilab.base.dtype_config import get_global_dtype diff --git a/unilab/envs/manipulation/inhand_rot_allegro/rotation_sac.py b/unilab/envs/manipulation/inhand_rot_allegro/rotation_sac.py index a8d5808d1..aee6dfb46 100644 --- a/unilab/envs/manipulation/inhand_rot_allegro/rotation_sac.py +++ b/unilab/envs/manipulation/inhand_rot_allegro/rotation_sac.py @@ -52,11 +52,10 @@ def _quat_to_axis_angle(q: np.ndarray) -> np.ndarray: return np.asarray(xyz / sin_half_over_angle) # (N, 3) -from unilab.envs.backend import create_backend -from unilab.envs.np_env import NpEnvState - from unilab.base import registry +from unilab.envs.backend import create_backend from unilab.envs.manipulation.inhand_rot_allegro.base import AllegroBaseCfg, AllegroBaseMjEnv +from unilab.envs.np_env import NpEnvState # ─────────────────────────── Configuration ──────────────────────────── diff --git a/unilab/utils/offpolicy_logger.py b/unilab/utils/offpolicy_logger.py index a1e66ebd6..b0d045a64 100644 --- a/unilab/utils/offpolicy_logger.py +++ b/unilab/utils/offpolicy_logger.py @@ -33,6 +33,7 @@ import time from collections import deque +from typing import Any from rich import box from rich.console import Console, Group @@ -373,9 +374,7 @@ def _backend_log_step( if self._wandb_run: import wandb - log_dict = {"iteration": iteration} - - # train/ + log_dict: dict[str, Any] = {"iteration": iteration} if metrics: for k, v in metrics.items(): log_dict[f"train/{k}"] = v diff --git a/unilab/utils/onpolicy_logger.py b/unilab/utils/onpolicy_logger.py index d11927f09..72f1288c6 100644 --- a/unilab/utils/onpolicy_logger.py +++ b/unilab/utils/onpolicy_logger.py @@ -202,7 +202,7 @@ def _backend_log_step( if self._wandb_run: import wandb - log_dict = {"iteration": iteration} + log_dict: dict[str, Any] = {"iteration": iteration} if metrics: for k, v in metrics.items(): log_dict[f"train/{k}"] = v diff --git a/unilab/utils/torch_utils.py b/unilab/utils/torch_utils.py index e68c77a6c..8e45d5025 100644 --- a/unilab/utils/torch_utils.py +++ b/unilab/utils/torch_utils.py @@ -17,7 +17,7 @@ def to_torch(x, device: str | torch.device) -> torch.Tensor: return torch.from_numpy(x).to(device) try: if hasattr(x, "__dlpack__"): - return torch.from_dlpack(x).to(device) + return torch.from_dlpack(x).to(device) # pyright: ignore[reportPrivateImportUsage] except Exception: pass arr = np.asarray(x, dtype=np.float32) diff --git a/uv.lock b/uv.lock index 711c5d86b..270a5dc71 100644 --- a/uv.lock +++ b/uv.lock @@ -1068,6 +1068,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "numpy" version = "2.2.6" @@ -1574,6 +1583,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -1993,6 +2015,7 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "mypy" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, @@ -2004,6 +2027,7 @@ motrix = [ [package.dev-dependencies] dev = [ { name = "mypy" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, @@ -2022,6 +2046,7 @@ requires-dist = [ { name = "mypy", marker = "extra == 'dev'" }, { name = "numpy" }, { name = "packaging" }, + { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.408" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "pytest-cov", marker = "extra == 'dev'" }, { name = "rich" }, @@ -2038,6 +2063,7 @@ provides-extras = ["dev", "cu124", "motrix"] [package.metadata.requires-dev] dev = [ { name = "mypy" }, + { name = "pyright", specifier = ">=1.1.408" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, From 9307216100db2d9d7d1d4aeefdf41e5e62100825 Mon Sep 17 00:00:00 2001 From: tatp-yf Date: Mon, 16 Mar 2026 18:53:51 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20support=20Python=203.13=20=E2=80=94?= =?UTF-8?q?=20bump=20requires-python=20and=20CI=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 325f3b29f..79888e73e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f864a0e7..4635a956a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,7 +95,7 @@ Push / PR 到 `main` 时自动触发三个 job: |-----|------|-----------| | `lint` | `ruff check` + `ruff format --check` | ✅ | | `typecheck` | `mypy unilab` + `pyright` | ✅ | -| `test` | `pytest -m "not slow" --cov` (matrix: ubuntu+macos × py3.10/3.11/3.12) | ✅ | +| `test` | `pytest -m "not slow" --cov` (matrix: ubuntu+macos × py3.10/3.11/3.12/3.13) | ✅ | 纯文档改动(`*.md`、`LICENSE`)不触发 CI。 diff --git a/pyproject.toml b/pyproject.toml index 22b8a550a..3ce0868c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "unilab" version = "0.1.0" description = "Universal Lab for Robot Learning" readme = "README.md" -requires-python = ">=3.10,<3.13" +requires-python = ">=3.10,<3.14" dependencies = [ "numpy", "torch",