diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1732dcd..0839c6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,3 +27,5 @@ jobs: run: uv run mypy - name: Test run: uv run pytest + - name: README table drift + run: uv run python -m design_patterns.readme_table --check diff --git a/Makefile b/Makefile index 26868d7..e315241 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,11 @@ typecheck: ## mypy --strict test: ## Run test suite with coverage uv run pytest +readme: ## Regenerate the README catalog table + uv run python -m design_patterns.readme_table + check: lint typecheck test ## Everything CI runs + uv run python -m design_patterns.readme_table --check clean: rm -rf .pytest_cache .mypy_cache .ruff_cache .coverage htmlcov dist build diff --git a/README.md b/README.md index b29300a..533c959 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,99 @@ # Python Design Patterns -A collection of design patterns for python: https://python-patterns.guide/ - ---- - -## GOF Patterns - -__Behavioral Patterns__: - -| Pattern | Description | -|:-------:| ----------- | -| [iterator](behavioral/iterator.py) | emplements pythonic style design with 3 rules | -| [command](behavioral/command.py) | Copy Paste implementation of command design pattern | -| [command_1](behavioral/command_1.py) | ![Command Design Pattern](behavioral/command-design-pattern.PNG) | - -__Creational Patterns__: - -| Pattern | Description | -|:-------:| ----------- | -| [builder_2](creational/builder_2.py) | builder description | -| [builder](creational/builder.py) | builder description | -| [prototype](creational/prototype.py) | use a factory and clones of a prototype for new instances (if instantiation is expensive) | -| [singleton_0](creational/singleton_0.py) | singleton description | -| [singleton_1](creational/singleton_1.py) | singleton description | -| [singleton_2](creational/singleton_2.py) | singleton description | -| [singleton_3](creational/singleton_3.py) | singleton description | -| [singleton_4](creational/singleton_4.py) | singleton description | - -__Structural Patterns__: - -| Pattern | Description | -|:-------:| ----------- | -| [composite](structural/composite.py) | lets clients treat individual objects and compositions uniformly | -| [decorator](structural/decorator.py) | wrap functionality with other functionality in order to affect outputs | -| [decorator_1](structural/decorator_1.py) | showing import time versus run time | -| [decorator_2](structural/decorator_2.py) | @functools.wraps & parameterized decorators | -| [decorator_3](structural/decorator_3.py) | making use of @singledispatch decorator to handle different input types | -| [flyweight](structural/flyweight.py) | transparently reuse existing instances of objects with similar/identical state | - ---- - -## Videos - -[Design Patterns in Python by Peter Ullrich](https://www.youtube.com/watch?v=bsyjSW46TDg) - -[Sebastian Buczyński - Why you don't need design patterns in Python?](https://www.youtube.com/watch?v=G5OeYHCJuv0) - -[You Don't Need That!](https://www.youtube.com/watch?v=imW-trt0i9I) - -[Pluggable Libs Through Design Patterns](https://www.youtube.com/watch?v=PfgEU3W0kyU) - ---- +Look up any design pattern and see what a fluent Python developer would +*actually* write — the classic GoF form, the pythonic form, and where the +standard library already does it — with an honest verdict when the right +answer is "don't". All 23 Gang of Four patterns plus Python-native and +modern ones, every example typed, tested, and runnable. + +## Use it + +Each pattern is a folder — read them in this order: + +``` +patterns/structural/decorator/ +├── README.md # the problem, the trade-offs, the verdict +├── naive.py # the classic 1994 translation +├── pythonic.py # what you actually write in Python +└── real_world.py # where the stdlib already does this +``` + +Run any example: `uv run python -m patterns.structural.decorator.pythonic` + +Give it to your agents (MCP server with search, runnable examples, and +pattern recommendations): + +```bash +claude mcp add design-patterns -- uv run --directory python-design-patterns-mcp +``` + +## Catalog + +Based on [python-patterns.guide](https://python-patterns.guide/). + + + +### Principles + +| Pattern | Verdict | Problem it solves | +|---|---|---| +| [Composition Over Inheritance](patterns/principle/composition_over_inheritance/) | ✅ pythonic | Vary independent behaviors without one subclass per combination of them. | + +### Python-native + +| Pattern | Verdict | Problem it solves | +|---|---|---| +| [Global Object](patterns/python/global_object/) | ⚠️ use with care | Give a whole program shared access to a constant or a pre-built object by assigning it at module level. | +| [Prebound Method](patterns/python/prebound_method/) | ✅ pythonic | Offer module-level functions that share state, by binding the methods of one hidden instance to module globals. | +| [Sentinel Object](patterns/python/sentinel_object/) | ✅ pythonic | Mark 'no value here' unambiguously when None itself is a legitimate value. | + +### Creational (GoF) + +| Pattern | Verdict | Problem it solves | +|---|---|---| +| [Abstract Factory](patterns/creational/abstract_factory/) | 🔄 prefer alternative | Let code build families of related objects without naming their concrete classes. | +| [Builder](patterns/creational/builder/) | ⚠️ use with care | Assemble a complex object step by step, so the assembly process is reusable and readable. | +| [Factory Method](patterns/creational/factory_method/) | 🔄 prefer alternative | Let a class defer which helper object it constructs, so subclasses or callers can substitute another. | +| [Prototype](patterns/creational/prototype/) | 🔄 prefer alternative | Create new objects by copying a pre-configured exemplar instead of constructing from scratch. | +| [Singleton](patterns/creational/singleton/) | 🔄 prefer alternative | Guarantee a class has exactly one instance and give the whole program access to it. | + +### Structural (GoF) + +| Pattern | Verdict | Problem it solves | +|---|---|---| +| [Adapter](patterns/structural/adapter/) | ✅ pythonic | Make an existing class usable through the interface your code expects, without editing either side. | +| [Bridge](patterns/structural/bridge/) | 🔄 prefer alternative | Let an abstraction and its implementation vary independently, instead of multiplying subclasses across both axes. | +| [Composite](patterns/structural/composite/) | ✅ pythonic | Let callers treat a single object and a whole tree of objects through one interface. | +| [Decorator](patterns/structural/decorator/) | ✅ pythonic | Add behavior around an object or callable without editing it or subclassing it. | +| [Facade](patterns/structural/facade/) | ✅ pythonic | Give a complicated subsystem one simple entry point for the common case. | +| [Flyweight](patterns/structural/flyweight/) | ⚠️ use with care | Support huge numbers of fine-grained objects by sharing immutable instances instead of duplicating them. | +| [Proxy](patterns/structural/proxy/) | ⚠️ use with care | Stand in for another object to control access to it — deferring, guarding, or instrumenting the real thing. | + +### Behavioral (GoF) + +| Pattern | Verdict | Problem it solves | +|---|---|---| +| [Chain of Responsibility](patterns/behavioral/chain_of_responsibility/) | 🔄 prefer alternative | Pass a request along a line of handlers until one of them takes it. | +| [Command](patterns/behavioral/command/) | ⚠️ use with care | Package a request as an object so it can be queued, logged, undone, or executed later by code that doesn't know its details. | +| [Interpreter](patterns/behavioral/interpreter/) | 🔄 prefer alternative | Represent a small language's grammar as data and evaluate sentences in it. | +| [Iterator](patterns/behavioral/iterator/) | ✅ pythonic | Traverse a container's elements without exposing how the container stores them. | +| [Mediator](patterns/behavioral/mediator/) | ⚠️ use with care | Stop a web of objects from referencing each other by routing their interactions through one coordinator. | +| [Memento](patterns/behavioral/memento/) | ⚠️ use with care | Capture an object's state so it can be restored later, without exposing its internals. | +| [Observer](patterns/behavioral/observer/) | ✅ pythonic | Notify interested parties when something changes, without the subject knowing who they are. | +| [State](patterns/behavioral/state/) | ⚠️ use with care | Change an object's behavior when its internal state changes, without an if-forest over a mode flag. | +| [Strategy](patterns/behavioral/strategy/) | 🔄 prefer alternative | Make an algorithm interchangeable at runtime without the caller knowing which variant it got. | +| [Template Method](patterns/behavioral/template_method/) | 🔄 prefer alternative | Fix an algorithm's skeleton while letting callers vary individual steps. | +| [Visitor](patterns/behavioral/visitor/) | 🔄 prefer alternative | Run a new operation over every node of an object structure without adding a method to every node class. | + +### Modern Python + +| Pattern | Verdict | Problem it solves | +|---|---|---| +| [Async Producer/Consumer](patterns/modern/async_producer_consumer/) | ⚠️ use with care | Decouple work generation from work processing under asyncio, with bounded memory and clean shutdown. | +| [Context Manager](patterns/modern/context_manager/) | ✅ pythonic | Guarantee acquire/release pairing around a block of code, even when it raises. | +| [Dependency Injection](patterns/modern/dependency_injection/) | ✅ pythonic | Hand an object its collaborators instead of letting it construct them, so they can be swapped — above all in tests. | +| [Registry](patterns/modern/registry/) | ✅ pythonic | Let implementations announce themselves by name, so dispatch is a lookup instead of an if/elif ladder. | +| [Repository](patterns/modern/repository/) | ⚠️ use with care | Keep domain logic ignorant of how objects are stored, behind a collection-like interface. | + + +More in [docs/](docs/index.md) — verdict definitions, MCP reference, contributing. diff --git a/docs/contributing.md b/docs/contributing.md index 55a9104..691d628 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,3 +1,24 @@ -# contributing +# Contributing -_Written in the docs phase._ +## Workflow + +Branches flow `main ← staging ← feat/`. PRs target `staging`; `main` +takes only reviewed milestone merges. CI (3.11/3.12/3.13) must pass. + +## Adding a pattern unit + +1. Scaffold: `/new-pattern / "Name"` (Claude Code) or copy an + existing unit's shape. +2. Fill the frontmatter — every key; `id` must equal `/`; pick + the verdict per [verdicts.md](verdicts.md). The catalog loader validates + this in CI and fails loudly. +3. Write the three variants (see [how-to-read-this-repo.md](how-to-read-this-repo.md) + for what each is for) and behavioral tests for all of them. +4. `make check` — ruff, mypy --strict, pytest must all pass. +5. `make readme` — regenerate the catalog table (CI rejects a stale one). + +## Quality bar + +- Full type hints; import-safe modules (no side effects at import). +- Tests assert behavior, not "it runs". +- Prose: one page, problem-first, no UML, no history lessons. diff --git a/docs/how-to-read-this-repo.md b/docs/how-to-read-this-repo.md index 292a0c2..df48409 100644 --- a/docs/how-to-read-this-repo.md +++ b/docs/how-to-read-this-repo.md @@ -1,3 +1,26 @@ -# how-to-read-this-repo +# How to read this repo -_Written in the docs phase._ +Every pattern lives in `patterns///` with the same five parts: + +| File | Job | +|---|---| +| `README.md` | YAML frontmatter (machine-readable metadata) + one page of prose: problem → naive → pythonic → in the wild → verdict | +| `naive.py` | The Gang-of-Four/Java translation, faithfully — even where it looks silly in Python. It exists so you can diff it against `pythonic.py` and *see* what the language absorbs. | +| `pythonic.py` | What a fluent Python developer writes for the same problem. When the pattern collapses into a language feature, this file shows the collapse and names it. | +| `real_world.py` | A small program using the stdlib's own embodiment of the pattern. | +| `tests/` | Behavioral tests for all three variants. | + +## Where to start + +- Reading for education: start with `principle/composition_over_inheritance`, + then any pattern whose *symptom* you recognize (the frontmatter lists them). +- Solving a problem now: search the catalog through the [MCP server](mcp.md) + (`recommend_pattern`) or skim the README table's "problem it solves" column. +- Every example runs: `uv run python -m patterns...`. + +## Groups + +`principle` · `python` (patterns native to the language, from +[python-patterns.guide](https://python-patterns.guide/)) · `creational` / +`structural` / `behavioral` (the GoF 23) · `modern` (post-GoF additions: +DI, Repository, Context Manager, Registry, async producer/consumer). diff --git a/docs/index.md b/docs/index.md index 3b3a0e7..8cfb22f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # Documentation -- [How to read this repo](how-to-read-this-repo.md) -- [Verdicts](verdicts.md) — what `pythonic` / `use-with-care` / `prefer-alternative` mean +- [How to read this repo](how-to-read-this-repo.md) — the unit anatomy and where to start +- [Verdicts](verdicts.md) — what ✅ / ⚠️ / 🔄 mean, and who decides - [MCP server](mcp.md) — connect agents to the catalog -- [Contributing](contributing.md) +- [Contributing](contributing.md) — adding or improving a pattern unit diff --git a/docs/verdicts.md b/docs/verdicts.md index 099c338..2ffe7bc 100644 --- a/docs/verdicts.md +++ b/docs/verdicts.md @@ -1,3 +1,15 @@ -# verdicts +# Verdicts -_Written in the docs phase._ +Every unit's frontmatter carries one verdict — the catalog's honest answer to +"should I write this in Python?" + +| Verdict | Meaning | +|---|---| +| ✅ `pythonic` | Use it as shown in `pythonic.py`; the pattern (in its Python form) is what we'd genuinely recommend. | +| ⚠️ `use-with-care` | Legitimate uses exist, but each caveat in the frontmatter names a concrete failure mode. Read them first. | +| 🔄 `prefer-alternative` | The honest answer is usually "don't". The naive form exists for study; `pythonic.py` shows what to write instead (e.g. Singleton → module global, Visitor → `functools.singledispatch`). | + +Where [python-patterns.guide](https://python-patterns.guide/) has a chapter, +its verdict wins and the unit links it. Where it doesn't, we reason from the +same principles: composition over inheritance, callables over class +hierarchies, the language's own features over ceremony. diff --git a/src/design_patterns/readme_table.py b/src/design_patterns/readme_table.py new file mode 100644 index 0000000..d95a43a --- /dev/null +++ b/src/design_patterns/readme_table.py @@ -0,0 +1,71 @@ +"""Generate the README catalog table from frontmatter -- and keep it honest. + +``python -m design_patterns.readme_table`` rewrites the block between the +markers in README.md; ``--check`` exits non-zero if the block has drifted, +which CI runs so the table can never rot. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from design_patterns.catalog import find_patterns_root, load_catalog + +BEGIN = "" +END = "" + +_GROUP_ORDER = ["principle", "python", "creational", "structural", "behavioral", "modern"] +_GROUP_TITLES = { + "principle": "Principles", + "python": "Python-native", + "creational": "Creational (GoF)", + "structural": "Structural (GoF)", + "behavioral": "Behavioral (GoF)", + "modern": "Modern Python", +} +_VERDICT_BADGES = { + "pythonic": "✅ pythonic", + "use-with-care": "⚠️ use with care", + "prefer-alternative": "🔄 prefer alternative", +} + + +def render_table() -> str: + catalog = load_catalog() + lines: list[str] = [] + for group in _GROUP_ORDER: + members = [p for p in catalog.patterns if p.group == group] + if not members: + continue + lines.append(f"\n### {_GROUP_TITLES[group]}\n") + lines.append("| Pattern | Verdict | Problem it solves |") + lines.append("|---|---|---|") + for p in sorted(members, key=lambda p: p.slug): + link = f"[{p.name}](patterns/{p.id}/)" + lines.append(f"| {link} | {_VERDICT_BADGES[p.verdict]} | {p.problem} |") + return "\n".join(lines) + "\n" + + +def apply(readme: Path) -> str: + text = readme.read_text() + head, rest = text.split(BEGIN, 1) + _, tail = rest.split(END, 1) + return f"{head}{BEGIN}\n{render_table()}{END}{tail}" + + +def main() -> None: + readme = find_patterns_root().parent / "README.md" + fresh = apply(readme) + if "--check" in sys.argv: + if fresh != readme.read_text(): + print("README catalog table is stale: run `make readme`", file=sys.stderr) + raise SystemExit(1) + print("README catalog table is current") + return + readme.write_text(fresh) + print(f"wrote {readme}") + + +if __name__ == "__main__": + main()