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
« 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):``)."""
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
13class SubjectEmptyRule(BaseRule):
14 """Require or forbid an empty subject. Rule name: ``subject-empty``."""
16 @property
17 def name(self) -> str:
18 return "subject-empty"
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
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
36class SubjectCaseRule(BaseRule):
37 """Enforce one or more case styles on the subject. Rule name: ``subject-case``."""
39 @property
40 def name(self) -> str:
41 return "subject-case"
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
49 case_types = (
50 config.value if isinstance(config.value, list) else [config.value]
51 )
53 matches_any = any(
54 CaseValidator.validate(commit.subject, CaseType(case_type))
55 for case_type in case_types
56 )
58 should_match = config.condition == RuleCondition.ALWAYS
60 if matches_any != should_match:
61 return self._create_error(
62 config, f"subject must match case {config.value}"
63 )
64 return None
67class SubjectFullStopRule(BaseRule):
68 """Require or forbid a trailing punctuation character. Rule name: ``subject-full-stop``."""
70 @property
71 def name(self) -> str:
72 return "subject-full-stop"
74 def validate(
75 self, commit: CommitMessage, config: RuleConfig
76 ) -> ValidationError | None:
77 if not commit.subject:
78 return None
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
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
94class SubjectMinLengthRule(BaseRule):
95 """Enforce a minimum subject length. Rule name: ``subject-min-length``."""
97 @property
98 def name(self) -> str:
99 return "subject-min-length"
101 def validate(
102 self, commit: CommitMessage, config: RuleConfig
103 ) -> ValidationError | None:
104 if not commit.subject:
105 return None
107 min_length = config_value_or(config, 0)
108 is_valid = len(commit.subject) >= min_length
109 should_be_valid = config.condition == RuleCondition.ALWAYS
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
118class SubjectMaxLengthRule(BaseRule):
119 """Enforce a maximum subject length. Rule name: ``subject-max-length``."""
121 @property
122 def name(self) -> str:
123 return "subject-max-length"
125 def validate(
126 self, commit: CommitMessage, config: RuleConfig
127 ) -> ValidationError | None:
128 if not commit.subject:
129 return None
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
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