1import keyword
2import re
3
4
5# Word delimiters and symbols that will not be preserved when re-casing.
6# language=PythonRegExp
7SYMBOLS = "[^a-zA-Z0-9]*"
8
9# Optionally capitalized word.
10# language=PythonRegExp
11WORD = "[A-Z]*[a-z]*[0-9]*"
12
13# Uppercase word, not followed by lowercase letters.
14# language=PythonRegExp
15WORD_UPPER = "[A-Z]+(?![a-z])[0-9]*"
16
17
18def safe_snake_case(value: str) -> str:
19 """Snake case a value taking into account Python keywords."""
20 value = snake_case(value)
21 value = sanitize_name(value)
22 return value
23
24
25def snake_case(value: str, strict: bool = True) -> str:
26 """
27 Join words with an underscore into lowercase and remove symbols.
28
29 Parameters
30 -----------
31 value: :class:`str`
32 The value to convert.
33 strict: :class:`bool`
34 Whether or not to force single underscores.
35
36 Returns
37 --------
38 :class:`str`
39 The value in snake_case.
40 """
41
42 def substitute_word(symbols: str, word: str, is_start: bool) -> str:
43 if not word:
44 return ""
45 if strict:
46 delimiter_count = 0 if is_start else 1 # Single underscore if strict.
47 elif is_start:
48 delimiter_count = len(symbols)
49 elif word.isupper() or word.islower():
50 delimiter_count = max(
51 1, len(symbols)
52 ) # Preserve all delimiters if not strict.
53 else:
54 delimiter_count = len(symbols) + 1 # Extra underscore for leading capital.
55
56 return ("_" * delimiter_count) + word.lower()
57
58 snake = re.sub(
59 f"(^)?({SYMBOLS})({WORD_UPPER}|{WORD})",
60 lambda groups: substitute_word(groups[2], groups[3], groups[1] is not None),
61 value,
62 )
63 return snake
64
65
66def pascal_case(value: str, strict: bool = True) -> str:
67 """
68 Capitalize each word and remove symbols.
69
70 Parameters
71 -----------
72 value: :class:`str`
73 The value to convert.
74 strict: :class:`bool`
75 Whether or not to output only alphanumeric characters.
76
77 Returns
78 --------
79 :class:`str`
80 The value in PascalCase.
81 """
82
83 def substitute_word(symbols, word):
84 if strict:
85 return word.capitalize() # Remove all delimiters
86
87 if word.islower():
88 delimiter_length = len(symbols[:-1]) # Lose one delimiter
89 else:
90 delimiter_length = len(symbols) # Preserve all delimiters
91
92 return ("_" * delimiter_length) + word.capitalize()
93
94 return re.sub(
95 f"({SYMBOLS})({WORD_UPPER}|{WORD})",
96 lambda groups: substitute_word(groups[1], groups[2]),
97 value,
98 )
99
100
101def camel_case(value: str, strict: bool = True) -> str:
102 """
103 Capitalize all words except first and remove symbols.
104
105 Parameters
106 -----------
107 value: :class:`str`
108 The value to convert.
109 strict: :class:`bool`
110 Whether or not to output only alphanumeric characters.
111
112 Returns
113 --------
114 :class:`str`
115 The value in camelCase.
116 """
117 return lowercase_first(pascal_case(value, strict=strict))
118
119
120def lowercase_first(value: str) -> str:
121 """
122 Lower cases the first character of the value.
123
124 Parameters
125 ----------
126 value: :class:`str`
127 The value to lower case.
128
129 Returns
130 -------
131 :class:`str`
132 The lower cased string.
133 """
134 return value[0:1].lower() + value[1:]
135
136
137def sanitize_name(value: str) -> str:
138 # https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles
139 if keyword.iskeyword(value):
140 return f"{value}_"
141 if not value.isidentifier():
142 return f"_{value}"
143 return value