feat: add hexagonal architecture scaffolding#2
Open
mariushelf wants to merge 4 commits into
Open
Conversation
Restructure the generated package into a hexagonal (ports-and-adapters)
layout with domain/services/adapters layers and main.py as the driving
adapter / composition root. A small, clearly-marked ("# --- example")
seed slice -- a Note entity, a NoteRepository port, an in-memory adapter,
and a NoteService -- demonstrates the pattern end to end and is meant to
be deleted once real domain modelling begins.
Enforce the layering with import-linter contracts in pyproject.toml
(layer ordering plus domain/services kept free of infrastructure), wired
into pre-commit (lint-imports hook), the Makefile (folded into `make
lint`), and the CI lint job. Document the architecture and the
"every port implementation must explicitly inherit from its port" rule
in AGENTS.md.
Switch the example NoteRepository port from typing.Protocol to an abc.ABC with @AbstractMethod, and update the adapter and AGENTS.md wording accordingly: an ABC enforces the contract at instantiation time (a subclass missing an abstract method cannot be instantiated), so inheritance is mandatory rather than a convention layered on top of structural typing. Also rename AGENTS.md -> AGENTS.md.jinja so the new {{ project_slug }} references in the Architecture section actually render (copier only templates files with the .jinja suffix), and add an Architecture section to the README stating the project uses hexagonal architecture.
…ecture # Conflicts: # template/pyproject.toml.jinja
The project_slug default sanitised only a blocklist of characters (spaces,
hyphens, dots), so other characters leaked into the slug. The default
project_name ("<author>'s new project") contains an apostrophe, producing the
invalid package name "joe_doe's_new_project" on a defaults render.
Switch to allowlist sanitisation (collapse every run of non-alphanumeric
characters into a single underscore, then trim) and add a validator that
rejects any project_slug that is not a valid Python package name, whether
derived or user-supplied. Add regression tests covering both paths.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Restructures the generated package into a hexagonal (ports-and-adapters) layout, ported from the structure of the
qualitative-feedback-analysis(QFA) repository.Package layout
No web framework is pulled in —
main.pyis the driving adapter. A small, clearly-marked seed slice (aNoteentity, aNoteRepositoryport, anInMemoryNoteRepositoryadapter, and aNoteService) demonstrates the full pattern and is meant to be deleted once real domain modelling starts. Search# --- exampleto find/remove it.Enforcement (the important part)
The layering is enforced by
import-lintercontracts inpyproject.toml, not just by convention:adapters > services > domain.main.pyis intentionally not a layer, so the composition root is free to wire adapters into services without exceptions.forbiddencontracts (seeded withhttpx/requests/sqlalchemyas illustrative examples to extend).Wired into:
.pre-commit-config.yaml(lint-importshook)Makefile(import_lint, folded intomake lint)lintjob incicd.yamlDocs / AGENTS.md
AGENTS.mdgains an Architecture section (layer rules, package responsibilities) and the QFA rule that every class implementing a port must explicitly inherit from it (discoverability over structural typing), plus a short Testing & Linting section.Deliberately excluded
docs/) — handled in a separate PR.Verification
The template's own test suite (
tests/test_template.py) passes end-to-end on the generated project —pytest,make lint(ruff + ty + import-linter),make test,python -m main, andpre-commit run --all-files(incl. the newlint-importshook). All 6 tests green.