Coverage for src / python_commitlint / rules / base.py: 100%

13 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-04-28 02:54 +0000

1"""Abstract base class for built-in commitlint rules.""" 

2 

3from abc import ABC, abstractmethod 

4 

5from python_commitlint.core.models import ( 

6 CommitMessage, 

7 RuleConfig, 

8 ValidationError, 

9) 

10from python_commitlint.core.protocols import RuleProtocol 

11 

12 

13def config_value_or[T](config: RuleConfig, default: T) -> T: 

14 """Return ``config.value`` if it is not ``None``, otherwise ``default``. 

15 

16 Avoids the falsy-zero bug of ``config.value or default``: a configured 

17 ``value: 0`` is preserved, not silently replaced by the default. 

18 

19 Note: 

20 ``RuleConfig.value`` is typed ``Any``, so the ``T`` return type is 

21 nominally a lie when the YAML stores a value whose Python type 

22 doesn't match ``default``. Callers are expected to validate the 

23 shape of ``config.value`` themselves (or accept downstream 

24 ``TypeError`` if the YAML is malformed). 

25 """ 

26 return config.value if config.value is not None else default 

27 

28 

29class BaseRule(RuleProtocol, ABC): 

30 """Abstract base for all built-in commitlint rules. 

31 

32 Inheriting from :class:`RuleProtocol` couples the ABC and the structural 

33 Protocol so they cannot drift independently — adding a method to 

34 ``RuleProtocol`` immediately makes ``BaseRule`` (and every subclass) 

35 fail to instantiate. 

36 

37 Third-party rules need not inherit from ``BaseRule``. Any class that 

38 structurally implements :class:`RuleProtocol` (a ``name`` property 

39 and a ``validate`` method with the correct signature) can be passed 

40 to :meth:`RuleRegistry.register`. 

41 """ 

42 

43 @property 

44 @abstractmethod 

45 def name(self) -> str: 

46 """The rule's stable identifier (e.g. ``type-case``).""" 

47 

48 @abstractmethod 

49 def validate( 

50 self, commit: CommitMessage, config: RuleConfig 

51 ) -> ValidationError | None: 

52 """Apply the rule to ``commit`` under ``config``. 

53 

54 Args: 

55 commit: Parsed commit to validate. 

56 config: This rule's severity, condition, and optional value. 

57 

58 Returns: 

59 A :class:`ValidationError` if violated, otherwise ``None``. 

60 """ 

61 

62 def _create_error( 

63 self, config: RuleConfig, message: str 

64 ) -> ValidationError: 

65 """Construct a :class:`ValidationError` carrying this rule's name. 

66 

67 Helper for subclasses so they don't repeat the boilerplate of 

68 threading ``self.name`` and ``config.severity`` through every 

69 violation site. 

70 """ 

71 return ValidationError( 

72 rule=self.name, 

73 message=message, 

74 severity=config.severity, 

75 )