Source code for behave_priority.parser
"""Tag priority parsing."""
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Protocol
from behave_priority.exceptions import PriorityParseError
if TYPE_CHECKING:
from behave_priority.config import PriorityConfig
_PRIORITY_RE = re.compile(r"^priority\(\s*([+-]?\d+)\s*\)$")
_FEATURE_PRIORITY_RE = re.compile(r"^feature-priority\(\s*([+-]?\d+)\s*\)$")
[docs]
class Taggable(Protocol):
"""Protocol for objects with tags (Scenario, Feature).
Attributes:
tags: List of tag strings as behave stores them (without '@' prefix).
"""
tags: list[str]
[docs]
def parse_priority(tags: list[str]) -> int | None:
"""Parse @priority(N) from a tag list.
Args:
tags: List of tag strings (without '@' prefix, as behave stores them).
Returns:
Priority integer, or None if no priority tag found.
Raises:
PriorityParseError: If a priority tag exists but has invalid syntax.
"""
found: int | None = None
for tag in tags:
tag = tag.strip().removeprefix("@")
if not tag.startswith("priority("):
continue
match = _PRIORITY_RE.match(tag)
if match:
value = int(match.group(1))
found = value if found is None else min(found, value)
continue
raise PriorityParseError(f"Invalid priority tag: {tag}")
return found
[docs]
def parse_feature_priority(tags: list[str]) -> int | None:
"""Parse @feature-priority(N) from a tag list.
Same semantics as parse_priority but for feature-level tags.
Args:
tags: List of tag strings (without '@' prefix, as behave stores them).
Returns:
Priority integer, or None if no feature-priority tag found.
Raises:
PriorityParseError: If a feature-priority tag exists but has invalid
syntax.
"""
found: int | None = None
for tag in tags:
tag = tag.strip().removeprefix("@")
if not tag.startswith("feature-priority("):
continue
match = _FEATURE_PRIORITY_RE.match(tag)
if match:
value = int(match.group(1))
found = value if found is None else min(found, value)
continue
raise PriorityParseError(f"Invalid feature-priority tag: {tag}")
return found
[docs]
def resolve_priority(
scenario_tags: list[str],
feature_tags: list[str],
config: PriorityConfig,
rule_tags: list[str] | None = None,
) -> int:
"""Resolve effective priority for a scenario.
Precedence: scenario > rule > feature > default.
Args:
scenario_tags: Tags on the scenario (without '@' prefix).
feature_tags: Tags on the parent feature (without '@' prefix).
config: Configuration containing the default priority fallback.
rule_tags: Tags on the parent rule (Gherkin v6), if any.
Returns:
The effective priority integer for the scenario.
"""
scenario_priority = parse_priority(scenario_tags)
if scenario_priority is not None:
return scenario_priority
if rule_tags:
rule_priority = parse_priority(rule_tags)
if rule_priority is not None:
return rule_priority
feature_priority = parse_feature_priority(feature_tags)
if feature_priority is not None:
return feature_priority
return config.default_priority
[docs]
def is_critical(tags: list[str], critical_tag: str = "critical") -> bool:
"""Check if a tag list contains the critical tag.
Both the tags and ``critical_tag`` are normalized by stripping a leading
'@' prefix and whitespace before comparison.
Args:
tags: List of tag strings to search.
critical_tag: The tag name that marks a scenario as critical.
Defaults to ``"critical"``.
Returns:
True if the critical tag is present in the tag list.
"""
normalized = {t.removeprefix("@").strip() for t in tags}
return critical_tag.removeprefix("@").strip() in normalized