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

1"""Case-style validators (lower, upper, camel, pascal, kebab, etc.).""" 

2 

3import re 

4from collections.abc import Callable 

5from typing import ClassVar 

6 

7from python_commitlint.core.enums import CaseType 

8 

9 

10class CaseValidator: 

11 """Static helpers and a dispatcher for checking text against a case style. 

12 

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 """ 

17 

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() 

24 

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() 

29 

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)) 

37 

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)) 

43 

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)) 

51 

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() 

58 

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)) 

64 

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) 

72 

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 } 

86 

87 @classmethod 

88 def validate(cls, text: str, case_type: CaseType) -> bool: 

89 """Return True when ``text`` matches the requested ``case_type``. 

90 

91 Args: 

92 text: The text to validate. 

93 case_type: The expected case style. 

94 

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)