From b43dc39ba4580570693e10f150f60ef0154c46eb Mon Sep 17 00:00:00 2001 From: Pepijn Date: Mon, 27 Apr 2026 14:15:03 +0200 Subject: [PATCH] Add docstrings to all new helpers; revert uv.lock Covers private helpers in recipe.py, language.py, language_render.py, and render_messages_processor.py. Also reverts uv.lock to main (it was re-generated by `uv run` during local checks). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lerobot/configs/recipe.py | 9 +++++ src/lerobot/datasets/language.py | 2 + src/lerobot/datasets/language_render.py | 21 ++++++++++ .../processor/render_messages_processor.py | 2 + uv.lock | 40 +++++++++---------- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/lerobot/configs/recipe.py b/src/lerobot/configs/recipe.py index 7c91123eb..ef496c3c4 100644 --- a/src/lerobot/configs/recipe.py +++ b/src/lerobot/configs/recipe.py @@ -58,6 +58,7 @@ class MessageTurn: tool_calls_from: str | None = None def __post_init__(self) -> None: + """Validate role, stream, and content after dataclass construction.""" if self.role not in _VALID_ROLES: raise ValueError(f"Unsupported message role: {self.role!r}") if self.stream is not None and self.stream not in _VALID_STREAMS: @@ -75,6 +76,7 @@ class MessageTurn: @classmethod def from_dict(cls, data: dict[str, Any]) -> MessageTurn: + """Construct a :class:`MessageTurn` from a plain dictionary.""" return cls(**data) @@ -93,6 +95,7 @@ class TrainingRecipe: weight: float | None = None def __post_init__(self) -> None: + """Validate that exactly one of ``messages`` or ``blend`` is set.""" if self.messages is not None and self.blend is not None: raise ValueError("TrainingRecipe must set only one of messages or blend.") if self.messages is None and self.blend is None: @@ -105,6 +108,7 @@ class TrainingRecipe: @classmethod def from_dict(cls, data: dict[str, Any]) -> TrainingRecipe: + """Construct a :class:`TrainingRecipe` from a nested dictionary.""" data = dict(data) if data.get("messages") is not None: data["messages"] = [ @@ -120,6 +124,7 @@ class TrainingRecipe: @classmethod def from_yaml(cls, path: str | Path) -> TrainingRecipe: + """Load a :class:`TrainingRecipe` from a YAML file at ``path``.""" import yaml # type: ignore[import-untyped] with open(path) as f: @@ -129,6 +134,7 @@ class TrainingRecipe: return cls.from_dict(data) def _validate_message_recipe(self) -> None: + """Ensure every templated binding is known and at least one turn is a target.""" assert self.messages is not None known_bindings = set(DEFAULT_BINDINGS) | set(self.bindings or {}) | {"task"} @@ -141,6 +147,7 @@ class TrainingRecipe: raise ValueError("Message recipes must contain at least one target turn.") def _validate_blend_recipe(self) -> None: + """Ensure each blend component is a non-empty, weighted message recipe.""" assert self.blend is not None if not self.blend: raise ValueError("Blend recipes must contain at least one component.") @@ -156,6 +163,7 @@ class TrainingRecipe: raise ValueError(f"Blend component {name!r} must have a positive weight.") def _referenced_bindings(self, turn: MessageTurn) -> set[str]: + """Return the binding names that ``turn`` references via placeholders or attributes.""" names: set[str] = set() if turn.if_present is not None: names.add(turn.if_present) @@ -166,6 +174,7 @@ class TrainingRecipe: def _placeholders_in_content(content: str | list[dict[str, Any]] | None) -> set[str]: + """Return the set of ``${name}`` placeholders found anywhere in ``content``.""" if content is None: return set() if isinstance(content, str): diff --git a/src/lerobot/datasets/language.py b/src/lerobot/datasets/language.py index 10e749b84..cc0f70bf9 100644 --- a/src/lerobot/datasets/language.py +++ b/src/lerobot/datasets/language.py @@ -38,10 +38,12 @@ LanguageColumn = Literal["language_persistent", "language_events"] def _json_arrow_type() -> pa.DataType: + """Return the Arrow JSON type, falling back to ``string`` on older pyarrow.""" return pa.json_() if hasattr(pa, "json_") else pa.string() def _json_feature() -> object: + """Return the HF ``datasets`` JSON feature, falling back to a string value.""" return datasets.Json() if hasattr(datasets, "Json") else datasets.Value("string") diff --git a/src/lerobot/datasets/language_render.py b/src/lerobot/datasets/language_render.py index bf78aeec8..954ac8141 100644 --- a/src/lerobot/datasets/language_render.py +++ b/src/lerobot/datasets/language_render.py @@ -175,6 +175,7 @@ def render_sample( def _select_recipe(recipe: TrainingRecipe, sample_idx: int) -> TrainingRecipe: + """Pick a deterministic blend component for ``sample_idx`` (or return ``recipe``).""" if recipe.blend is None: return recipe @@ -204,6 +205,7 @@ def _resolve_bindings( task: str | None, dataset_ctx: Any | None, ) -> dict[str, LanguageRow | str | None]: + """Resolve every binding in ``recipe`` (plus ``task``) at time ``t``.""" bindings: dict[str, LanguageRow | str | None] = {"task": _resolve_task(task, dataset_ctx)} specs = {**DEFAULT_BINDINGS, **(recipe.bindings or {})} for name, spec in specs.items(): @@ -212,6 +214,7 @@ def _resolve_bindings( def _resolve_task(task: str | None, dataset_ctx: Any | None) -> str | None: + """Return ``task`` if set, otherwise look it up on ``dataset_ctx``.""" if task is not None: return task if dataset_ctx is None: @@ -228,6 +231,7 @@ def _resolve_spec( events: Sequence[LanguageRow], t: float, ) -> LanguageRow | None: + """Parse a single binding's resolver expression and dispatch to its function.""" match = _RESOLVER_RE.match(spec.strip()) if match is None: raise ValueError(f"Invalid resolver expression: {spec!r}") @@ -247,6 +251,7 @@ def _resolve_spec( def _parse_resolver_args(args: str) -> dict[str, Any]: + """Parse a comma-separated resolver argument list into a kwargs dict.""" kwargs: dict[str, Any] = {} if not args.strip(): return kwargs @@ -270,6 +275,7 @@ def _render_message_recipe( recipe: TrainingRecipe, bindings: dict[str, LanguageRow | str | None], ) -> RenderedMessages | None: + """Expand ``recipe.messages`` into rendered chat messages using ``bindings``.""" assert recipe.messages is not None messages: list[dict[str, Any]] = [] streams: list[str | None] = [] @@ -311,6 +317,7 @@ def _render_content( content: str | list[dict[str, Any]], bindings: dict[str, LanguageRow | str | None], ) -> str | list[dict[str, Any]]: + """Substitute bindings into a string or each string field of multimodal blocks.""" if isinstance(content, str): return _substitute(content, bindings) @@ -325,7 +332,10 @@ def _render_content( def _substitute(template: str, bindings: dict[str, LanguageRow | str | None]) -> str: + """Replace ``${name}`` placeholders in ``template`` with their bound values.""" + def replace(match: re.Match[str]) -> str: + """Resolve a single ``${name}`` match to its bound string value.""" name = match.group(1) if name not in bindings: raise ValueError(f"Unknown template binding: {name!r}") @@ -341,6 +351,7 @@ def _substitute(template: str, bindings: dict[str, LanguageRow | str | None]) -> def _validate_rendered(rendered: RenderedMessages) -> None: + """Sanity-check the rendered output for stream/target alignment.""" messages = rendered["messages"] streams = rendered["message_streams"] target_indices = rendered["target_message_indices"] @@ -367,6 +378,7 @@ def _nth_relative( tool_name: str | None, resolver_name: str, ) -> LanguageRow | None: + """Shared body for ``nth_prev`` / ``nth_next`` with signed ``offset``.""" _validate_persistent_resolver(resolver_name, style) if abs(offset) < 1: raise ValueError(f"{resolver_name} offset must be non-zero.") @@ -393,6 +405,7 @@ def _nth_relative( def _validate_persistent_resolver(resolver_name: str, style: str | None) -> None: + """Reject calls with missing or event-only ``style`` for persistent resolvers.""" if style is None: raise ValueError(f"{resolver_name} requires a persistent style.") if style in EVENT_ONLY_STYLES: @@ -408,6 +421,7 @@ def _matching_rows( role: str | None, tool_name: str | None, ) -> list[LanguageRow]: + """Return ``rows`` filtered by optional ``style``/``role``/``tool_name`` selectors.""" return [ row for row in rows @@ -424,6 +438,7 @@ def _select_latest( role: str | None, tool_name: str | None, ) -> LanguageRow | None: + """Return the row tied for the latest ``timestamp`` (disambiguated by selectors).""" if not rows: return None rows = sorted(rows, key=_persistent_sort_key) @@ -445,6 +460,7 @@ def _select_one( tool_name: str | None, sort_key: Any, ) -> LanguageRow | None: + """Return the single matching row, or raise if the selectors are ambiguous.""" if not rows: return None if len(rows) > 1 and role is None and tool_name is None: @@ -455,19 +471,23 @@ def _select_one( def _persistent_sort_key(row: LanguageRow) -> tuple[float, str, str]: + """Sort key for persistent rows: ``(timestamp, style, role)``.""" return (_timestamp(row), row.get("style") or "", row.get("role") or "") def _event_sort_key(row: LanguageRow) -> tuple[str, str]: + """Sort key for event rows: ``(style, role)`` (timestamp is implicit in the frame).""" return (row.get("style") or "", row.get("role") or "") def _timestamp(row: LanguageRow) -> float: + """Extract a row's ``timestamp`` as a Python float (unwrapping numpy scalars).""" value = row["timestamp"] return float(value.item() if hasattr(value, "item") else value) def _row_has_tool_name(row: LanguageRow, tool_name: str) -> bool: + """Return ``True`` if any of the row's tool calls invokes ``tool_name``.""" for tool_call in row.get("tool_calls") or []: if isinstance(tool_call, str): continue @@ -478,6 +498,7 @@ def _row_has_tool_name(row: LanguageRow, tool_name: str) -> bool: def _normalize_rows(rows: Sequence[Any]) -> list[LanguageRow]: + """Convert pyarrow scalars / mappings into a fresh list of plain dict rows.""" normalized = [] for row in rows: if row is None: diff --git a/src/lerobot/processor/render_messages_processor.py b/src/lerobot/processor/render_messages_processor.py index f7156ff57..7d88fab73 100644 --- a/src/lerobot/processor/render_messages_processor.py +++ b/src/lerobot/processor/render_messages_processor.py @@ -79,10 +79,12 @@ class RenderMessagesStep(ProcessorStep): def transform_features( self, features: dict[PipelineFeatureType, dict[str, PolicyFeature]] ) -> dict[PipelineFeatureType, dict[str, PolicyFeature]]: + """Pass features through unchanged; rendering only touches complementary data.""" return features def _scalar(value: Any) -> float | int: + """Unwrap a tensor/array/single-element list into a Python scalar.""" if hasattr(value, "item"): return value.item() if isinstance(value, list) and len(value) == 1: diff --git a/uv.lock b/uv.lock index e7dc8882d..6e141f11d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "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'", @@ -951,7 +951,7 @@ name = "cuda-bindings" version = "12.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "cuda-pathfinder", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' 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" }, @@ -1038,7 +1038,7 @@ name = "decord" version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { 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')" }, + { name = "numpy", marker = "(platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l') or 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" }, @@ -2993,7 +2993,7 @@ requires-dist = [ { name = "av", marker = "extra == 'av-dep'", specifier = ">=15.0.0,<16.0.0" }, { name = "cmake", specifier = ">=3.29.0.1,<4.2.0" }, { name = "contourpy", marker = "extra == 'matplotlib-dep'", specifier = ">=1.3.0,<2.0.0" }, - { name = "datasets", marker = "extra == 'dataset'", specifier = ">=4.7.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", marker = "extra == 'deepdiff-dep'", specifier = ">=7.0.1,<9.0.0" }, @@ -4039,7 +4039,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { 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-cublas-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' 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" }, @@ -4050,7 +4050,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-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 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" }, @@ -4077,9 +4077,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { 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'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'armv7l' 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" }, @@ -4090,7 +4090,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-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 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" }, @@ -4933,10 +4933,10 @@ name = "pyobjc-framework-applicationservices" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-coretext", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-quartz", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { 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 = [ @@ -4952,7 +4952,7 @@ name = "pyobjc-framework-cocoa" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { 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 = [ @@ -4968,9 +4968,9 @@ name = "pyobjc-framework-coretext" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-quartz", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { 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 = [ @@ -4986,8 +4986,8 @@ name = "pyobjc-framework-quartz" version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "pyobjc-framework-cocoa", marker = "(platform_machine != 's390x' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')" }, + { 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 = [