diff options
Diffstat (limited to '')
-rw-r--r-- | src/aristaproto/casing.py | 143 |
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 |