Source code for behave_priority.sorter

"""Reorders behave's feature and scenario lists by priority."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Protocol

from behave_priority.parser import resolve_priority

if TYPE_CHECKING:
    from behave_priority.config import PriorityConfig


[docs] class ScenarioLike(Protocol): """Protocol for scenario-like objects. Attributes: name: Display name of the scenario. tags: List of tag strings (without '@' prefix). status: Execution status (``"passed"``, ``"failed"``, etc.). duration: Execution time in seconds. """ name: str tags: list[str] status: str duration: float
[docs] class FeatureLike(Protocol): """Protocol for feature-like objects. Attributes: name: Display name of the feature, or None. filename: Path to the feature file. tags: List of tag strings (without '@' prefix). scenarios: List of scenarios belonging to this feature. """ name: str | None filename: str tags: list[str] scenarios: list[ScenarioLike]
[docs] class ScenarioSorter: """Reorders behave's feature and scenario lists by priority.""" def __init__(self, config: PriorityConfig) -> None: """Initialize the sorter. Args: config: Configuration controlling sort behavior. """ self._config = config
[docs] def sort(self, features: list[FeatureLike]) -> list[FeatureLike]: """Sort features and their scenarios by priority. Each feature's ``scenarios`` list and ``run_items`` list (if present) are reordered in-place. The features list itself is also sorted when ordering is enabled. The sort is **stable**: scenarios with the same priority preserve their original relative order. This ensures that Scenario Outlines expanded by behave (which share the same tags and therefore the same priority) remain grouped together after sorting. When a feature's ``run_items`` contains ``Rule`` objects (Gherkin v6), each rule's inner ``run_items`` and ``scenarios`` are also sorted. Args: features: List of features to sort. Returns: A new list of features sorted by priority. """ for feature in features: sorted_scenarios = self._sort_scenarios( feature.scenarios, feature.tags ) feature.scenarios = sorted_scenarios feature_obj: Any = feature run_items = getattr(feature_obj, "run_items", None) if run_items is not None: sorted_items = self._sort_run_items( run_items, feature.tags ) feature_obj.run_items = sorted_items if self._config.order or self._config.reverse or self._config.priority_tag: features = sorted(features, key=self._feature_sort_key) return features
def _sort_run_items( self, run_items: list[Any], feature_tags: list[str] ) -> list[Any]: """Sort run_items, recursing into Rule objects. Args: run_items: List of run items (Scenario, ScenarioOutline, or Rule). feature_tags: Tags from the parent feature. Returns: A new list of run items sorted by priority. """ for item in run_items: if self._is_rule(item): rule_obj: Any = item rule_tags = getattr(rule_obj, "tags", []) inner_items = getattr(rule_obj, "run_items", None) if inner_items is not None: rule_obj.run_items = self._sort_scenarios( inner_items, feature_tags, rule_tags ) inner_scenarios = getattr(rule_obj, "scenarios", None) if inner_scenarios is not None: rule_obj.scenarios = self._sort_scenarios( inner_scenarios, feature_tags, rule_tags ) return sorted( run_items, key=lambda item: self._run_item_sort_key(item, feature_tags), ) @staticmethod def _is_rule(item: Any) -> bool: """Check if a run item is a Rule object (Gherkin v6). Within ``feature.run_items``, only ``Rule`` objects have a ``run_items`` attribute. ``Scenario`` and ``ScenarioOutline`` do not. Args: item: The run item to check. Returns: True if the item is a Rule (has ``run_items``). """ return hasattr(item, "run_items") def _run_item_sort_key( self, item: Any, feature_tags: list[str] ) -> tuple[int, int]: """Compute sort key for a run item (Scenario or Rule). Args: item: The run item to evaluate. feature_tags: Tags from the parent feature. Returns: A tuple of (primary, secondary) sort keys. """ if self._is_rule(item): rule_tags = getattr(item, "tags", []) inner_items: Any = ( getattr(item, "run_items", None) or item.scenarios ) if not inner_items: return (1, self._config.default_priority) return min( self._sort_key(s.tags, feature_tags, rule_tags) for s in inner_items ) return self._sort_key(item.tags, feature_tags) def _sort_scenarios( self, scenarios: list[ScenarioLike], feature_tags: list[str], rule_tags: list[str] | None = None, ) -> list[ScenarioLike]: """Sort scenarios within a feature (or rule) by priority. Args: scenarios: Scenarios to sort. feature_tags: Tags from the parent feature. rule_tags: Tags from the parent rule (Gherkin v6), if any. Returns: A new list of scenarios sorted by priority. """ return sorted( scenarios, key=lambda s: self._sort_key(s.tags, feature_tags, rule_tags), ) def _sort_key( self, scenario_tags: list[str], feature_tags: list[str], rule_tags: list[str] | None = None, ) -> tuple[int, int]: """Compute sort key for a scenario. Args: scenario_tags: Tags on the scenario. feature_tags: Tags from the parent feature. rule_tags: Tags from the parent rule (Gherkin v6), if any. Returns: A tuple of (primary, secondary) sort keys. """ priority = resolve_priority( scenario_tags, feature_tags, self._config, rule_tags ) if self._config.priority_tag: tag = self._config.priority_tag.removeprefix("@") all_tags = list(scenario_tags) if rule_tags: all_tags.extend(rule_tags) all_tags.extend(feature_tags) normalized = {t.removeprefix("@").strip() for t in all_tags} primary = 0 if tag in normalized else 1 else: primary = 0 secondary = -priority if self._config.reverse else priority return (primary, secondary) def _feature_sort_key(self, feature: FeatureLike) -> tuple[int, int]: """Compute sort key for a feature based on its best run item. Handles both plain scenarios and Rule objects (Gherkin v6). Args: feature: The feature to evaluate. Returns: A tuple of (primary, secondary) sort keys. """ items: Any = getattr(feature, "run_items", None) if items: best = min( self._run_item_sort_key(item, feature.tags) for item in items ) return best scenarios = feature.scenarios if not scenarios: return (1, self._config.default_priority) return min( self._sort_key(s.tags, feature.tags) for s in scenarios )