Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,25 +36,28 @@ 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:
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
- 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: |
Expand Down
10 changes: 4 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
12 changes: 7 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 测试 + 覆盖率报告
Expand Down Expand Up @@ -94,12 +94,14 @@ Push / PR 到 `main` 时自动触发三个 job:
| Job | 内容 | 失败即阻断 |
|-----|------|-----------|
| `lint` | `ruff check` + `ruff format --check` | ✅ |
| `typecheck` | `mypy unilab` | ✅ |
| `test` | `pytest -m "not slow" --cov` (matrix: ubuntu+macos × py3.10/3.11/3.12) | ✅ |
| `typecheck` | `mypy unilab` + `pyright` | ✅ |
| `test` | `pytest -m "not slow" --cov` (matrix: ubuntu+macos × py3.10/3.11/3.12/3.13) | ✅ |

纯文档改动(`*.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 全绿
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ format:
.PHONY: type
type:
uv run mypy unilab
uv run pyright

.PHONY: check
check: format type
Expand Down
1 change: 0 additions & 1 deletion benchmark/benchmark_postprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import mlx.core as mx
import numpy as np
import torch

from unilab.base import registry

try:
Expand Down
26 changes: 22 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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"]
Expand Down Expand Up @@ -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"
1 change: 0 additions & 1 deletion scripts/train_appo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion scripts/train_mlx_ppo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion scripts/train_offpolicy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions scripts/train_rsl_rl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
1 change: 0 additions & 1 deletion scripts/visualization_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import time

import numpy as np

from unilab.base import registry

parser = argparse.ArgumentParser(description="Visualize robot tasks")
Expand Down
1 change: 0 additions & 1 deletion tests/algos/test_mlx_ppo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 13 additions & 1 deletion tests/base/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
1 change: 0 additions & 1 deletion tests/config/test_locomotion_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from copy import deepcopy

import pytest

from unilab.config.locomotion_params import appo_config, offpolicy_config, ppo_config

# ---------------------------------------------------------------------------
Expand Down
11 changes: 10 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
1 change: 0 additions & 1 deletion tests/ipc/test_async_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from unittest.mock import MagicMock

import pytest

from unilab.ipc.async_runner import AsyncRunner

_SPAWN_CTX = mp.get_context("spawn")
Expand Down
1 change: 0 additions & 1 deletion tests/ipc/test_replay_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 0 additions & 1 deletion tests/ipc/test_shared_obs_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import queue

import numpy as np

from unilab.ipc.shared_obs_stats import SharedObsNormStats


Expand Down
1 change: 0 additions & 1 deletion tests/ipc/test_shared_onpolicy_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import numpy as np
import pytest
import torch

from unilab.ipc.shared_onpolicy_storage import SharedOnPolicyStorage

_NUM_ENVS = 4
Expand Down
1 change: 0 additions & 1 deletion tests/ipc/test_shared_weight_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion unilab/algos/torch/appo/learner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
2 changes: 2 additions & 0 deletions unilab/algos/torch/appo/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading