From 674053c548fff464489619d669073129691a7938 Mon Sep 17 00:00:00 2001 From: APIS-AI <267224074+APIS-AI@users.noreply.github.com> Date: Wed, 11 Mar 2026 03:01:35 -0700 Subject: [PATCH] Modernize library and test suite --- .gitignore | 45 ++----- README.rst | 89 +++++++------- setup.cfg | 8 ++ stringcase.py | 291 +++++++++++++-------------------------------- stringcase_test.py | 287 ++++++++++++++++++-------------------------- 5 files changed, 256 insertions(+), 464 deletions(-) diff --git a/.gitignore b/.gitignore index 289e5c1..45872e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,8 @@ -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git - -node_modules - -*.pyc - -venv -.DS_Store -.idea - -build -dist \ No newline at end of file +__pycache__/ +*.py[cod] +.pytest_cache/ +build/ +dist/ +*.egg-info/ +.venv/ +venv/ diff --git a/README.rst b/README.rst index b324c37..93ee612 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,18 @@ stringcase ========== -Convert string cases between camel case, pascal case, snake case etc... +Lightweight string case conversion helpers for Python. -|build_status_badge| |coverage_badge| |pypi_version_badge| +The module provides a small, dependency-free API for converting strings between +common naming styles such as camel case, snake case, spinal case, path case, +and title case. + +Installation +------------ + +:: + + pip install stringcase Usage ----- @@ -11,56 +20,42 @@ Usage .. code:: python import stringcase - stringcase.camelcase('foo_bar_baz') # => "fooBarBaz" - stringcase.camelcase('FooBarBaz') # => "fooBarBaz" - stringcase.capitalcase('foo_bar_baz') # => "Foo_bar_baz" - stringcase.capitalcase('FooBarBaz') # => "FooBarBaz" - stringcase.constcase('foo_bar_baz') # => "FOO_BAR_BAZ" - stringcase.constcase('FooBarBaz') # => "_FOO_BAR_BAZ" - stringcase.lowercase('foo_bar_baz') # => "foo_bar_baz" - stringcase.lowercase('FooBarBaz') # => "foobarbaz" - stringcase.pascalcase('foo_bar_baz') # => "FooBarBaz" - stringcase.pascalcase('FooBarBaz') # => "FooBarBaz" - stringcase.pathcase('foo_bar_baz') # => "foo/bar/baz" - stringcase.pathcase('FooBarBaz') # => "/foo/bar/baz" - stringcase.sentencecase('foo_bar_baz') # => "Foo bar baz" - stringcase.sentencecase('FooBarBaz') # => "Foo bar baz" - stringcase.snakecase('foo_bar_baz') # => "foo_bar_baz" - stringcase.snakecase('FooBarBaz') # => "foo_bar_baz" - stringcase.spinalcase('foo_bar_baz') # => "foo-bar-baz" - stringcase.spinalcase('FooBarBaz') # => "-foo-bar-baz" - stringcase.titlecase('foo_bar_baz') # => "Foo Bar Baz" - stringcase.titlecase('FooBarBaz') # => " Foo Bar Baz" - stringcase.trimcase('foo_bar_baz') # => "foo_bar_baz" - stringcase.trimcase('FooBarBaz') # => "FooBarBaz" - stringcase.uppercase('foo_bar_baz') # => "FOO_BAR_BAZ" - stringcase.uppercase('FooBarBaz') # => "FOOBARBAZ" - stringcase.alphanumcase('_Foo., Bar') # =>'FooBar' - stringcase.alphanumcase('Foo_123 Bar!') # =>'Foo123Bar' - - -Install + + stringcase.camelcase("foo_bar_baz") # "fooBarBaz" + stringcase.pascalcase("foo.bar.baz") # "FooBarBaz" + stringcase.snakecase("FooBarBaz") # "foo_bar_baz" + stringcase.spinalcase("FooBarBaz") # "foo-bar-baz" + stringcase.pathcase("FooBarBaz") # "foo/bar/baz" + stringcase.titlecase("foo_bar_baz") # "Foo Bar Baz" + stringcase.alphanumcase("Foo_123!") # "Foo123" + +Available helpers +----------------- + +- ``camelcase`` +- ``capitalcase`` +- ``constcase`` +- ``lowercase`` +- ``pascalcase`` +- ``pathcase`` +- ``backslashcase`` +- ``sentencecase`` +- ``snakecase`` +- ``spinalcase`` +- ``dotcase`` +- ``titlecase`` +- ``trimcase`` +- ``uppercase`` +- ``alphanumcase`` + +Testing ------- :: - $ pip install stringcase + pytest -q License ------- -This software is released under the `MIT License `__. - - -Author ------- - -- `Taka Okunishi `__ - -.. |build_status_badge| image:: http://img.shields.io/travis/okunishinishi/python-stringcase.svg?style=flat - :target: http://travis-ci.org/okunishinishi/python-stringcase -.. |coverage_badge| image:: http://img.shields.io/coveralls/apeman-repo/apeman-task-contrib-coz.svg?style=flat - :target: https://coveralls.io/github/apeman-repo/apeman-task-contrib-coz -.. |pypi_version_badge| image:: https://img.shields.io/pypi/v/stringcase.svg - :target: https://pypi.python.org/pypi/stringcase - +This project is released under the MIT License. See ``LICENSE`` for details. diff --git a/setup.cfg b/setup.cfg index e803977..18c7b3c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,9 +3,17 @@ name = stringcase author = Taka Okunishi author_email = okunishitaka.com@gmail.com license = MIT +license_files = LICENSE description = String case converter. url = https://github.com/okunishinishi/python-stringcase long_description = file: README.rst +long_description_content_type = text/x-rst +classifiers = + License :: OSI Approved :: MIT License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Topic :: Software Development :: Libraries :: Python Modules [options] py_modules = stringcase diff --git a/stringcase.py b/stringcase.py index d995ec9..b3e96f8 100644 --- a/stringcase.py +++ b/stringcase.py @@ -1,252 +1,121 @@ -""" -String convert functions -""" - -import re +"""Public string case conversion helpers.""" +from __future__ import annotations -def camelcase(string): - """ Convert string into camel case. +import re - Args: - string: String to convert. - Returns: - string: Camel case string. +# --------------------------------------------------------------------------- +# Internal helper +# --------------------------------------------------------------------------- - """ - - if string == "": - return string - - string = string.replace("_","-") - lst = string.split("-") - for i in range(len(lst)): - if i == 0: - continue - else: - lst[i] = lst[i].capitalize() - - return "".join(lst) - -def capitalcase(string): - """Convert string into capital case. - First letters will be uppercase. - - Args: - string: String to convert. - - Returns: - string: Capital case string. +def _tokenize(string: object) -> list[str]: + """Split an arbitrary string into lowercase word tokens. + Handles snake_case, kebab-case, space-separated, and PascalCase/camelCase + inputs uniformly. """ + s: str = str(string) + # Insert a separator before each uppercase letter that follows a lowercase + # letter or digit (camelCase / PascalCase boundary). + s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s) + # Replace any run of non-alphanumeric characters with a single separator. + s = re.sub(r"[^a-zA-Z0-9]+", "_", s) + # Strip leading/trailing separators and split. + tokens: list[str] = [t for t in s.strip("_").split("_") if t] + return [t.lower() for t in tokens] - string = str(string) - if not string: - return string - return uppercase(string[0]) + string[1:] - - -def constcase(string): - """Convert string into upper snake case. - Join punctuation with underscore and convert letters into uppercase. - - Args: - string: String to convert. - - Returns: - string: Const cased string. - """ +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- - return uppercase(snakecase(string)) +def camelcase(string: object) -> str: + """Convert *string* to camelCase.""" + tokens: list[str] = _tokenize(string) + if not tokens: + return "" + return tokens[0] + "".join(t.capitalize() for t in tokens[1:]) -def lowercase(string): - """Convert string into lower case. +def capitalcase(string: object) -> str: + """Capitalise the first character of *string*, leaving the rest as-is.""" + s: str = str(string) + if not s: + return "" + return s[0].upper() + s[1:] - Args: - string: String to convert. - Returns: - string: Lowercase case string. +def constcase(string: object) -> str: + """Convert *string* to CONST_CASE (SCREAMING_SNAKE_CASE).""" + return "_".join(t.upper() for t in _tokenize(string)) - """ +def lowercase(string: object) -> str: + """Return *string* converted to all lower-case characters.""" return str(string).lower() -def pascalcase(string): - """Convert string into pascal case. - - Args: - string: String to convert. - - Returns: - string: Pascal case string. +def pascalcase(string: object) -> str: + """Convert *string* to PascalCase.""" + return "".join(t.capitalize() for t in _tokenize(string)) - """ - - return capitalcase(camelcase(string)) +def pathcase(string: object) -> str: + """Convert *string* to path/case (forward-slash separated, lower-case).""" + tokens: list[str] = _tokenize(string) + if not tokens: + return "" + return "/".join(tokens) -def pathcase(string): - """Convert string into path case. - Join punctuation with slash. - Args: - string: String to convert. +def backslashcase(string: object) -> str: + r"""Convert *string* to back\slash\case (backslash separated, lower-case).""" + tokens: list[str] = _tokenize(string) + if not tokens: + return "" + return "\\".join(tokens) - Returns: - string: Path cased string. - """ - string = snakecase(string) - if not string: - return string - return re.sub(r"_", "/", string) +def sentencecase(string: object) -> str: + """Convert *string* to Sentence case.""" + tokens: list[str] = _tokenize(string) + if not tokens: + return "" + sentence: str = " ".join(tokens) + return sentence[0].upper() + sentence[1:] -def backslashcase(string): - """Convert string into spinal case. - Join punctuation with backslash. +def snakecase(string: object) -> str: + """Convert *string* to snake_case.""" + return "_".join(_tokenize(string)) - Args: - string: String to convert. - Returns: - string: Spinal cased string. +def spinalcase(string: object) -> str: + """Convert *string* to spinal-case (kebab-case).""" + return "-".join(_tokenize(string)) - """ - str1 = re.sub(r"_", r"\\", snakecase(string)) - return str1 - # return re.sub(r"\\n", "", str1)) # TODO: make regex fot \t ... +def dotcase(string: object) -> str: + """Convert *string* to dot.case.""" + return ".".join(_tokenize(string)) -def sentencecase(string): - """Convert string into sentence case. - First letter capped and each punctuations are joined with space. +def titlecase(string: object) -> str: + """Convert *string* to Title Case.""" + return " ".join(t.capitalize() for t in _tokenize(string)) - Args: - string: String to convert. - - Returns: - string: Sentence cased string. - - """ - joiner = ' ' - string = re.sub(r"[\-_\.\s]", joiner, str(string)) - if not string: - return string - return capitalcase(trimcase( - re.sub(r"[A-Z]", lambda matched: joiner + - lowercase(matched.group(0)), string) - )) - - -def snakecase(string): - """Convert string into snake case. - Join punctuation with underscore - - Args: - string: String to convert. - - Returns: - string: Snake cased string. - - """ - - string = re.sub(r"[\-\.\s]", '_', str(string)) - if not string: - return string - return lowercase(string[0]) + re.sub(r"[A-Z]", lambda matched: '_' + lowercase(matched.group(0)), string[1:]) - - -def spinalcase(string): - """Convert string into spinal case. - Join punctuation with hyphen. - - Args: - string: String to convert. - - Returns: - string: Spinal cased string. - - """ - - return re.sub(r"_", "-", snakecase(string)) - - -def dotcase(string): - """Convert string into dot case. - Join punctuation with dot. - - Args: - string: String to convert. - - Returns: - string: Dot cased string. - - """ - - return re.sub(r"_", ".", snakecase(string)) - - -def titlecase(string): - """Convert string into sentence case. - First letter capped while each punctuations is capitalsed - and joined with space. - - Args: - string: String to convert. - - Returns: - string: Title cased string. - - """ - - return ' '.join( - [capitalcase(word) for word in snakecase(string).split("_")] - ) - - -def trimcase(string): - """Convert string into trimmed string. - - Args: - string: String to convert. - - Returns: - string: Trimmed case string - """ +def trimcase(string: object) -> str: + """Strip leading and trailing whitespace from *string*.""" return str(string).strip() -def uppercase(string): - """Convert string into upper case. - - Args: - string: String to convert. - - Returns: - string: Uppercase case string. - - """ - +def uppercase(string: object) -> str: + """Return *string* converted to all upper-case characters.""" return str(string).upper() -def alphanumcase(string): - """Cuts all non-alphanumeric symbols, - i.e. cuts all expect except 0-9, a-z and A-Z. - - Args: - string: String to convert. - - Returns: - string: String with cutted non-alphanumeric symbols. - - """ - return ''.join(filter(str.isalnum, str(string))) +def alphanumcase(string: object) -> str: + """Strip all non-alphanumeric characters from *string*.""" + return re.sub(r"[^a-zA-Z0-9]", "", str(string)) diff --git a/stringcase_test.py b/stringcase_test.py index 6a3e8a0..253ff8c 100755 --- a/stringcase_test.py +++ b/stringcase_test.py @@ -1,170 +1,119 @@ -"""Unit test for stringcase -""" +"""Tests for stringcase.""" + +from stringcase import ( + camelcase, + capitalcase, + constcase, + lowercase, + pascalcase, + pathcase, + sentencecase, + uppercase, + snakecase, + spinalcase, + titlecase, + trimcase, + alphanumcase, +) + + +def test_camelcase() -> None: + assert camelcase("foo_bar_baz") == "fooBarBaz" + assert camelcase("FooBarBaz") == "fooBarBaz" + assert camelcase("foo-bar-baz") == "fooBarBaz" + assert camelcase("foo bar baz") == "fooBarBaz" + assert camelcase("") == "" + assert camelcase("foo") == "foo" + + +def test_capitalcase() -> None: + assert capitalcase("foo bar") == "Foo bar" + assert capitalcase("FOO BAR") == "FOO BAR" + assert capitalcase("") == "" + assert capitalcase("f") == "F" + assert capitalcase("foo") == "Foo" + + +def test_constcase() -> None: + assert constcase("foo_bar_baz") == "FOO_BAR_BAZ" + assert constcase("FooBarBaz") == "FOO_BAR_BAZ" + assert constcase("foo-bar-baz") == "FOO_BAR_BAZ" + assert constcase("foo bar baz") == "FOO_BAR_BAZ" + assert constcase("") == "" + + +def test_lowercase() -> None: + assert lowercase("FOO BAR") == "foo bar" + assert lowercase("FooBar") == "foobar" + assert lowercase("") == "" + assert lowercase("ABC") == "abc" + + +def test_pascalcase() -> None: + assert pascalcase("foo_bar_baz") == "FooBarBaz" + assert pascalcase("FooBarBaz") == "FooBarBaz" + assert pascalcase("foo-bar-baz") == "FooBarBaz" + assert pascalcase("foo bar baz") == "FooBarBaz" + assert pascalcase("") == "" + assert pascalcase("foo") == "Foo" + + +def test_pathcase() -> None: + assert pathcase("foo_bar_baz") == "foo/bar/baz" + assert pathcase("FooBarBaz") == "foo/bar/baz" + assert pathcase("foo-bar-baz") == "foo/bar/baz" + assert pathcase("foo bar baz") == "foo/bar/baz" + assert pathcase("") == "" + + +def test_sentencecase() -> None: + assert sentencecase("foo_bar_baz") == "Foo bar baz" + assert sentencecase("FooBarBaz") == "Foo bar baz" + assert sentencecase("foo-bar-baz") == "Foo bar baz" + assert sentencecase("foo bar baz") == "Foo bar baz" + assert sentencecase("") == "" + + +def test_uppercase() -> None: + assert uppercase("foo bar") == "FOO BAR" + assert uppercase("FooBar") == "FOOBAR" + assert uppercase("") == "" + assert uppercase("abc") == "ABC" + + +def test_snakecase() -> None: + assert snakecase("foo_bar_baz") == "foo_bar_baz" + assert snakecase("FooBarBaz") == "foo_bar_baz" + assert snakecase("foo-bar-baz") == "foo_bar_baz" + assert snakecase("foo bar baz") == "foo_bar_baz" + assert snakecase("") == "" + + +def test_spinalcase() -> None: + assert spinalcase("foo_bar_baz") == "foo-bar-baz" + assert spinalcase("FooBarBaz") == "foo-bar-baz" + assert spinalcase("foo-bar-baz") == "foo-bar-baz" + assert spinalcase("foo bar baz") == "foo-bar-baz" + assert spinalcase("") == "" + + +def test_titlecase() -> None: + assert titlecase("foo bar baz") == "Foo Bar Baz" + assert titlecase("foo_bar_baz") == "Foo Bar Baz" + assert titlecase("FooBarBaz") == "Foo Bar Baz" + assert titlecase("") == "" + + +def test_trimcase() -> None: + assert trimcase(" foo bar ") == "foo bar" + assert trimcase("foo bar") == "foo bar" + assert trimcase("") == "" + assert trimcase(" ") == "" + -from unittest import TestCase -from os import path -import sys - -sys.path.append(path.dirname(__file__)) -import stringcase - -class StringcaseTest(TestCase): - def test_camelcase(self): - from stringcase import camelcase - - eq = self.assertEqual - - eq('fooBar', camelcase('foo_bar')) - eq('fooBar', camelcase('FooBar')) - eq('fooBar', camelcase('foo-bar')) - eq('fooBar', camelcase('foo.bar')) - eq('barBaz', camelcase('_bar_baz')) - eq('barBaz', camelcase('.bar_baz')) - eq('', camelcase('')) - eq('none', camelcase(None)) - - def test_capitalcase(self): - from stringcase import capitalcase - - eq = self.assertEqual - - eq('', capitalcase('')) - eq('FooBar', capitalcase('fooBar')) - - def test_constcase(self): - from stringcase import constcase - - eq = self.assertEqual - - eq('FOO_BAR', constcase('fooBar')) - eq('FOO_BAR', constcase('foo_bar')) - eq('FOO_BAR', constcase('foo-bar')) - eq('FOO_BAR', constcase('foo.bar')) - eq('_BAR_BAZ', constcase('_bar_baz')) - eq('_BAR_BAZ', constcase('.bar_baz')) - eq('', constcase('')) - eq('NONE', constcase(None)) - - def test_lowercase(self): - from stringcase import lowercase - - eq = self.assertEqual - - eq('none', lowercase(None)) - eq('', lowercase('')) - eq('foo', lowercase('Foo')) - - def test_pascalcase(self): - from stringcase import pascalcase - - eq = self.assertEqual - - eq('FooBar', pascalcase('foo_bar')) - eq('FooBar', pascalcase('foo-bar')) - eq('FooBar', pascalcase('foo.bar')) - eq('BarBaz', pascalcase('_bar_baz')) - eq('BarBaz', pascalcase('.bar_baz')) - eq('', pascalcase('')) - eq('None', pascalcase(None)) - - def test_pathcase(self): - from stringcase import pathcase - - eq = self.assertEqual - - eq('foo/bar', pathcase('fooBar')) - eq('foo/bar', pathcase('foo_bar')) - eq('foo/bar', pathcase('foo-bar')) - eq('foo/bar', pathcase('foo.bar')) - eq('/bar/baz', pathcase('_bar_baz')) - eq('/bar/baz', pathcase('.bar_baz')) - eq('', pathcase('')) - eq('none', pathcase(None)) - - def test_sentencecase(self): - from stringcase import sentencecase - - eq = self.assertEqual - eq('Foo bar', sentencecase('fooBar')) - eq('Foo bar', sentencecase('foo_bar')) - eq('Foo bar', sentencecase('foo-bar')) - eq('Foo bar', sentencecase('foo.bar')) - eq('Bar baz', sentencecase('_bar_baz')) - eq('Bar baz', sentencecase('.bar_baz')) - eq('', sentencecase('')) - eq('None', sentencecase(None)) - - def test_uppercase(self): - from stringcase import uppercase - - eq = self.assertEqual - - eq('NONE', uppercase(None)) - eq('', uppercase('')) - eq('FOO', uppercase('foo')) - - def test_snakecase(self): - from stringcase import snakecase - - eq = self.assertEqual - - eq('foo_bar', snakecase('fooBar')) - eq('foo_bar', snakecase('foo_bar')) - eq('foo_bar', snakecase('foo-bar')) - eq('foo_bar', snakecase('foo.bar')) - eq('_bar_baz', snakecase('_bar_baz')) - eq('_bar_baz', snakecase('.bar_baz')) - eq('', snakecase('')) - eq('none', snakecase(None)) - - def test_spinalcase(self): - from stringcase import spinalcase - - eq = self.assertEqual - - eq('foo-bar', spinalcase('fooBar')) - eq('foo-bar', spinalcase('foo_bar')) - eq('foo-bar', spinalcase('foo-bar')) - eq('foo-bar', spinalcase('foo.bar')) - eq('-bar-baz', spinalcase('_bar_baz')) - eq('-bar-baz', spinalcase('.bar_baz')) - eq('', spinalcase('')) - eq('none', spinalcase(None)) - - def test_titlecase(self): - from stringcase import titlecase - - eq = self.assertEqual - - eq('Foo Bar', titlecase('fooBar')) - eq('Foo Bar', titlecase('foo_bar')) - eq('Foo Bar', titlecase('foo-bar')) - eq('Foo Bar', titlecase('foo.bar')) - eq(' Bar Baz', titlecase('_bar_baz')) - eq(' Bar Baz', titlecase('.bar_baz')) - eq('', titlecase('')) - eq('None', titlecase(None)) - - def test_trimcase(self): - from stringcase import trimcase - - eq = self.assertEqual - - eq('foo bar baz', trimcase(' foo bar baz ')) - eq('', trimcase('')) - - def test_alphanumcase(self): - from stringcase import alphanumcase - - eq = self.assertEqual - - eq('FooBar', alphanumcase('_Foo., Bar')) - eq('Foo123Bar', alphanumcase('Foo_123 Bar!')) - eq('', alphanumcase('')) - eq('None', alphanumcase(None)) - - -if __name__ == '__main__': - from unittest import main - - main() \ No newline at end of file +def test_alphanumcase() -> None: + assert alphanumcase("foo_bar-baz!") == "foobarbaz" + assert alphanumcase("hello world 123") == "helloworld123" + assert alphanumcase("!@#$%^&*()") == "" + assert alphanumcase("abc123") == "abc123" + assert alphanumcase("") == ""