diff --git a/.claude/commands/new-pattern.md b/.claude/commands/new-pattern.md index d903a1c..2edae05 100644 --- a/.claude/commands/new-pattern.md +++ b/.claude/commands/new-pattern.md @@ -17,9 +17,9 @@ Scaffold a new pattern module for $ARGUMENTS. - `docs/fundamentals.md`, `docs/implementation.md`, `docs/examples.md` — each a heading plus a `TODO` line naming what belongs there (classic-form contrast in fundamentals; never use the word "naive"). - - `examples/__init__.py` and `examples/demo/__init__.py` both completely - empty; `examples/demo/__main__.py` with a typed `main() -> None` + script - guard that imports from `...pattern`. + - `examples/demo/__main__.py` with a typed `main() -> None` + script guard + that imports from `...pattern`. Create NO other `__init__.py` — empty ones + are banned (PEP 420 namespace packages); the loader rejects them. - `tests/test_.py` with one failing `test_todo` marked `xfail(reason="unit not yet written")`. 3. Run `make check` and report the result. Note: the catalog loader will fail the diff --git a/AGENTS.md b/AGENTS.md index 88b55a2..45d9d38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,11 +30,13 @@ patterns/// │ ├── implementation.md# introducing it into a real system: smell, steps, idioms, pitfalls │ └── examples.md # cited EXTERNAL usages: stdlib, OSS, articles ├── examples/ # runnable mini-projects that import pattern/ -│ ├── __init__.py │ └── / # realistic domain, no Foo/Bar; __main__.py + modules └── tests/ # isolated: test_.py + test_.py ``` +No other `__init__.py` exist — namespace packages (PEP 420) carry the rest; +the loader rejects empty ones. + - Everything import-safe (no side effects at import); mini-projects run via `uv run python -m patterns...examples.`. - Tests assert behavior, never just "it runs"; load-bearing claims get the @@ -49,8 +51,10 @@ patterns/// `replace=True`); ordered collections append freely. - `ParamSpec` only where a wrapper callable is returned; identity typing otherwise. - Never `None` as a cache sentinel; immutability guards recurse into containers. -- `__init__.py` is empty unless it is the unit's public-API re-export file - (`from .pattern.x import Y as Y` — the as-alias form); never `__all__`. +- No empty `__init__.py` — delete them (PEP 420 namespace packages). One exists + only when load-bearing: the unit's public-API re-exports + (`from .pattern.x import Y as Y` — the as-alias form) or an import-time + effect the unit teaches. Never `__all__`. ## Frontmatter schema (the MCP server indexes this — keep it valid) diff --git a/CLAUDE.md b/CLAUDE.md index 6af2aed..42097cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,11 +30,13 @@ patterns/// │ ├── implementation.md# introducing it into a real system: smell, steps, idioms, pitfalls │ └── examples.md # cited EXTERNAL usages: stdlib, OSS, articles ├── examples/ # runnable mini-projects that import pattern/ -│ ├── __init__.py │ └── / # realistic domain, no Foo/Bar; __main__.py + modules └── tests/ # isolated: test_.py + test_.py ``` +No other `__init__.py` exist — namespace packages (PEP 420) carry the rest; +the loader rejects empty ones. + - Everything import-safe (no side effects at import); mini-projects run via `uv run python -m patterns...examples.`. - Tests assert behavior, never just "it runs"; load-bearing claims get the @@ -49,8 +51,10 @@ patterns/// `replace=True`); ordered collections append freely. - `ParamSpec` only where a wrapper callable is returned; identity typing otherwise. - Never `None` as a cache sentinel; immutability guards recurse into containers. -- `__init__.py` is empty unless it is the unit's public-API re-export file - (`from .pattern.x import Y as Y` — the as-alias form); never `__all__`. +- No empty `__init__.py` — delete them (PEP 420 namespace packages). One exists + only when load-bearing: the unit's public-API re-exports + (`from .pattern.x import Y as Y` — the as-alias form) or an import-time + effect the unit teaches. Never `__all__`. ## Frontmatter schema (the MCP server indexes this — keep it valid) diff --git a/patterns/__init__.py b/patterns/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/__init__.py b/patterns/behavioral/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/chain_of_responsibility/examples/__init__.py b/patterns/behavioral/chain_of_responsibility/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/chain_of_responsibility/examples/ticket_escalation/__init__.py b/patterns/behavioral/chain_of_responsibility/examples/ticket_escalation/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/chain_of_responsibility/tests/__init__.py b/patterns/behavioral/chain_of_responsibility/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/command/examples/__init__.py b/patterns/behavioral/command/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/command/examples/editor_undo/__init__.py b/patterns/behavioral/command/examples/editor_undo/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/command/tests/__init__.py b/patterns/behavioral/command/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/interpreter/examples/__init__.py b/patterns/behavioral/interpreter/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/interpreter/examples/flag_rules/__init__.py b/patterns/behavioral/interpreter/examples/flag_rules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/interpreter/tests/__init__.py b/patterns/behavioral/interpreter/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/iterator/examples/__init__.py b/patterns/behavioral/iterator/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/iterator/examples/paginated_client/__init__.py b/patterns/behavioral/iterator/examples/paginated_client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/iterator/tests/__init__.py b/patterns/behavioral/iterator/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/mediator/examples/__init__.py b/patterns/behavioral/mediator/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/mediator/examples/checkout_form/__init__.py b/patterns/behavioral/mediator/examples/checkout_form/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/mediator/tests/__init__.py b/patterns/behavioral/mediator/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/memento/examples/__init__.py b/patterns/behavioral/memento/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/memento/examples/config_checkpoints/__init__.py b/patterns/behavioral/memento/examples/config_checkpoints/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/memento/tests/__init__.py b/patterns/behavioral/memento/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/observer/examples/__init__.py b/patterns/behavioral/observer/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/observer/examples/order_events/__init__.py b/patterns/behavioral/observer/examples/order_events/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/observer/tests/__init__.py b/patterns/behavioral/observer/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/state/examples/__init__.py b/patterns/behavioral/state/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/state/examples/order_lifecycle/__init__.py b/patterns/behavioral/state/examples/order_lifecycle/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/state/tests/__init__.py b/patterns/behavioral/state/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/strategy/examples/__init__.py b/patterns/behavioral/strategy/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/strategy/examples/promotions/__init__.py b/patterns/behavioral/strategy/examples/promotions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/strategy/tests/__init__.py b/patterns/behavioral/strategy/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/template_method/examples/__init__.py b/patterns/behavioral/template_method/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/template_method/examples/report_pipeline/__init__.py b/patterns/behavioral/template_method/examples/report_pipeline/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/template_method/tests/__init__.py b/patterns/behavioral/template_method/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/visitor/examples/__init__.py b/patterns/behavioral/visitor/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/visitor/examples/doc_exporters/__init__.py b/patterns/behavioral/visitor/examples/doc_exporters/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/behavioral/visitor/tests/__init__.py b/patterns/behavioral/visitor/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/__init__.py b/patterns/creational/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/abstract_factory/examples/__init__.py b/patterns/creational/abstract_factory/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/abstract_factory/examples/report_renderer/__init__.py b/patterns/creational/abstract_factory/examples/report_renderer/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/abstract_factory/tests/__init__.py b/patterns/creational/abstract_factory/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/builder/examples/__init__.py b/patterns/creational/builder/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/builder/examples/sql_select_builder/__init__.py b/patterns/creational/builder/examples/sql_select_builder/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/builder/tests/__init__.py b/patterns/creational/builder/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/factory_method/examples/__init__.py b/patterns/creational/factory_method/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/factory_method/examples/feed_client/__init__.py b/patterns/creational/factory_method/examples/feed_client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/factory_method/tests/__init__.py b/patterns/creational/factory_method/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/prototype/examples/__init__.py b/patterns/creational/prototype/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/prototype/examples/report_job_templates/__init__.py b/patterns/creational/prototype/examples/report_job_templates/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/prototype/tests/__init__.py b/patterns/creational/prototype/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/singleton/examples/__init__.py b/patterns/creational/singleton/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/singleton/examples/app_config/__init__.py b/patterns/creational/singleton/examples/app_config/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/creational/singleton/tests/__init__.py b/patterns/creational/singleton/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/__init__.py b/patterns/modern/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/async_producer_consumer/examples/__init__.py b/patterns/modern/async_producer_consumer/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/async_producer_consumer/examples/feed_fetcher/__init__.py b/patterns/modern/async_producer_consumer/examples/feed_fetcher/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/async_producer_consumer/tests/__init__.py b/patterns/modern/async_producer_consumer/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/context_manager/examples/__init__.py b/patterns/modern/context_manager/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/context_manager/examples/atomic_deploy/__init__.py b/patterns/modern/context_manager/examples/atomic_deploy/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/context_manager/tests/__init__.py b/patterns/modern/context_manager/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/dependency_injection/examples/__init__.py b/patterns/modern/dependency_injection/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/dependency_injection/examples/invoice_reminders/__init__.py b/patterns/modern/dependency_injection/examples/invoice_reminders/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/dependency_injection/tests/__init__.py b/patterns/modern/dependency_injection/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/registry/examples/__init__.py b/patterns/modern/registry/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/registry/tests/__init__.py b/patterns/modern/registry/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/repository/examples/__init__.py b/patterns/modern/repository/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/repository/examples/invoice_ledger/__init__.py b/patterns/modern/repository/examples/invoice_ledger/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/modern/repository/tests/__init__.py b/patterns/modern/repository/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/principle/__init__.py b/patterns/principle/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/principle/composition_over_inheritance/examples/__init__.py b/patterns/principle/composition_over_inheritance/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/principle/composition_over_inheritance/examples/notification_router/__init__.py b/patterns/principle/composition_over_inheritance/examples/notification_router/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/principle/composition_over_inheritance/tests/__init__.py b/patterns/principle/composition_over_inheritance/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/__init__.py b/patterns/python/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/global_object/examples/__init__.py b/patterns/python/global_object/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/global_object/examples/settings_module/__init__.py b/patterns/python/global_object/examples/settings_module/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/global_object/tests/__init__.py b/patterns/python/global_object/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/prebound_method/examples/__init__.py b/patterns/python/prebound_method/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/prebound_method/examples/metrics/__init__.py b/patterns/python/prebound_method/examples/metrics/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/prebound_method/tests/__init__.py b/patterns/python/prebound_method/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/sentinel_object/examples/__init__.py b/patterns/python/sentinel_object/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/sentinel_object/examples/layered_config/__init__.py b/patterns/python/sentinel_object/examples/layered_config/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/python/sentinel_object/tests/__init__.py b/patterns/python/sentinel_object/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/__init__.py b/patterns/structural/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/adapter/examples/__init__.py b/patterns/structural/adapter/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/adapter/examples/payment_gateways/__init__.py b/patterns/structural/adapter/examples/payment_gateways/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/adapter/tests/__init__.py b/patterns/structural/adapter/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/bridge/examples/__init__.py b/patterns/structural/bridge/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/bridge/examples/notification_center/__init__.py b/patterns/structural/bridge/examples/notification_center/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/bridge/tests/__init__.py b/patterns/structural/bridge/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/composite/examples/__init__.py b/patterns/structural/composite/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/composite/examples/org_chart/__init__.py b/patterns/structural/composite/examples/org_chart/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/composite/tests/__init__.py b/patterns/structural/composite/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/decorator/examples/__init__.py b/patterns/structural/decorator/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/decorator/examples/resilient_client/__init__.py b/patterns/structural/decorator/examples/resilient_client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/decorator/tests/__init__.py b/patterns/structural/decorator/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/facade/examples/__init__.py b/patterns/structural/facade/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/facade/examples/order_checkout/__init__.py b/patterns/structural/facade/examples/order_checkout/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/facade/tests/__init__.py b/patterns/structural/facade/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/flyweight/examples/__init__.py b/patterns/structural/flyweight/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/flyweight/examples/glyph_styles/__init__.py b/patterns/structural/flyweight/examples/glyph_styles/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/flyweight/tests/__init__.py b/patterns/structural/flyweight/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/proxy/examples/__init__.py b/patterns/structural/proxy/examples/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/proxy/examples/db_gateway/__init__.py b/patterns/structural/proxy/examples/db_gateway/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/patterns/structural/proxy/tests/__init__.py b/patterns/structural/proxy/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml index 6646e48..8277bd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,16 +55,24 @@ src = ["src", "patterns", "tests"] [tool.ruff.lint] select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"] +[tool.ruff.lint.isort] +# Namespace packages (PEP 420): no __init__.py to detect these by, so declare them. +known-first-party = ["patterns", "design_patterns"] + [tool.mypy] strict = true python_version = "3.11" files = ["src", "patterns"] +# Namespace packages (PEP 420): no empty __init__.py — resolve modules by +# their full dotted path from the package bases below. +explicit_package_bases = true +mypy_path = ["src", "."] [tool.pytest.ini_options] testpaths = ["tests", "patterns"] pythonpath = ["."] asyncio_mode = "auto" -addopts = "-q --cov=src --cov=patterns --cov-report=term-missing" +addopts = "-q --import-mode=importlib --cov=src --cov=patterns --cov-report=term-missing" timeout = 30 # a shutdown regression must fail CI, not hang it [tool.coverage.report] diff --git a/src/design_patterns/__init__.py b/src/design_patterns/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/design_patterns/catalog.py b/src/design_patterns/catalog.py index eb69c12..985b821 100644 --- a/src/design_patterns/catalog.py +++ b/src/design_patterns/catalog.py @@ -164,16 +164,23 @@ def _validate_shape(pattern: Pattern, readme: Path) -> None: raise CatalogError( f"{readme}: module unit ships no runnable examples//__main__.py" ) - if not (unit / "examples" / "__init__.py").is_file(): - raise CatalogError(f"{readme}: examples/ is not a package (no __init__.py)") - for name, path in examples.items(): - if not (path / "__init__.py").is_file(): - raise CatalogError(f"{readme}: example {name!r} is not a package (no __init__.py)") + for stray in _empty_inits(unit): + raise CatalogError( + f"{readme}: delete empty __init__.py ({stray.relative_to(unit)}) — " + "namespace packages (PEP 420) carry the structure" + ) tests_dir = unit / "tests" if not any(tests_dir.glob("test_*.py")): raise CatalogError(f"{readme}: module unit has no tests/test_*.py") +def _empty_inits(unit: Path) -> list[Path]: + """Empty ``__init__.py`` anywhere in the unit — banned; only load-bearing ones exist.""" + return sorted( + f for f in unit.rglob("__init__.py") if f.stat().st_size == 0 or not f.read_text().strip() + ) + + @dataclass(frozen=True) class Catalog: """All validated pattern units, ordered by id.""" diff --git a/src/design_patterns/mcp/__init__.py b/src/design_patterns/mcp/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/conftest.py b/tests/conftest.py index 7d71ceb..5c04ec4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,9 +15,8 @@ def write_module_unit(root: Path) -> Path: """Build ``/creational/thing`` as a complete, runnable module-shape unit.""" unit = root / "creational" / "thing" (unit / "pattern").mkdir(parents=True) - for pkg in (root, root / "creational"): - (pkg / "__init__.py").write_text("") - # The two API files model the house style: bare as-alias re-exports. + # No empty __init__.py anywhere — namespace packages (PEP 420) carry the + # structure. The two API files model the house style: bare as-alias re-exports. (unit / "__init__.py").write_text("from .pattern.thing import build as build\n") (unit / "pattern" / "__init__.py").write_text("from .thing import build as build\n") (unit / "README.md").write_text( @@ -34,8 +33,6 @@ def write_module_unit(root: Path) -> Path: (docs / f"{name}.md").write_text(f"# {name} of Thing\n") project = unit / "examples" / "demo" project.mkdir(parents=True) - (unit / "examples" / "__init__.py").write_text("") - (project / "__init__.py").write_text("") (project / "__main__.py").write_text( "from patterns.creational.thing.pattern.thing import build\n\nprint(build())\n" ) diff --git a/tests/test_catalog.py b/tests/test_catalog.py index c5684b5..5662fb0 100644 --- a/tests/test_catalog.py +++ b/tests/test_catalog.py @@ -130,8 +130,9 @@ def _write_module_unit(root: Path, group: str, slug: str, frontmatter: str) -> P unit = root / group / slug (unit / "pattern").mkdir(parents=True) (unit / "README.md").write_text(f"---\n{frontmatter}\n---\n\n# x\n") - (unit / "__init__.py").write_text("") - (unit / "pattern" / "__init__.py").write_text("") + # Only load-bearing __init__.py exist (house rule): the two API files. + (unit / "__init__.py").write_text("from .pattern.thing import build as build\n") + (unit / "pattern" / "__init__.py").write_text("from .thing import build as build\n") (unit / "pattern" / "thing.py").write_text("def build() -> str:\n return 'thing'\n") docs = unit / "docs" docs.mkdir() @@ -139,8 +140,6 @@ def _write_module_unit(root: Path, group: str, slug: str, frontmatter: str) -> P (docs / f"{name}.md").write_text(f"# {name}\n") project = unit / "examples" / "demo" project.mkdir(parents=True) - (unit / "examples" / "__init__.py").write_text("") - (project / "__init__.py").write_text("") (project / "__main__.py").write_text("print('demo ran')\n") tests = unit / "tests" tests.mkdir() @@ -168,10 +167,10 @@ def test_no_example_fails(self, tmp_path: Path) -> None: with pytest.raises(CatalogError, match="no runnable examples"): load_catalog(tmp_path) - def test_example_not_a_package_fails(self, tmp_path: Path) -> None: + def test_empty_init_in_example_fails(self, tmp_path: Path) -> None: unit = _write_module_unit(tmp_path, "creational", "thing", GOOD) - (unit / "examples" / "demo" / "__init__.py").unlink() - with pytest.raises(CatalogError, match="not a package"): + (unit / "examples" / "demo" / "__init__.py").write_text("") + with pytest.raises(CatalogError, match=r"delete empty __init__\.py"): load_catalog(tmp_path) def test_empty_tests_fails(self, tmp_path: Path) -> None: @@ -200,12 +199,20 @@ def test_partial_unit_fails_loudly(self, tmp_path: Path) -> None: with pytest.raises(CatalogError, match="module unit missing docs"): load_catalog(tmp_path) - def test_examples_dir_needs_init(self, tmp_path: Path) -> None: + def test_empty_init_in_examples_dir_fails(self, tmp_path: Path) -> None: unit = _write_module_unit(tmp_path, "creational", "thing", GOOD) - (unit / "examples" / "__init__.py").unlink() - with pytest.raises(CatalogError, match=r"examples/ is not a package"): + (unit / "examples" / "__init__.py").write_text("") + with pytest.raises(CatalogError, match=r"delete empty __init__\.py"): load_catalog(tmp_path) + def test_loadbearing_example_init_is_allowed(self, tmp_path: Path) -> None: + # A NON-empty example __init__.py (e.g. plugin self-registration) is fine. + unit = _write_module_unit(tmp_path, "creational", "thing", GOOD) + (unit / "examples" / "demo" / "__init__.py").write_text( + "# load-bearing: demo of import-time registration\n" + ) + assert load_catalog(tmp_path).get("creational/thing").examples() + def test_index_json_carries_docs_and_examples(self, tmp_path: Path) -> None: _write_module_unit(tmp_path, "creational", "thing", GOOD) entries = json.loads(load_catalog(tmp_path).to_json())