Coverage for src / python_commitlint / rules / case_validators.py: 96%
51 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"""Case-style validators (lower, upper, camel, pascal, kebab, etc.)."""
3import re
4from collections.abc import Callable
5from typing import ClassVar
7from python_commitlint.core.enums import CaseType
10class CaseValidator:
11 """Static helpers and a dispatcher for checking text against a case style.
13 Each ``is_<style>_case`` method returns ``True`` when the input matches
14 the named style. :meth:`validate` provides a single dispatch entry
15 point keyed by :class:`CaseType`.
16 """
18 @staticmethod
19 def is_lower_case(text: str) -> bool:
20 """Return True when ``text`` is non-empty and entirely lower case."""
21 if not text:
22 return False
23 return text == text.lower()
25 @staticmethod
26 def is_upper_case(text: str) -> bool:
27 """Return True when ``text`` is entirely upper case."""
28 return text == text.upper() and text.isupper()
30 @staticmethod
31 def is_camel_case(text: str) -> bool:
32 """Return True when ``text`` matches camelCase (lowercase first char)."""
33 if not text:
34 return False
35 pattern = r"^[a-z][a-zA-Z0-9]*$"
36 return bool(re.match(pattern, text))
38 @staticmethod
39 def is_kebab_case(text: str) -> bool:
40 """Return True when ``text`` matches kebab-case (hyphen separated)."""
41 pattern = r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$"
42 return bool(re.match(pattern, text))
44 @staticmethod
45 def is_pascal_case(text: str) -> bool:
46 """Return True when ``text`` matches PascalCase (uppercase first char)."""
47 if not text:
48 return False
49 pattern = r"^[A-Z][a-zA-Z0-9]*$"
50 return bool(re.match(pattern, text))
52 @staticmethod
53 def is_sentence_case(text: str) -> bool:
54 """Return True when only the first character of ``text`` is uppercase."""
55 if not text:
56 return False
57 return text[0].isupper() and text[1:] == text[1:].lower()
59 @staticmethod
60 def is_snake_case(text: str) -> bool:
61 """Return True when ``text`` matches snake_case (underscore separated)."""
62 pattern = r"^[a-z][a-z0-9]*(_[a-z0-9]+)*$"
63 return bool(re.match(pattern, text))
65 @staticmethod
66 def is_start_case(text: str) -> bool:
67 """Return True when every word in ``text`` starts with an uppercase letter."""
68 if not text:
69 return False
70 words = text.split()
71 return all(word[0].isupper() for word in words if word)
73 # Defined inline to avoid a two-phase init. `staticmethod` objects are
74 # callable in Python 3.10+, so the dispatcher can store and invoke
75 # them directly.
76 _VALIDATORS: ClassVar[dict[CaseType, Callable[[str], bool]]] = {
77 CaseType.LOWER_CASE: is_lower_case,
78 CaseType.UPPER_CASE: is_upper_case,
79 CaseType.CAMEL_CASE: is_camel_case,
80 CaseType.KEBAB_CASE: is_kebab_case,
81 CaseType.PASCAL_CASE: is_pascal_case,
82 CaseType.SENTENCE_CASE: is_sentence_case,
83 CaseType.SNAKE_CASE: is_snake_case,
84 CaseType.START_CASE: is_start_case,
85 }
87 @classmethod
88 def validate(cls, text: str, case_type: CaseType) -> bool:
89 """Return True when ``text`` matches the requested ``case_type``.
91 Args:
92 text: The text to validate.
93 case_type: The expected case style.
95 Returns:
96 True on a match, False otherwise (including when ``case_type``
97 is unknown).
98 """
99 validator = cls._VALIDATORS.get(case_type)
100 if not validator:
101 return False
102 return validator(text)