Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/commands/new-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<slug>.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
Expand Down
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ patterns/<group>/<slug>/
│ ├── 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
│ └── <project>/ # realistic domain, no Foo/Bar; __main__.py + modules
└── tests/ # isolated: test_<named>.py + test_<project>.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.<group>.<slug>.examples.<project>`.
- Tests assert behavior, never just "it runs"; load-bearing claims get the
Expand All @@ -49,8 +51,10 @@ patterns/<group>/<slug>/
`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)

Expand Down
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ patterns/<group>/<slug>/
│ ├── 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
│ └── <project>/ # realistic domain, no Foo/Bar; __main__.py + modules
└── tests/ # isolated: test_<named>.py + test_<project>.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.<group>.<slug>.examples.<project>`.
- Tests assert behavior, never just "it runs"; load-bearing claims get the
Expand All @@ -49,8 +51,10 @@ patterns/<group>/<slug>/
`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)

Expand Down
Empty file removed patterns/__init__.py
Empty file.
Empty file removed patterns/behavioral/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file removed patterns/creational/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file removed patterns/modern/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file removed patterns/principle/__init__.py
Empty file.
Empty file.
Empty file.
Empty file removed patterns/python/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file removed patterns/structural/__init__.py
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Empty file removed src/design_patterns/__init__.py
Empty file.
17 changes: 12 additions & 5 deletions src/design_patterns/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,23 @@ def _validate_shape(pattern: Pattern, readme: Path) -> None:
raise CatalogError(
f"{readme}: module unit ships no runnable examples/<project>/__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."""
Expand Down
Empty file.
7 changes: 2 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ def write_module_unit(root: Path) -> Path:
"""Build ``<root>/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(
Expand All @@ -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"
)
Expand Down
27 changes: 17 additions & 10 deletions tests/test_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,16 @@ 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()
for name in ("fundamentals", "implementation", "examples"):
(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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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())
Expand Down
Loading