From e69322442d3d17161c42315760f291995c4f26c4 Mon Sep 17 00:00:00 2001 From: Clay Rosenthal Date: Wed, 1 Jul 2026 21:47:45 +0000 Subject: [PATCH] Ship .pyi type stub deterministically + guard py.typed The published wheels shipped py.typed without a .pyi stub (pybind11-stubgen ran best-effort/silently-non-fatal while py.typed was touched unconditionally), so mypy saw a typed-but-empty module and reported attr-defined on every symbol downstream (broke holosoma static checks). - cibw_before_build.sh: stage the committed python_binding/unitree_interface.pyi deterministically; stubgen only as a fallback that must produce a file; hard guard refusing to write py.typed without a .pyi (exit 1). - unitree_interface.pyi: fix 5 latent stub bugs (3 zero-member enums -> X=..., 3 __init__ overloads -> @overload). - version 0.1.4 -> 0.1.5 (pyproject + uv.lock). --- pyproject.toml | 2 +- python_binding/unitree_interface.pyi | 35 +++++++++++++++------------- scripts/cibw_before_build.sh | 35 +++++++++++++++++++++------- uv.lock | 2 +- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0658fc30..4ad106b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" # PyPI distribution name. Prefixed with "far-" to avoid colliding with any # upstream Unitree project. The IMPORT name is unchanged: `import unitree_interface`. name = "far-unitree-sdk" -version = "0.1.4" +version = "0.1.5" description = "Unitree robot SDK Python bindings (FAR fork)" readme = "README.md" # Binary-only distribution (no sdist fallback): every interpreter allowed here diff --git a/python_binding/unitree_interface.pyi b/python_binding/unitree_interface.pyi index a0555e8a..309b6ad2 100644 --- a/python_binding/unitree_interface.pyi +++ b/python_binding/unitree_interface.pyi @@ -1,7 +1,7 @@ """ Type definitions for General Unitree Interface Python bindings """ -from typing import List, Any, Optional +from typing import List, Any, Optional, overload from enum import Enum # Constants @@ -18,21 +18,21 @@ GO2_GO2_CONFIG: RobotConfig class RobotType(Enum): """Robot types supported by the interface""" - G1: int # G1 humanoid (29 motors) - H1: int # H1 humanoid (19 motors) - H1_2: int # H1-2 humanoid (29 motors) - GO2: int # GO2 quadruped (12 motors) - CUSTOM: int # Custom robot with specified motor count + G1 = ... # G1 humanoid (29 motors) + H1 = ... # H1 humanoid (19 motors) + H1_2 = ... # H1-2 humanoid (29 motors) + GO2 = ... # GO2 quadruped (12 motors) + CUSTOM = ... # Custom robot with specified motor count class MessageType(Enum): """Message types for robot communication""" - HG: int # Humanoid/Go1 message format - GO2: int # Go2 message format + HG = ... # Humanoid/Go1 message format + GO2 = ... # Go2 message format class ControlMode(Enum): """Control mode for robots""" - PR: int # Pitch/Roll mode - AB: int # A/B mode + PR = ... # Pitch/Roll mode + AB = ... # A/B mode class RobotConfig: """Robot configuration structure""" @@ -103,12 +103,13 @@ class LowState: class UnitreeInterface: """Main interface class for general Unitree robot control""" - - def __init__(self, network_interface: str, robot_type: RobotType, + + @overload + def __init__(self, network_interface: str, robot_type: RobotType, message_type: MessageType) -> None: """ Initialize UnitreeInterface with robot type and message type - + Args: network_interface: Network interface name (e.g., "eth0", "enp2s0") robot_type: Type of robot (G1, H1, H1_2, GO2, CUSTOM) @@ -116,21 +117,23 @@ class UnitreeInterface: """ ... + @overload def __init__(self, network_interface: str, config: RobotConfig) -> None: """ Initialize UnitreeInterface with robot configuration - + Args: network_interface: Network interface name (e.g., "eth0", "enp2s0") config: Robot configuration """ ... - def __init__(self, network_interface: str, robot_type: RobotType, + @overload + def __init__(self, network_interface: str, robot_type: RobotType, message_type: MessageType, num_motors: int) -> None: """ Initialize UnitreeInterface with custom motor count - + Args: network_interface: Network interface name (e.g., "eth0", "enp2s0") robot_type: Type of robot (G1, H1, H1_2, GO2, CUSTOM) diff --git a/scripts/cibw_before_build.sh b/scripts/cibw_before_build.sh index 8ba0f6be..88c1c46a 100755 --- a/scripts/cibw_before_build.sh +++ b/scripts/cibw_before_build.sh @@ -53,17 +53,36 @@ cp -v "thirdparty/lib/$ARCH/"*.so* unitree_interface/ 2>/dev/null || true [ -f libfastcdr.so ] && ln -sf libfastcdr.so libfastcdr.so.2 || true ) -# Generate type stubs (best-effort; never fail the build over stubs). Importing -# the module requires its staged DDS deps on the loader path. Wrapped in `if` so -# a stubgen/import failure is non-fatal under `set -e`. -if PYTHONPATH="unitree_interface:${PYTHONPATH:-}" \ - LD_LIBRARY_PATH="$PWD/unitree_interface:${LD_LIBRARY_PATH:-}" \ - pybind11-stubgen -o "$BUILD_DIR/stubs" unitree_interface >/dev/null 2>&1; then - cp -v "$BUILD_DIR/stubs/unitree_interface.pyi" unitree_interface/ 2>/dev/null || true +# Stage the type stub. The authoritative, hand-maintained stub lives at +# python_binding/unitree_interface.pyi and is the source of truth for the public +# API — prefer it so the wheel is reproducible and does not depend on +# pybind11-stubgen succeeding inside the manylinux container (importing the freshly +# linked extension there is fragile because of the vendored DDS .so deps). +# pybind11-stubgen is only a fallback for a checkout that somehow lacks the committed +# stub, and it must actually produce the file — no silent skip. +if [ -f python_binding/unitree_interface.pyi ]; then + cp -v python_binding/unitree_interface.pyi unitree_interface/unitree_interface.pyi + echo "[before-build] staged committed stub python_binding/unitree_interface.pyi" +elif PYTHONPATH="unitree_interface:${PYTHONPATH:-}" \ + LD_LIBRARY_PATH="$PWD/unitree_interface:${LD_LIBRARY_PATH:-}" \ + pybind11-stubgen -o "$BUILD_DIR/stubs" unitree_interface \ + && [ -f "$BUILD_DIR/stubs/unitree_interface.pyi" ]; then + cp -v "$BUILD_DIR/stubs/unitree_interface.pyi" unitree_interface/unitree_interface.pyi + echo "[before-build] staged stubgen-generated stub" else - echo "[before-build] stub generation skipped (non-fatal)" + echo "[before-build] ERROR: no unitree_interface.pyi available (committed stub missing and stubgen failed)" >&2 + exit 1 fi +# Ship py.typed ONLY alongside a real .pyi. A py.typed marker without a stub makes +# mypy treat the compiled extension as a typed-but-empty module and report +# `attr-defined` on every symbol downstream (exactly the holosoma CI break this +# fixes) — worse than shipping no type info at all. Fail loudly rather than +# regress that. +if [ ! -f unitree_interface/unitree_interface.pyi ]; then + echo "[before-build] ERROR: refusing to write py.typed without a .pyi stub" >&2 + exit 1 +fi touch unitree_interface/py.typed echo "[before-build] staged files:" ls -la unitree_interface/ diff --git a/uv.lock b/uv.lock index cb1627ad..36840f10 100644 --- a/uv.lock +++ b/uv.lock @@ -4,5 +4,5 @@ requires-python = ">=3.8, <3.13" [[package]] name = "far-unitree-sdk" -version = "0.1.4" +version = "0.1.5" source = { editable = "." }