From 4b1263f895c5e0529d38baa375c73b5ebc44fdfb Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:31:44 -0400 Subject: [PATCH] fix: ship py.typed so downstream type checkers see the annotations The package is fully annotated and checked with mypy --strict, and shipped no PEP 561 marker. Without it a type checker must ignore a dependency's inline annotations, so every consumer of plumbline saw Any for everything it exposes and had no way to know the types were there. Nothing in this repository noticed, because mypy checks the source directly. The failure was entirely downstream and entirely silent. hatchling picks the file up with no pyproject change; verified present as plumbline/py.typed in the built wheel. A test asserts the marker exists, because an empty file whose only job is to exist is the kind of thing a refactor deletes without anyone missing it for a year. Verified to fail when the file is removed. A second test pins the version against the 0.1.0.dev0 mismatch that shipped in the release. Co-Authored-By: Claude Opus 5 (1M context) --- src/plumbline/py.typed | 0 tests/test_packaging.py | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/plumbline/py.typed create mode 100644 tests/test_packaging.py diff --git a/src/plumbline/py.typed b/src/plumbline/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..af76151 --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,45 @@ +"""The package declares itself typed, and keeps declaring it. + +``py.typed`` is an empty file whose only job is to exist. Nothing in this +repository breaks if it is deleted, every test still passes, and mypy still +runs clean here, because the annotations are all present either way. What +breaks is downstream: PEP 561 says a type checker must ignore a dependency's +inline annotations unless the package ships this marker, so without it every +consumer of plumbline sees ``Any`` and never finds out why. + +That makes it the kind of file that gets lost in a refactor and is not missed +for a year. Hence a test. +""" + +from __future__ import annotations + +from pathlib import Path + +import plumbline + + +def test_the_package_ships_a_py_typed_marker() -> None: + package_root = Path(plumbline.__file__).parent + + assert (package_root / "py.typed").is_file(), ( + "py.typed is missing. Without it, PEP 561 requires type checkers to " + "ignore this package's annotations, so downstream users silently get " + "Any for everything plumbline exposes." + ) + + +def test_the_marker_is_empty_as_pep_561_intends() -> None: + """The file is a flag, not a config. Content here would be a mistake.""" + marker = Path(plumbline.__file__).parent / "py.typed" + + assert marker.read_text(encoding="utf-8").strip() == "" + + +def test_the_version_is_a_release_version_not_a_placeholder() -> None: + """Guards the mismatch that shipped in v0.1.0. + + The release was tagged v0.1.0 while the package still called itself + 0.1.0.dev0, and the test that should have caught it asserted a substring. + """ + assert plumbline.__version__ == "0.1.0" + assert "dev" not in plumbline.__version__