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

68 statements  

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

1"""Rules that validate the subject (the text after ``type(scope):``).""" 

2 

3from python_commitlint.core.enums import CaseType, RuleCondition 

4from python_commitlint.core.models import ( 

5 CommitMessage, 

6 RuleConfig, 

7 ValidationError, 

8) 

9from python_commitlint.rules.base import BaseRule, config_value_or 

10from python_commitlint.rules.case_validators import CaseValidator 

11 

12 

13class SubjectEmptyRule(BaseRule): 

14 """Require or forbid an empty subject. Rule name: ``subject-empty``.""" 

15 

16 @property 

17 def name(self) -> str: 

18 return "subject-empty" 

19 

20 def validate( 

21 self, commit: CommitMessage, config: RuleConfig 

22 ) -> ValidationError | None: 

23 is_empty = not commit.subject 

24 should_be_empty = config.condition == RuleCondition.ALWAYS 

25 

26 if is_empty != should_be_empty: 

27 msg = ( 

28 "subject may not be empty" 

29 if not should_be_empty 

30 else "subject must be empty" 

31 ) 

32 return self._create_error(config, msg) 

33 return None 

34 

35 

36class SubjectCaseRule(BaseRule): 

37 """Enforce one or more case styles on the subject. Rule name: ``subject-case``.""" 

38 

39 @property 

40 def name(self) -> str: 

41 return "subject-case" 

42 

43 def validate( 

44 self, commit: CommitMessage, config: RuleConfig 

45 ) -> ValidationError | None: 

46 if not commit.subject or config.value is None: 

47 return None 

48 

49 case_types = ( 

50 config.value if isinstance(config.value, list) else [config.value] 

51 ) 

52 

53 matches_any = any( 

54 CaseValidator.validate(commit.subject, CaseType(case_type)) 

55 for case_type in case_types 

56 ) 

57 

58 should_match = config.condition == RuleCondition.ALWAYS 

59 

60 if matches_any != should_match: 

61 return self._create_error( 

62 config, f"subject must match case {config.value}" 

63 ) 

64 return None 

65 

66 

67class SubjectFullStopRule(BaseRule): 

68 """Require or forbid a trailing punctuation character. Rule name: ``subject-full-stop``.""" 

69 

70 @property 

71 def name(self) -> str: 

72 return "subject-full-stop" 

73 

74 def validate( 

75 self, commit: CommitMessage, config: RuleConfig 

76 ) -> ValidationError | None: 

77 if not commit.subject: 

78 return None 

79 

80 stop_char = config_value_or(config, ".") 

81 ends_with_stop = commit.subject.endswith(stop_char) 

82 should_end_with_stop = config.condition == RuleCondition.ALWAYS 

83 

84 if ends_with_stop != should_end_with_stop: 

85 msg = ( 

86 f"subject must end with '{stop_char}'" 

87 if should_end_with_stop 

88 else f"subject may not end with '{stop_char}'" 

89 ) 

90 return self._create_error(config, msg) 

91 return None 

92 

93 

94class SubjectMinLengthRule(BaseRule): 

95 """Enforce a minimum subject length. Rule name: ``subject-min-length``.""" 

96 

97 @property 

98 def name(self) -> str: 

99 return "subject-min-length" 

100 

101 def validate( 

102 self, commit: CommitMessage, config: RuleConfig 

103 ) -> ValidationError | None: 

104 if not commit.subject: 

105 return None 

106 

107 min_length = config_value_or(config, 0) 

108 is_valid = len(commit.subject) >= min_length 

109 should_be_valid = config.condition == RuleCondition.ALWAYS 

110 

111 if is_valid != should_be_valid: 

112 return self._create_error( 

113 config, f"subject must be at least {min_length} characters" 

114 ) 

115 return None 

116 

117 

118class SubjectMaxLengthRule(BaseRule): 

119 """Enforce a maximum subject length. Rule name: ``subject-max-length``.""" 

120 

121 @property 

122 def name(self) -> str: 

123 return "subject-max-length" 

124 

125 def validate( 

126 self, commit: CommitMessage, config: RuleConfig 

127 ) -> ValidationError | None: 

128 if not commit.subject: 

129 return None 

130 

131 max_length = config_value_or(config, float("inf")) 

132 is_valid = len(commit.subject) <= max_length 

133 should_be_valid = config.condition == RuleCondition.ALWAYS 

134 

135 if is_valid != should_be_valid: 

136 return self._create_error( 

137 config, f"subject must be at most {max_length} characters" 

138 ) 

139 return None