Design Decisions¶
1. Behave parser reuse¶
Decision: Wrap Behave's parser instead of reimplementing Gherkin parsing.
Rationale: Behave's parser is battle-tested, handles edge cases, supports multiple languages, and is compatible with Tag Expression v2. Reimplementing Gherkin parsing would be a massive effort and would inevitably lag behind Behave's updates.
Trade-off: We depend on behave as a runtime dependency. This is acceptable since behave-model is designed for Behave projects.
2. Mutable dataclasses¶
Decision: Model classes are plain (mutable) dataclasses. Only Location is frozen.
Rationale: Transformations mutate the model in place, which requires mutable nodes. Keeping the model mutable keeps the transformation code simple and avoids object.__setattr__ hacks or expensive dataclasses.replace chains.
Trade-off: The model is not protected against accidental mutation. Location is frozen because it is shared freely and acts as a value object.
3. Composition over inheritance¶
Decision: Model classes are flat dataclasses, not a deep inheritance hierarchy.
Rationale: ScenarioOutline mirrors Scenario's interface (same attributes plus examples) but is a sibling class, not a subclass — code distinguishes them with isinstance checks. Everything else uses composition (e.g., Feature has rules, scenarios, tags — it doesn't inherit from them).
Trade-off: Some code duplication between Feature and Rule (both have background, scenarios, tags). This is preferable to a shared base class that would couple them.
4. Visitor pattern¶
Decision: Use the visitor pattern for tree traversal.
Rationale: Visitors decouple traversal logic from the model. You can add new operations (counting, collecting, analysis) without modifying model classes. The model just calls visitor.visit_* and the visitor decides what to do.
Trade-off: Adding a new model class requires adding a visit_* method to the base Visitor. This is a small, well-contained change.
5. In-place transformations¶
Decision: Transformations modify the model in place.
Rationale: Copying the entire model for every transformation would be expensive for large projects. Users who need immutability can copy.deepcopy before transforming.
Trade-off: Users must be aware that transformations are destructive. This is documented in the docstrings and guides.
6. Query mixin pattern¶
Decision: Query functions are mixed into Project and also available as standalone functions.
Rationale: project.find_scenarios(tag="@smoke") is more ergonomic than find_scenarios(project, tag="@smoke"). But standalone functions are useful for functional programming and composition.
Trade-off: The Project class has many methods. This is acceptable since they're all query-related and well-organized.
7. Gherkin v6 Rules as first-class objects¶
Decision: Rule is a first-class domain object, not just a grouping mechanism.
Rationale: Rules have their own background, tags, and scenarios. Treating them as first-class objects makes the model more accurate and enables Rule-specific queries, validation, and serialization.
Trade-off: feature.scenarios only returns scenarios not inside any Rule. Users must use project.all_scenarios() to get everything. This is documented clearly.
8. No external dependencies beyond Behave¶
Decision: behave-model only depends on behave at runtime.
Rationale: Minimizing dependencies makes the library easier to install and use in any environment. All dev dependencies (pytest, ruff, mkdocs) are optional.
Trade-off: We don't use libraries like pydantic for validation or rich for pretty printing. Our implementations are minimal but sufficient.