Best Practices¶
Rules of thumb for structuring hexa pipelines that stay easy to extend and verify. Grounded in the extraction pipeline sample and the test suite.
๐ณ Layer Discipline¶
- Never subclass an ABC for a bank. Bank classes always subclass the concrete
Impl*classes. The ABC defines the contract once;Impl*carries behavior; banks only vary. - Pin concrete types at
Impl*, not at the ABC. The ABC declares shape (number: NumberPort);Impl*decides implementation (number: ImplNumberPort). - Keep the root thin. A bank root is one annotated subclass:
๐ฃ Only Redeclare What You Change¶
The redundancy rule: if a slot's behavior is already correct from Impl*, leave it out.
dateis not redeclared onAxisTransactionParserPortโ it inheritsImplDatePort.ambiguityis not redeclared onAxisAmountBalancePortโ it inheritsImplAmbiguityPort.- Re-declaring slots you don't change adds mental load on the extender with zero behavior change โ and keeps the diff between a bank and
Impl*minimal.
If you do redeclare a slot, re-pin it back to your own bank's port (e.g. amount_balance.number: AxisNumberPort) so a single bank's number formatting is consistent down the subtree.
๐ Representational Fidelity¶
Keep YAML and ABC in agreement:
- Every converter (
parse_yaml,generate_abc,parse_abc,generate_yaml) serializes through the samePortNodetree โ any tree fits, nothing is tied to a particular sample. - Use
check_matches(spec.yaml, abc.py)(orhexa check ...) as a CI guard whenever both representations are maintained. - Class-level attributes on ABCs are used as documentation of the config surface; pin defaults at the
Impl*level so the ABC stays a pure contract. - Nested grouped values are expressed once per representation: a
@dataclassannotation (ABC) โ a nested struct mapping (YAML).
๐งฉ Wiring Discipline¶
- Put plain runtime objects that must not be re-created (repos, handlers, clients) in
instances=โ wired verbatim by name, never reconstructed. - Put plain config values in
config=โ propagated by name across the whole tree, including nested ports. - Name a slot whatever the annotated field is called;
buildmatchesinstances/configby that annotated name, so names must agree across the tree. - Keep port slot annotations to concrete
Impl*/bank classes (or ABCs when the leaf is a config-only option). Builtin/typing/union-annotated fields are treated as config, not ports โ so don't annotate a port with# type: str.
๐งช Test Layout¶
Mirror tests/:
- One test module per component (
test_parse_yaml.py,test_parse_abc.py,test_generate_abc.py, ...). - A dedicated
test_container.pyforbuildbehavior (injection semantics, config propagation, defaults). - Use
samples/minimalas the smallest contrived base fixture: one root, one nested port, a few config vars, a single abstract method. It bikes the whole YAML โ ABC โ runtime loop in a few lines. - Test the round-trip property directly โ parsed models from both representations must be equal (
node1.diff(node3) == []) and regeneration must be idempotent.