From 60acdda4774b61fd94c66a7d2101f7f652f2d0b4 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Thu, 13 Aug 2026 02:00:22 -0400 Subject: [PATCH] fix(packaging): ship py.typed with the four broker distributions mypy checks `keel_broker_api`, `keel_broker_coinbase`, `keel_broker_fake` and `keel_broker_robinhood` with `strict = true`, but none of them shipped a PEP 561 marker, so all four wheels through 0.7.0 carried their annotations invisibly. In this repo it never showed: mypy reads the source tree. Off it, a consumer gets `module is installed, but missing library stubs or py.typed marker` and falls back to `Any` for every symbol crossing the boundary -- the wheel still builds, still installs, still imports, and the type contract is simply gone. That is the same silent shape as an unpinned sibling, which is why this lands as a test and not a note. `keel_broker_api` is the one that costs most: the port's types are the contract every adapter and every consumer codes against. The test derives its subject list from the `[tool.mypy]` strict overrides rather than naming packages, so tightening a package into strict mode brings it under the rule automatically. It stays scoped to strict modules on purpose -- `keel.*` and `keel_core.*` are still `ignore_errors`, and a marker on unchecked code promises a guarantee nothing verifies. `keel_core` has one anyway, historically; the rule is a floor, not an equality, so that stays legal. Verified against a built wheel, not just the source tree: `unzip -l keel_broker_api-0.7.0-py3-none-any.whl` now lists `py.typed`. Co-Authored-By: Claude Opus 5 (1M context) --- .../keel-broker-api/keel_broker_api/py.typed | 0 .../keel_broker_coinbase/py.typed | 0 .../keel_broker_fake/py.typed | 0 .../keel_broker_robinhood/py.typed | 0 tests/test_packaging.py | 41 +++++++++++++++++++ 5 files changed, 41 insertions(+) create mode 100644 packages/keel-broker-api/keel_broker_api/py.typed create mode 100644 packages/keel-broker-coinbase/keel_broker_coinbase/py.typed create mode 100644 packages/keel-broker-fake/keel_broker_fake/py.typed create mode 100644 packages/keel-broker-robinhood/keel_broker_robinhood/py.typed diff --git a/packages/keel-broker-api/keel_broker_api/py.typed b/packages/keel-broker-api/keel_broker_api/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/packages/keel-broker-coinbase/keel_broker_coinbase/py.typed b/packages/keel-broker-coinbase/keel_broker_coinbase/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/packages/keel-broker-fake/keel_broker_fake/py.typed b/packages/keel-broker-fake/keel_broker_fake/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/packages/keel-broker-robinhood/keel_broker_robinhood/py.typed b/packages/keel-broker-robinhood/keel_broker_robinhood/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 6c6ae542..5d7aaf4f 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -73,3 +73,44 @@ def test_the_dev_only_fake_venue_is_not_a_runtime_dependency_of_anything(): for name, data in _pyprojects().items(): deps = [_requirement_name(s) for s in data["project"].get("dependencies", [])] assert "keel-broker-fake" not in deps, f"{name} must not depend on keel-broker-fake" + + +def _strict_modules() -> list[str]: + """Import packages the root `[tool.mypy]` config checks in strict mode. + + Read from the config rather than listed here, so that tightening a package (moving it into a + `strict = true` override) automatically brings it under the marker rule below instead of + requiring someone to remember this file. + """ + overrides = tomllib.loads((_ROOT / "pyproject.toml").read_text())["tool"]["mypy"]["overrides"] + modules: list[str] = [] + for override in overrides: + if not override.get("strict"): + continue + entry = override["module"] + for pattern in [entry] if isinstance(entry, str) else entry: + modules.append(pattern.removesuffix(".*")) + return sorted(modules) + + +@pytest.mark.parametrize("module", _strict_modules()) +def test_strictly_typed_packages_ship_a_py_typed_marker(module): + """A package mypy checks strictly must declare that fact to whoever installs it (PEP 561). + + Without the marker the annotations are invisible off the source tree: mypy in a CONSUMER's + project reports `module is installed, but missing library stubs or py.typed marker` and falls + back to `Any` for every symbol crossing the boundary. The wheel still builds and imports, so + the loss is silent -- which is the same failure shape as the unpinned siblings above, and the + reason this is a test rather than a note. All four broker distributions shipped that way + through 0.7.0; `keel_broker_api` is the one that matters most, since the port's types are the + contract every adapter and every consumer codes against. + + Scoped to strict modules on purpose: `keel.*` and `keel_core.*` are still `ignore_errors`, and + a marker on unchecked code promises a guarantee nothing verifies. `keel_core` carries one + anyway for historical reasons -- that is allowed, this rule is a floor, not an equality. + """ + candidates = [*(_ROOT / "packages").glob(f"*/{module}/py.typed"), _ROOT / module / "py.typed"] + assert any(p.is_file() for p in candidates), ( + f"{module} is type-checked with `strict = true` but ships no py.typed marker; " + f"create an empty one beside its `__init__.py` so installers can see the annotations" + )