From df3280dce0d7e894dc20f0eba1713ff6ff1a05be Mon Sep 17 00:00:00 2001 From: jeqcho Date: Mon, 13 Jul 2026 17:53:30 -0700 Subject: [PATCH] docs: enforce public docstring coverage via ruff D1 and backfill Enable Ruff's public pydocstyle rules and exempt tests plus constructor and magic-method boilerplate. Backfill package contracts and document the gate so new public APIs cannot regress. Interrogate public docstring coverage: 49.3% before, 97.4% after. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 ++ README.md | 3 +++ pyproject.toml | 6 +++++- src/kitchenbench/distributions.py | 16 ++++++++++++++-- src/kitchenbench/embodiment.py | 3 +++ src/kitchenbench/instances.py | 1 + src/kitchenbench/policies.py | 6 ++++++ src/kitchenbench/sim/blueprint.py | 1 + src/kitchenbench/sim/success.py | 2 ++ src/kitchenbench/tasks.py | 10 ++++++++++ 10 files changed, 47 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3b1ecda..7f21de2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,6 +62,8 @@ success probability **P̂[Yᵢ=1]**. Instances are AI-authored drafts (strict), `pytest --cov` at **100% coverage**. Pre-commit runs ruff+mypy on commit and the coverage gate on push (via `uv run`). CI (Linux+macOS × py3.11/3.12) and the 100% gate are **required, blocking PR checks**. + Every public module, class, and function needs a docstring, enforced by Ruff + D1; state the contract instead of restating the symbol name. - **Authoring imports come from the top-level `inspect_robots` package** (its public API, stable since v0.3.0): `from inspect_robots import Task, Scene, Target, task, ActionChunk, ...`. Don't import from `inspect_robots.` unless a symbol diff --git a/README.md b/README.md index b2eb804..f77cbfb 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,9 @@ uv run pytest --cov # 100% coverage required uv run ruff check . && uv run mypy ``` +Every public module, class, and function needs a docstring, enforced by Ruff D1; +state the contract instead of restating the symbol name. + ## Citation If you use KitchenBench in your research, please cite it: diff --git a/pyproject.toml b/pyproject.toml index 7cabd71..e4bfe41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,11 @@ target-version = "py310" src = ["src", "tests"] [tool.ruff.lint] -select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM", "RUF"] +select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM", "RUF", "D1"] +ignore = ["D105", "D107"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["D1"] [tool.ruff.lint.isort] known-first-party = ["kitchenbench"] diff --git a/src/kitchenbench/distributions.py b/src/kitchenbench/distributions.py index e303b63..5c99fff 100644 --- a/src/kitchenbench/distributions.py +++ b/src/kitchenbench/distributions.py @@ -31,9 +31,13 @@ class Distribution(Protocol): """A sampleable, self-describing setup variable.""" - def sample(self, rng: np.random.Generator) -> Scalar: ... + def sample(self, rng: np.random.Generator) -> Scalar: + """Draw one JSON-native scalar from the distribution.""" + ... - def describe(self) -> str: ... + def describe(self) -> str: + """Render the distribution in physical-automation methodology notation.""" + ... @dataclass(frozen=True) @@ -48,9 +52,11 @@ class Uniform: high: float def sample(self, rng: np.random.Generator) -> float: + """Draw a builtin float using NumPy's half-open interval convention.""" return float(rng.uniform(self.low, self.high)) def describe(self) -> str: + """Render the closed-interval notation used by the methodology.""" return f"Uniform[{_num(self.low)}, {_num(self.high)}]" @@ -77,6 +83,7 @@ def __post_init__(self) -> None: raise ValueError("weights must sum to a positive value") def sample(self, rng: np.random.Generator) -> Scalar: + """Draw by index so mixed-category scalar types are preserved.""" probs = None if self.weights is None else np.asarray(self.weights, dtype=np.float64) if probs is not None: probs = probs / probs.sum() @@ -84,6 +91,7 @@ def sample(self, rng: np.random.Generator) -> Scalar: return self.values[idx] def describe(self) -> str: + """Render the finite support and optional weights in methodology notation.""" body = ", ".join(_num(v) if isinstance(v, int | float) else str(v) for v in self.values) if self.weights is None: return f"Categorical({{{body}}})" @@ -99,9 +107,11 @@ class Normal: std: float def sample(self, rng: np.random.Generator) -> float: + """Draw a builtin float from the configured Gaussian.""" return float(rng.normal(self.mean, self.std)) def describe(self) -> str: + """Render mean and variance notation from the configured standard deviation.""" return f"N({_num(self.mean)}, {_num(self.std)}²)" @@ -112,9 +122,11 @@ class Constant: value: Scalar def sample(self, rng: np.random.Generator) -> Scalar: + """Return the fixed value without consuming randomness.""" return self.value def describe(self) -> str: + """Render the fixed value as a Python literal.""" return repr(self.value) diff --git a/src/kitchenbench/embodiment.py b/src/kitchenbench/embodiment.py index 4a6aaa6..a19e7c5 100644 --- a/src/kitchenbench/embodiment.py +++ b/src/kitchenbench/embodiment.py @@ -93,6 +93,7 @@ def __init__( ) def reset(self, scene: Scene, *, seed: int | None = None) -> Observation: + """Seed the hidden goal and expose the scene's per-epoch instruction.""" self._rng = np.random.RandomState(seed if seed is not None else 0) goal = self._rng.normal(size=6) self._goal = goal / (np.linalg.norm(goal) or 1.0) @@ -112,6 +113,7 @@ def reset(self, scene: Scene, *, seed: int | None = None) -> Observation: return self._observe() def step(self, action: Action) -> StepResult: + """Advance progress when the dual-arm command aligns with the hidden goal.""" self.num_steps += 1 data = np.clip(np.asarray(action.data, dtype=np.float64), -1.0, 1.0) self._last = data @@ -132,6 +134,7 @@ def step(self, action: Action) -> StepResult: ) def close(self) -> None: + """Release no resources because the mock has no external state.""" return None def _observe(self) -> Observation: diff --git a/src/kitchenbench/instances.py b/src/kitchenbench/instances.py index cd1c86d..4881821 100644 --- a/src/kitchenbench/instances.py +++ b/src/kitchenbench/instances.py @@ -42,6 +42,7 @@ class Validation: @property def validated(self) -> bool: + """Whether the ratings meet the methodology's expert count and accept threshold.""" return ( len(self.representativeness) >= K_EXPERTS and len(self.quality) >= K_EXPERTS diff --git a/src/kitchenbench/policies.py b/src/kitchenbench/policies.py index 826aa30..68d1ff7 100644 --- a/src/kitchenbench/policies.py +++ b/src/kitchenbench/policies.py @@ -45,9 +45,11 @@ def __init__(self, *, chunk_size: int = 4): self.config = PolicyConfig(action_horizon=chunk_size) def reset(self, scene: Scene) -> None: + """Clear inference accounting for a new scene.""" self.num_inferences = 0 def act(self, observation: Observation) -> ActionChunk: + """Use the privileged goal direction to emit one aligned action chunk.""" self.num_inferences += 1 goal = np.asarray(observation.state["goal_dir"], dtype=np.float64) data = np.concatenate([goal, np.array([1.0, 1.0])]) # arms aligned; grippers closed @@ -67,11 +69,13 @@ def __init__(self, *, chunk_size: int = 4, seed: int = 0): self.config = PolicyConfig(action_horizon=chunk_size) def reset(self, scene: Scene) -> None: + """Start the next scene's deterministic per-reset random stream.""" self._rng = np.random.RandomState(self._base_seed + self._reset_count) self._reset_count += 1 self.num_inferences = 0 def act(self, observation: Observation) -> ActionChunk: + """Draw one open-loop chunk from the current scene's random stream.""" self.num_inferences += 1 actions = [ Action(data=self._rng.uniform(-1.0, 1.0, size=8)) for _ in range(self.chunk_size) @@ -89,8 +93,10 @@ def __init__(self, *, chunk_size: int = 1): self.config = PolicyConfig(action_horizon=chunk_size) def reset(self, scene: Scene) -> None: + """Clear inference accounting for a new scene.""" self.num_inferences = 0 def act(self, observation: Observation) -> ActionChunk: + """Emit a zero-filled chunk with the configured horizon.""" self.num_inferences += 1 return ActionChunk(actions=[Action(data=np.zeros(8)) for _ in range(self.chunk_size)]) diff --git a/src/kitchenbench/sim/blueprint.py b/src/kitchenbench/sim/blueprint.py index c71977c..82e426c 100644 --- a/src/kitchenbench/sim/blueprint.py +++ b/src/kitchenbench/sim/blueprint.py @@ -124,6 +124,7 @@ class SceneBlueprint: values: Mapping[str, Scalar] def object_names(self) -> tuple[str, ...]: + """Return the object names in spawn order.""" return tuple(obj.name for obj in self.objects) diff --git a/src/kitchenbench/sim/success.py b/src/kitchenbench/sim/success.py index a304325..af91f43 100644 --- a/src/kitchenbench/sim/success.py +++ b/src/kitchenbench/sim/success.py @@ -71,6 +71,8 @@ def opening_fraction(self, name: str) -> float: @dataclass(frozen=True) class Verdict: + """Result of a quantitative check, paired with a human-readable reason.""" + success: bool explanation: str diff --git a/src/kitchenbench/tasks.py b/src/kitchenbench/tasks.py index 7c69a84..878bfe3 100644 --- a/src/kitchenbench/tasks.py +++ b/src/kitchenbench/tasks.py @@ -112,51 +112,61 @@ def make_task(spec: TaskSpec) -> Task: @task("kitchenbench/place_cutlery") def place_cutlery() -> Task: + """Build the registered cutlery placement task from its stochastic instances.""" return make_task(SPEC_BY_KEY["place_cutlery"]) @task("kitchenbench/stack") def stack() -> Task: + """Build the registered dishware stacking task from its stochastic instances.""" return make_task(SPEC_BY_KEY["stack"]) @task("kitchenbench/place_in_rack") def place_in_rack() -> Task: + """Build the registered rack insertion task from its stochastic instances.""" return make_task(SPEC_BY_KEY["place_in_rack"]) @task("kitchenbench/pour_pasta") def pour_pasta() -> Task: + """Build the registered granular pouring task from its stochastic instances.""" return make_task(SPEC_BY_KEY["pour_pasta"]) @task("kitchenbench/open_container") def open_container() -> Task: + """Build the registered container opening task from its stochastic instances.""" return make_task(SPEC_BY_KEY["open_container"]) @task("kitchenbench/fold_cloth") def fold_cloth() -> Task: + """Build the registered cloth folding task from its stochastic instances.""" return make_task(SPEC_BY_KEY["fold_cloth"]) @task("kitchenbench/seal_container") def seal_container() -> Task: + """Build the registered lid sealing task from its stochastic instances.""" return make_task(SPEC_BY_KEY["seal_container"]) @task("kitchenbench/handoff") def handoff() -> Task: + """Build the registered two-arm handoff task from its stochastic instances.""" return make_task(SPEC_BY_KEY["handoff"]) @task("kitchenbench/sort_cutlery") def sort_cutlery() -> Task: + """Build the registered cutlery sorting task from its stochastic instances.""" return make_task(SPEC_BY_KEY["sort_cutlery"]) @task("kitchenbench/scoop_pasta") def scoop_pasta() -> Task: + """Build the registered measured scooping task from its stochastic instances.""" return make_task(SPEC_BY_KEY["scoop_pasta"])