├── .gitignore ├── .gitlab-ci.yml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── caseconverter ├── __init__.py ├── alternating.py ├── alternating_test.py ├── boundaries.py ├── camel.py ├── camel_test.py ├── caseconverter.py ├── caseconverter_test.py ├── cobol.py ├── cobol_test.py ├── flat.py ├── flat_test.py ├── kebab.py ├── kebab_test.py ├── macro.py ├── macro_test.py ├── pascal.py ├── pascal_test.py ├── snake.py ├── snake_test.py ├── title.py └── title_test.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - deploy 4 | 5 | build: 6 | stage: build 7 | image: python:3.8-alpine 8 | before_script: 9 | - apk add make 10 | - pip install pytest pytest-cov build 11 | - pip install -e . 12 | script: 13 | - make coverage 14 | after_script: 15 | - make package 16 | artifacts: 17 | paths: 18 | - dist/ 19 | expire_in: 1 week 20 | 21 | # deploy-test: 22 | # stage: deploy 23 | # image: python:3.8-buster 24 | # before_script: 25 | # - pip install twine 26 | # script: 27 | # - find 28 | # - make check 29 | # - TWINE_PASSWORD=${TWINE_TEST_PASSWORD} make upload-test 30 | 31 | deploy: 32 | stage: deploy 33 | image: python:3.8-buster 34 | rules: 35 | # Only execute deployment if we're on a release branch and have /release 36 | # in the commit message. 37 | - if: '$CI_COMMIT_TAG =~ /^v.*/' 38 | before_script: 39 | - pip install twine 40 | script: 41 | - make check 42 | - make upload -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "pythonTestExplorer.testFramework": "pytest", 3 | "python.testing.pytestEnabled": true, 4 | "python.testing.pytestArgs": [ 5 | "-v", 6 | "." 7 | ], 8 | "runOnSave.commands": [ 9 | 10 | { 11 | "match": ".*\\.py$", 12 | "command": "black ${file}", 13 | "runIn": "backend", 14 | } 15 | ], 16 | "python.testing.unittestEnabled": false, 17 | "python.testing.nosetestsEnabled": false, 18 | "python.formatting.provider": "black" 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chris Doherty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON := python3 2 | 3 | test: 4 | $(PYTHON) -m pytest ./caseconverter/ 5 | 6 | coverage: 7 | $(PYTHON) -m pytest --cov-report=term --cov=caseconverter **/*_test.py 8 | 9 | package: 10 | $(PYTHON) -m build 11 | 12 | check: 13 | $(PYTHON) -m twine check dist/* 14 | 15 | upload-test: 16 | $(PYTHON) -m twine upload --repository testpypi dist/* 17 | 18 | upload: 19 | $(PYTHON) -m twine upload dist/* 20 | 21 | dependencies: 22 | $(PYTHON) -m pip install pytest twine setuptools -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Case Converter 2 | 3 | [![pipline](https://img.shields.io/gitlab/pipeline/chrisdoherty4/python-case-converter)](https://gitlab.com/chrisdoherty4/python-case-converter/-/pipelines) [![wheel](https://img.shields.io/pypi/wheel/case-converter)](https://pypi.org/project/case-converter/) ![coverage](https://gitlab.com/chrisdoherty4/python-case-converter/badges/master/coverage.svg) ![license](https://img.shields.io/github/license/chrisdoherty4/python-case-converter) 4 | 5 | A robust python package for transforming string cases such as `Hello, world!` into 6 | `helloWorld` (camelcase). 7 | 8 | ## General usage 9 | 10 | ```python 11 | from caseconverter import camelcase 12 | 13 | camelcase("Hello, world!") # output: helloWorld 14 | ``` 15 | 16 | ## Available conversions 17 | 18 | ### `alternatingcase` 19 | 20 | ```python 21 | from caseconverter import alternatingcase 22 | 23 | alternatingcase("Hello, world!") 24 | ``` 25 | 26 | ```text 27 | hElLo WoRlD 28 | ``` 29 | 30 | 31 | ### `camelcase` 32 | 33 | ```python 34 | from caseconverter import camelcase 35 | 36 | camelcase("Hello, world!") 37 | ``` 38 | 39 | ```text 40 | helloWorld 41 | ``` 42 | 43 | ### `cobolcase` 44 | 45 | ```python 46 | from caseconverter import cobolcase 47 | 48 | cobolcase("Hello, world!") 49 | ``` 50 | 51 | ```text 52 | HELLO-WORLD 53 | ``` 54 | 55 | ### `flatcase` 56 | 57 | ```python 58 | from caseconverter import flatcase 59 | 60 | flatcase("Hello, world!") 61 | ``` 62 | 63 | ```text 64 | helloworld 65 | ``` 66 | 67 | ### `kebabcase` 68 | 69 | ```python 70 | 71 | from caseconverter import kebabcase 72 | 73 | kebabcase("Hello, world!") 74 | ``` 75 | 76 | ```text 77 | hello-world 78 | ``` 79 | 80 | ### `macrocase` 81 | 82 | ```python 83 | from caseconverter import macrocase 84 | 85 | macrocase("Hello, world!") 86 | ``` 87 | 88 | ```text 89 | HELLO_WORLD 90 | ``` 91 | 92 | #### Additional options 93 | 94 | `delims_only : bool` - Only consider delimiters as boundaries (default: `False`). 95 | 96 | ### `pascalcase` 97 | 98 | ```python 99 | from caseconverter import pascalcase 100 | 101 | pascalcase("Hello, world!") 102 | ``` 103 | 104 | ```text 105 | HelloWorld 106 | ``` 107 | 108 | ### `snakecase` 109 | 110 | ```python 111 | from caseconverter import snakecase 112 | 113 | snakecase("Hello, world!") 114 | ``` 115 | 116 | ```text 117 | hello_world 118 | ``` 119 | 120 | ### `titlecase` 121 | 122 | ```python 123 | from caseconverter import titlecase 124 | 125 | titlecase("Hello, world!") 126 | ``` 127 | 128 | ```text 129 | Hello World 130 | ``` 131 | 132 | ## Options for all conversions 133 | 134 | ### Stripping punctuation 135 | 136 | Punctuation is stripped when doing a case conversion. However, should you 137 | wish to keep the punctuation you can do so by passing `strip_punctuation=False`. 138 | 139 | ```python 140 | camelcase("Hello, world!", strip_punctuation=False) # output: hello,World! 141 | ``` 142 | 143 | ### Delimeter customization 144 | 145 | Default delimiters used to denote a token boundary. 146 | 147 | ```python 148 | DELIMITERS = " -_" 149 | ``` 150 | 151 | You can pass `delims` to each case conversion function to specify a custom 152 | set of delimiters. 153 | 154 | ```python 155 | # Use a pipe `|` as the only delimiter. 156 | camelcase("Hello,|world!", delims="|") # output: helloWorld 157 | ``` 158 | 159 | ## Behavior 160 | 161 | ### Delimiters 162 | 163 | If multiple delimeter characters are identified next to eachother they will be considered as a single delimeter. For example, `-_` contains 2 different delimeter characters and is considered a single delimeter. 164 | 165 | ### Boundary definitions 166 | 167 | |Name|Description| 168 | |---|---| 169 | |OnDelimeterUppercaseNext|On a delimieter, upper case the following character| 170 | |OnDelimeterLowercaseNext|On a delimeter, lower case the following character| 171 | |OnUpperPrecededByLowerAppendUpper|On an upper case character followed by a lower case character, append the upper case character| 172 | |OnUpperPrecededByLowerAppendLower|On an upper case character preceeded by a lower case character append the lower case character| 173 | |OnUpperPrecededByUpperAppendJoin|On an upper case caharacter preceeded by an upper append the join character. Join characters are context dependent. Example: macro cast join character is `_`| 174 | |OnUpperPrecededByUpperAppendCurrent|On an upper case character preceeded by an upper case character append the upper case character| 175 | 176 | ## Contributing 177 | 178 | 1. Write clean code. 179 | 2. Write new tests for new use-cases. 180 | 3. Test your code before raising a PR. 181 | 4. Use [black](https://pypi.org/project/black/) to format your code. 182 | -------------------------------------------------------------------------------- /caseconverter/__init__.py: -------------------------------------------------------------------------------- 1 | from .caseconverter import CaseConverter, DELIMITERS 2 | from .alternating import Alternating, alternatingcase 3 | from .camel import Camel, camelcase 4 | from .cobol import Cobol, cobolcase 5 | from .flat import Flat, flatcase 6 | from .kebab import Kebab, kebabcase 7 | from .macro import Macro, macrocase 8 | from .pascal import Pascal, pascalcase 9 | from .snake import Snake, snakecase 10 | from .title import Title, titlecase 11 | -------------------------------------------------------------------------------- /caseconverter/alternating.py: -------------------------------------------------------------------------------- 1 | from .caseconverter import CaseConverter 2 | from .boundaries import BoundaryHandler 3 | 4 | class Alternating(CaseConverter): 5 | 6 | def define_boundaries(self): 7 | self.add_boundary_handler(self.BoundaryOverride()) 8 | 9 | def prepare_string(self, s): 10 | self._toggle_character = False 11 | return s.lower() 12 | 13 | def mutate(self, c): 14 | if not c.isalpha(): 15 | return c 16 | 17 | if self._toggle_character: 18 | self._toggle_character = False 19 | return c.upper() 20 | 21 | self._toggle_character = True 22 | return c 23 | 24 | 25 | class BoundaryOverride(BoundaryHandler): 26 | def is_boundary(self, pc, c): 27 | return False 28 | 29 | 30 | def alternatingcase(s, **kwargs): 31 | """Convert a string to alternating case, or its better known name: mocking Spongebob case. 32 | 33 | Example 34 | 35 | Hello World => hElLo WoRlD 36 | 37 | """ 38 | return Alternating(s, **kwargs).convert() 39 | -------------------------------------------------------------------------------- /caseconverter/alternating_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation. 9 | ("Hello, world!", "hElLo WoRlD"), 10 | # Camel cased 11 | ("helloWorld", "hElLoWoRlD"), 12 | # Joined by delimeter. 13 | ("Hello-World", "hElLo WoRlD"), 14 | # Cobol cased 15 | ("HELLO-WORLD", "hElLo WoRlD"), 16 | # Without punctuation. 17 | ("Hello world", "hElLo WoRlD"), 18 | # Repeating single delimeter 19 | ("Hello World", "hElLo WoRlD"), 20 | # Repeating delimeters of different types 21 | ("Hello -__ World", "hElLo WoRlD"), 22 | # Wrapped in delimeter 23 | (" hello world ", "hElLo WoRlD"), 24 | # End in capital letter 25 | ("hellO", "hElLo"), 26 | # Long sentence with punctuation 27 | ( 28 | r"the quick !b@rown fo%x jumped over the laZy Do'G", 29 | "tHe QuIcK bRoWn FoX jUmPeD oVeR tHe LaZy DoG", 30 | ), 31 | # Alternating character cases 32 | ("heLlo WoRld", "hElLo WoRlD"), 33 | ], 34 | ) 35 | def test_alternating_with_default_args(input, output): 36 | assert alternatingcase(input) == output 37 | -------------------------------------------------------------------------------- /caseconverter/boundaries.py: -------------------------------------------------------------------------------- 1 | class BoundaryHandler(object): 2 | """Detect and handle boundaries in a string. 3 | 4 | The BoundaryHandler is an interface for a CaseConverter instance. It provides 5 | methods for detecting a boundary in a string as well as how to handle 6 | the boundary. 7 | """ 8 | 9 | def is_boundary(self, pc, c): 10 | """Determine if we're on a boundary. 11 | 12 | :param pc: Previous character 13 | :param cc: Current character 14 | :return: True if a boundary is found, else false. 15 | :rtype: boolean 16 | """ 17 | raise NotImplementedError() 18 | 19 | def handle(self, pc, cc, input_buffer, output_buffer): 20 | """Handle a detected boundary. 21 | 22 | :param pc: Previous character 23 | :type pc: str 24 | :param cc: Current character 25 | :type cc: str 26 | :param input_buffer: The raw string wrapped in a buffer. 27 | :type input_buffer: StringBuffer 28 | :param output_buffer: The output buffer that stores the new string as 29 | it's constructed. 30 | :type output_buffer: StringBuffer 31 | """ 32 | raise NotImplementedError() 33 | 34 | 35 | class OnDelimeterUppercaseNext(BoundaryHandler): 36 | def __init__(self, delimiters, join_char=""): 37 | self._delimiters = delimiters 38 | self._join_char = join_char 39 | 40 | def is_boundary(self, pc, c): 41 | return c in self._delimiters 42 | 43 | def handle(self, pc, cc, input_buffer, output_buffer): 44 | output_buffer.write(self._join_char) 45 | output_buffer.write(input_buffer.read(1).upper()) 46 | 47 | 48 | class OnDelimeterLowercaseNext(BoundaryHandler): 49 | def __init__(self, delimiters, join_char=""): 50 | self._delimiters = delimiters 51 | self._join_char = join_char 52 | 53 | def is_boundary(self, pc, c): 54 | return c in self._delimiters 55 | 56 | def handle(self, pc, cc, input_buffer, output_buffer): 57 | output_buffer.write(self._join_char) 58 | output_buffer.write(input_buffer.read(1).lower()) 59 | 60 | 61 | class OnUpperPrecededByLowerAppendUpper(BoundaryHandler): 62 | def __init__(self, join_char=""): 63 | self._join_char = join_char 64 | 65 | def is_boundary(self, pc, c): 66 | return pc != None and pc.isalpha() and pc.islower() and c.isupper() 67 | 68 | def handle(self, pc, cc, input_buffer, output_buffer): 69 | output_buffer.write(self._join_char) 70 | output_buffer.write(cc) 71 | 72 | 73 | class OnUpperPrecededByLowerAppendLower(BoundaryHandler): 74 | def __init__(self, join_char=""): 75 | self._join_char = join_char 76 | 77 | def is_boundary(self, pc, c): 78 | return pc != None and pc.isalpha() and pc.islower() and c.isupper() 79 | 80 | def handle(self, pc, cc, input_buffer, output_buffer): 81 | output_buffer.write(self._join_char) 82 | output_buffer.write(cc.lower()) 83 | 84 | 85 | class OnUpperPrecededByUpperAppendJoin(BoundaryHandler): 86 | def __init__(self, join_char=""): 87 | self._join_char = join_char 88 | 89 | def is_boundary(self, pc, c): 90 | return pc != None and pc.isalpha() and pc.isupper() and c.isupper() 91 | 92 | def handle(self, pc, cc, input_buffer, output_buffer): 93 | output_buffer.write(self._join_char) 94 | output_buffer.write(cc) 95 | 96 | 97 | class OnUpperPrecededByUpperAppendCurrent(BoundaryHandler): 98 | def __init__(self, join_char=""): 99 | self._join_char = join_char 100 | 101 | def is_boundary(self, pc, c): 102 | return pc != None and pc.isalpha() and pc.isupper() and c.isupper() 103 | 104 | def handle(self, pc, cc, input_buffer, output_buffer): 105 | output_buffer.write(cc) 106 | -------------------------------------------------------------------------------- /caseconverter/camel.py: -------------------------------------------------------------------------------- 1 | from .caseconverter import CaseConverter 2 | from .boundaries import OnDelimeterUppercaseNext, OnUpperPrecededByLowerAppendUpper 3 | 4 | 5 | class Camel(CaseConverter): 6 | def define_boundaries(self): 7 | self.add_boundary_handler(OnDelimeterUppercaseNext(self.delimiters())) 8 | self.add_boundary_handler(OnUpperPrecededByLowerAppendUpper()) 9 | 10 | def prepare_string(self, s): 11 | if s.isupper(): 12 | return s.lower() 13 | 14 | return s 15 | 16 | def mutate(self, c): 17 | return c.lower() 18 | 19 | 20 | def camelcase(s, **kwargs): 21 | """Convert a string to camel case. 22 | 23 | Example 24 | 25 | Hello World => helloWorld 26 | 27 | """ 28 | return Camel(s, **kwargs).convert() 29 | -------------------------------------------------------------------------------- /caseconverter/camel_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation. 9 | ("Hello, world!", "helloWorld"), 10 | # Camel cased 11 | ("helloWorld", "helloWorld"), 12 | # Joined by delimeter. 13 | ("Hello-World", "helloWorld"), 14 | # Cobol cased 15 | ("HELLO-WORLD", "helloWorld"), 16 | # Without punctuation. 17 | ("Hello world", "helloWorld"), 18 | # Repeating single delimeter 19 | ("Hello World", "helloWorld"), 20 | # Repeating delimeters of different types 21 | ("Hello -__ World", "helloWorld"), 22 | # Wrapped in delimeter 23 | (" hello world ", "helloWorld"), 24 | # End in capital letter 25 | ("hellO", "hellO"), 26 | # Long sentence with punctuation 27 | ( 28 | r"the quick !b@rown fo%x jumped over the laZy Do'G", 29 | "theQuickBrownFoxJumpedOverTheLaZyDoG", 30 | ), 31 | # Alternating character cases 32 | ("heLlo WoRld", "heLloWoRld"), 33 | ], 34 | ) 35 | def test_camel_with_default_args(input, output): 36 | assert camelcase(input) == output 37 | -------------------------------------------------------------------------------- /caseconverter/caseconverter.py: -------------------------------------------------------------------------------- 1 | import re 2 | import string 3 | import logging 4 | from io import StringIO 5 | 6 | logger = logging.getLogger(__name__) 7 | logger.setLevel(logging.INFO) 8 | 9 | ch = logging.StreamHandler() 10 | ch.setLevel(logging.DEBUG) 11 | ch.setFormatter( 12 | logging.Formatter("%(asctime)s : %(name)s : %(levelname)s : %(message)s") 13 | ) 14 | logger.addHandler(ch) 15 | 16 | 17 | class StringBuffer(StringIO): 18 | """StringBuffer is a wrapper around StringIO. 19 | 20 | By wrapping StringIO, adding debugging information is easy. 21 | """ 22 | 23 | def write(self, s): 24 | super(StringBuffer, self).write(s) 25 | 26 | 27 | DELIMITERS = " -_" 28 | 29 | 30 | def stripable_punctuation(delimiters): 31 | """Construct a string of stripable punctuation based on delimiters. 32 | 33 | Stripable punctuation is defined as all punctuation that is not a delimeter. 34 | """ 35 | return "".join([c for c in string.punctuation if c not in delimiters]) 36 | 37 | 38 | class CaseConverter(object): 39 | def __init__(self, s, delimiters=DELIMITERS, strip_punctuation=True): 40 | """Initialize a case conversion. 41 | 42 | On initialization, punctuation can be optionally stripped. If 43 | punctuation is not stripped, it will appear in the output at the 44 | same position as the input. 45 | 46 | BoundaryHandlers should take into consideration whether or not 47 | they are evaluating the first character in a string and whether or 48 | not a character is punctuation. 49 | 50 | Delimeters are taken into consideration when defining stripable 51 | punctuation. 52 | 53 | Delimeters will be reduced to single instances of a delimeter. This 54 | includes transforming ` -_-__ ` to `-`. 55 | 56 | During initialization, the raw input string will be passed through 57 | the prepare_string() method. Child classes should override this 58 | method if they wish to perform pre-conversion checks and manipulate 59 | the string accordingly. 60 | 61 | :param s: The raw string to convert. 62 | :type s: str 63 | :param delimiters: A set of delimiters used to identify boundaries. 64 | Defaults to DELIMITERS 65 | :type delimiters: str 66 | """ 67 | self._delimiters = delimiters 68 | 69 | s = s.strip(delimiters) 70 | 71 | if strip_punctuation: 72 | punctuation = stripable_punctuation(delimiters) 73 | s = re.sub("[{}]+".format(re.escape(punctuation)), "", s) 74 | 75 | # Change recurring delimiters into single delimiters. 76 | s = re.sub("[{}]+".format(re.escape(delimiters)), delimiters[0], s) 77 | 78 | self._raw_input = s 79 | self._input_buffer = StringBuffer(self.prepare_string(s)) 80 | self._output_buffer = StringBuffer() 81 | self._boundary_handlers = [] 82 | 83 | self.define_boundaries() 84 | 85 | def add_boundary_handler(self, handler): 86 | """Add a boundary handler. 87 | 88 | :type handler: BoundaryHandler 89 | """ 90 | self._boundary_handlers.append(handler) 91 | 92 | def define_boundaries(self): 93 | """Define boundary handlers. 94 | 95 | define_boundaries() is called when a CaseConverter is initialized. 96 | define_boundaries() should be overridden in a child class to add 97 | boundary handlers. 98 | 99 | A CaseConverter without boundary handlers makes little sense. 100 | """ 101 | logger.warn("No boundaries defined") 102 | return 103 | 104 | def delimiters(self): 105 | """Retrieve the delimiters. 106 | 107 | :rtype: str 108 | """ 109 | return self._delimiters 110 | 111 | def raw(self): 112 | """Retrieve the raw string to be converted. 113 | 114 | :rtype: str 115 | """ 116 | return self._raw_input 117 | 118 | def init(self, input_buffer, output_buffer): 119 | """Initialize the output buffer. 120 | 121 | Can be overridden. 122 | 123 | See convert() for call order. 124 | """ 125 | return 126 | 127 | def mutate(self, c): 128 | """Mutate a character not on a boundary. 129 | 130 | Can be overridden. 131 | 132 | See convert() for call order. 133 | """ 134 | return c 135 | 136 | def prepare_string(self, s) -> str: 137 | """Prepare the raw intput string for conversion. 138 | 139 | Executed during CaseConverter initialization providing an opportunity 140 | for child classes to manipulate the string. By default, the string 141 | is not manipulated. 142 | 143 | Can be overridden. 144 | 145 | :param s: The raw string supplied to the CaseConverter constructor. 146 | :type s: str 147 | :return: A raw string to be used in conversion. 148 | :rtype: str 149 | """ 150 | return s 151 | 152 | def _is_boundary(self, pc, c): 153 | """Determine if we've hit a boundary or not. 154 | 155 | :rtype: BoundaryHandler 156 | """ 157 | for bh in self._boundary_handlers: 158 | if bh.is_boundary(pc, c): 159 | return bh 160 | 161 | return None 162 | 163 | def convert(self) -> str: 164 | """Convert the raw string. 165 | 166 | convert() follows a series of steps. 167 | 168 | 1. Initialize the output buffer using `init()`. 169 | For every character in the input buffer: 170 | 2. Check if the current position lies on a boundary as defined 171 | by the BoundaryHandler instances. 172 | 3. If on a boundary, execute the handler. 173 | 4. Else apply a mutation to the character via `mutate()` and add 174 | the mutated character to the output buffer. 175 | 176 | :return: The converted string. 177 | :rtype: str 178 | """ 179 | self.init(self._input_buffer, self._output_buffer) 180 | 181 | logger.debug("input_buffer = {}".format(self._input_buffer.getvalue())) 182 | 183 | # Previous character (pc) and current character (cc) 184 | pc = None 185 | cc = self._input_buffer.read(1) 186 | 187 | while cc: 188 | logger.debug( 189 | "pc = '{}'; cc = '{}'; input_buffer.tell() = {}; output_buffer = '{}'".format( 190 | pc, cc, self._input_buffer.tell(), self._output_buffer.getvalue() 191 | ) 192 | ) 193 | bh = self._is_boundary(pc, cc) 194 | if bh: 195 | bh.handle(pc, cc, self._input_buffer, self._output_buffer) 196 | else: 197 | self._output_buffer.write(self.mutate(cc)) 198 | 199 | pc = cc 200 | cc = self._input_buffer.read(1) 201 | 202 | return self._output_buffer.getvalue() 203 | -------------------------------------------------------------------------------- /caseconverter/caseconverter_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | ("Hell9o, world!", "hell9oWorld"), 9 | ("0Hello, world!", "0helloWorld"), 10 | ("Hello, world!0", "helloWorld0"), 11 | ], 12 | ) 13 | def test_with_numbers(input, output): 14 | assert camelcase(input) == output 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "input, output", 19 | [ 20 | ("Hello, world!", "hello,World!"), 21 | ], 22 | ) 23 | def test_no_strip_punctuation(input, output): 24 | assert camelcase(input, strip_punctuation=False) == output 25 | -------------------------------------------------------------------------------- /caseconverter/cobol.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .caseconverter import CaseConverter 3 | from .boundaries import OnDelimeterUppercaseNext, OnUpperPrecededByLowerAppendUpper 4 | 5 | 6 | class Cobol(CaseConverter): 7 | 8 | JOIN_CHAR = "-" 9 | 10 | def define_boundaries(self): 11 | self.add_boundary_handler( 12 | OnDelimeterUppercaseNext(self.delimiters(), self.JOIN_CHAR) 13 | ) 14 | self.add_boundary_handler(OnUpperPrecededByLowerAppendUpper(self.JOIN_CHAR)) 15 | 16 | def convert(self): 17 | if self.raw().isupper(): 18 | return re.sub( 19 | "[{}]+".format(re.escape(self.delimiters())), 20 | self.JOIN_CHAR, 21 | self.raw(), 22 | ) 23 | 24 | return super(Cobol, self).convert() 25 | 26 | def mutate(self, c): 27 | return c.upper() 28 | 29 | 30 | def cobolcase(s, **kwargs): 31 | """Convert a string to cobol case 32 | 33 | Example 34 | 35 | Hello World => HELLO-WORLD 36 | 37 | """ 38 | return Cobol(s, **kwargs).convert() 39 | -------------------------------------------------------------------------------- /caseconverter/cobol_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation. 9 | ("Hello, world!", "HELLO-WORLD"), 10 | # Camel cased 11 | ("helloWorld", "HELLO-WORLD"), 12 | # Joined by delimeter. 13 | ("Hello-World", "HELLO-WORLD"), 14 | # Cobol cased 15 | ("HELLO-WORLD", "HELLO-WORLD"), 16 | # Without punctuation. 17 | ("Hello world", "HELLO-WORLD"), 18 | # Repeating single delimeter 19 | ("Hello World", "HELLO-WORLD"), 20 | # Repeating delimeters of different types 21 | ("Hello -__ World", "HELLO-WORLD"), 22 | # Wrapped in delimeter 23 | (" hello world ", "HELLO-WORLD"), 24 | # End in capital letter 25 | ("hellO", "HELL-O"), 26 | # Long sentence with punctuation 27 | ( 28 | r"the quick !b@rown fo%x jumped over the laZy Do'G", 29 | "THE-QUICK-BROWN-FOX-JUMPED-OVER-THE-LA-ZY-DO-G", 30 | ), 31 | # Alternating character cases 32 | ("heLlo WoRld", "HE-LLO-WO-RLD"), 33 | ], 34 | ) 35 | def test_cobol_with_default_args(input, output): 36 | assert cobolcase(input) == output 37 | -------------------------------------------------------------------------------- /caseconverter/flat.py: -------------------------------------------------------------------------------- 1 | from .caseconverter import CaseConverter 2 | from .boundaries import OnDelimeterLowercaseNext, OnUpperPrecededByLowerAppendLower 3 | 4 | 5 | class Flat(CaseConverter): 6 | def define_boundaries(self): 7 | self.add_boundary_handler(OnDelimeterLowercaseNext(self.delimiters())) 8 | self.add_boundary_handler(OnUpperPrecededByLowerAppendLower()) 9 | 10 | def prepare_string(self, s): 11 | if s.isupper(): 12 | return s.lower() 13 | 14 | return s 15 | 16 | def mutate(self, c): 17 | return c.lower() 18 | 19 | 20 | def flatcase(s, **kwargs): 21 | """Convert a string to flat case 22 | 23 | Example 24 | 25 | Hello World => helloworld 26 | 27 | """ 28 | return Flat(s, **kwargs).convert() 29 | -------------------------------------------------------------------------------- /caseconverter/flat_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation. 9 | ("Hello, world!", "helloworld"), 10 | # Camel cased 11 | ("helloWorld", "helloworld"), 12 | # Joined by delimeter. 13 | ("Hello-World", "helloworld"), 14 | # Cobol cased 15 | ("HELLO-WORLD", "helloworld"), 16 | # Without punctuation. 17 | ("Hello world", "helloworld"), 18 | # Repeating single delimeter 19 | ("Hello World", "helloworld"), 20 | # Repeating delimeters of different types 21 | ("Hello -__ World", "helloworld"), 22 | # Wrapped in delimeter 23 | (" hello world ", "helloworld"), 24 | # End in capital letter 25 | ("hellO", "hello"), 26 | # Long sentence with punctuation 27 | ( 28 | r"the quick !b@rown fo%x jumped over the laZy Do'G", 29 | "thequickbrownfoxjumpedoverthelazydog", 30 | ), 31 | # Alternating character cases 32 | ("heLlo WoRld", "helloworld"), 33 | ], 34 | ) 35 | def test_flat_with_default_args(input, output): 36 | assert flatcase(input) == output 37 | -------------------------------------------------------------------------------- /caseconverter/kebab.py: -------------------------------------------------------------------------------- 1 | from .caseconverter import CaseConverter 2 | from .boundaries import OnDelimeterLowercaseNext, OnUpperPrecededByLowerAppendLower 3 | 4 | 5 | class Kebab(CaseConverter): 6 | 7 | JOIN_CHAR = "-" 8 | 9 | def define_boundaries(self): 10 | self.add_boundary_handler( 11 | OnDelimeterLowercaseNext(self.delimiters(), self.JOIN_CHAR) 12 | ) 13 | self.add_boundary_handler(OnUpperPrecededByLowerAppendLower(self.JOIN_CHAR)) 14 | 15 | def prepare_string(self, s): 16 | if s.isupper(): 17 | return s.lower() 18 | 19 | return s 20 | 21 | def mutate(self, c): 22 | return c.lower() 23 | 24 | 25 | def kebabcase(s, **kwargs): 26 | """Convert a string to kebab case 27 | 28 | Example 29 | 30 | Hello World => hello-world 31 | 32 | """ 33 | return Kebab(s, **kwargs).convert() 34 | -------------------------------------------------------------------------------- /caseconverter/kebab_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation. 9 | ("Hello, world!", "hello-world"), 10 | # Camel cased 11 | ("helloWorld", "hello-world"), 12 | # Joined by delimeter. 13 | ("Hello-World", "hello-world"), 14 | # Cobol cased 15 | ("HELLO-WORLD", "hello-world"), 16 | # Without punctuation. 17 | ("Hello world", "hello-world"), 18 | # Repeating single delimeter 19 | ("Hello World", "hello-world"), 20 | # Repeating delimeters of different types 21 | ("Hello -__ World", "hello-world"), 22 | # Wrapped in delimeter 23 | (" hello world ", "hello-world"), 24 | # End in capital letter 25 | ("hellO", "hell-o"), 26 | # Long sentence with punctuation 27 | ( 28 | r"the quick !b@rown fo%x jumped over the laZy Do'G", 29 | "the-quick-brown-fox-jumped-over-the-la-zy-do-g", 30 | ), 31 | # Alternating character cases 32 | ("heLlo WoRld", "he-llo-wo-rld"), 33 | ], 34 | ) 35 | def test_kebab_with_default_args(input, output): 36 | assert kebabcase(input) == output 37 | -------------------------------------------------------------------------------- /caseconverter/macro.py: -------------------------------------------------------------------------------- 1 | import re 2 | from .caseconverter import CaseConverter 3 | from .boundaries import ( 4 | OnDelimeterUppercaseNext, 5 | OnUpperPrecededByLowerAppendUpper, 6 | OnUpperPrecededByUpperAppendJoin, 7 | ) 8 | 9 | 10 | class Macro(CaseConverter): 11 | 12 | JOIN_CHAR = "_" 13 | 14 | def __init__(self, *args, delims_only=False, **kwargs): 15 | self._delims_only = delims_only 16 | super(Macro, self).__init__(*args, **kwargs) 17 | 18 | def define_boundaries(self): 19 | self.add_boundary_handler( 20 | OnDelimeterUppercaseNext(self.delimiters(), self.JOIN_CHAR) 21 | ) 22 | 23 | if not self._delims_only: 24 | self.add_boundary_handler(OnUpperPrecededByLowerAppendUpper(self.JOIN_CHAR)) 25 | self.add_boundary_handler(OnUpperPrecededByUpperAppendJoin(self.JOIN_CHAR)) 26 | 27 | def convert(self): 28 | if self.raw().isupper(): 29 | return re.sub( 30 | "[{}]+".format(re.escape(self.delimiters())), 31 | self.JOIN_CHAR, 32 | self.raw(), 33 | ) 34 | 35 | return super(Macro, self).convert() 36 | 37 | def mutate(self, c): 38 | return c.upper() 39 | 40 | 41 | def macrocase(s, **kwargs): 42 | """Convert a string to macro case 43 | 44 | Example 45 | 46 | Hello World => HELLO_WORLD 47 | 48 | """ 49 | return Macro(s, **kwargs).convert() 50 | -------------------------------------------------------------------------------- /caseconverter/macro_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation. 9 | ("Hello, world!", "HELLO_WORLD"), 10 | # Camel cased 11 | ("helloWorld", "HELLO_WORLD"), 12 | # Joined by delimeter. 13 | ("Hello-World", "HELLO_WORLD"), 14 | # Cobol cased 15 | ("HELLO-WORLD", "HELLO_WORLD"), 16 | # Without punctuation. 17 | ("Hello world", "HELLO_WORLD"), 18 | # Repeating single delimeter 19 | ("Hello World", "HELLO_WORLD"), 20 | # Repeating delimeters of different types 21 | ("Hello -__ World", "HELLO_WORLD"), 22 | # Wrapped in delimeter 23 | (" hello world ", "HELLO_WORLD"), 24 | # End in capital letter 25 | ("hellO", "HELL_O"), 26 | # Long sentence with punctuation 27 | ( 28 | r"the quick !b@rown fo%x jumped over the laZy Do'G", 29 | "THE_QUICK_BROWN_FOX_JUMPED_OVER_THE_LA_ZY_DO_G", 30 | ), 31 | # Alternating character cases 32 | ("heLlo WoRld", "HE_LLO_WO_RLD"), 33 | ("HelloXWorld", "HELLO_X_WORLD"), 34 | ], 35 | ) 36 | def test_macro_with_default_args(input, output): 37 | assert macrocase(input) == output 38 | 39 | 40 | @pytest.mark.parametrize( 41 | "input, output", 42 | [("IP Address", "IP_ADDRESS"), ("Hello IP Address", "HELLO_IP_ADDRESS")], 43 | ) 44 | def test_macro_with_delims_only(input, output): 45 | assert macrocase(input, delims_only=True) == output 46 | -------------------------------------------------------------------------------- /caseconverter/pascal.py: -------------------------------------------------------------------------------- 1 | from .caseconverter import CaseConverter 2 | from .boundaries import ( 3 | OnDelimeterUppercaseNext, 4 | OnUpperPrecededByLowerAppendUpper, 5 | OnUpperPrecededByUpperAppendCurrent, 6 | ) 7 | 8 | 9 | class Pascal(CaseConverter): 10 | def init(self, input_buffer, output_buffer): 11 | output_buffer.write(input_buffer.read(1).upper()) 12 | 13 | def define_boundaries(self): 14 | self.add_boundary_handler(OnDelimeterUppercaseNext(self.delimiters())) 15 | self.add_boundary_handler(OnUpperPrecededByLowerAppendUpper()) 16 | self.add_boundary_handler(OnUpperPrecededByUpperAppendCurrent()) 17 | 18 | def prepare_string(self, s): 19 | if s.isupper(): 20 | return s.lower() 21 | 22 | return s 23 | 24 | def mutate(self, c): 25 | return c.lower() 26 | 27 | 28 | def pascalcase(s, **kwargs): 29 | """Convert a string to pascal case 30 | 31 | Example 32 | 33 | Hello World => HelloWorld 34 | hello world => HelloWorld 35 | 36 | """ 37 | return Pascal(s, **kwargs).convert() 38 | -------------------------------------------------------------------------------- /caseconverter/pascal_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation. 9 | ("Hello, world!", "HelloWorld"), 10 | # Camel cased 11 | ("helloWorld", "HelloWorld"), 12 | # Joined by delimeter. 13 | ("Hello-World", "HelloWorld"), 14 | # Cobol cased 15 | ("HELLO-WORLD", "HelloWorld"), 16 | # Without punctuation. 17 | ("Hello world", "HelloWorld"), 18 | # Repeating single delimeter 19 | ("Hello World", "HelloWorld"), 20 | # Repeating delimeters of different types 21 | ("Hello -__ World", "HelloWorld"), 22 | # Wrapped in delimeter 23 | (" hello world ", "HelloWorld"), 24 | # End in capital letter 25 | ("hellO", "HellO"), 26 | # Long sentence with punctuation 27 | ( 28 | r"the quick !b@rown fo%x jumped over the laZy Do'G", 29 | "TheQuickBrownFoxJumpedOverTheLaZyDoG", 30 | ), 31 | # Alternating character cases 32 | ("heLlo WoRld", "HeLloWoRld"), 33 | ("helloWORLD", "HelloWORLD"), 34 | ], 35 | ) 36 | def test_pascal_with_default_args(input, output): 37 | assert pascalcase(input) == output 38 | -------------------------------------------------------------------------------- /caseconverter/snake.py: -------------------------------------------------------------------------------- 1 | from .caseconverter import CaseConverter 2 | from .boundaries import OnDelimeterLowercaseNext, OnUpperPrecededByLowerAppendLower 3 | 4 | 5 | class Snake(CaseConverter): 6 | 7 | JOIN_CHAR = "_" 8 | 9 | def define_boundaries(self): 10 | self.add_boundary_handler( 11 | OnDelimeterLowercaseNext(self.delimiters(), self.JOIN_CHAR) 12 | ) 13 | self.add_boundary_handler(OnUpperPrecededByLowerAppendLower(self.JOIN_CHAR)) 14 | 15 | def prepare_string(self, s): 16 | if s.isupper(): 17 | return s.lower() 18 | 19 | return s 20 | 21 | def mutate(self, c): 22 | return c.lower() 23 | 24 | 25 | def snakecase(s, **kwargs): 26 | """Convert a string to snake case. 27 | 28 | Example 29 | 30 | Hello World => hello_world 31 | 32 | """ 33 | return Snake(s, **kwargs).convert() 34 | -------------------------------------------------------------------------------- /caseconverter/snake_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import * 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation. 9 | ("Hello, world!", "hello_world"), 10 | # Camel cased 11 | ("helloWorld", "hello_world"), 12 | # Joined by delimeter. 13 | ("Hello-World", "hello_world"), 14 | # Cobol cased 15 | ("HELLO-WORLD", "hello_world"), 16 | # Without punctuation. 17 | ("Hello world", "hello_world"), 18 | # Repeating single delimeter 19 | ("Hello World", "hello_world"), 20 | # Repeating delimeters of different types 21 | ("Hello -__ World", "hello_world"), 22 | # Wrapped in delimeter 23 | (" hello world ", "hello_world"), 24 | # End in capital letter 25 | ("hellO", "hell_o"), 26 | # Long sentence with punctuation 27 | ( 28 | r"the quick !b@rown fo%x jumped over the laZy Do'G", 29 | "the_quick_brown_fox_jumped_over_the_la_zy_do_g", 30 | ), 31 | # Alternating character cases 32 | ("heLlo WoRld", "he_llo_wo_rld"), 33 | ], 34 | ) 35 | def test_snake_with_default_args(input, output): 36 | assert snakecase(input) == output 37 | -------------------------------------------------------------------------------- /caseconverter/title.py: -------------------------------------------------------------------------------- 1 | from .caseconverter import CaseConverter 2 | from .boundaries import OnDelimeterUppercaseNext, BoundaryHandler 3 | 4 | 5 | class Title(CaseConverter): 6 | def init(self, input_buffer, output_buffer): 7 | # Capitalize the first character 8 | output_buffer.write(input_buffer.read(1).upper()) 9 | 10 | def define_boundaries(self): 11 | # On delimiters, write the space and make the next character uppercase 12 | self.add_boundary_handler(OnDelimeterPreserveAndUpperNext(self.delimiters())) 13 | # Handle camelCase -> Title Case 14 | self.add_boundary_handler(OnUpperPrecededByLowerAddSpace()) 15 | 16 | def prepare_string(self, s): 17 | if s.isupper(): 18 | return s.lower() 19 | return s 20 | 21 | def mutate(self, c): 22 | return c.lower() 23 | 24 | 25 | # Boundary handler that preserves spaces 26 | class OnDelimeterPreserveAndUpperNext(OnDelimeterUppercaseNext): 27 | def __init__(self, delimiters): 28 | super().__init__(delimiters) 29 | self._delimiters = delimiters 30 | 31 | def handle(self, pc, cc, input_buffer, output_buffer): 32 | # Write a single space for any delimiter 33 | output_buffer.write(" ") 34 | # Get and capitalize the next character 35 | output_buffer.write(input_buffer.read(1).upper()) 36 | 37 | 38 | # New boundary handler for camelCase 39 | class OnUpperPrecededByLowerAddSpace(BoundaryHandler): 40 | def is_boundary(self, pc, c): 41 | return pc is not None and pc.isalpha() and pc.islower() and c.isupper() 42 | 43 | def handle(self, pc, cc, input_buffer, output_buffer): 44 | output_buffer.write(" ") 45 | output_buffer.write(cc) 46 | 47 | 48 | def titlecase(s, **kwargs): 49 | """Convert a string to title case. 50 | 51 | Example: 52 | Hello world => Hello World 53 | hello-world => Hello World 54 | helloWorld => Hello World 55 | """ 56 | return Title(s, **kwargs).convert() 57 | -------------------------------------------------------------------------------- /caseconverter/title_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import titlecase 3 | 4 | 5 | @pytest.mark.parametrize( 6 | "input, output", 7 | [ 8 | # With punctuation 9 | ("Hello, world!", "Hello World"), 10 | # Camel cased 11 | ("helloWorld", "Hello World"), 12 | # Joined by delimiter 13 | ("Hello-World", "Hello World"), 14 | # All caps 15 | ("HELLO-WORLD", "Hello World"), 16 | # Without punctuation 17 | ("Hello world", "Hello World"), 18 | # Repeating single delimiter 19 | ("Hello World", "Hello World"), 20 | # Repeating delimiters of different types 21 | ("Hello -__ World", "Hello World"), 22 | # Wrapped in delimiter 23 | (" hello world ", "Hello World"), 24 | # Long sentence with punctuation 25 | ( 26 | r"the quick !b@rown fo%x jumped over the lazy Dog", 27 | "The Quick Brown Fox Jumped Over The Lazy Dog" 28 | ), 29 | # Multiple words 30 | ("this is a long title", "This Is A Long Title"), 31 | ], 32 | ) 33 | def test_title_with_default_args(input, output): 34 | assert titlecase(input) == output 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | from os import path 4 | 5 | this_directory = path.abspath(path.dirname(__file__)) 6 | with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: 7 | long_description = f.read() 8 | 9 | setup( 10 | name="case-converter", 11 | version="1.0.5-rc", 12 | url="https://github.com/chrisdoherty4/python-case-converter", 13 | packages=find_packages(exclude=["*_test.py"]), 14 | author="Chris Doherty", 15 | author_email="chris@chrisdoherty.io", 16 | description="A string case conversion package.", 17 | long_description=long_description, 18 | long_description_content_type="text/markdown", 19 | keywords=["case", "convert", "converter", "string"], 20 | classifiers=[ 21 | "Development Status :: 5 - Production/Stable", 22 | "License :: OSI Approved :: MIT License", 23 | "Natural Language :: English", 24 | "Topic :: Utilities", 25 | "Topic :: Text Processing", 26 | "Programming Language :: Python :: 3", 27 | ], 28 | ) 29 | --------------------------------------------------------------------------------