summaryrefslogtreecommitdiffstats
path: root/src/aristaproto/casing.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/aristaproto/casing.py143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/aristaproto/casing.py b/src/aristaproto/casing.py
new file mode 100644
index 0000000..f7d0832
--- /dev/null
+++ b/src/aristaproto/casing.py
@@ -0,0 +1,143 @@
+import keyword
+import re
+
+
+# Word delimiters and symbols that will not be preserved when re-casing.
+# language=PythonRegExp
+SYMBOLS = "[^a-zA-Z0-9]*"
+
+# Optionally capitalized word.
+# language=PythonRegExp
+WORD = "[A-Z]*[a-z]*[0-9]*"
+
+# Uppercase word, not followed by lowercase letters.
+# language=PythonRegExp
+WORD_UPPER = "[A-Z]+(?![a-z])[0-9]*"
+
+
+def safe_snake_case(value: str) -> str:
+ """Snake case a value taking into account Python keywords."""
+ value = snake_case(value)
+ value = sanitize_name(value)
+ return value
+
+
+def snake_case(value: str, strict: bool = True) -> str:
+ """
+ Join words with an underscore into lowercase and remove symbols.
+
+ Parameters
+ -----------
+ value: :class:`str`
+ The value to convert.
+ strict: :class:`bool`
+ Whether or not to force single underscores.
+
+ Returns
+ --------
+ :class:`str`
+ The value in snake_case.
+ """
+
+ def substitute_word(symbols: str, word: str, is_start: bool) -> str:
+ if not word:
+ return ""
+ if strict:
+ delimiter_count = 0 if is_start else 1 # Single underscore if strict.
+ elif is_start:
+ delimiter_count = len(symbols)
+ elif word.isupper() or word.islower():
+ delimiter_count = max(
+ 1, len(symbols)
+ ) # Preserve all delimiters if not strict.
+ else:
+ delimiter_count = len(symbols) + 1 # Extra underscore for leading capital.
+
+ return ("_" * delimiter_count) + word.lower()
+
+ snake = re.sub(
+ f"(^)?({SYMBOLS})({WORD_UPPER}|{WORD})",
+ lambda groups: substitute_word(groups[2], groups[3], groups[1] is not None),
+ value,
+ )
+ return snake
+
+
+def pascal_case(value: str, strict: bool = True) -> str:
+ """
+ Capitalize each word and remove symbols.
+
+ Parameters
+ -----------
+ value: :class:`str`
+ The value to convert.
+ strict: :class:`bool`
+ Whether or not to output only alphanumeric characters.
+
+ Returns
+ --------
+ :class:`str`
+ The value in PascalCase.
+ """
+
+ def substitute_word(symbols, word):
+ if strict:
+ return word.capitalize() # Remove all delimiters
+
+ if word.islower():
+ delimiter_length = len(symbols[:-1]) # Lose one delimiter
+ else:
+ delimiter_length = len(symbols) # Preserve all delimiters
+
+ return ("_" * delimiter_length) + word.capitalize()
+
+ return re.sub(
+ f"({SYMBOLS})({WORD_UPPER}|{WORD})",
+ lambda groups: substitute_word(groups[1], groups[2]),
+ value,
+ )
+
+
+def camel_case(value: str, strict: bool = True) -> str:
+ """
+ Capitalize all words except first and remove symbols.
+
+ Parameters
+ -----------
+ value: :class:`str`
+ The value to convert.
+ strict: :class:`bool`
+ Whether or not to output only alphanumeric characters.
+
+ Returns
+ --------
+ :class:`str`
+ The value in camelCase.
+ """
+ return lowercase_first(pascal_case(value, strict=strict))
+
+
+def lowercase_first(value: str) -> str:
+ """
+ Lower cases the first character of the value.
+
+ Parameters
+ ----------
+ value: :class:`str`
+ The value to lower case.
+
+ Returns
+ -------
+ :class:`str`
+ The lower cased string.
+ """
+ return value[0:1].lower() + value[1:]
+
+
+def sanitize_name(value: str) -> str:
+ # https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles
+ if keyword.iskeyword(value):
+ return f"{value}_"
+ if not value.isidentifier():
+ return f"_{value}"
+ return value