The handoff had grown to 264 lines -- a violation of the very cap it exists to enforce. The Python recipes move to docs/python-split-recipes.md; the handoff keeps a pointer. Also refreshed what had gone stale: the directory distribution said python_pkg 50 and '48 near-misses' from before this session's 9 splits. A future session reading a stale count would not know which other claims in the file to trust, so the numbers are now explicitly labelled a snapshot with the script as the authority, and the per-package breakdown points at brother_printer (15) and code_tutor (13) as where the bulk now sits. Corrected the test recipe too: shared builders go in tests/conftest.py, not a _helpers.py -- name-tests-test rejects the latter, which the earlier wording would have walked the next session straight into. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
4.1 KiB
Python split recipes for the 250-line cap
Settled empirically while clearing python_pkg/wsg_grabber. Every rule here
cost at least one failed gate run or one revert to learn — do not re-derive
them. Referenced from refactor_claude_todo_resume.md.
The re-export form
When a split moves a public name out of a module that callers/tests still
reach through the old path, the re-export must be marked with __all__:
from python_pkg.pkg._new_module import show_logs, summarise
__all__ = ["build_parser", "main", ..., "show_logs", "summarise"]
The PEP 484 alias form from x import y as y does not work here: ruff
raises PLC0414 (useless-import-alias), auto-fixes it away, and then removes
the now-"unused" import under F401 — silently breaking cli.summarise.
Verified on wsg_grabber/cli.py; __all__ passes ruff + mypy + pylint clean.
Splits that only move private helpers (leading _, no external caller)
need no shim at all — check with grep first, as in catalog.py.
__all__ only needs the re-exported names, not every public name in the
module — names defined locally stay reachable either way. Verified against the
full gate on store.py.
__all__ does not save you from ruff's TC001. It suppresses F401, but
a re-exported class used only in annotations gets rewritten into the
TYPE_CHECKING block, which lints perfectly and raises AttributeError at
import time. This nearly shipped a broken ui.Callbacks that cli.py
constructs. A class that must be reachable at runtime from module X stays
defined in module X. Always finish a re-export split with
PYTHONPATH=. python3 -c "from python_pkg.pkg import mod; mod.TheName"
Two other things the gate will not catch for you:
- Check what the tests patch before choosing a seam.
test_ui.pydoespatch.object(ui, "tk", fake), so every line touchingtkinterhad to stay inui.py; moving widget construction out errored 22 tests. Grep the test file forpatch.object(<module>first and treat those names as pinned. - Tests that call private methods pin them too —
test_player.pycallsmpv._note_errors(...), so that method stayed as a one-line delegate to the moved implementation.
Budget the new import block and __all__ before deciding a two-way split is
enough. Five splits so far landed 2–16 lines over the cap after the first cut
and needed a further seam; the header costs 10–20 lines that the arithmetic on
function sizes does not show.
Splitting a class: if tests call private methods as bound attributes
(worker._fetch_thread(...) — grep the tests first), module-level functions
will not do; use mixins. The shape that passes every gate with no
suppression (settled on downloader.py, do not re-derive):
# _worker_base.py
class WorkerBase(ABC):
_deps: WorkerDeps # bare annotations for read-only attributes
_downloaded: int = 0 # real default for any attribute the mixin ASSIGNS
@abstractmethod
def _publish(self, event: DownloadEvent) -> None:
"""Supplied by the concrete Worker."""
Both mixins inherit WorkerBase; Worker(ScanMixin, DownloadMixin). Why not
the obvious alternatives:
- Naming a class
*Mixindoes not get you pylint'sno-memberexemption here —ignored-checks-for-mixinsis not configured for it. Don't rely on it. raise NotImplementedErrorstub bodies are uncovered lines under the 100% branch gate.if TYPE_CHECKING: def _publish(...) -> None: ...satisfies mypy but leaves pylint reportingno-member.- A bare annotation (
_downloaded: int) is not enough for an attribute the mixin assigns to (+= 1); pylint wants a real default.
Watch the MRO: Worker(ScanMixin, DownloadMixin) means ScanMixin wins any
name collision.
Coverage: meta/pyproject.toml sets source = ["python_pkg"], so a new
sibling module is measured automatically. Confirmed: new modules show up in
the report at 100% rather than being silently skipped. Add no new branches
(no if TYPE_CHECKING guards beyond what moved, no defensive try/except).