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