diff --git a/docs/source/lerobot-dataset-v3.mdx b/docs/source/lerobot-dataset-v3.mdx index 235a355bd..aadc6f4ab 100644 --- a/docs/source/lerobot-dataset-v3.mdx +++ b/docs/source/lerobot-dataset-v3.mdx @@ -168,7 +168,7 @@ Use the `image_transforms` parameter when loading a dataset for training: ```python from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.datasets.transforms import ImageTransforms, ImageTransformsConfig, ImageTransformConfig +from lerobot.transforms import ImageTransforms, ImageTransformsConfig, ImageTransformConfig # Option 1: Use default transform configuration (disabled by default) transforms_config = ImageTransformsConfig( diff --git a/examples/dataset/use_dataset_image_transforms.py b/examples/dataset/use_dataset_image_transforms.py index c28f2ef0c..d21beee0c 100644 --- a/examples/dataset/use_dataset_image_transforms.py +++ b/examples/dataset/use_dataset_image_transforms.py @@ -27,7 +27,7 @@ from torchvision.transforms import v2 from torchvision.transforms.functional import to_pil_image from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.datasets.transforms import ImageTransformConfig, ImageTransforms, ImageTransformsConfig +from lerobot.transforms import ImageTransformConfig, ImageTransforms, ImageTransformsConfig def save_image(tensor, filename): diff --git a/pyproject.toml b/pyproject.toml index 79409a200..e44b3cbc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,44 +58,64 @@ classifiers = [ keywords = ["lerobot", "huggingface", "robotics", "machine learning", "artificial intelligence"] dependencies = [ - - # Hugging Face dependencies - "datasets>=4.0.0,<5.0.0", - "diffusers>=0.27.2,<0.36.0", - "huggingface-hub>=1.0.0,<2.0.0", - "accelerate>=1.10.0,<2.0.0", - - # Core dependencies - "numpy>=2.0.0,<2.3.0", # NOTE: Explicitly listing numpy helps the resolver converge faster. Upper bound imposed by opencv-python-headless. - "setuptools>=71.0.0,<81.0.0", - "cmake>=3.29.0.1,<4.2.0", - "packaging>=24.2,<26.0", - + # Core ML "torch>=2.7,<2.11.0", - "torchcodec>=0.3.0,<0.11.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", # NOTE: Windows support starts at version 0.7 (needs torch==2.8), ffmpeg>=8 support starts at version 0.8.1 (needs torch==2.9), system-wide ffmpeg support starts at version 0.10 (needs torch==2.10). "torchvision>=0.22.0,<0.26.0", - - "einops>=0.8.0,<0.9.0", + "numpy>=2.0.0,<2.3.0", # NOTE: Explicitly listing numpy helps the resolver converge faster. Upper bound imposed by opencv-python-headless. "opencv-python-headless>=4.9.0,<4.14.0", - "av>=15.0.0,<16.0.0", - "jsonlines>=4.0.0,<5.0.0", - "pynput>=1.7.8,<1.9.0", - "pyserial>=3.5,<4.0", + "einops>=0.8.0,<0.9.0", - "wandb>=0.24.0,<0.25.0", + # Config & Hub "draccus==0.10.0", # TODO: Relax version constraint - "gymnasium>=1.1.1,<2.0.0", - "rerun-sdk>=0.24.0,<0.27.0", + "huggingface-hub>=1.0.0,<2.0.0", - # Support dependencies - "deepdiff>=7.0.1,<9.0.0", - "imageio[ffmpeg]>=2.34.0,<3.0.0", + # Environments + "gymnasium>=1.1.1,<2.0.0", + + # Lightweight utilities + "packaging>=24.2,<26.0", "termcolor>=2.4.0,<4.0.0", ] # Optional dependencies [project.optional-dependencies] +# ── Feature-scoped extras ────────────────────────────────── +dataset = [ + "datasets>=4.0.0,<5.0.0", + "av>=15.0.0,<16.0.0", + "torchcodec>=0.3.0,<0.11.0; sys_platform != 'win32' and (sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l')) and (sys_platform != 'darwin' or platform_machine != 'x86_64')", # NOTE: Windows support starts at version 0.7 (needs torch==2.8), ffmpeg>=8 support starts at version 0.8.1 (needs torch==2.9), system-wide ffmpeg support starts at version 0.10 (needs torch==2.10). + "jsonlines>=4.0.0,<5.0.0", +] +train = [ + "lerobot[dataset]", + "accelerate>=1.10.0,<2.0.0", + "wandb>=0.24.0,<0.25.0", + "diffusers>=0.27.2,<0.36.0", +] +hardware = [ + "pynput>=1.7.8,<1.9.0", + "pyserial>=3.5,<4.0", + "deepdiff>=7.0.1,<9.0.0", +] +viz = [ + "rerun-sdk>=0.24.0,<0.27.0", +] +build = [ + "cmake>=3.29.0.1,<4.2.0", + "setuptools>=71.0.0,<81.0.0", +] + +# ── User-facing composite extras (map to CLI scripts) ───── +# lerobot-record, lerobot-replay, lerobot-calibrate, lerobot-teleoperate, etc. +robot = ["lerobot[dataset]", "lerobot[hardware]", "lerobot[viz]"] +# lerobot-eval +evaluation = ["av>=15.0.0,<16.0.0"] +# lerobot-train +training = ["lerobot[train]"] +# lerobot-dataset-viz, lerobot-imgtransform-viz +dataset_viz = ["lerobot[dataset]", "lerobot[viz]"] + # Common pygame-dep = ["pygame>=2.5.1,<2.7.0"] placo-dep = ["placo>=0.9.6,<0.9.17"] @@ -166,19 +186,25 @@ async = ["lerobot[grpcio-dep]", "lerobot[matplotlib-dep]"] peft = ["lerobot[transformers-dep]", "lerobot[peft-dep]"] # Development -dev = ["pre-commit>=3.7.0,<5.0.0", "debugpy>=1.8.1,<1.9.0", "lerobot[grpcio-dep]", "grpcio-tools==1.73.1", "mypy>=1.19.1"] -test = ["pytest>=8.1.0,<9.0.0", "pytest-timeout>=2.4.0,<3.0.0", "pytest-cov>=5.0.0,<8.0.0", "mock-serial>=0.0.1,<0.1.0 ; sys_platform != 'win32'"] +dev = ["lerobot[dataset]", "lerobot[train]", "lerobot[hardware]", "lerobot[viz]", "pre-commit>=3.7.0,<5.0.0", "debugpy>=1.8.1,<1.9.0", "lerobot[grpcio-dep]", "grpcio-tools==1.73.1", "mypy>=1.19.1"] +test = ["lerobot[dataset]", "lerobot[train]", "lerobot[hardware]", "lerobot[viz]", "pytest>=8.1.0,<9.0.0", "pytest-timeout>=2.4.0,<3.0.0", "pytest-cov>=5.0.0,<8.0.0", "mock-serial>=0.0.1,<0.1.0 ; sys_platform != 'win32'"] video_benchmark = ["scikit-image>=0.23.2,<0.26.0", "pandas>=2.2.2,<2.4.0"] # Simulation # NOTE: Explicitly listing scipy helps flatten the dependecy tree. -aloha = ["gym-aloha>=0.1.2,<0.2.0", "lerobot[scipy-dep]"] -pusht = ["gym-pusht>=0.1.5,<0.2.0", "pymunk>=6.6.0,<7.0.0"] # TODO: Fix pymunk version in gym-pusht instead -libero = ["lerobot[transformers-dep]", "hf-libero>=0.1.3,<0.2.0; sys_platform == 'linux'", "lerobot[scipy-dep]"] -metaworld = ["metaworld==3.0.0", "lerobot[scipy-dep]"] +aloha = ["lerobot[dataset]", "gym-aloha>=0.1.2,<0.2.0", "lerobot[scipy-dep]"] +pusht = ["lerobot[dataset]", "gym-pusht>=0.1.5,<0.2.0", "pymunk>=6.6.0,<7.0.0"] # TODO: Fix pymunk version in gym-pusht instead +libero = ["lerobot[dataset]", "lerobot[transformers-dep]", "hf-libero>=0.1.3,<0.2.0; sys_platform == 'linux'", "lerobot[scipy-dep]"] +metaworld = ["lerobot[dataset]", "metaworld==3.0.0", "lerobot[scipy-dep]"] # All all = [ + # Feature-scoped extras + "lerobot[dataset]", + "lerobot[train]", + "lerobot[hardware]", + "lerobot[viz]", + "lerobot[build]", # NOTE(resolver hint): scipy is pulled in transitively via lerobot[scipy-dep] through # multiple extras (aloha, metaworld, pi, wallx, phone). Listing it explicitly # helps pip's resolver converge by constraining scipy early, before it encounters @@ -267,7 +293,8 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401", "F403"] +"__init__.py" = ["F401", "F403", "E402"] +"src/lerobot/scripts/*" = ["E402"] # require_package gates before imports "src/lerobot/policies/wall_x/**" = ["N801", "N812", "SIM102", "SIM108", "SIM210", "SIM211", "B006", "B007", "SIM118"] # Supprese these as they are coming from original Qwen2_5_vl code TODO(pepijn): refactor original [tool.ruff.lint.isort] diff --git a/src/lerobot/configs/default.py b/src/lerobot/configs/default.py index d6ad665bf..b05e96fde 100644 --- a/src/lerobot/configs/default.py +++ b/src/lerobot/configs/default.py @@ -16,8 +16,8 @@ from dataclasses import dataclass, field -from lerobot.datasets.transforms import ImageTransformsConfig -from lerobot.datasets.video_utils import get_safe_default_codec +from lerobot.transforms import ImageTransformsConfig +from lerobot.utils.import_utils import get_safe_default_codec @dataclass diff --git a/src/lerobot/datasets/__init__.py b/src/lerobot/datasets/__init__.py index 42c4ab810..d1522eda2 100644 --- a/src/lerobot/datasets/__init__.py +++ b/src/lerobot/datasets/__init__.py @@ -15,17 +15,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +from lerobot.utils.import_utils import require_package + +require_package("datasets", extra="dataset") + from lerobot.datasets.dataset_metadata import LeRobotDatasetMetadata from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.multi_dataset import MultiLeRobotDataset from lerobot.datasets.sampler import EpisodeAwareSampler from lerobot.datasets.streaming_dataset import StreamingLeRobotDataset -from lerobot.datasets.transforms import ImageTransforms, ImageTransformsConfig __all__ = [ "EpisodeAwareSampler", - "ImageTransforms", - "ImageTransformsConfig", "LeRobotDataset", "LeRobotDatasetMetadata", "MultiLeRobotDataset", diff --git a/src/lerobot/datasets/dataset_tools.py b/src/lerobot/datasets/dataset_tools.py index 16bf24822..fa91ef412 100644 --- a/src/lerobot/datasets/dataset_tools.py +++ b/src/lerobot/datasets/dataset_tools.py @@ -829,7 +829,7 @@ def _copy_and_reindex_episodes_metadata( data_metadata: Dict mapping new episode index to its data file metadata video_metadata: Optional dict mapping new episode index to its video metadata """ - from lerobot.datasets.utils import flatten_dict + from lerobot.utils.utils import flatten_dict if src_dataset.meta.episodes is None: src_dataset.meta.episodes = load_episodes(src_dataset.meta.root) diff --git a/src/lerobot/datasets/factory.py b/src/lerobot/datasets/factory.py index 76ece8961..d2b3090c2 100644 --- a/src/lerobot/datasets/factory.py +++ b/src/lerobot/datasets/factory.py @@ -24,7 +24,7 @@ from lerobot.datasets.dataset_metadata import LeRobotDatasetMetadata from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.multi_dataset import MultiLeRobotDataset from lerobot.datasets.streaming_dataset import StreamingLeRobotDataset -from lerobot.datasets.transforms import ImageTransforms +from lerobot.transforms import ImageTransforms from lerobot.utils.constants import ACTION, OBS_PREFIX, REWARD IMAGENET_STATS = { diff --git a/src/lerobot/datasets/io_utils.py b/src/lerobot/datasets/io_utils.py index cee6cfba8..446d6ca34 100644 --- a/src/lerobot/datasets/io_utils.py +++ b/src/lerobot/datasets/io_utils.py @@ -13,7 +13,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import json from pathlib import Path from typing import Any @@ -41,6 +40,7 @@ from lerobot.datasets.utils import ( serialize_dict, unflatten_dict, ) +from lerobot.utils.io_utils import load_json, write_json from lerobot.utils.utils import SuppressProgressBars @@ -116,33 +116,6 @@ def embed_images(dataset: datasets.Dataset) -> datasets.Dataset: return dataset -def load_json(fpath: Path) -> Any: - """Load data from a JSON file. - - Args: - fpath (Path): Path to the JSON file. - - Returns: - Any: The data loaded from the JSON file. - """ - with open(fpath) as f: - return json.load(f) - - -def write_json(data: dict, fpath: Path) -> None: - """Write data to a JSON file. - - Creates parent directories if they don't exist. - - Args: - data (dict): The dictionary to write. - fpath (Path): The path to the output JSON file. - """ - fpath.parent.mkdir(exist_ok=True, parents=True) - with open(fpath, "w") as f: - json.dump(data, f, indent=4, ensure_ascii=False) - - def write_info(info: dict, local_dir: Path) -> None: write_json(info, local_dir / INFO_PATH) diff --git a/src/lerobot/datasets/utils.py b/src/lerobot/datasets/utils.py index 36e7934ed..380d07955 100644 --- a/src/lerobot/datasets/utils.py +++ b/src/lerobot/datasets/utils.py @@ -28,6 +28,8 @@ import torch from huggingface_hub import DatasetCard, DatasetCardData, HfApi from huggingface_hub.errors import RevisionNotFoundError +from lerobot.utils.utils import flatten_dict, unflatten_dict + V30_MESSAGE = """ The dataset you requested ({repo_id}) is in {version} format. @@ -123,59 +125,6 @@ def update_chunk_file_indices(chunk_idx: int, file_idx: int, chunks_size: int) - return chunk_idx, file_idx -def flatten_dict(d: dict, parent_key: str = "", sep: str = "/") -> dict: - """Flatten a nested dictionary by joining keys with a separator. - - Example: - >>> dct = {"a": {"b": 1, "c": {"d": 2}}, "e": 3} - >>> print(flatten_dict(dct)) - {'a/b': 1, 'a/c/d': 2, 'e': 3} - - Args: - d (dict): The dictionary to flatten. - parent_key (str): The base key to prepend to the keys in this level. - sep (str): The separator to use between keys. - - Returns: - dict: A flattened dictionary. - """ - items = [] - for k, v in d.items(): - new_key = f"{parent_key}{sep}{k}" if parent_key else k - if isinstance(v, dict): - items.extend(flatten_dict(v, new_key, sep=sep).items()) - else: - items.append((new_key, v)) - return dict(items) - - -def unflatten_dict(d: dict, sep: str = "/") -> dict: - """Unflatten a dictionary with delimited keys into a nested dictionary. - - Example: - >>> flat_dct = {"a/b": 1, "a/c/d": 2, "e": 3} - >>> print(unflatten_dict(flat_dct)) - {'a': {'b': 1, 'c': {'d': 2}}, 'e': 3} - - Args: - d (dict): A dictionary with flattened keys. - sep (str): The separator used in the keys. - - Returns: - dict: A nested dictionary. - """ - outdict = {} - for key, value in d.items(): - parts = key.split(sep) - d = outdict - for part in parts[:-1]: - if part not in d: - d[part] = {} - d = d[part] - d[parts[-1]] = value - return outdict - - def serialize_dict(stats: dict[str, torch.Tensor | np.ndarray | dict]) -> dict: """Serialize a dictionary containing tensors or numpy arrays to be JSON-compatible. diff --git a/src/lerobot/datasets/video_utils.py b/src/lerobot/datasets/video_utils.py index 59c8c7d3e..141c2eb64 100644 --- a/src/lerobot/datasets/video_utils.py +++ b/src/lerobot/datasets/video_utils.py @@ -37,6 +37,8 @@ import torchvision from datasets.features.features import register_feature from PIL import Image +from lerobot.utils.import_utils import get_safe_default_codec + logger = logging.getLogger(__name__) # List of hardware encoders to probe for auto-selection. Availability depends on the platform and FFmpeg build. @@ -116,16 +118,6 @@ def resolve_vcodec(vcodec: str) -> str: return "libsvtav1" -def get_safe_default_codec(): - if importlib.util.find_spec("torchcodec"): - return "torchcodec" - else: - logger.warning( - "'torchcodec' is not available in your platform, falling back to 'pyav' as a default decoder" - ) - return "pyav" - - def decode_video_frames( video_path: Path | str, timestamps: list[float], diff --git a/src/lerobot/optim/optimizers.py b/src/lerobot/optim/optimizers.py index e2e3d8937..0bdd7a37e 100644 --- a/src/lerobot/optim/optimizers.py +++ b/src/lerobot/optim/optimizers.py @@ -23,13 +23,12 @@ import draccus import torch from safetensors.torch import load_file, save_file -from lerobot.datasets.io_utils import write_json -from lerobot.datasets.utils import flatten_dict, unflatten_dict from lerobot.utils.constants import ( OPTIMIZER_PARAM_GROUPS, OPTIMIZER_STATE, ) -from lerobot.utils.io_utils import deserialize_json_into_object +from lerobot.utils.io_utils import deserialize_json_into_object, write_json +from lerobot.utils.utils import flatten_dict, unflatten_dict # Type alias for parameters accepted by optimizer build() methods. # This matches PyTorch's optimizer signature while also supporting: diff --git a/src/lerobot/optim/schedulers.py b/src/lerobot/optim/schedulers.py index 19c3fd7bd..f39db6efd 100644 --- a/src/lerobot/optim/schedulers.py +++ b/src/lerobot/optim/schedulers.py @@ -23,9 +23,8 @@ import draccus from torch.optim import Optimizer from torch.optim.lr_scheduler import LambdaLR, LRScheduler -from lerobot.datasets.io_utils import write_json from lerobot.utils.constants import SCHEDULER_STATE -from lerobot.utils.io_utils import deserialize_json_into_object +from lerobot.utils.io_utils import deserialize_json_into_object, write_json @dataclass diff --git a/src/lerobot/policies/factory.py b/src/lerobot/policies/factory.py index 501dd7af1..d3864a82d 100644 --- a/src/lerobot/policies/factory.py +++ b/src/lerobot/policies/factory.py @@ -18,14 +18,15 @@ from __future__ import annotations import importlib import logging -from typing import Any, TypedDict, Unpack +from typing import TYPE_CHECKING, Any, TypedDict, Unpack import torch +if TYPE_CHECKING: + from lerobot.datasets.dataset_metadata import LeRobotDatasetMetadata + from lerobot.configs.policies import PreTrainedConfig from lerobot.configs.types import FeatureType -from lerobot.datasets.dataset_metadata import LeRobotDatasetMetadata -from lerobot.datasets.feature_utils import dataset_to_policy_features from lerobot.envs.configs import EnvConfig from lerobot.envs.utils import env_to_policy_features from lerobot.policies.act.configuration_act import ACTConfig @@ -494,6 +495,8 @@ def make_policy( kwargs = {} if ds_meta is not None: + from lerobot.datasets.feature_utils import dataset_to_policy_features + features = dataset_to_policy_features(ds_meta.features) else: if not cfg.pretrained_path: diff --git a/src/lerobot/policies/utils.py b/src/lerobot/policies/utils.py index 82ab51005..635286c48 100644 --- a/src/lerobot/policies/utils.py +++ b/src/lerobot/policies/utils.py @@ -23,7 +23,6 @@ from torch import nn from lerobot.configs.policies import PreTrainedConfig from lerobot.configs.types import FeatureType, PolicyFeature -from lerobot.datasets.feature_utils import build_dataset_frame from lerobot.types import PolicyAction, RobotAction, RobotObservation from lerobot.utils.constants import ACTION, OBS_STR @@ -163,6 +162,8 @@ def build_inference_frame( Returns: A dictionary of preprocessed tensors ready for model inference. """ + from lerobot.datasets.feature_utils import build_dataset_frame + # Extracts the correct keys from the incoming raw observation observation = build_dataset_frame(ds_features, observation, prefix=OBS_STR) diff --git a/src/lerobot/processor/normalize_processor.py b/src/lerobot/processor/normalize_processor.py index 8a7a1176a..9099538d2 100644 --- a/src/lerobot/processor/normalize_processor.py +++ b/src/lerobot/processor/normalize_processor.py @@ -19,14 +19,17 @@ from __future__ import annotations from copy import deepcopy from dataclasses import dataclass, field -from typing import Any +from typing import TYPE_CHECKING, Any import torch from torch import Tensor from lerobot.configs.types import FeatureType, NormalizationMode, PipelineFeatureType, PolicyFeature -from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.types import EnvTransition, PolicyAction, TransitionKey + +if TYPE_CHECKING: + from lerobot.datasets.lerobot_dataset import LeRobotDataset + from lerobot.utils.constants import ACTION from .converters import from_tensor_to_numpy, to_tensor diff --git a/src/lerobot/scripts/lerobot_dataset_viz.py b/src/lerobot/scripts/lerobot_dataset_viz.py index c4b676c67..5b2aef54c 100644 --- a/src/lerobot/scripts/lerobot_dataset_viz.py +++ b/src/lerobot/scripts/lerobot_dataset_viz.py @@ -66,7 +66,6 @@ import time from pathlib import Path import numpy as np -import rerun as rr import torch import torch.utils.data import tqdm @@ -117,6 +116,11 @@ def visualize_dataset( if mode not in ["local", "distant"]: raise ValueError(mode) + from lerobot.utils.import_utils import require_package + + require_package("rerun-sdk", extra="viz", import_name="rerun") + import rerun as rr + spawn_local_viewer = mode == "local" and not save rr.init(f"{repo_id}/episode_{episode_index}", spawn=spawn_local_viewer) diff --git a/src/lerobot/scripts/lerobot_find_port.py b/src/lerobot/scripts/lerobot_find_port.py index e32b9cb99..93065c473 100644 --- a/src/lerobot/scripts/lerobot_find_port.py +++ b/src/lerobot/scripts/lerobot_find_port.py @@ -28,7 +28,10 @@ from pathlib import Path def find_available_ports(): - from serial.tools import list_ports # Part of pyserial library + from lerobot.utils.import_utils import require_package + + require_package("pyserial", extra="hardware", import_name="serial") + from serial.tools import list_ports if platform.system() == "Windows": # List COM ports using pyserial diff --git a/src/lerobot/scripts/lerobot_imgtransform_viz.py b/src/lerobot/scripts/lerobot_imgtransform_viz.py index bc13f0508..37579f3ab 100644 --- a/src/lerobot/scripts/lerobot_imgtransform_viz.py +++ b/src/lerobot/scripts/lerobot_imgtransform_viz.py @@ -37,7 +37,7 @@ from torchvision.transforms import ToPILImage from lerobot.configs.default import DatasetConfig from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.datasets.transforms import ( +from lerobot.transforms import ( ImageTransforms, ImageTransformsConfig, make_transform_from_config, diff --git a/src/lerobot/scripts/lerobot_teleoperate.py b/src/lerobot/scripts/lerobot_teleoperate.py index f050d572a..8f465739c 100644 --- a/src/lerobot/scripts/lerobot_teleoperate.py +++ b/src/lerobot/scripts/lerobot_teleoperate.py @@ -56,8 +56,6 @@ import time from dataclasses import asdict, dataclass from pprint import pformat -import rerun as rr - from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig # noqa: F401 from lerobot.cameras.realsense.configuration_realsense import RealSenseCameraConfig # noqa: F401 from lerobot.cameras.zmq.configuration_zmq import ZMQCameraConfig # noqa: F401 @@ -103,7 +101,7 @@ from lerobot.teleoperators import ( # noqa: F401 from lerobot.utils.import_utils import register_third_party_plugins from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.utils import init_logging, move_cursor_up -from lerobot.utils.visualization_utils import init_rerun, log_rerun_data +from lerobot.utils.visualization_utils import init_rerun, log_rerun_data, shutdown_rerun @dataclass @@ -240,7 +238,7 @@ def teleoperate(cfg: TeleoperateConfig): pass finally: if cfg.display_data: - rr.rerun_shutdown() + shutdown_rerun() teleop.disconnect() robot.disconnect() diff --git a/src/lerobot/scripts/lerobot_train.py b/src/lerobot/scripts/lerobot_train.py index 0a7212911..ae743cba5 100644 --- a/src/lerobot/scripts/lerobot_train.py +++ b/src/lerobot/scripts/lerobot_train.py @@ -20,6 +20,10 @@ from contextlib import nullcontext from pprint import pformat from typing import Any +from lerobot.utils.import_utils import require_package + +require_package("accelerate", extra="training") + import torch from accelerate import Accelerator from termcolor import colored diff --git a/src/lerobot/datasets/transforms.py b/src/lerobot/transforms/__init__.py similarity index 100% rename from src/lerobot/datasets/transforms.py rename to src/lerobot/transforms/__init__.py diff --git a/src/lerobot/utils/control_utils.py b/src/lerobot/utils/control_utils.py index 94cd82fa1..94ec26714 100644 --- a/src/lerobot/utils/control_utils.py +++ b/src/lerobot/utils/control_utils.py @@ -12,25 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + ######################################################################################## # Utilities ######################################################################################## - - import logging import traceback from contextlib import nullcontext from copy import copy from functools import cache -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np import torch -from deepdiff import DeepDiff -from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.datasets.utils import DEFAULT_FEATURES from lerobot.policies.pretrained import PreTrainedPolicy + +if TYPE_CHECKING: + from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.policies.utils import prepare_observation_for_inference from lerobot.processor import PolicyProcessorPipeline from lerobot.robots import Robot @@ -218,6 +218,13 @@ def sanity_check_dataset_robot_compatibility( Raises: ValueError: If any of the checked metadata fields do not match. """ + from lerobot.utils.import_utils import require_package + + require_package("deepdiff", extra="hardware") + from deepdiff import DeepDiff + + from lerobot.datasets.utils import DEFAULT_FEATURES + fields = [ ("robot_type", dataset.meta.robot_type, robot.robot_type), ("fps", dataset.fps, fps), diff --git a/src/lerobot/utils/import_utils.py b/src/lerobot/utils/import_utils.py index 2b26b2302..9027b2f91 100644 --- a/src/lerobot/utils/import_utils.py +++ b/src/lerobot/utils/import_utils.py @@ -69,6 +69,24 @@ def is_package_available( return package_exists +def get_safe_default_codec(): + if importlib.util.find_spec("torchcodec"): + return "torchcodec" + else: + logging.warning( + "'torchcodec' is not available in your platform, falling back to 'pyav' as a default decoder" + ) + return "pyav" + + +def require_package(pkg_name: str, extra: str, import_name: str | None = None) -> None: + """Raise an informative ImportError if a package required by an optional feature is missing.""" + if not is_package_available(pkg_name, import_name): + raise ImportError( + f"'{pkg_name}' is required but not installed. Install it with: pip install 'lerobot[{extra}]'" + ) + + _transformers_available = is_package_available("transformers") _peft_available = is_package_available("peft") _scipy_available = is_package_available("scipy") diff --git a/src/lerobot/utils/io_utils.py b/src/lerobot/utils/io_utils.py index d70ea8b6a..b1c7511ab 100644 --- a/src/lerobot/utils/io_utils.py +++ b/src/lerobot/utils/io_utils.py @@ -14,21 +14,64 @@ # See the License for the specific language governing permissions and # limitations under the License. import json -import warnings from pathlib import Path - -import imageio +from typing import Any JsonLike = str | int | float | bool | None | list["JsonLike"] | dict[str, "JsonLike"] | tuple["JsonLike", ...] -def write_video(video_path, stacked_frames, fps): - # Filter out DeprecationWarnings raised from pkg_resources - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", "pkg_resources is deprecated as an API", category=DeprecationWarning - ) - imageio.mimsave(video_path, stacked_frames, fps=fps) +def load_json(fpath: Path) -> Any: + """Load data from a JSON file. + + Args: + fpath (Path): Path to the JSON file. + + Returns: + Any: The data loaded from the JSON file. + """ + with open(fpath) as f: + return json.load(f) + + +def write_json(data: dict, fpath: Path) -> None: + """Write data to a JSON file. + + Creates parent directories if they don't exist. + + Args: + data (dict): The dictionary to write. + fpath (Path): The path to the output JSON file. + """ + fpath.parent.mkdir(exist_ok=True, parents=True) + with open(fpath, "w") as f: + json.dump(data, f, indent=4, ensure_ascii=False) + + +def write_video(video_path: str | Path, stacked_frames: list, fps: int) -> None: + """Write a sequence of RGB frames to an MP4 video file using libx264. + + Args: + video_path: Output file path. + stacked_frames: List of HWC uint8 numpy arrays (RGB). + fps: Frames per second for the output video. + """ + import av + + with av.open(str(video_path), mode="w") as container: + height, width = stacked_frames[0].shape[:2] + # Ensure dimensions are even for yuv420p compatibility + height = height if height % 2 == 0 else height - 1 + width = width if width % 2 == 0 else width - 1 + stream = container.add_stream("libx264", rate=fps) + stream.width = width + stream.height = height + stream.pix_fmt = "yuv420p" + for frame_array in stacked_frames: + frame = av.VideoFrame.from_ndarray(frame_array, format="rgb24") + for packet in stream.encode(frame): + container.mux(packet) + for packet in stream.encode(): + container.mux(packet) def deserialize_json_into_object[T: JsonLike](fpath: Path, obj: T) -> T: diff --git a/src/lerobot/utils/random_utils.py b/src/lerobot/utils/random_utils.py index b34d357aa..7bd55cd20 100644 --- a/src/lerobot/utils/random_utils.py +++ b/src/lerobot/utils/random_utils.py @@ -23,8 +23,8 @@ import numpy as np import torch from safetensors.torch import load_file, save_file -from lerobot.datasets.utils import flatten_dict, unflatten_dict from lerobot.utils.constants import RNG_STATE +from lerobot.utils.utils import flatten_dict, unflatten_dict def serialize_python_rng_state() -> dict[str, torch.Tensor]: diff --git a/src/lerobot/utils/train_utils.py b/src/lerobot/utils/train_utils.py index 02f6aebb3..2ace347ee 100644 --- a/src/lerobot/utils/train_utils.py +++ b/src/lerobot/utils/train_utils.py @@ -19,7 +19,6 @@ from torch.optim import Optimizer from torch.optim.lr_scheduler import LRScheduler from lerobot.configs.train import TrainPipelineConfig -from lerobot.datasets.io_utils import load_json, write_json from lerobot.optim.optimizers import load_optimizer_state, save_optimizer_state from lerobot.optim.schedulers import load_scheduler_state, save_scheduler_state from lerobot.policies.pretrained import PreTrainedPolicy @@ -31,6 +30,7 @@ from lerobot.utils.constants import ( TRAINING_STATE_DIR, TRAINING_STEP, ) +from lerobot.utils.io_utils import load_json, write_json from lerobot.utils.random_utils import load_rng_state, save_rng_state diff --git a/src/lerobot/utils/utils.py b/src/lerobot/utils/utils.py index f6aa93bea..e2cf382b9 100644 --- a/src/lerobot/utils/utils.py +++ b/src/lerobot/utils/utils.py @@ -199,6 +199,59 @@ def get_elapsed_time_in_days_hours_minutes_seconds(elapsed_time_s: float): return days, hours, minutes, seconds +def flatten_dict(d: dict, parent_key: str = "", sep: str = "/") -> dict: + """Flatten a nested dictionary by joining keys with a separator. + + Example: + >>> dct = {"a": {"b": 1, "c": {"d": 2}}, "e": 3} + >>> print(flatten_dict(dct)) + {'a/b': 1, 'a/c/d': 2, 'e': 3} + + Args: + d (dict): The dictionary to flatten. + parent_key (str): The base key to prepend to the keys in this level. + sep (str): The separator to use between keys. + + Returns: + dict: A flattened dictionary. + """ + items = [] + for k, v in d.items(): + new_key = f"{parent_key}{sep}{k}" if parent_key else k + if isinstance(v, dict): + items.extend(flatten_dict(v, new_key, sep=sep).items()) + else: + items.append((new_key, v)) + return dict(items) + + +def unflatten_dict(d: dict, sep: str = "/") -> dict: + """Unflatten a dictionary with delimited keys into a nested dictionary. + + Example: + >>> flat_dct = {"a/b": 1, "a/c/d": 2, "e": 3} + >>> print(unflatten_dict(flat_dct)) + {'a': {'b': 1, 'c': {'d': 2}}, 'e': 3} + + Args: + d (dict): A dictionary with flattened keys. + sep (str): The separator used in the keys. + + Returns: + dict: A nested dictionary. + """ + outdict = {} + for key, value in d.items(): + parts = key.split(sep) + d_inner = outdict + for part in parts[:-1]: + if part not in d_inner: + d_inner[part] = {} + d_inner = d_inner[part] + d_inner[parts[-1]] = value + return outdict + + class SuppressProgressBars: """ Context manager to suppress progress bars. diff --git a/src/lerobot/utils/visualization_utils.py b/src/lerobot/utils/visualization_utils.py index 782358c9e..2c24ff1d8 100644 --- a/src/lerobot/utils/visualization_utils.py +++ b/src/lerobot/utils/visualization_utils.py @@ -16,11 +16,16 @@ import numbers import os import numpy as np -import rerun as rr -from lerobot.types import RobotAction, RobotObservation +from lerobot.utils.import_utils import require_package -from .constants import ACTION, ACTION_PREFIX, OBS_PREFIX, OBS_STR +require_package("rerun-sdk", extra="viz", import_name="rerun") + +import rerun as rr # noqa: E402 + +from lerobot.types import RobotAction, RobotObservation # noqa: E402 + +from .constants import ACTION, ACTION_PREFIX, OBS_PREFIX, OBS_STR # noqa: E402 def init_rerun( @@ -44,6 +49,11 @@ def init_rerun( rr.spawn(memory_limit=memory_limit) +def shutdown_rerun() -> None: + """Shuts down the Rerun SDK gracefully.""" + rr.rerun_shutdown() + + def _is_scalar(x): return isinstance(x, (float | numbers.Real | np.integer | np.floating)) or ( isinstance(x, np.ndarray) and x.ndim == 0 diff --git a/tests/artifacts/image_transforms/save_image_transforms_to_safetensors.py b/tests/artifacts/image_transforms/save_image_transforms_to_safetensors.py index ce15d16fd..182058563 100644 --- a/tests/artifacts/image_transforms/save_image_transforms_to_safetensors.py +++ b/tests/artifacts/image_transforms/save_image_transforms_to_safetensors.py @@ -19,7 +19,7 @@ import torch from safetensors.torch import save_file from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.datasets.transforms import ( +from lerobot.transforms import ( ImageTransformConfig, ImageTransforms, ImageTransformsConfig, diff --git a/tests/async_inference/test_policy_server.py b/tests/async_inference/test_policy_server.py index c3ee37c8f..5cec2051c 100644 --- a/tests/async_inference/test_policy_server.py +++ b/tests/async_inference/test_policy_server.py @@ -24,7 +24,7 @@ import torch from lerobot.configs.types import PolicyFeature from lerobot.utils.constants import OBS_STATE -from tests.utils import require_package +from tests.utils import skip_if_package_missing # ----------------------------------------------------------------------------- # Test fixtures @@ -62,7 +62,7 @@ class MockPolicy: @pytest.fixture -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def policy_server(): """Fresh `PolicyServer` instance with a stubbed-out policy model.""" # Import only when the test actually runs (after decorator check) diff --git a/tests/datasets/test_dataset_reader.py b/tests/datasets/test_dataset_reader.py index 4c8a8b23f..be3616922 100644 --- a/tests/datasets/test_dataset_reader.py +++ b/tests/datasets/test_dataset_reader.py @@ -16,7 +16,7 @@ """Contract tests for DatasetReader.""" from lerobot.datasets.dataset_reader import DatasetReader -from lerobot.datasets.video_utils import get_safe_default_codec +from lerobot.utils.import_utils import get_safe_default_codec # ── Loading ────────────────────────────────────────────────────────── diff --git a/tests/datasets/test_datasets.py b/tests/datasets/test_datasets.py index d4e9e88b8..af77a40e3 100644 --- a/tests/datasets/test_datasets.py +++ b/tests/datasets/test_datasets.py @@ -35,7 +35,6 @@ from lerobot.datasets.image_writer import image_array_to_pil_image from lerobot.datasets.io_utils import hf_transform_to_torch from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.multi_dataset import MultiLeRobotDataset -from lerobot.datasets.transforms import ImageTransforms, ImageTransformsConfig from lerobot.datasets.utils import ( DEFAULT_CHUNK_SIZE, DEFAULT_DATA_FILE_SIZE_IN_MB, @@ -46,6 +45,7 @@ from lerobot.datasets.video_utils import VALID_VIDEO_CODECS from lerobot.envs.factory import make_env_config from lerobot.policies.factory import make_policy_config from lerobot.robots import make_robot_from_config +from lerobot.transforms import ImageTransforms, ImageTransformsConfig from lerobot.utils.constants import ACTION, DONE, OBS_IMAGES, OBS_STATE, OBS_STR, REWARD from tests.fixtures.constants import DUMMY_CHW, DUMMY_HWC, DUMMY_REPO_ID from tests.mocks.mock_robot import MockRobotConfig diff --git a/tests/datasets/test_image_transforms.py b/tests/datasets/test_image_transforms.py index ef7e8c395..399204c26 100644 --- a/tests/datasets/test_image_transforms.py +++ b/tests/datasets/test_image_transforms.py @@ -21,7 +21,11 @@ from safetensors.torch import load_file from torchvision.transforms import v2 from torchvision.transforms.v2 import functional as F # noqa: N812 -from lerobot.datasets.transforms import ( +from lerobot.scripts.lerobot_imgtransform_viz import ( + save_all_transforms, + save_each_transform, +) +from lerobot.transforms import ( ImageTransformConfig, ImageTransforms, ImageTransformsConfig, @@ -29,10 +33,6 @@ from lerobot.datasets.transforms import ( SharpnessJitter, make_transform_from_config, ) -from lerobot.scripts.lerobot_imgtransform_viz import ( - save_all_transforms, - save_each_transform, -) from lerobot.utils.random_utils import seeded_context from tests.artifacts.image_transforms.save_image_transforms_to_safetensors import ARTIFACT_DIR from tests.utils import require_x86_64_kernel diff --git a/tests/policies/hilserl/test_modeling_classifier.py b/tests/policies/hilserl/test_modeling_classifier.py index a62ef3ebb..6d262c01b 100644 --- a/tests/policies/hilserl/test_modeling_classifier.py +++ b/tests/policies/hilserl/test_modeling_classifier.py @@ -21,7 +21,7 @@ from lerobot.configs.types import FeatureType, NormalizationMode, PolicyFeature from lerobot.policies.sac.reward_model.configuration_classifier import RewardClassifierConfig from lerobot.policies.sac.reward_model.modeling_classifier import ClassifierOutput from lerobot.utils.constants import OBS_IMAGE, REWARD -from tests.utils import require_package +from tests.utils import skip_if_package_missing def test_classifier_output(): @@ -37,7 +37,7 @@ def test_classifier_output(): ) -@require_package("transformers") +@skip_if_package_missing("transformers") @pytest.mark.skip( reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" ) @@ -81,7 +81,7 @@ def test_binary_classifier_with_default_params(): assert not torch.isnan(output.hidden_states).any(), "Tensor contains NaN values" -@require_package("transformers") +@skip_if_package_missing("transformers") @pytest.mark.skip( reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" ) @@ -123,7 +123,7 @@ def test_multiclass_classifier(): assert not torch.isnan(output.hidden_states).any(), "Tensor contains NaN values" -@require_package("transformers") +@skip_if_package_missing("transformers") @pytest.mark.skip( reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" ) @@ -138,7 +138,7 @@ def test_default_device(): assert p.device == torch.device("cpu") -@require_package("transformers") +@skip_if_package_missing("transformers") @pytest.mark.skip( reason="helper2424/resnet10 needs to be updated to work with the latest version of transformers" ) diff --git a/tests/policies/smolvla/test_smolvla_rtc.py b/tests/policies/smolvla/test_smolvla_rtc.py index 53e74d940..85da92a34 100644 --- a/tests/policies/smolvla/test_smolvla_rtc.py +++ b/tests/policies/smolvla/test_smolvla_rtc.py @@ -24,10 +24,10 @@ from lerobot.policies.factory import make_pre_post_processors # noqa: E402 from lerobot.policies.rtc.configuration_rtc import RTCConfig # noqa: E402 from lerobot.policies.smolvla.configuration_smolvla import SmolVLAConfig # noqa: F401 from lerobot.utils.random_utils import set_seed # noqa: E402 -from tests.utils import require_cuda, require_package # noqa: E402 +from tests.utils import require_cuda, skip_if_package_missing # noqa: E402 -@require_package("transformers") +@skip_if_package_missing("transformers") @require_cuda def test_smolvla_rtc_initialization(): from lerobot.policies.smolvla.modeling_smolvla import SmolVLAPolicy # noqa: F401 @@ -65,7 +65,7 @@ def test_smolvla_rtc_initialization(): print("✓ SmolVLA RTC initialization: Test passed") -@require_package("transformers") +@skip_if_package_missing("transformers") @require_cuda def test_smolvla_rtc_initialization_without_rtc_config(): from lerobot.policies.smolvla.modeling_smolvla import SmolVLAPolicy # noqa: F401 @@ -87,7 +87,7 @@ def test_smolvla_rtc_initialization_without_rtc_config(): print("✓ SmolVLA RTC initialization without RTC config: Test passed") -@require_package("transformers") +@skip_if_package_missing("transformers") @require_cuda @pytest.mark.skipif(True, reason="Requires pretrained SmolVLA model weights") def test_smolvla_rtc_inference_with_prev_chunk(): @@ -170,7 +170,7 @@ def test_smolvla_rtc_inference_with_prev_chunk(): print("✓ SmolVLA RTC inference with prev_chunk: Test passed") -@require_package("transformers") +@skip_if_package_missing("transformers") @require_cuda @pytest.mark.skipif(True, reason="Requires pretrained SmolVLA model weights") def test_smolvla_rtc_inference_without_prev_chunk(): @@ -244,7 +244,7 @@ def test_smolvla_rtc_inference_without_prev_chunk(): print("✓ SmolVLA RTC inference without prev_chunk: Test passed") -@require_package("transformers") +@skip_if_package_missing("transformers") @require_cuda @pytest.mark.skipif(True, reason="Requires pretrained SmolVLA model weights") def test_smolvla_rtc_validation_rules(): diff --git a/tests/processor/test_tokenizer_processor.py b/tests/processor/test_tokenizer_processor.py index 76dce2537..5708e6e81 100644 --- a/tests/processor/test_tokenizer_processor.py +++ b/tests/processor/test_tokenizer_processor.py @@ -36,7 +36,7 @@ from lerobot.utils.constants import ( OBS_LANGUAGE_SUBTASK_TOKENS, OBS_STATE, ) -from tests.utils import require_package +from tests.utils import skip_if_package_missing class MockTokenizer: @@ -94,7 +94,7 @@ def mock_tokenizer(): return MockTokenizer(vocab_size=100) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_basic_tokenization(mock_auto_tokenizer): """Test basic string tokenization functionality.""" @@ -129,7 +129,7 @@ def test_basic_tokenization(mock_auto_tokenizer): assert attention_mask.shape == (10,) -@require_package("transformers") +@skip_if_package_missing("transformers") def test_basic_tokenization_with_tokenizer_object(): """Test basic string tokenization functionality using tokenizer object directly.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -161,7 +161,7 @@ def test_basic_tokenization_with_tokenizer_object(): assert attention_mask.shape == (10,) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_list_of_strings_tokenization(mock_auto_tokenizer): """Test tokenization of a list of strings.""" @@ -189,7 +189,7 @@ def test_list_of_strings_tokenization(mock_auto_tokenizer): assert attention_mask.shape == (2, 8) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_tuple_of_strings_tokenization(mock_auto_tokenizer): """Test tokenization of a tuple of strings (returned by VectorEnv.call()).""" @@ -213,7 +213,7 @@ def test_tuple_of_strings_tokenization(mock_auto_tokenizer): assert attention_mask.shape == (2, 8) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_custom_keys(mock_auto_tokenizer): """Test using custom task_key.""" @@ -239,7 +239,7 @@ def test_custom_keys(mock_auto_tokenizer): assert tokens.shape == (5,) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_none_complementary_data(mock_auto_tokenizer): """Test handling of None complementary_data.""" @@ -255,7 +255,7 @@ def test_none_complementary_data(mock_auto_tokenizer): processor(transition) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_missing_task_key(mock_auto_tokenizer): """Test handling when task key is missing.""" @@ -270,7 +270,7 @@ def test_missing_task_key(mock_auto_tokenizer): processor(transition) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_none_task_value(mock_auto_tokenizer): """Test handling when task value is None.""" @@ -285,7 +285,7 @@ def test_none_task_value(mock_auto_tokenizer): processor(transition) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_unsupported_task_type(mock_auto_tokenizer): """Test handling of unsupported task types.""" @@ -307,14 +307,14 @@ def test_unsupported_task_type(mock_auto_tokenizer): processor(transition) -@require_package("transformers") +@skip_if_package_missing("transformers") def test_no_tokenizer_error(): """Test that ValueError is raised when neither tokenizer nor tokenizer_name is provided.""" with pytest.raises(ValueError, match="Either 'tokenizer' or 'tokenizer_name' must be provided"): TokenizerProcessorStep() -@require_package("transformers") +@skip_if_package_missing("transformers") def test_invalid_tokenizer_name_error(): """Test that error is raised when invalid tokenizer_name is provided.""" with patch("lerobot.processor.tokenizer_processor.AutoTokenizer") as mock_auto_tokenizer: @@ -325,7 +325,7 @@ def test_invalid_tokenizer_name_error(): TokenizerProcessorStep(tokenizer_name="invalid-tokenizer") -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_get_config_with_tokenizer_name(mock_auto_tokenizer): """Test configuration serialization when using tokenizer_name.""" @@ -354,7 +354,7 @@ def test_get_config_with_tokenizer_name(mock_auto_tokenizer): assert config == expected -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_config_with_tokenizer_object(): """Test configuration serialization when using tokenizer object.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -382,7 +382,7 @@ def test_get_config_with_tokenizer_object(): assert "tokenizer_name" not in config -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_state_dict_methods(mock_auto_tokenizer): """Test state_dict and load_state_dict methods.""" @@ -399,7 +399,7 @@ def test_state_dict_methods(mock_auto_tokenizer): processor.load_state_dict({}) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_reset_method(mock_auto_tokenizer): """Test reset method.""" @@ -412,7 +412,7 @@ def test_reset_method(mock_auto_tokenizer): processor.reset() -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_integration_with_robot_processor(mock_auto_tokenizer): """Test integration with RobotProcessor.""" @@ -449,7 +449,7 @@ def test_integration_with_robot_processor(mock_auto_tokenizer): assert torch.equal(result[TransitionKey.ACTION], transition[TransitionKey.ACTION]) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_save_and_load_pretrained_with_tokenizer_name(mock_auto_tokenizer): """Test saving and loading processor with tokenizer_name.""" @@ -489,7 +489,7 @@ def test_save_and_load_pretrained_with_tokenizer_name(mock_auto_tokenizer): assert f"{OBS_LANGUAGE}.attention_mask" in result[TransitionKey.OBSERVATION] -@require_package("transformers") +@skip_if_package_missing("transformers") def test_save_and_load_pretrained_with_tokenizer_object(): """Test saving and loading processor with tokenizer object using overrides.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -528,7 +528,7 @@ def test_save_and_load_pretrained_with_tokenizer_object(): assert f"{OBS_LANGUAGE}.attention_mask" in result[TransitionKey.OBSERVATION] -@require_package("transformers") +@skip_if_package_missing("transformers") def test_registry_functionality(): """Test that the processor is properly registered.""" from lerobot.processor import ProcessorStepRegistry @@ -541,7 +541,7 @@ def test_registry_functionality(): assert retrieved_class is TokenizerProcessorStep -@require_package("transformers") +@skip_if_package_missing("transformers") def test_features_basic(): """Test basic feature contract functionality.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -574,7 +574,7 @@ def test_features_basic(): assert attention_mask_feature.shape == (128,) -@require_package("transformers") +@skip_if_package_missing("transformers") def test_features_with_custom_max_length(): """Test feature contract with custom max_length.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -596,7 +596,7 @@ def test_features_with_custom_max_length(): assert attention_mask_feature.shape == (64,) -@require_package("transformers") +@skip_if_package_missing("transformers") def test_features_existing_features(): """Test feature contract when tokenized features already exist.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -618,7 +618,7 @@ def test_features_existing_features(): assert output_features[PipelineFeatureType.OBSERVATION][f"{OBS_LANGUAGE}.attention_mask"].shape == (100,) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_tokenization_parameters(mock_auto_tokenizer): """Test that tokenization parameters are correctly passed to tokenizer.""" @@ -666,7 +666,7 @@ def test_tokenization_parameters(mock_auto_tokenizer): assert tracking_tokenizer.last_call_kwargs["return_tensors"] == "pt" -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_preserves_other_complementary_data(mock_auto_tokenizer): """Test that other complementary data fields are preserved.""" @@ -701,7 +701,7 @@ def test_preserves_other_complementary_data(mock_auto_tokenizer): assert f"{OBS_LANGUAGE}.attention_mask" in observation -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_deterministic_tokenization(mock_auto_tokenizer): """Test that tokenization is deterministic for the same input.""" @@ -729,7 +729,7 @@ def test_deterministic_tokenization(mock_auto_tokenizer): assert torch.equal(attention_mask1, attention_mask2) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_empty_string_task(mock_auto_tokenizer): """Test handling of empty string task.""" @@ -753,7 +753,7 @@ def test_empty_string_task(mock_auto_tokenizer): assert tokens.shape == (8,) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_very_long_task(mock_auto_tokenizer): """Test handling of very long task strings.""" @@ -779,7 +779,7 @@ def test_very_long_task(mock_auto_tokenizer): assert attention_mask.shape == (5,) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_custom_padding_side(mock_auto_tokenizer): """Test using custom padding_side parameter.""" @@ -833,7 +833,7 @@ def test_custom_padding_side(mock_auto_tokenizer): assert tracking_tokenizer.padding_side_calls[-1] == "right" -@require_package("transformers") +@skip_if_package_missing("transformers") def test_device_detection_cpu(): """Test that tokenized tensors stay on CPU when other tensors are on CPU.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -857,7 +857,7 @@ def test_device_detection_cpu(): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -@require_package("transformers") +@skip_if_package_missing("transformers") def test_device_detection_cuda(): """Test that tokenized tensors are moved to CUDA when other tensors are on CUDA.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -882,7 +882,7 @@ def test_device_detection_cuda(): @pytest.mark.skipif(torch.cuda.device_count() < 2, reason="Requires at least 2 GPUs") -@require_package("transformers") +@skip_if_package_missing("transformers") def test_device_detection_multi_gpu(): """Test that tokenized tensors match device in multi-GPU setup.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -906,7 +906,7 @@ def test_device_detection_multi_gpu(): assert attention_mask.device == device -@require_package("transformers") +@skip_if_package_missing("transformers") def test_device_detection_no_tensors(): """Test that tokenized tensors stay on CPU when no other tensors exist.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -928,7 +928,7 @@ def test_device_detection_no_tensors(): assert attention_mask.device.type == "cpu" -@require_package("transformers") +@skip_if_package_missing("transformers") def test_device_detection_mixed_devices(): """Test device detection when tensors are on different devices (uses first found).""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -956,7 +956,7 @@ def test_device_detection_mixed_devices(): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -@require_package("transformers") +@skip_if_package_missing("transformers") def test_device_detection_from_action(): """Test that device is detected from action tensor when no observation tensors exist.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -979,7 +979,7 @@ def test_device_detection_from_action(): assert attention_mask.device.type == "cuda" -@require_package("transformers") +@skip_if_package_missing("transformers") def test_device_detection_preserves_dtype(): """Test that device detection doesn't affect dtype of tokenized tensors.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1000,7 +1000,7 @@ def test_device_detection_preserves_dtype(): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_integration_with_device_processor(mock_auto_tokenizer): """Test that TokenizerProcessorStep works correctly with DeviceProcessorStep in pipeline.""" @@ -1039,7 +1039,7 @@ def test_integration_with_device_processor(mock_auto_tokenizer): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -@require_package("transformers") +@skip_if_package_missing("transformers") def test_simulated_accelerate_scenario(): """Test scenario simulating Accelerate with data already on GPU.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1077,7 +1077,7 @@ def test_simulated_accelerate_scenario(): # ============================================================================= -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_missing_key(): """Test get_subtask returns None when subtask key is missing from complementary_data.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1093,7 +1093,7 @@ def test_get_subtask_missing_key(): assert result is None -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_none_value(): """Test get_subtask returns None when subtask value is None.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1109,7 +1109,7 @@ def test_get_subtask_none_value(): assert result is None -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_none_complementary_data(): """Test get_subtask returns None when complementary_data is None.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1125,7 +1125,7 @@ def test_get_subtask_none_complementary_data(): assert result is None -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_string(): """Test get_subtask returns list with single string when subtask is a string.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1143,7 +1143,7 @@ def test_get_subtask_string(): assert len(result) == 1 -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_list_of_strings(): """Test get_subtask returns the list when subtask is already a list of strings.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1162,7 +1162,7 @@ def test_get_subtask_list_of_strings(): assert len(result) == 3 -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_unsupported_type_integer(): """Test get_subtask returns None when subtask is an unsupported type (integer).""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1178,7 +1178,7 @@ def test_get_subtask_unsupported_type_integer(): assert result is None -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_unsupported_type_mixed_list(): """Test get_subtask returns None when subtask is a list with mixed types.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1194,7 +1194,7 @@ def test_get_subtask_unsupported_type_mixed_list(): assert result is None -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_unsupported_type_dict(): """Test get_subtask returns None when subtask is a dictionary.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1210,7 +1210,7 @@ def test_get_subtask_unsupported_type_dict(): assert result is None -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_empty_string(): """Test get_subtask with empty string returns list with empty string.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1226,7 +1226,7 @@ def test_get_subtask_empty_string(): assert result == [""] -@require_package("transformers") +@skip_if_package_missing("transformers") def test_get_subtask_empty_list(): """Test get_subtask with empty list returns empty list.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1247,7 +1247,7 @@ def test_get_subtask_empty_list(): # ============================================================================= -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_tokenization_when_present(): """Test that subtask is tokenized and added to observation when present.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1276,7 +1276,7 @@ def test_subtask_tokenization_when_present(): assert subtask_attention_mask.dtype == torch.bool -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_tokenization_not_added_when_none(): """Test that subtask tokens are NOT added to observation when subtask is None.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1300,7 +1300,7 @@ def test_subtask_tokenization_not_added_when_none(): assert f"{OBS_LANGUAGE}.attention_mask" in observation -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_tokenization_not_added_when_subtask_value_is_none(): """Test that subtask tokens are NOT added when subtask value is explicitly None.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1320,7 +1320,7 @@ def test_subtask_tokenization_not_added_when_subtask_value_is_none(): assert OBS_LANGUAGE_SUBTASK_ATTENTION_MASK not in observation -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_tokenization_list_of_strings(): """Test subtask tokenization with list of strings.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1346,7 +1346,7 @@ def test_subtask_tokenization_list_of_strings(): assert subtask_attention_mask.shape == (2, 8) -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_tokenization_device_cpu(): """Test that subtask tokens are on CPU when other tensors are on CPU.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1372,7 +1372,7 @@ def test_subtask_tokenization_device_cpu(): @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available") -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_tokenization_device_cuda(): """Test that subtask tokens are moved to CUDA when other tensors are on CUDA.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1397,7 +1397,7 @@ def test_subtask_tokenization_device_cuda(): assert subtask_attention_mask.device.type == "cuda" -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_tokenization_preserves_other_observation_data(): """Test that subtask tokenization preserves other observation data.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1423,7 +1423,7 @@ def test_subtask_tokenization_preserves_other_observation_data(): assert OBS_LANGUAGE_SUBTASK_ATTENTION_MASK in observation -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_attention_mask_dtype(): """Test that subtask attention mask has correct dtype (bool).""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1442,7 +1442,7 @@ def test_subtask_attention_mask_dtype(): assert subtask_attention_mask.dtype == torch.bool -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_tokenization_deterministic(): """Test that subtask tokenization is deterministic for the same input.""" mock_tokenizer = MockTokenizer(vocab_size=100) @@ -1467,7 +1467,7 @@ def test_subtask_tokenization_deterministic(): assert torch.equal(subtask_mask1, subtask_mask2) -@require_package("transformers") +@skip_if_package_missing("transformers") @patch("lerobot.processor.tokenizer_processor.AutoTokenizer") def test_subtask_tokenization_integration_with_pipeline(mock_auto_tokenizer): """Test subtask tokenization works correctly with DataProcessorPipeline.""" @@ -1504,7 +1504,7 @@ def test_subtask_tokenization_integration_with_pipeline(mock_auto_tokenizer): assert observation[OBS_LANGUAGE_SUBTASK_TOKENS].shape == (6,) -@require_package("transformers") +@skip_if_package_missing("transformers") def test_subtask_not_added_for_unsupported_types(): """Test that subtask tokens are not added when subtask has unsupported type.""" mock_tokenizer = MockTokenizer(vocab_size=100) diff --git a/tests/rl/test_actor.py b/tests/rl/test_actor.py index 54e4d2870..688d59842 100644 --- a/tests/rl/test_actor.py +++ b/tests/rl/test_actor.py @@ -23,7 +23,7 @@ from torch.multiprocessing import Event, Queue from lerobot.utils.constants import OBS_STR from lerobot.utils.transition import Transition -from tests.utils import require_package +from tests.utils import skip_if_package_missing def create_learner_service_stub(): @@ -64,7 +64,7 @@ def close_service_stub(channel, server): server.stop(None) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_establish_learner_connection_success(): from lerobot.rl.actor import establish_learner_connection @@ -81,7 +81,7 @@ def test_establish_learner_connection_success(): close_service_stub(channel, server) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_establish_learner_connection_failure(): from lerobot.rl.actor import establish_learner_connection @@ -100,7 +100,7 @@ def test_establish_learner_connection_failure(): close_service_stub(channel, server) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_push_transitions_to_transport_queue(): from lerobot.rl.actor import push_transitions_to_transport_queue from lerobot.transport.utils import bytes_to_transitions @@ -135,7 +135,7 @@ def test_push_transitions_to_transport_queue(): assert_transitions_equal(deserialized_transition, transitions[i]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(3) # force cross-platform watchdog def test_transitions_stream(): from lerobot.rl.actor import transitions_stream @@ -167,7 +167,7 @@ def test_transitions_stream(): assert streamed_data[2].data == b"transition_data_3" -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(3) # force cross-platform watchdog def test_interactions_stream(): from lerobot.rl.actor import interactions_stream diff --git a/tests/rl/test_actor_learner.py b/tests/rl/test_actor_learner.py index e13862d82..4ce27a757 100644 --- a/tests/rl/test_actor_learner.py +++ b/tests/rl/test_actor_learner.py @@ -26,7 +26,7 @@ from lerobot.configs.train import TrainRLServerPipelineConfig from lerobot.policies.sac.configuration_sac import SACConfig from lerobot.utils.constants import OBS_STR from lerobot.utils.transition import Transition -from tests.utils import require_package +from tests.utils import skip_if_package_missing def create_test_transitions(count: int = 3) -> list[Transition]: @@ -88,7 +88,7 @@ def cfg(): return cfg -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(10) # force cross-platform watchdog def test_end_to_end_transitions_flow(cfg): from lerobot.rl.actor import ( @@ -150,7 +150,7 @@ def test_end_to_end_transitions_flow(cfg): assert_transitions_equal(transition, input_transitions[i]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(10) def test_end_to_end_interactions_flow(cfg): from lerobot.rl.actor import ( @@ -223,7 +223,7 @@ def test_end_to_end_interactions_flow(cfg): assert received == expected -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.parametrize("data_size", ["small", "large"]) @pytest.mark.timeout(10) def test_end_to_end_parameters_flow(cfg, data_size): diff --git a/tests/rl/test_learner_service.py b/tests/rl/test_learner_service.py index d967388f0..f1023f0f3 100644 --- a/tests/rl/test_learner_service.py +++ b/tests/rl/test_learner_service.py @@ -20,7 +20,7 @@ from multiprocessing import Event, Queue import pytest -from tests.utils import require_package # our gRPC servicer class +from tests.utils import skip_if_package_missing # our gRPC servicer class @pytest.fixture(scope="function") @@ -39,7 +39,7 @@ def learner_service_stub(): close_learner_service_stub(channel, server) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def create_learner_service_stub( shutdown_event: Event, parameters_queue: Queue, @@ -75,7 +75,7 @@ def create_learner_service_stub( return services_pb2_grpc.LearnerServiceStub(channel), channel, server -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def close_learner_service_stub(channel, server): channel.close() server.stop(None) @@ -91,7 +91,7 @@ def test_ready_method(learner_service_stub): assert response == services_pb2.Empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(3) # force cross-platform watchdog def test_send_interactions(): from lerobot.transport import services_pb2 @@ -135,7 +135,7 @@ def test_send_interactions(): assert interactions == [b"123", b"4", b"5", b"678"] -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(3) # force cross-platform watchdog def test_send_transitions(): from lerobot.transport import services_pb2 @@ -181,7 +181,7 @@ def test_send_transitions(): assert transitions == [b"transition_1transition_2transition_3", b"batch_1batch_2"] -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(3) # force cross-platform watchdog def test_send_transitions_empty_stream(): from lerobot.transport import services_pb2 @@ -209,7 +209,7 @@ def test_send_transitions_empty_stream(): assert transitions_queue.empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(10) # force cross-platform watchdog def test_stream_parameters(): import time @@ -267,7 +267,7 @@ def test_stream_parameters(): assert time_diff == pytest.approx(seconds_between_pushes, abs=0.1) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(3) # force cross-platform watchdog def test_stream_parameters_with_shutdown(): from lerobot.transport import services_pb2 @@ -319,7 +319,7 @@ def test_stream_parameters_with_shutdown(): assert received_params == [b"param_batch_1", b"stop"] -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") @pytest.mark.timeout(3) # force cross-platform watchdog def test_stream_parameters_waits_and_retries_on_empty_queue(): import threading diff --git a/tests/test_cli_peft.py b/tests/test_cli_peft.py index 42fef4741..5d653ee6b 100644 --- a/tests/test_cli_peft.py +++ b/tests/test_cli_peft.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest from safetensors.torch import load_file -from .utils import require_package +from .utils import skip_if_package_missing # Skip this entire module in CI pytestmark = pytest.mark.skipif( @@ -37,7 +37,7 @@ def resolve_model_id_for_peft_training(policy_type): @pytest.mark.parametrize("policy_type", ["smolvla"]) -@require_package("peft") +@skip_if_package_missing("peft") def test_peft_training_push_to_hub_works(policy_type, tmp_path): """Ensure that push to hub stores PEFT only the adapter, not the full model weights.""" output_dir = tmp_path / f"output_{policy_type}" @@ -76,7 +76,7 @@ def test_peft_training_push_to_hub_works(policy_type, tmp_path): @pytest.mark.parametrize("policy_type", ["smolvla"]) -@require_package("peft") +@skip_if_package_missing("peft") def test_peft_training_works(policy_type, tmp_path): """Check whether the standard case of fine-tuning a (partially) pre-trained policy with PEFT works.""" output_dir = tmp_path / f"output_{policy_type}" @@ -125,7 +125,7 @@ def test_peft_training_works(policy_type, tmp_path): @pytest.mark.parametrize("policy_type", ["smolvla"]) -@require_package("peft") +@skip_if_package_missing("peft") def test_peft_training_params_are_fewer(policy_type, tmp_path): """Check whether the standard case of fine-tuning a (partially) pre-trained policy with PEFT works.""" output_dir = tmp_path / f"output_{policy_type}" @@ -176,7 +176,7 @@ def dummy_make_robot_from_config(*args, **kwargs): @pytest.mark.parametrize("policy_type", ["smolvla"]) -@require_package("peft") +@skip_if_package_missing("peft") def test_peft_record_loads_policy(policy_type, tmp_path): """Train a policy with PEFT and attempt to load it with `lerobot-record`.""" from peft import PeftModel diff --git a/tests/transport/test_transport_utils.py b/tests/transport/test_transport_utils.py index 63632a8f4..d0df3d941 100644 --- a/tests/transport/test_transport_utils.py +++ b/tests/transport/test_transport_utils.py @@ -23,10 +23,10 @@ import torch from lerobot.utils.constants import ACTION from lerobot.utils.transition import Transition -from tests.utils import require_cuda, require_package +from tests.utils import require_cuda, skip_if_package_missing -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_bytes_buffer_size_empty_buffer(): from lerobot.transport.utils import bytes_buffer_size @@ -37,7 +37,7 @@ def test_bytes_buffer_size_empty_buffer(): assert buffer.tell() == 0 -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_bytes_buffer_size_small_buffer(): from lerobot.transport.utils import bytes_buffer_size @@ -47,7 +47,7 @@ def test_bytes_buffer_size_small_buffer(): assert buffer.tell() == 0 -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_bytes_buffer_size_large_buffer(): from lerobot.transport.utils import CHUNK_SIZE, bytes_buffer_size @@ -58,7 +58,7 @@ def test_bytes_buffer_size_large_buffer(): assert buffer.tell() == 0 -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_send_bytes_in_chunks_empty_data(): from lerobot.transport.utils import send_bytes_in_chunks, services_pb2 @@ -68,7 +68,7 @@ def test_send_bytes_in_chunks_empty_data(): assert len(chunks) == 0 -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_single_chunk_small_data(): from lerobot.transport.utils import send_bytes_in_chunks, services_pb2 @@ -82,7 +82,7 @@ def test_single_chunk_small_data(): assert chunks[0].transfer_state == services_pb2.TransferState.TRANSFER_END -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_not_silent_mode(): from lerobot.transport.utils import send_bytes_in_chunks, services_pb2 @@ -94,7 +94,7 @@ def test_not_silent_mode(): assert chunks[0].data == b"Some data" -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_send_bytes_in_chunks_large_data(): from lerobot.transport.utils import CHUNK_SIZE, send_bytes_in_chunks, services_pb2 @@ -111,7 +111,7 @@ def test_send_bytes_in_chunks_large_data(): assert chunks[2].transfer_state == services_pb2.TransferState.TRANSFER_END -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_send_bytes_in_chunks_large_data_with_exact_chunk_size(): from lerobot.transport.utils import CHUNK_SIZE, send_bytes_in_chunks, services_pb2 @@ -124,7 +124,7 @@ def test_send_bytes_in_chunks_large_data_with_exact_chunk_size(): assert chunks[0].transfer_state == services_pb2.TransferState.TRANSFER_END -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_empty_data(): from lerobot.transport.utils import receive_bytes_in_chunks @@ -138,7 +138,7 @@ def test_receive_bytes_in_chunks_empty_data(): assert queue.empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_single_chunk(): from lerobot.transport.utils import receive_bytes_in_chunks, services_pb2 @@ -157,7 +157,7 @@ def test_receive_bytes_in_chunks_single_chunk(): assert queue.empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_single_not_end_chunk(): from lerobot.transport.utils import receive_bytes_in_chunks, services_pb2 @@ -175,7 +175,7 @@ def test_receive_bytes_in_chunks_single_not_end_chunk(): assert queue.empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_multiple_chunks(): from lerobot.transport.utils import receive_bytes_in_chunks, services_pb2 @@ -199,7 +199,7 @@ def test_receive_bytes_in_chunks_multiple_chunks(): assert queue.empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_multiple_messages(): from lerobot.transport.utils import receive_bytes_in_chunks, services_pb2 @@ -235,7 +235,7 @@ def test_receive_bytes_in_chunks_multiple_messages(): assert queue.empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_shutdown_during_receive(): from lerobot.transport.utils import receive_bytes_in_chunks, services_pb2 @@ -259,7 +259,7 @@ def test_receive_bytes_in_chunks_shutdown_during_receive(): assert queue.empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_only_begin_chunk(): from lerobot.transport.utils import receive_bytes_in_chunks, services_pb2 @@ -279,7 +279,7 @@ def test_receive_bytes_in_chunks_only_begin_chunk(): assert queue.empty() -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_missing_begin(): from lerobot.transport.utils import receive_bytes_in_chunks, services_pb2 @@ -303,7 +303,7 @@ def test_receive_bytes_in_chunks_missing_begin(): # Tests for state_to_bytes and bytes_to_state_dict -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_state_to_bytes_empty_dict(): from lerobot.transport.utils import bytes_to_state_dict, state_to_bytes @@ -314,7 +314,7 @@ def test_state_to_bytes_empty_dict(): assert reconstructed == state_dict -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_bytes_to_state_dict_empty_data(): from lerobot.transport.utils import bytes_to_state_dict @@ -323,7 +323,7 @@ def test_bytes_to_state_dict_empty_data(): bytes_to_state_dict(b"") -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_state_to_bytes_simple_dict(): from lerobot.transport.utils import bytes_to_state_dict, state_to_bytes @@ -347,7 +347,7 @@ def test_state_to_bytes_simple_dict(): assert torch.allclose(state_dict[key], reconstructed[key]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_state_to_bytes_various_dtypes(): from lerobot.transport.utils import bytes_to_state_dict, state_to_bytes @@ -372,7 +372,7 @@ def test_state_to_bytes_various_dtypes(): assert torch.allclose(state_dict[key], reconstructed[key]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_bytes_to_state_dict_invalid_data(): from lerobot.transport.utils import bytes_to_state_dict @@ -382,7 +382,7 @@ def test_bytes_to_state_dict_invalid_data(): @require_cuda -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_state_to_bytes_various_dtypes_cuda(): from lerobot.transport.utils import bytes_to_state_dict, state_to_bytes @@ -407,7 +407,7 @@ def test_state_to_bytes_various_dtypes_cuda(): assert torch.allclose(state_dict[key], reconstructed[key]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_python_object_to_bytes_none(): from lerobot.transport.utils import bytes_to_python_object, python_object_to_bytes @@ -439,7 +439,7 @@ def test_python_object_to_bytes_none(): (1, 2, 3), ], ) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_python_object_to_bytes_simple_types(obj): from lerobot.transport.utils import bytes_to_python_object, python_object_to_bytes @@ -450,7 +450,7 @@ def test_python_object_to_bytes_simple_types(obj): assert type(reconstructed) is type(obj) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_python_object_to_bytes_with_tensors(): from lerobot.transport.utils import bytes_to_python_object, python_object_to_bytes @@ -475,7 +475,7 @@ def test_python_object_to_bytes_with_tensors(): assert torch.equal(obj["nested"]["tensor2"], reconstructed["nested"]["tensor2"]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_transitions_to_bytes_empty_list(): from lerobot.transport.utils import bytes_to_transitions, transitions_to_bytes @@ -487,7 +487,7 @@ def test_transitions_to_bytes_empty_list(): assert isinstance(reconstructed, list) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_transitions_to_bytes_single_transition(): from lerobot.transport.utils import bytes_to_transitions, transitions_to_bytes @@ -509,7 +509,7 @@ def test_transitions_to_bytes_single_transition(): assert_transitions_equal(transitions[0], reconstructed[0]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def assert_transitions_equal(t1: Transition, t2: Transition): """Helper to assert two transitions are equal.""" assert_observation_equal(t1["state"], t2["state"]) @@ -519,7 +519,7 @@ def assert_transitions_equal(t1: Transition, t2: Transition): assert_observation_equal(t1["next_state"], t2["next_state"]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def assert_observation_equal(o1: dict, o2: dict): """Helper to assert two observations are equal.""" assert set(o1.keys()) == set(o2.keys()) @@ -527,7 +527,7 @@ def assert_observation_equal(o1: dict, o2: dict): assert torch.allclose(o1[key], o2[key]) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_transitions_to_bytes_multiple_transitions(): from lerobot.transport.utils import bytes_to_transitions, transitions_to_bytes @@ -551,7 +551,7 @@ def test_transitions_to_bytes_multiple_transitions(): assert_transitions_equal(original, reconstructed_item) -@require_package("grpcio", "grpc") +@skip_if_package_missing("grpcio", "grpc") def test_receive_bytes_in_chunks_unknown_state(): from lerobot.transport.utils import receive_bytes_in_chunks diff --git a/tests/utils.py b/tests/utils.py index 33c554804..b93682521 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -152,7 +152,7 @@ def require_env(func): return wrapper -def require_package_arg(func): +def skip_if_package_arg_missing(func): """ Decorator that skips the test if the required package is not installed. This is similar to `require_env` but more general in that it can check any package (not just environments). @@ -184,7 +184,7 @@ def require_package_arg(func): return wrapper -def require_package(package_name, import_name=None): +def skip_if_package_missing(package_name, import_name=None): """ Decorator that skips the test if the specified package is not installed. """ diff --git a/tests/utils/test_visualization_utils.py b/tests/utils/test_visualization_utils.py index c8e5a92a8..711bbc8c0 100644 --- a/tests/utils/test_visualization_utils.py +++ b/tests/utils/test_visualization_utils.py @@ -48,6 +48,9 @@ def mock_rerun(monkeypatch): calls.append((key, obj, kwargs)) dummy_rr = SimpleNamespace( + __name__="rerun", + __package__="rerun", + __spec__=SimpleNamespace(name="rerun", submodule_search_locations=None), Scalars=DummyScalar, Image=DummyImage, log=dummy_log, diff --git a/uv.lock b/uv.lock index 4a8f37bc1..c7691edb1 100644 --- a/uv.lock +++ b/uv.lock @@ -2,11 +2,14 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'armv7l' and sys_platform == 'linux')", "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'armv7l' and sys_platform == 'linux')", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux'", "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux'", @@ -820,7 +823,7 @@ name = "cuda-bindings" version = "12.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, + { name = "cuda-pathfinder", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, @@ -907,7 +910,7 @@ name = "decord" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x') or (platform_machine != 's390x' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/11/79/936af42edf90a7bd4e41a6cac89c913d4b47fa48a26b042d5129a9242ee3/decord-0.6.0-py3-none-manylinux2010_x86_64.whl", hash = "sha256:51997f20be8958e23b7c4061ba45d0efcd86bffd5fe81c695d0befee0d442976", size = 13602299, upload-time = "2021-06-14T21:30:55.486Z" }, @@ -1010,7 +1013,8 @@ name = "dm-tree" version = "0.1.9" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'armv7l' and sys_platform == 'linux')", "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux'", "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux'", @@ -1043,9 +1047,11 @@ name = "dm-tree" version = "0.1.10" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'armv7l' and sys_platform == 'linux')", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", - "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'arm64' and sys_platform == 'linux') or (python_full_version < '3.13' and platform_machine == 'armv7l' and sys_platform == 'linux')", "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux'", "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform != 'emscripten' and sys_platform != 'linux'", @@ -2187,37 +2193,28 @@ name = "lerobot" version = "0.5.2" source = { editable = "." } dependencies = [ - { name = "accelerate" }, - { name = "av" }, - { name = "cmake" }, - { name = "datasets" }, - { name = "deepdiff" }, - { name = "diffusers" }, { name = "draccus" }, { name = "einops" }, { name = "gymnasium" }, { name = "huggingface-hub" }, - { name = "imageio", extra = ["ffmpeg"] }, - { name = "jsonlines" }, { name = "numpy" }, { name = "opencv-python-headless" }, { name = "packaging" }, - { name = "pynput" }, - { name = "pyserial" }, - { name = "rerun-sdk" }, - { name = "setuptools" }, { name = "termcolor" }, { name = "torch" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, { name = "torchvision" }, - { name = "wandb" }, ] [package.optional-dependencies] all = [ { name = "accelerate" }, + { name = "av" }, + { name = "cmake" }, { name = "contourpy" }, + { name = "datasets" }, { name = "debugpy" }, + { name = "deepdiff" }, + { name = "diffusers" }, { name = "dynamixel-sdk" }, { name = "faker" }, { name = "fastapi" }, @@ -2230,6 +2227,7 @@ all = [ { name = "hebi-py" }, { name = "hf-libero", marker = "sys_platform == 'linux'" }, { name = "hidapi" }, + { name = "jsonlines" }, { name = "matplotlib" }, { name = "metaworld" }, { name = "mock-serial", marker = "sys_platform != 'win32'" }, @@ -2242,24 +2240,34 @@ all = [ { name = "protobuf" }, { name = "pygame" }, { name = "pymunk" }, + { name = "pynput" }, { name = "pyrealsense2", marker = "sys_platform != 'darwin'" }, { name = "pyrealsense2-macosx", marker = "sys_platform == 'darwin'" }, + { name = "pyserial" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-timeout" }, { name = "pyzmq" }, { name = "qwen-vl-utils" }, { name = "reachy2-sdk" }, + { name = "rerun-sdk" }, { name = "safetensors" }, { name = "scikit-image" }, { name = "scipy" }, + { name = "setuptools" }, { name = "teleop" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, { name = "torchdiffeq" }, { name = "transformers" }, + { name = "wandb" }, ] aloha = [ + { name = "av" }, + { name = "datasets" }, { name = "gym-aloha" }, + { name = "jsonlines" }, { name = "scipy" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, ] async = [ { name = "contourpy" }, @@ -2267,23 +2275,54 @@ async = [ { name = "matplotlib" }, { name = "protobuf" }, ] +build = [ + { name = "cmake" }, + { name = "setuptools" }, +] can-dep = [ { name = "python-can" }, ] damiao = [ { name = "python-can" }, ] +dataset = [ + { name = "av" }, + { name = "datasets" }, + { name = "jsonlines" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] +dataset-viz = [ + { name = "av" }, + { name = "datasets" }, + { name = "jsonlines" }, + { name = "rerun-sdk" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] dev = [ + { name = "accelerate" }, + { name = "av" }, + { name = "datasets" }, { name = "debugpy" }, + { name = "deepdiff" }, + { name = "diffusers" }, { name = "grpcio" }, { name = "grpcio-tools" }, + { name = "jsonlines" }, { name = "mypy" }, { name = "pre-commit" }, { name = "protobuf" }, + { name = "pynput" }, + { name = "pyserial" }, + { name = "rerun-sdk" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "wandb" }, ] dynamixel = [ { name = "dynamixel-sdk" }, ] +evaluation = [ + { name = "av" }, +] feetech = [ { name = "feetech-servo-sdk" }, ] @@ -2307,6 +2346,11 @@ grpcio-dep = [ { name = "grpcio" }, { name = "protobuf" }, ] +hardware = [ + { name = "deepdiff" }, + { name = "pynput" }, + { name = "pyserial" }, +] hilserl = [ { name = "grpcio" }, { name = "gym-hil" }, @@ -2330,8 +2374,12 @@ lekiwi = [ { name = "pyzmq" }, ] libero = [ + { name = "av" }, + { name = "datasets" }, { name = "hf-libero", marker = "sys_platform == 'linux'" }, + { name = "jsonlines" }, { name = "scipy" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, { name = "transformers" }, ] matplotlib-dep = [ @@ -2339,8 +2387,12 @@ matplotlib-dep = [ { name = "matplotlib" }, ] metaworld = [ + { name = "av" }, + { name = "datasets" }, + { name = "jsonlines" }, { name = "metaworld" }, { name = "scipy" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, ] multi-task-dit = [ { name = "transformers" }, @@ -2369,8 +2421,12 @@ placo-dep = [ { name = "placo" }, ] pusht = [ + { name = "av" }, + { name = "datasets" }, { name = "gym-pusht" }, + { name = "jsonlines" }, { name = "pymunk" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, ] pygame-dep = [ { name = "pygame" }, @@ -2381,6 +2437,16 @@ qwen-vl-utils-dep = [ reachy2 = [ { name = "reachy2-sdk" }, ] +robot = [ + { name = "av" }, + { name = "datasets" }, + { name = "deepdiff" }, + { name = "jsonlines" }, + { name = "pynput" }, + { name = "pyserial" }, + { name = "rerun-sdk" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, +] robstride = [ { name = "python-can" }, ] @@ -2401,10 +2467,39 @@ smolvla = [ { name = "transformers" }, ] test = [ + { name = "accelerate" }, + { name = "av" }, + { name = "datasets" }, + { name = "deepdiff" }, + { name = "diffusers" }, + { name = "jsonlines" }, { name = "mock-serial", marker = "sys_platform != 'win32'" }, + { name = "pynput" }, + { name = "pyserial" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-timeout" }, + { name = "rerun-sdk" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "wandb" }, +] +train = [ + { name = "accelerate" }, + { name = "av" }, + { name = "datasets" }, + { name = "diffusers" }, + { name = "jsonlines" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "wandb" }, +] +training = [ + { name = "accelerate" }, + { name = "av" }, + { name = "datasets" }, + { name = "diffusers" }, + { name = "jsonlines" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { name = "wandb" }, ] transformers-dep = [ { name = "transformers" }, @@ -2422,6 +2517,9 @@ video-benchmark = [ { name = "pandas" }, { name = "scikit-image" }, ] +viz = [ + { name = "rerun-sdk" }, +] wallx = [ { name = "peft" }, { name = "qwen-vl-utils" }, @@ -2435,16 +2533,17 @@ xvla = [ [package.metadata] requires-dist = [ - { name = "accelerate", specifier = ">=1.10.0,<2.0.0" }, { name = "accelerate", marker = "extra == 'smolvla'", specifier = ">=1.7.0,<2.0.0" }, - { name = "av", specifier = ">=15.0.0,<16.0.0" }, - { name = "cmake", specifier = ">=3.29.0.1,<4.2.0" }, + { name = "accelerate", marker = "extra == 'train'", specifier = ">=1.10.0,<2.0.0" }, + { name = "av", marker = "extra == 'dataset'", specifier = ">=15.0.0,<16.0.0" }, + { name = "av", marker = "extra == 'evaluation'", specifier = ">=15.0.0,<16.0.0" }, + { name = "cmake", marker = "extra == 'build'", specifier = ">=3.29.0.1,<4.2.0" }, { name = "contourpy", marker = "extra == 'matplotlib-dep'", specifier = ">=1.3.0,<2.0.0" }, - { name = "datasets", specifier = ">=4.0.0,<5.0.0" }, + { name = "datasets", marker = "extra == 'dataset'", specifier = ">=4.0.0,<5.0.0" }, { name = "debugpy", marker = "extra == 'dev'", specifier = ">=1.8.1,<1.9.0" }, { name = "decord", marker = "(platform_machine == 'AMD64' and extra == 'groot') or (platform_machine == 'x86_64' and extra == 'groot')", specifier = ">=0.6.0,<1.0.0" }, - { name = "deepdiff", specifier = ">=7.0.1,<9.0.0" }, - { name = "diffusers", specifier = ">=0.27.2,<0.36.0" }, + { name = "deepdiff", marker = "extra == 'hardware'", specifier = ">=7.0.1,<9.0.0" }, + { name = "diffusers", marker = "extra == 'train'", specifier = ">=0.27.2,<0.36.0" }, { name = "dm-tree", marker = "extra == 'groot'", specifier = ">=0.1.8,<1.0.0" }, { name = "draccus", specifier = "==0.10.0" }, { name = "dynamixel-sdk", marker = "extra == 'dynamixel'", specifier = ">=3.7.31,<3.9.0" }, @@ -2463,13 +2562,23 @@ requires-dist = [ { name = "hf-libero", marker = "sys_platform == 'linux' and extra == 'libero'", specifier = ">=0.1.3,<0.2.0" }, { name = "hidapi", marker = "extra == 'gamepad'", specifier = ">=0.14.0,<0.15.0" }, { name = "huggingface-hub", specifier = ">=1.0.0,<2.0.0" }, - { name = "imageio", extras = ["ffmpeg"], specifier = ">=2.34.0,<3.0.0" }, - { name = "jsonlines", specifier = ">=4.0.0,<5.0.0" }, + { name = "jsonlines", marker = "extra == 'dataset'", specifier = ">=4.0.0,<5.0.0" }, { name = "lerobot", extras = ["aloha"], marker = "extra == 'all'" }, { name = "lerobot", extras = ["async"], marker = "extra == 'all'" }, + { name = "lerobot", extras = ["build"], marker = "extra == 'all'" }, { name = "lerobot", extras = ["can-dep"], marker = "extra == 'damiao'" }, { name = "lerobot", extras = ["can-dep"], marker = "extra == 'robstride'" }, { name = "lerobot", extras = ["damiao"], marker = "extra == 'openarms'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'all'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'aloha'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'dataset-viz'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'dev'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'libero'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'metaworld'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'pusht'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'robot'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'test'" }, + { name = "lerobot", extras = ["dataset"], marker = "extra == 'train'" }, { name = "lerobot", extras = ["dev"], marker = "extra == 'all'" }, { name = "lerobot", extras = ["dynamixel"], marker = "extra == 'all'" }, { name = "lerobot", extras = ["feetech"], marker = "extra == 'hopejr'" }, @@ -2478,6 +2587,10 @@ requires-dist = [ { name = "lerobot", extras = ["grpcio-dep"], marker = "extra == 'async'" }, { name = "lerobot", extras = ["grpcio-dep"], marker = "extra == 'dev'" }, { name = "lerobot", extras = ["grpcio-dep"], marker = "extra == 'hilserl'" }, + { name = "lerobot", extras = ["hardware"], marker = "extra == 'all'" }, + { name = "lerobot", extras = ["hardware"], marker = "extra == 'dev'" }, + { name = "lerobot", extras = ["hardware"], marker = "extra == 'robot'" }, + { name = "lerobot", extras = ["hardware"], marker = "extra == 'test'" }, { name = "lerobot", extras = ["hilserl"], marker = "extra == 'all'" }, { name = "lerobot", extras = ["hopejr"], marker = "extra == 'all'" }, { name = "lerobot", extras = ["intelrealsense"], marker = "extra == 'all'" }, @@ -2512,6 +2625,10 @@ requires-dist = [ { name = "lerobot", extras = ["scipy-dep"], marker = "extra == 'wallx'" }, { name = "lerobot", extras = ["smolvla"], marker = "extra == 'all'" }, { name = "lerobot", extras = ["test"], marker = "extra == 'all'" }, + { name = "lerobot", extras = ["train"], marker = "extra == 'all'" }, + { name = "lerobot", extras = ["train"], marker = "extra == 'dev'" }, + { name = "lerobot", extras = ["train"], marker = "extra == 'test'" }, + { name = "lerobot", extras = ["train"], marker = "extra == 'training'" }, { name = "lerobot", extras = ["transformers-dep"], marker = "extra == 'groot'" }, { name = "lerobot", extras = ["transformers-dep"], marker = "extra == 'hilserl'" }, { name = "lerobot", extras = ["transformers-dep"], marker = "extra == 'libero'" }, @@ -2523,6 +2640,11 @@ requires-dist = [ { name = "lerobot", extras = ["transformers-dep"], marker = "extra == 'wallx'" }, { name = "lerobot", extras = ["transformers-dep"], marker = "extra == 'xvla'" }, { name = "lerobot", extras = ["video-benchmark"], marker = "extra == 'all'" }, + { name = "lerobot", extras = ["viz"], marker = "extra == 'all'" }, + { name = "lerobot", extras = ["viz"], marker = "extra == 'dataset-viz'" }, + { name = "lerobot", extras = ["viz"], marker = "extra == 'dev'" }, + { name = "lerobot", extras = ["viz"], marker = "extra == 'robot'" }, + { name = "lerobot", extras = ["viz"], marker = "extra == 'test'" }, { name = "lerobot", extras = ["wallx"], marker = "extra == 'all'" }, { name = "lerobot", extras = ["xvla"], marker = "extra == 'all'" }, { name = "matplotlib", marker = "extra == 'matplotlib-dep'", specifier = ">=3.10.3,<4.0.0" }, @@ -2545,10 +2667,10 @@ requires-dist = [ { name = "protobuf", marker = "extra == 'grpcio-dep'", specifier = ">=6.31.1,<6.32.0" }, { name = "pygame", marker = "extra == 'pygame-dep'", specifier = ">=2.5.1,<2.7.0" }, { name = "pymunk", marker = "extra == 'pusht'", specifier = ">=6.6.0,<7.0.0" }, - { name = "pynput", specifier = ">=1.7.8,<1.9.0" }, + { name = "pynput", marker = "extra == 'hardware'", specifier = ">=1.7.8,<1.9.0" }, { name = "pyrealsense2", marker = "sys_platform != 'darwin' and extra == 'intelrealsense'", specifier = ">=2.55.1.6486,<2.57.0" }, { name = "pyrealsense2-macosx", marker = "sys_platform == 'darwin' and extra == 'intelrealsense'", specifier = ">=2.54,<2.57.0" }, - { name = "pyserial", specifier = ">=3.5,<4.0" }, + { name = "pyserial", marker = "extra == 'hardware'", specifier = ">=3.5,<4.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.1.0,<9.0.0" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0.0,<8.0.0" }, { name = "pytest-timeout", marker = "extra == 'test'", specifier = ">=2.4.0,<3.0.0" }, @@ -2557,24 +2679,24 @@ requires-dist = [ { name = "pyzmq", marker = "extra == 'unitree-g1'", specifier = ">=26.2.1,<28.0.0" }, { name = "qwen-vl-utils", marker = "extra == 'qwen-vl-utils-dep'", specifier = ">=0.0.11,<0.1.0" }, { name = "reachy2-sdk", marker = "extra == 'reachy2'", specifier = ">=1.0.15,<1.1.0" }, - { name = "rerun-sdk", specifier = ">=0.24.0,<0.27.0" }, + { name = "rerun-sdk", marker = "extra == 'viz'", specifier = ">=0.24.0,<0.27.0" }, { name = "safetensors", marker = "extra == 'groot'", specifier = ">=0.4.3,<1.0.0" }, { name = "safetensors", marker = "extra == 'smolvla'", specifier = ">=0.4.3,<1.0.0" }, { name = "scikit-image", marker = "extra == 'video-benchmark'", specifier = ">=0.23.2,<0.26.0" }, { name = "scipy", marker = "extra == 'all'", specifier = ">=1.14.0,<2.0.0" }, { name = "scipy", marker = "extra == 'scipy-dep'", specifier = ">=1.14.0,<2.0.0" }, - { name = "setuptools", specifier = ">=71.0.0,<81.0.0" }, + { name = "setuptools", marker = "extra == 'build'", specifier = ">=71.0.0,<81.0.0" }, { name = "teleop", marker = "extra == 'phone'", specifier = ">=0.1.0,<0.2.0" }, { name = "termcolor", specifier = ">=2.4.0,<4.0.0" }, { name = "timm", marker = "extra == 'groot'", specifier = ">=1.0.0,<1.1.0" }, { name = "torch", specifier = ">=2.7,<2.11.0" }, - { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux') or (platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", specifier = ">=0.3.0,<0.11.0" }, + { name = "torchcodec", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux' and extra == 'dataset') or (platform_machine != 'x86_64' and sys_platform == 'darwin' and extra == 'dataset') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'dataset')", specifier = ">=0.3.0,<0.11.0" }, { name = "torchdiffeq", marker = "extra == 'wallx'", specifier = ">=0.2.4,<0.3.0" }, { name = "torchvision", specifier = ">=0.22.0,<0.26.0" }, { name = "transformers", marker = "extra == 'transformers-dep'", specifier = "==5.3.0" }, - { name = "wandb", specifier = ">=0.24.0,<0.25.0" }, + { name = "wandb", marker = "extra == 'train'", specifier = ">=0.24.0,<0.25.0" }, ] -provides-extras = ["pygame-dep", "placo-dep", "transformers-dep", "grpcio-dep", "can-dep", "peft-dep", "scipy-dep", "qwen-vl-utils-dep", "matplotlib-dep", "feetech", "dynamixel", "damiao", "robstride", "openarms", "gamepad", "hopejr", "lekiwi", "unitree-g1", "reachy2", "kinematics", "intelrealsense", "phone", "wallx", "pi", "smolvla", "multi-task-dit", "groot", "sarm", "xvla", "hilserl", "async", "peft", "dev", "test", "video-benchmark", "aloha", "pusht", "libero", "metaworld", "all"] +provides-extras = ["dataset", "train", "hardware", "viz", "build", "robot", "evaluation", "training", "dataset-viz", "pygame-dep", "placo-dep", "transformers-dep", "grpcio-dep", "can-dep", "peft-dep", "scipy-dep", "qwen-vl-utils-dep", "matplotlib-dep", "feetech", "dynamixel", "damiao", "robstride", "openarms", "gamepad", "hopejr", "lekiwi", "unitree-g1", "reachy2", "kinematics", "intelrealsense", "phone", "wallx", "pi", "smolvla", "multi-task-dit", "groot", "sarm", "xvla", "hilserl", "async", "peft", "dev", "test", "video-benchmark", "aloha", "pusht", "libero", "metaworld", "all"] [[package]] name = "librt" @@ -3359,7 +3481,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -3370,7 +3492,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -3397,9 +3519,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -3410,7 +3532,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -4231,10 +4353,10 @@ name = "pyobjc-framework-applicationservices" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "sys_platform != 'linux'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'linux'" }, - { name = "pyobjc-framework-coretext", marker = "sys_platform != 'linux'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform != 'linux'" }, + { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-framework-coretext", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" } wheels = [ @@ -4250,7 +4372,7 @@ name = "pyobjc-framework-cocoa" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "sys_platform != 'linux'" }, + { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } wheels = [ @@ -4266,9 +4388,9 @@ name = "pyobjc-framework-coretext" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "sys_platform != 'linux'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'linux'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform != 'linux'" }, + { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" } wheels = [ @@ -4284,8 +4406,8 @@ name = "pyobjc-framework-quartz" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "sys_platform != 'linux'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'linux'" }, + { name = "pyobjc-core", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform != 'emscripten' and sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } wheels = [