├── humps ├── py.typed ├── __init__.pyi ├── main.pyi ├── __init__.py └── main.py ├── tests ├── __init__.py ├── test_separate_words.py ├── test_dekebabize.py ├── test_kebabize.py ├── test_pascalize.py ├── test_decamelize.py ├── test_camelize.py └── test_humps.py ├── .envrc ├── .pylintrc ├── setup.cfg ├── artwork ├── humps.png └── Github Social.sketch ├── tea.yaml ├── docs ├── api.rst ├── user │ ├── quickstart.rst │ └── install.rst ├── Makefile ├── index.rst └── conf.py ├── .gitignore ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── ci.yml └── CODE_OF_CONDUCT.md ├── .readthedocs.yaml ├── LICENSE ├── pyproject.toml ├── .pre-commit-config.yaml ├── Makefile └── README.md /humps/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | layout_poetry -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [Format] 2 | good-names=i,j,k,ex,_,pk,x,y,a,b,c,d,r,s,t,q,fn 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [coverage:run] 2 | source = humps 3 | omit = humps/compat.py 4 | -------------------------------------------------------------------------------- /artwork/humps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nficano/humps/HEAD/artwork/humps.png -------------------------------------------------------------------------------- /artwork/Github Social.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nficano/humps/HEAD/artwork/Github Social.sketch -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0x4Cd8D746BC939B22BaE7F49a484ff6918feD0671' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API 4 | === 5 | 6 | .. module:: humps 7 | 8 | Main 9 | ---- 10 | 11 | .. automodule:: humps.main 12 | :members: 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .vscode/ 3 | build/ 4 | dist/ 5 | pyhumps.egg-info/ 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # Jetbrains 13 | .idea/ 14 | -------------------------------------------------------------------------------- /docs/user/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quickstart 4 | ========== 5 | 6 | This guide will walk you through the basic usage of humps. 7 | 8 | Let's get started with some examples. 9 | 10 | Converting strings 11 | ------------------ 12 | 13 | Begin by importing humps:: 14 | 15 | >>> import humps 16 | 17 | >>> humps.camelize('jack_in_the_box') # jackInTheBox 18 | >>> humps.decamelize('rubyTuesdays') # ruby_tuesdays 19 | >>> humps.pascalize('red_robin') # RedRobin 20 | -------------------------------------------------------------------------------- /humps/__init__.pyi: -------------------------------------------------------------------------------- 1 | from humps.main import ( 2 | camelize, 3 | decamelize, 4 | kebabize, 5 | dekebabize, 6 | depascalize, 7 | is_camelcase, 8 | is_pascalcase, 9 | is_kebabcase, 10 | is_snakecase, 11 | pascalize, 12 | ) 13 | 14 | __all__ = [ 15 | "camelize", 16 | "decamelize", 17 | "kebabize", 18 | "dekebabize", 19 | "depascalize", 20 | "is_camelcase", 21 | "is_pascalcase", 22 | "is_kebabcase", 23 | "is_snakecase", 24 | "pascalize", 25 | ] 26 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love contributions from everyone. By participating in this project, you agree to abide by our code of conduct. 4 | 5 | ## Contributing Code 6 | 7 | 1. Fork the repo 8 | 2. Install the dev dependencies and setup the pre-commit hook. 9 | 10 | ```bash 11 | $ pipenv install --dev 12 | $ pipenv shell 13 | $ pre-commit install 14 | ``` 15 | 16 | 3. Push to your fork. 17 | 4. Submit a pull request. 18 | 19 | Others will give constructive feedback. This is a time for discussion and improvements, and making the necessary 20 | changes will be required before we can merge the contribution. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = humps 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: twine 11 | versions: 12 | - 3.3.0 13 | - 3.4.0 14 | - dependency-name: sphinx 15 | versions: 16 | - 3.4.3 17 | - 3.5.0 18 | - 3.5.1 19 | - 3.5.2 20 | - dependency-name: ipython 21 | versions: 22 | - 7.19.0 23 | - 7.20.0 24 | - 7.21.0 25 | - dependency-name: bleach 26 | versions: 27 | - 3.3.0 28 | - dependency-name: coveralls 29 | versions: 30 | - 3.0.0 31 | - dependency-name: flake8 32 | versions: 33 | - 3.8.4 34 | -------------------------------------------------------------------------------- /humps/main.pyi: -------------------------------------------------------------------------------- 1 | from typing import Iterable, TypeVar, Mapping, Union 2 | 3 | 4 | StrOrIter = TypeVar('StrOrIter', bound=Union[str, Mapping, list]) 5 | 6 | 7 | def pascalize(str_or_iter: StrOrIter) -> StrOrIter: ... 8 | def camelize(str_or_iter: StrOrIter) -> StrOrIter: ... 9 | def kebabize(str_or_iter: StrOrIter) -> StrOrIter: ... 10 | def decamelize(str_or_iter: StrOrIter) -> StrOrIter: ... 11 | def depascalize(str_or_iter: StrOrIter) -> StrOrIter: ... 12 | def dekebabize(str_or_iter: StrOrIter) -> StrOrIter: ... 13 | def is_camelcase(str_or_iter: StrOrIter) -> bool: ... 14 | def is_pascalcase(str_or_iter: StrOrIter) -> bool: ... 15 | def is_kebabcase(str_or_iter: StrOrIter) -> bool: ... 16 | def is_snakecase(str_or_iter: StrOrIter) -> bool: ... 17 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.10" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # Optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | # python: 28 | # install: 29 | # - requirements: docs/requirements.txt 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Status 2 | **READY/IN DEVELOPMENT/HOLD** 3 | 4 | ## Description 5 | A few sentences describing the overall goals of the pull request's commits. 6 | 7 | ## Related PRs 8 | List related PRs against other branches: 9 | 10 | branch | PR 11 | ------ | ------ 12 | other_pr_production | [link]() 13 | other_pr_master | [link]() 14 | 15 | 16 | ## Todos 17 | - [ ] Tests 18 | - [ ] Documentation 19 | 20 | 21 | ## Deploy Notes 22 | Notes regarding deployment the contained body of work. These should note any 23 | db migrations, etc. 24 | 25 | ## Steps to Test or Reproduce 26 | Outline the steps to test or reproduce the PR here. 27 | 28 | ```sh 29 | git pull --prune 30 | git checkout 31 | pytest 32 | ``` 33 | 34 | 1. 35 | 36 | ## Impacted Areas in Application 37 | List general components of the application that this PR will affect: 38 | 39 | * 40 | -------------------------------------------------------------------------------- /docs/user/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installation of humps 4 | ====================== 5 | 6 | This part of the documentation covers the installation of humps. 7 | 8 | To install humps, simply use pipenv (or pip, of course):: 9 | 10 | $ pipenv install pyhumps 11 | 12 | Get the Source Code 13 | ------------------- 14 | 15 | humps is actively developed on GitHub, where the source is `available `_. 16 | 17 | You can either clone the public repository:: 18 | 19 | $ git clone git://github.com/nficano/humps.git 20 | 21 | Or, download the `tarball `_:: 22 | 23 | $ curl -OL https://github.com/nficano/humps/tarball/master 24 | # optionally, zipball is also available (for Windows users). 25 | 26 | Once you have a copy of the source, you can embed it in your Python package, or install it into your site-packages by running:: 27 | 28 | $ cd humps 29 | $ pipenv install . 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI testing 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | paths-ignore: 7 | - '**/README.md' 8 | - 'docs/**' 9 | push: 10 | branches: [master] 11 | paths-ignore: 12 | - '**/README.md' 13 | - 'docs/**' 14 | 15 | jobs: 16 | ci: 17 | runs-on: ubuntu-latest 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | strategy: 22 | matrix: 23 | python: [3.7, 3.8, 3.9, "3.10", "3.11"] 24 | 25 | steps: 26 | - name: Checkout repo 27 | uses: actions/checkout@v2 28 | 29 | - name: Setup python 30 | uses: actions/setup-python@v2 31 | with: 32 | python-version: ${{ matrix.python }} 33 | 34 | - name: Upgrade pip 35 | run: pip install --upgrade pip poetry 36 | 37 | - name: Install poetry 38 | run: poetry install 39 | 40 | - name: Run make ci 41 | run: make ci 42 | 43 | - name: Coveralls 44 | run: poetry run coveralls --service=github 45 | -------------------------------------------------------------------------------- /humps/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # noreorder 3 | """ 4 | Underscore-to-camelCase converter (and vice versa) for strings and dict keys in Python. 5 | """ 6 | import sys 7 | 8 | from humps.main import (camelize, decamelize, dekebabize, depascalize, 9 | is_camelcase, is_kebabcase, is_pascalcase, 10 | is_snakecase, kebabize, pascalize) 11 | 12 | if sys.version_info >= (3, 8): # pragma: no cover 13 | from importlib.metadata import metadata as _importlib_metadata 14 | else: 15 | from importlib_metadata import metadata as _importlib_metadata # pragma: no cover 16 | 17 | __title__ = "pyhumps" 18 | __version__ = _importlib_metadata(__title__)["version"] 19 | __author__ = "Nick Ficano" 20 | __license__ = "Unlicense License" 21 | __copyright__ = "Copyright 2019 Nick Ficano" 22 | 23 | __all__ = ( 24 | "camelize", 25 | "decamelize", 26 | "kebabize", 27 | "dekebabize", 28 | "pascalize", 29 | "depascalize", 30 | "is_camelcase", 31 | "is_kebabcase", 32 | "is_pascalcase", 33 | "is_snakecase", 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "pyhumps" 7 | version = "3.8.0" 8 | description = "Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python." 9 | license = "The Unlicense (Unlicense)" 10 | authors = ["Nick Ficano "] 11 | readme = "README.md" 12 | homepage = "https://github.com/nficano/humps" 13 | repository = "https://github.com/nficano/humps" 14 | documentation = "https://humps.readthedocs.io/" 15 | keywords = ["humps", "snakecase", "convert case", "camelcase", "kebabcase"] 16 | 17 | # Note that Python classifiers are still automatically added for you and are determined by your python requirement. 18 | # The license property will also set the License classifier automatically. 19 | classifiers = [ 20 | "Operating System :: OS Independent", 21 | "Topic :: Utilities", 22 | "Programming Language :: Python :: Implementation :: CPython", 23 | "Programming Language :: Python :: Implementation :: PyPy", 24 | ] 25 | packages = [ 26 | { include = "humps" }, 27 | ] 28 | 29 | [tool.poetry.dependencies] 30 | python = "^3.7" 31 | 32 | [tool.poetry.dev-dependencies] 33 | pytest = "^7.1.3" 34 | pylint = "^2.12.2" 35 | flake8 = "^5.0.4" 36 | pytest-cov = "^4.0.0" 37 | coveralls = "^3.3.1" 38 | sphinx-rtd-theme = "^1.0.0" 39 | Sphinx = "^4.3.2" 40 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v2.3.0 5 | hooks: 6 | - id: check-ast 7 | - id: check-byte-order-marker 8 | - id: check-builtin-literals 9 | - id: check-case-conflict 10 | - id: check-docstring-first 11 | - id: check-json 12 | - id: pretty-format-json 13 | args: [--autofix] 14 | - id: check-merge-conflict 15 | - id: check-symlinks 16 | - id: check-toml 17 | - id: check-vcs-permalinks 18 | - id: check-xml 19 | - id: check-yaml 20 | - id: debug-statements 21 | - id: detect-aws-credentials 22 | - id: detect-private-key 23 | - id: end-of-file-fixer 24 | - id: file-contents-sorter 25 | - id: fix-encoding-pragma 26 | args: ["--remove"] 27 | - id: mixed-line-ending 28 | args: ["--fix=lf"] 29 | - id: requirements-txt-fixer 30 | - id: sort-simple-yaml 31 | - id: check-added-large-files 32 | args: ["--maxkb=500"] 33 | - repo: https://github.com/pre-commit/pre-commit 34 | rev: v1.18.3 35 | hooks: 36 | - id: validate_manifest 37 | - repo: https://github.com/asottile/pyupgrade 38 | rev: v1.25.1 39 | hooks: 40 | - id: pyupgrade 41 | - repo: meta 42 | hooks: 43 | - id: check-useless-excludes 44 | - repo: https://github.com/ambv/black 45 | rev: 19.3b0 46 | hooks: 47 | - id: black 48 | language_version: python3.7 49 | args: [-S, -l 79, --exclude="migrations|.venv|node_modules"] 50 | -------------------------------------------------------------------------------- /tests/test_separate_words.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the utility for splitting words. 3 | """ 4 | import pytest 5 | 6 | from humps.main import _separate_words 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "input_str, expected_output", 11 | [ 12 | # Pascals. 13 | ("HelloWorld", "Hello_World"), 14 | ("_HelloWorld", "_Hello_World"), 15 | ("__HelloWorld", "__Hello_World"), 16 | ("HelloWorld_", "Hello_World_"), 17 | ("HelloWorld__", "Hello_World__"), 18 | # Camels 19 | ("helloWorld", "hello_World"), 20 | ("_helloWorld", "_hello_World"), 21 | ("__helloWorld", "__hello_World"), 22 | ("helloWorld_", "hello_World_"), 23 | ("helloWorld__", "hello_World__"), 24 | # Snakes 25 | ("hello_world", "hello_world"), 26 | ("_hello_world", "_hello_world"), 27 | ("__hello_world", "__hello_world"), 28 | ("hello_world_", "hello_world_"), 29 | ("hello_world__", "hello_world__"), 30 | # Fixes issue #128 31 | ("whatever_hi", "whatever_hi"), 32 | ("whatever_10", "whatever_10"), 33 | # Fixes issue #127 34 | ("sizeX", "size_X"), 35 | # Fixes issue #168 36 | ("aB", "a_B"), 37 | # Fixed issue #201. 2021-10-12 38 | ("testNTest", "test_N_Test"), 39 | ], 40 | ) 41 | def test_separate_words(input_str, expected_output): 42 | """ 43 | :param input_str: String that will be transformed. 44 | :param expected_output: The expected transformation. 45 | """ 46 | output = _separate_words(input_str) 47 | assert output == expected_output, "{} != {}".format( 48 | output, expected_output 49 | ) 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | deploy-patch: clean version-patch git-push-on-deploy upload clean 2 | 3 | deploy-minor: clean version-minor git-push-on-deploy upload clean 4 | 5 | deploy-major: clean version-major git-push-on-deploy upload clean 6 | 7 | # Version prior to update 8 | VERSION := ${shell poetry version -s} 9 | 10 | version-patch: 11 | poetry version patch 12 | 13 | version-minor: 14 | poetry version minor 15 | 16 | version-major: 17 | poetry version major 18 | 19 | git-push-on-deploy: 20 | git commit -m 'Bump version: $(VERSION) → $(shell poetry version -s)' pyproject.toml 21 | git push 22 | git tag v${shell poetry version -s} 23 | git push --tags 24 | 25 | upload: 26 | poetry build 27 | poetry publish 28 | 29 | help: 30 | @echo "clean - remove all build, test, coverage and Python artifacts" 31 | @echo "clean-build - remove build artifacts" 32 | @echo "clean-pyc - remove Python file artifacts" 33 | @echo "install - install the package to the active Python's site-packages" 34 | 35 | ci: 36 | pip install poetry 37 | poetry install 38 | poetry run flake8 humps 39 | poetry run pylint humps 40 | # poetry run pytest --cov-report term-missing # --cov=humps 41 | poetry run coverage run -m pytest 42 | 43 | lint: 44 | poetry run flake8 humps 45 | poetry run pylint humps 46 | 47 | clean: clean-build clean-pyc 48 | 49 | clean-build: 50 | rm -fr build/ 51 | rm -fr dist/ 52 | rm -fr .eggs/ 53 | find . -name '*.egg-info' -exec rm -fr {} + 54 | find . -name '*.egg' -exec rm -f {} + 55 | find . -name '*.DS_Store' -exec rm -f {} + 56 | 57 | clean-pyc: 58 | find . -name '*.pyc' -exec rm -f {} + 59 | find . -name '*.pyo' -exec rm -f {} + 60 | find . -name '*~' -exec rm -f {} + 61 | find . -name '__pycache__' -exec rm -fr {} + 62 | 63 | install: clean 64 | poetry install 65 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. humps documentation master file, created by 2 | sphinx-quickstart on Mon Oct 9 02:11:41 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | humps 7 | ====== 8 | Release v\ |version|. (:ref:`Installation `) 9 | 10 | .. image:: https://img.shields.io/pypi/v/pyhumps.svg 11 | :alt: Pypi 12 | :target: https://pypi.python.org/pypi/pyhumps/ 13 | 14 | .. image:: https://travis-ci.org/nficano/humps.svg?branch=master 15 | :alt: Build status 16 | :target: https://travis-ci.org/nficano/humps 17 | 18 | .. image:: https://readthedocs.org/projects/humps/badge/?version=latest 19 | :target: http://humps.readthedocs.io/en/latest/?badge=latest 20 | :alt: Documentation Status 21 | 22 | .. image:: https://coveralls.io/repos/github/nficano/humps/badge.svg?branch=master 23 | :alt: Coverage 24 | :target: https://coveralls.io/github/nficano/humps?branch=master 25 | 26 | .. image:: https://img.shields.io/pypi/pyversions/pyhumps.svg 27 | :alt: Python Versions 28 | :target: https://pypi.python.org/pypi/pyhumps/ 29 | 30 | **humps** 🐫 Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by Humps for Node 31 | 32 | ------------------- 33 | 34 | **Introducing Humps for Python! Let's see it in action**:: 35 | 36 | >>> import humps 37 | >>> humps.decamelize('illWearYourGranddadsClothes') # 'ill_wear_your_granddads_clothes' 38 | >>> humps.camelize('i_look_incredible') # 'iLookIncredible' 39 | >>> humps.kebabize('i_look_incredible') # 'i-look-incredible' 40 | >>> humps.pascalize('im_in_this_big_ass_coat') # 'ImInThisBigAssCoat' 41 | >>> humps.decamelize('FROMThatThriftShop') # 'from_that_thrift_shop' 42 | >>> humps.decamelize([{'downTheRoad': True}]) # [{'down_the_road': True}] 43 | >>> humps.dekebabize('FROM-That-Thrift-Shop') # 'FROM_That_Thrift_Shop' 44 | 45 | Features 46 | -------- 47 | 48 | - Convert from ``snake_case`` to ``camelCase`` and ``PascalCase`` and ``kebab-case`` 49 | - Convert from ``camelCase`` to ``snake_case`` and ``PascalCase`` 50 | - Convert from ``PascalCase`` to ``snake_case`` and ``camelCase`` 51 | - Convert from ``kebab-case`` to ``snake_case`` 52 | - Supports recursively converting ``dict`` keys 53 | - Supports recursively converting lists of dictionaries 54 | - Gracefully handles abbrevations, acronyms, and initialisms 55 | - Extensively documented source code 56 | - No third-party dependencies 57 | 58 | Installation 59 | ------------ 60 | 61 | To install humps, simply use pipenv (or pip, of course):: 62 | 63 | $ pipenv install pyhumps 64 | 65 | The API Documentation / Guide 66 | ----------------------------- 67 | 68 | If you are looking for information on a specific function, this part of the documentation is for you. 69 | 70 | .. toctree:: 71 | :maxdepth: 2 72 | 73 | api 74 | 75 | 76 | Indices and tables 77 | ================== 78 | 79 | * :ref:`genindex` 80 | * :ref:`modindex` 81 | * :ref:`search` 82 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at nficano@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Humps logo 3 |

4 | 5 | 22 |

23 | 24 | Convert strings (and dictionary keys) between snake case, camel case and pascal case in Python. Inspired by [Humps](https://github.com/domchristie/humps) for Node. 25 | 26 | ## Installation 27 | 28 | To install humps, simply use pipenv (or pip, of course): 29 | 30 | ```bash 31 | $ pipenv install pyhumps 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### Converting strings 37 | 38 | ```python 39 | import humps 40 | 41 | humps.camelize("jack_in_the_box") # jackInTheBox 42 | humps.decamelize("rubyTuesdays") # ruby_tuesdays 43 | humps.pascalize("red_robin") # RedRobin 44 | humps.kebabize("white_castle") # white-castle 45 | ``` 46 | 47 | ### Converting dictionary keys 48 | 49 | ```python 50 | import humps 51 | 52 | array = [{"attrOne": "foo"}, {"attrOne": "bar"}] 53 | humps.decamelize(array) # [{"attr_one": "foo"}, {"attr_one": "bar"}] 54 | 55 | array = [{"attr_one": "foo"}, {"attr_one": "bar"}] 56 | humps.camelize(array) # [{"attrOne": "foo"}, {"attrOne": "bar"}] 57 | 58 | array = [{'attr_one': 'foo'}, {'attr_one': 'bar'}] 59 | humps.kebabize(array) # [{'attr-one': 'foo'}, {'attr-one': 'bar'}] 60 | 61 | array = [{"attr_one": "foo"}, {"attr_one": "bar"}] 62 | humps.pascalize(array) # [{"AttrOne": "foo"}, {"AttrOne": "bar"}] 63 | ``` 64 | 65 | ### Checking character casing 66 | 67 | ```python 68 | import humps 69 | 70 | humps.is_camelcase("illWearYourGranddadsClothes") # True 71 | humps.is_pascalcase("ILookIncredible") # True 72 | humps.is_snakecase("im_in_this_big_ass_coat") # True 73 | humps.is_kebabcase('from-that-thrift-shop') # True 74 | humps.is_camelcase("down_the_road") # False 75 | 76 | humps.is_snakecase("imGonnaPopSomeTags") # False 77 | humps.is_kebabcase('only_got_twenty_dollars_in_my_pocket') # False 78 | 79 | 80 | # what about abbrevations, acronyms, and initialisms? No problem! 81 | humps.decamelize("APIResponse") # api_response 82 | ``` 83 | 84 |
85 | 86 | ## Cookbook 87 | 88 | #### Pythonic Boto3 API Wrapper 89 | 90 | ```python 91 | # aws.py 92 | import humps 93 | import boto3 94 | 95 | def api(service, decamelize=True, *args, **kwargs): 96 | service, func = service.split(":") 97 | client = boto3.client(service) 98 | kwargs = humps.pascalize(kwargs) 99 | response = getattr(client, func)(*args, **kwargs) 100 | return (depascalize(response) if decamelize else response) 101 | 102 | # usage 103 | api("s3:download_file", bucket="bucket", key="hello.png", filename="hello.png") 104 | ``` 105 | -------------------------------------------------------------------------------- /tests/test_dekebabize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test dekabization. 3 | """ 4 | import pytest 5 | 6 | import humps 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "input_str, expected_output", 11 | [ 12 | ("symbol", "symbol"), 13 | ("last-price", "last_price"), 14 | ("Change-Pct", "Change_Pct"), 15 | ("implied-Volatility", "implied_Volatility"), 16 | ("_symbol", "_symbol"), 17 | ("change-pct_", "change_pct_"), 18 | ("_last-price__", "_last_price__"), 19 | ("__implied-volatility_", "__implied_volatility_"), 20 | ("API", "API"), 21 | ("_API_", "_API_"), 22 | ("__API__", "__API__"), 23 | ("API-Response", "API_Response"), 24 | ("_API-Response_", "_API_Response_"), 25 | ("__API-Response__", "__API_Response__"), 26 | ("12345", "12345"), 27 | ], 28 | ) 29 | def test_dekebabize(input_str, expected_output): 30 | """ 31 | :param input_str: String that will be transformed. 32 | :param expected_output: The expected transformation. 33 | """ 34 | output = humps.dekebabize(input_str) 35 | assert output == expected_output, "{} != {}".format( 36 | output, expected_output 37 | ) 38 | 39 | 40 | def test_dekebabize_dict_list(): 41 | actual = humps.dekebabize( 42 | [ 43 | { 44 | "symbol": "AAL", 45 | "last-price": 31.78, 46 | "Change-Pct": 2.8146, 47 | "implied-Volatility": 0.482, 48 | }, 49 | { 50 | "symbol": "LBTYA", 51 | "last-price": 25.95, 52 | "Change-Pct": 2.6503, 53 | "implied-Volatility": 0.7287, 54 | }, 55 | { 56 | "_symbol": "LBTYK", 57 | "Change-Pct_": 2.5827, 58 | "_last-price__": 25.42, 59 | "__implied-Volatility_": 0.4454, 60 | }, 61 | { 62 | "API": "test_upper", 63 | "_API_": "test_upper", 64 | "__API__": "test_upper", 65 | "API-Response": "test_acronym", 66 | "_API-Response_": "test_acronym", 67 | "__API-Response__": "test_acronym", 68 | "ruby_tuesdays": "ruby_tuesdays", 69 | "_item-ID": "_item_id", 70 | }, 71 | ] 72 | ) 73 | expected = [ 74 | { 75 | "symbol": "AAL", 76 | "last_price": 31.78, 77 | "Change_Pct": 2.8146, 78 | "implied_Volatility": 0.482, 79 | }, 80 | { 81 | "symbol": "LBTYA", 82 | "last_price": 25.95, 83 | "Change_Pct": 2.6503, 84 | "implied_Volatility": 0.7287, 85 | }, 86 | { 87 | "_symbol": "LBTYK", 88 | "Change_Pct_": 2.5827, 89 | "_last_price__": 25.42, 90 | "__implied_Volatility_": 0.4454, 91 | }, 92 | { 93 | "API": "test_upper", 94 | "_API_": "test_upper", 95 | "__API__": "test_upper", 96 | "API_Response": "test_acronym", 97 | "_API_Response_": "test_acronym", 98 | "__API_Response__": "test_acronym", 99 | "ruby_tuesdays": "ruby_tuesdays", 100 | "_item_ID": "_item_id", 101 | }, 102 | ] 103 | 104 | assert actual == expected, "{} != {}".format(actual, expected) 105 | -------------------------------------------------------------------------------- /tests/test_kebabize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test kebabization. 3 | """ 4 | import pytest 5 | 6 | import humps 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "input_str, expected_output", 11 | [ 12 | ("fallback_url", "fallback-url"), 13 | ("scrubber_media_url", "scrubber-media-url"), 14 | ("dash_url", "dash-url"), 15 | ("_fallback_url", "_fallback-url"), 16 | ("__scrubber_media___url_", "__scrubber-media-url_"), 17 | ("_url__", "_url__"), 18 | ("API", "API"), 19 | ("_API_", "_API_"), 20 | ("__API__", "__API__"), 21 | ("API_Response", "API-Response"), 22 | ("_API_Response_", "_API-Response_"), 23 | ("__API_Response__", "__API-Response__"), 24 | ], 25 | ) 26 | def test_kebabize(input_str, expected_output): 27 | """ 28 | :param input_str: String that will be transformed. 29 | :param expected_output: The expected transformation. 30 | """ 31 | output = humps.kebabize(input_str) 32 | assert output == expected_output, "{} != {}".format( 33 | output, expected_output 34 | ) 35 | 36 | 37 | def test_kebabize_dict_list(): 38 | actual = humps.kebabize( 39 | { 40 | "videos": [ 41 | { 42 | "fallback_url": "https://media.io/video", 43 | "scrubber_Media_Url": "https://media.io/video", 44 | "dash_Url": "https://media.io/video", 45 | } 46 | ], 47 | "images": [ 48 | { 49 | "fallback_url": "https://media.io/image", 50 | "scrubber_Media_Url": "https://media.io/image", 51 | "url": "https://media.io/image", 52 | } 53 | ], 54 | "other": [ 55 | { 56 | "_fallback_url": "https://media.io/image", 57 | "__scrubber_Media___Url_": "https://media.io/image", 58 | "_url__": "https://media.io/image", 59 | }, 60 | { 61 | "API": "test_upper", 62 | "_API_": "test_upper", 63 | "__API__": "test_upper", 64 | "APIResponse": "test_acronym", 65 | "_APIResponse_": "test_acronym", 66 | "__APIResponse__": "test_acronym", 67 | }, 68 | ], 69 | } 70 | ) 71 | expected = { 72 | "videos": [ 73 | { 74 | "fallback-url": "https://media.io/video", 75 | "scrubber-Media-Url": "https://media.io/video", 76 | "dash-Url": "https://media.io/video", 77 | } 78 | ], 79 | "images": [ 80 | { 81 | "fallback-url": "https://media.io/image", 82 | "scrubber-Media-Url": "https://media.io/image", 83 | "url": "https://media.io/image", 84 | } 85 | ], 86 | "other": [ 87 | { 88 | "_fallback-url": "https://media.io/image", 89 | "__scrubber-Media-Url_": "https://media.io/image", 90 | "_url__": "https://media.io/image", 91 | }, 92 | { 93 | "API": "test_upper", 94 | "_API_": "test_upper", 95 | "__API__": "test_upper", 96 | "api-response": "test_acronym", 97 | "_api-response_": "test_acronym", 98 | "__api-response__": "test_acronym", 99 | }, 100 | ], 101 | } 102 | assert actual == expected 103 | -------------------------------------------------------------------------------- /tests/test_pascalize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test pascalizing. 3 | """ 4 | import pytest 5 | 6 | import humps 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "input_str, expected_output", 11 | [ 12 | ("fallback_url", "FallbackUrl"), 13 | ("scrubber_media_url", "ScrubberMediaUrl"), 14 | ("dash_url", "DashUrl"), 15 | ("_fallback_url", "_FallbackUrl"), 16 | ("__scrubber_media___url_", "__ScrubberMediaUrl_"), 17 | ("_url__", "_Url__"), 18 | ("API", "API"), 19 | ("_API_", "_API_"), 20 | ("__API__", "__API__"), 21 | ("APIResponse", "APIResponse"), 22 | ("_APIResponse_", "_APIResponse_"), 23 | ("__APIResponse__", "__APIResponse__"), 24 | # Fixed issue # 256 25 | ("", ""), 26 | (None, ""), 27 | ], 28 | ) 29 | def test_pascalize(input_str, expected_output): 30 | """ 31 | :param input_str: String that will be transformed. 32 | :param expected_output: The expected transformation. 33 | """ 34 | output = humps.pascalize(input_str) 35 | assert output == expected_output, "{} != {}".format( 36 | output, expected_output 37 | ) 38 | 39 | 40 | def test_pascalize_dict_list(): 41 | actual = humps.pascalize( 42 | { 43 | "videos": [ 44 | { 45 | "fallback_url": "https://media.io/video", 46 | "scrubber_media_url": "https://media.io/video", 47 | "dash_url": "https://media.io/video", 48 | } 49 | ], 50 | "images": [ 51 | { 52 | "fallback_url": "https://media.io/image", 53 | "scrubber_media_url": "https://media.io/image", 54 | "url": "https://media.io/image", 55 | } 56 | ], 57 | "other": [ 58 | { 59 | "_fallback_url": "https://media.io/image", 60 | "__scrubber_media___url_": "https://media.io/image", 61 | "_url__": "https://media.io/image", 62 | }, 63 | { 64 | "API": "test_upper", 65 | "_API_": "test_upper", 66 | "__API__": "test_upper", 67 | "APIResponse": "test_acronym", 68 | "_APIResponse_": "test_acronym", 69 | "__APIResponse__": "test_acronym", 70 | }, 71 | ], 72 | } 73 | ) 74 | expected = { 75 | "Videos": [ 76 | { 77 | "FallbackUrl": "https://media.io/video", 78 | "ScrubberMediaUrl": "https://media.io/video", 79 | "DashUrl": "https://media.io/video", 80 | } 81 | ], 82 | "Images": [ 83 | { 84 | "FallbackUrl": "https://media.io/image", 85 | "ScrubberMediaUrl": "https://media.io/image", 86 | "Url": "https://media.io/image", 87 | } 88 | ], 89 | "Other": [ 90 | { 91 | "_FallbackUrl": "https://media.io/image", 92 | "__ScrubberMediaUrl_": "https://media.io/image", 93 | "_Url__": "https://media.io/image", 94 | }, 95 | { 96 | "API": "test_upper", 97 | "_API_": "test_upper", 98 | "__API__": "test_upper", 99 | "APIResponse": "test_acronym", 100 | "_APIResponse_": "test_acronym", 101 | "__APIResponse__": "test_acronym", 102 | }, 103 | ], 104 | } 105 | assert actual == expected, "{} != {}".format(actual, expected) 106 | -------------------------------------------------------------------------------- /tests/test_decamelize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test decamelization. 3 | """ 4 | import pytest 5 | 6 | import humps 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "input_str, expected_output", 11 | [ 12 | ("symbol", "symbol"), 13 | ("lastPrice", "last_price"), 14 | ("changePct", "change_pct"), 15 | ("impliedVolatility", "implied_volatility"), 16 | ("_symbol", "_symbol"), 17 | ("changePct_", "change_pct_"), 18 | ("_lastPrice__", "_last_price__"), 19 | ("__impliedVolatility_", "__implied_volatility_"), 20 | ("API", "API"), 21 | ("_API_", "_API_"), 22 | ("__API__", "__API__"), 23 | ("APIResponse", "api_response"), 24 | ("_APIResponse_", "_api_response_"), 25 | ("__APIResponse__", "__api_response__"), 26 | # Fixed issue #2. 2021-05-01 27 | ("_itemID", "_item_id"), 28 | # Fixed issue #4. 2021-05-01 29 | ("memMB", "mem_mb"), 30 | # Fixed issue #127. 2021-09-13 31 | ("sizeX", "size_x"), 32 | # Fixed issue #168. 2021-09-13 33 | ("aB", "a_b"), 34 | # Fixed issue #201. 2021-10-12 35 | ("testNTest", "test_n_test"), 36 | ], 37 | ) 38 | def test_decamelize(input_str, expected_output): 39 | """ 40 | :param input_str: String that will be transformed. 41 | :param expected_output: The expected transformation. 42 | """ 43 | output = humps.decamelize(input_str) 44 | assert output == expected_output, "{} != {}".format( 45 | output, expected_output 46 | ) 47 | 48 | 49 | def test_decamelize_dict_list(): 50 | actual = humps.decamelize( 51 | [ 52 | { 53 | "symbol": "AAL", 54 | "lastPrice": 31.78, 55 | "changePct": 2.8146, 56 | "impliedVolatility": 0.482, 57 | }, 58 | { 59 | "symbol": "LBTYA", 60 | "lastPrice": 25.95, 61 | "changePct": 2.6503, 62 | "impliedVolatility": 0.7287, 63 | }, 64 | { 65 | "_symbol": "LBTYK", 66 | "changePct_": 2.5827, 67 | "_lastPrice__": 25.42, 68 | "__impliedVolatility_": 0.4454, 69 | }, 70 | { 71 | "API": "test_upper", 72 | "_API_": "test_upper", 73 | "__API__": "test_upper", 74 | "APIResponse": "test_acronym", 75 | "_APIResponse_": "test_acronym", 76 | "__APIResponse__": "test_acronym", 77 | "ruby_tuesdays": "ruby_tuesdays", 78 | "_itemID": "_item_id", 79 | }, 80 | ] 81 | ) 82 | expected = [ 83 | { 84 | "symbol": "AAL", 85 | "last_price": 31.78, 86 | "change_pct": 2.8146, 87 | "implied_volatility": 0.482, 88 | }, 89 | { 90 | "symbol": "LBTYA", 91 | "last_price": 25.95, 92 | "change_pct": 2.6503, 93 | "implied_volatility": 0.7287, 94 | }, 95 | { 96 | "_symbol": "LBTYK", 97 | "change_pct_": 2.5827, 98 | "_last_price__": 25.42, 99 | "__implied_volatility_": 0.4454, 100 | }, 101 | { 102 | "API": "test_upper", 103 | "_API_": "test_upper", 104 | "__API__": "test_upper", 105 | "api_response": "test_acronym", 106 | "_api_response_": "test_acronym", 107 | "__api_response__": "test_acronym", 108 | "ruby_tuesdays": "ruby_tuesdays", 109 | "_item_id": "_item_id", 110 | }, 111 | ] 112 | 113 | assert actual == expected, "{} != {}".format(actual, expected) 114 | -------------------------------------------------------------------------------- /tests/test_camelize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test camelization. 3 | """ 4 | import pytest 5 | 6 | import humps 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "input_str, expected_output", 11 | [ 12 | ("fallback_url", "fallbackUrl"), 13 | ("scrubber_media_url", "scrubberMediaUrl"), 14 | ("dash_url", "dashUrl"), 15 | ("_fallback_url", "_fallbackUrl"), 16 | ("__scrubber_media___url_", "__scrubberMediaUrl_"), 17 | ("_url__", "_url__"), 18 | ("API", "API"), 19 | ("_API_", "_API_"), 20 | ("__API__", "__API__"), 21 | ("APIResponse", "APIResponse"), 22 | ("_APIResponse_", "_APIResponse_"), 23 | ("__APIResponse__", "__APIResponse__"), 24 | # Fixed issue #128 25 | ("whatever_10", "whatever10"), 26 | # Fixed issue # 18 27 | ("test-1-2-3-4-5-6", "test123456"), 28 | # Fixed issue # 61 29 | ("test_n_test", "testNTest"), 30 | # Fixed issue # 148 31 | ("field_value_2_type", "fieldValue2Type"), 32 | # Fixed issue # 256 33 | ("", ""), 34 | (None, ""), 35 | ], 36 | ) 37 | def test_camelize(input_str, expected_output): 38 | """ 39 | :param input_str: String that will be transformed. 40 | :param expected_output: The expected transformation. 41 | """ 42 | output = humps.camelize(input_str) 43 | assert output == expected_output, "{} != {}".format( 44 | output, expected_output 45 | ) 46 | 47 | 48 | def test_camelize_dict_list(): 49 | actual = humps.camelize( 50 | { 51 | "videos": [ 52 | { 53 | "fallback_url": "https://media.io/video", 54 | "scrubber_media_url": "https://media.io/video", 55 | "dash_url": "https://media.io/video", 56 | } 57 | ], 58 | "images": [ 59 | { 60 | "fallback_url": "https://media.io/image", 61 | "scrubber_media_url": "https://media.io/image", 62 | "url": "https://media.io/image", 63 | } 64 | ], 65 | "other": [ 66 | { 67 | "_fallback_url": "https://media.io/image", 68 | "__scrubber_media___url_": "https://media.io/image", 69 | "_url__": "https://media.io/image", 70 | }, 71 | { 72 | "API": "test_upper", 73 | "_API_": "test_upper", 74 | "__API__": "test_upper", 75 | "APIResponse": "test_acronym", 76 | "_APIResponse_": "test_acronym", 77 | "__APIResponse__": "test_acronym", 78 | }, 79 | ], 80 | } 81 | ) 82 | expected = { 83 | "videos": [ 84 | { 85 | "fallbackUrl": "https://media.io/video", 86 | "scrubberMediaUrl": "https://media.io/video", 87 | "dashUrl": "https://media.io/video", 88 | } 89 | ], 90 | "images": [ 91 | { 92 | "fallbackUrl": "https://media.io/image", 93 | "scrubberMediaUrl": "https://media.io/image", 94 | "url": "https://media.io/image", 95 | } 96 | ], 97 | "other": [ 98 | { 99 | "_fallbackUrl": "https://media.io/image", 100 | "__scrubberMediaUrl_": "https://media.io/image", 101 | "_url__": "https://media.io/image", 102 | }, 103 | { 104 | "API": "test_upper", 105 | "_API_": "test_upper", 106 | "__API__": "test_upper", 107 | "APIResponse": "test_acronym", 108 | "_APIResponse_": "test_acronym", 109 | "__APIResponse__": "test_acronym", 110 | }, 111 | ], 112 | } 113 | assert actual == expected 114 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """humps documentation build configuration file.""" 3 | import os 4 | import sys 5 | 6 | import sphinx_rtd_theme 7 | 8 | sys.path.insert(0, os.path.abspath("../")) 9 | 10 | from humps import __version__ # noqa 11 | 12 | 13 | # -- General configuration ------------------------------------------------ 14 | 15 | extensions = [ 16 | "sphinx.ext.autodoc", 17 | "sphinx.ext.autosummary", 18 | "sphinx.ext.todo", 19 | "sphinx.ext.intersphinx", 20 | "sphinx.ext.viewcode", 21 | ] 22 | 23 | autosummary_generate = True 24 | 25 | # Add any paths that contain templates here, relative to this directory. 26 | templates_path = ["_templates"] 27 | 28 | # The suffix(es) of source filenames. 29 | # You can specify multiple suffix as a list of string: 30 | # 31 | # source_suffix = ['.rst', '.md'] 32 | source_suffix = ".rst" 33 | 34 | # The master toctree document. 35 | master_doc = "index" 36 | 37 | # General information about the project. 38 | project = "humps" 39 | copyright = "2019, Nick Ficano" 40 | author = "Nick Ficano" 41 | 42 | # The version info for the project you're documenting, acts as replacement for 43 | # |version| and |release|, also used in various other places throughout the 44 | # built documents. 45 | # 46 | # The short X.Y version. 47 | version = __version__ 48 | # The full version, including alpha/beta/rc tags. 49 | release = __version__ 50 | 51 | # The language for content autogenerated by Sphinx. Refer to documentation 52 | # for a list of supported languages. 53 | # 54 | # This is also used if you do content translation via gettext catalogs. 55 | # Usually you set "language" from the command line for these cases. 56 | language = None 57 | 58 | # List of patterns, relative to source directory, that match files and 59 | # directories to ignore when looking for source files. 60 | # This patterns also effect to html_static_path and html_extra_path 61 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 62 | 63 | # The name of the Pygments (syntax highlighting) style to use. 64 | pygments_style = "sphinx" 65 | 66 | # If true, `todo` and `todoList` produce output, else they produce nothing. 67 | todo_include_todos = True 68 | 69 | intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} 70 | 71 | 72 | # -- Options for HTML output ---------------------------------------------- 73 | 74 | # The theme to use for HTML and HTML Help pages. See the documentation for 75 | # a list of builtin themes. 76 | # 77 | html_theme = "sphinx_rtd_theme" 78 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 79 | 80 | # Theme options are theme-specific and customize the look and feel of a theme 81 | # further. For a list of options available for each theme, see the 82 | # documentation. 83 | # 84 | # html_theme_options = {} 85 | 86 | # Add any paths that contain custom static files (such as style sheets) here, 87 | # relative to this directory. They are copied after the builtin static files, 88 | # so a file named "default.css" will overwrite the builtin "default.css". 89 | html_static_path = ["_static"] 90 | 91 | # Custom sidebar templates, must be a dictionary that maps document names 92 | # to template names. 93 | # 94 | # This is required for the alabaster theme 95 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 96 | html_sidebars = { 97 | "**": [ 98 | "about.html", 99 | "navigation.html", 100 | "relations.html", # needs 'show_related': True theme option to display 101 | "searchbox.html", 102 | "donate.html", 103 | ] 104 | } 105 | 106 | 107 | # -- Options for HTMLHelp output ------------------------------------------ 108 | 109 | # Output file base name for HTML help builder. 110 | htmlhelp_basename = "humpsdoc" 111 | 112 | 113 | # -- Options for LaTeX output --------------------------------------------- 114 | 115 | latex_elements = {} 116 | 117 | # Grouping the document tree into LaTeX files. List of tuples 118 | # (source start file, target name, title, 119 | # author, documentclass [howto, manual, or own class]). 120 | latex_documents = [ 121 | (master_doc, "humps.tex", "humps Documentation", "Nick Ficano", "manual") 122 | ] 123 | 124 | 125 | # -- Options for manual page output --------------------------------------- 126 | 127 | # One entry per manual page. List of tuples 128 | # (source start file, name, description, authors, manual section). 129 | man_pages = [(master_doc, "humps", "humps Documentation", [author], 1)] 130 | 131 | 132 | # -- Options for Texinfo output ------------------------------------------- 133 | 134 | # Grouping the document tree into Texinfo files. List of tuples 135 | # (source start file, target name, title, author, 136 | # dir menu entry, description, category) 137 | texinfo_documents = [ 138 | ( 139 | master_doc, 140 | "humps", 141 | "humps Documentation", 142 | author, 143 | "humps", 144 | "One line description of project.", 145 | "Miscellaneous", 146 | ) 147 | ] 148 | -------------------------------------------------------------------------------- /tests/test_humps.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import humps 4 | 5 | 6 | def test_converting_strings(): 7 | assert humps.camelize("jack_in_the_box") == "jackInTheBox" 8 | assert humps.decamelize("rubyTuesdays") == "ruby_tuesdays" 9 | assert humps.depascalize("UnosPizza") == "unos_pizza" 10 | assert humps.pascalize("red_robin") == "RedRobin" 11 | assert humps.kebabize("white_castle") == "white-castle" 12 | assert humps.dekebabize("taco-bell") == "taco_bell" 13 | 14 | 15 | @pytest.mark.parametrize( 16 | "input_str, expected_output", 17 | [ 18 | ("PERatio", "pe_ratio"), 19 | ("HTTPResponse", "http_response"), 20 | ("_HTTPResponse", "_http_response"), 21 | ("_HTTPResponse__", "_http_response__"), 22 | ("BIP73", "BIP73"), 23 | ("BIP72b", "bip72b"), 24 | ("memMB", "mem_mb"), 25 | # Fixed issue #258 26 | ("B52Thing", "b52_thing"), 27 | ], 28 | ) 29 | def test_camelized_acronyms(input_str, expected_output): 30 | """ 31 | Validate decamelizing acronyms works as expected. 32 | :type input_str: str 33 | :type expected_output: str 34 | """ 35 | assert humps.decamelize(input_str) == expected_output 36 | 37 | 38 | def test_conditionals(): 39 | assert humps.is_pascalcase("RedRobin") 40 | assert humps.is_snakecase("RedRobin") is False 41 | assert humps.is_camelcase("RedRobin") is False 42 | assert humps.is_kebabcase("RedRobin") is False 43 | 44 | assert humps.is_snakecase("ruby_tuesdays") 45 | assert humps.is_camelcase("ruby_tuesdays") is False 46 | assert humps.is_pascalcase("ruby_tuesdays") is False 47 | assert humps.is_kebabcase("ruby_tuesdays") is False 48 | 49 | assert humps.is_camelcase("jackInTheBox") 50 | assert humps.is_snakecase("jackInTheBox") is False 51 | assert humps.is_pascalcase("jackInTheBox") is False 52 | assert humps.is_kebabcase("jackInTheBox") is False 53 | 54 | assert humps.is_kebabcase("white-castle") 55 | assert humps.is_snakecase("white-castle") is False 56 | assert humps.is_camelcase("white-castle") is False 57 | assert humps.is_pascalcase("white-castle") is False 58 | 59 | assert humps.is_camelcase("API") 60 | assert humps.is_pascalcase("API") 61 | assert humps.is_snakecase("API") 62 | assert humps.is_kebabcase("API") 63 | 64 | # Fixed issue #128 65 | assert humps.is_snakecase("whatever_10") 66 | assert humps.is_camelcase("whatever_10") is False 67 | assert humps.is_pascalcase("whatever_10") is False 68 | assert humps.is_kebabcase("whatever_10") is False 69 | 70 | 71 | def test_numeric(): 72 | assert humps.camelize(1234) == 1234 73 | assert humps.decamelize(123) == 123 74 | assert humps.pascalize(123) == 123 75 | assert humps.kebabize(123) == 123 76 | 77 | 78 | def test_upper(): 79 | assert humps.camelize("API") == "API" 80 | assert humps.decamelize("API") == "API" 81 | assert humps.pascalize("API") == "API" 82 | assert humps.depascalize("API") == "API" 83 | assert humps.kebabize("API") == "API" 84 | assert humps.dekebabize("API") == "API" 85 | 86 | 87 | def test_pascalize(): 88 | actual = humps.pascalize( 89 | { 90 | "videos": [ 91 | { 92 | "fallback_url": "https://media.io/video", 93 | "scrubber_media_url": "https://media.io/video", 94 | "dash_url": "https://media.io/video", 95 | } 96 | ], 97 | "images": [ 98 | { 99 | "fallback_url": "https://media.io/image", 100 | "scrubber_media_url": "https://media.io/image", 101 | "url": "https://media.io/image", 102 | } 103 | ], 104 | "other": [ 105 | { 106 | "_fallback_url": "https://media.io/image", 107 | "__scrubber_media___url_": "https://media.io/image", 108 | "_url__": "https://media.io/image", 109 | }, 110 | { 111 | "API": "test_upper", 112 | "_API_": "test_upper", 113 | "__API__": "test_upper", 114 | "APIResponse": "test_acronym", 115 | "_APIResponse_": "test_acronym", 116 | "__APIResponse__": "test_acronym", 117 | }, 118 | ], 119 | } 120 | ) 121 | expected = { 122 | "Videos": [ 123 | { 124 | "FallbackUrl": "https://media.io/video", 125 | "ScrubberMediaUrl": "https://media.io/video", 126 | "DashUrl": "https://media.io/video", 127 | } 128 | ], 129 | "Images": [ 130 | { 131 | "FallbackUrl": "https://media.io/image", 132 | "ScrubberMediaUrl": "https://media.io/image", 133 | "Url": "https://media.io/image", 134 | } 135 | ], 136 | "Other": [ 137 | { 138 | "_FallbackUrl": "https://media.io/image", 139 | "__ScrubberMediaUrl_": "https://media.io/image", 140 | "_Url__": "https://media.io/image", 141 | }, 142 | { 143 | "API": "test_upper", 144 | "_API_": "test_upper", 145 | "__API__": "test_upper", 146 | "APIResponse": "test_acronym", 147 | "_APIResponse_": "test_acronym", 148 | "__APIResponse__": "test_acronym", 149 | }, 150 | ], 151 | } 152 | assert actual == expected 153 | 154 | 155 | def test_depascalize(): 156 | actual = humps.depascalize( 157 | [ 158 | { 159 | "Symbol": "AAL", 160 | "LastPrice": 31.78, 161 | "ChangePct": 2.8146, 162 | "ImpliedVolatality": 0.482, 163 | }, 164 | { 165 | "Symbol": "LBTYA", 166 | "LastPrice": 25.95, 167 | "ChangePct": 2.6503, 168 | "ImpliedVolatality": 0.7287, 169 | }, 170 | { 171 | "_Symbol": "LBTYK", 172 | "ChangePct_": 2.5827, 173 | "_LastPrice__": 25.42, 174 | "__ImpliedVolatality_": 0.4454, 175 | }, 176 | { 177 | "API": "test_upper", 178 | "_API_": "test_upper", 179 | "__API__": "test_upper", 180 | "APIResponse": "test_acronym", 181 | "_APIResponse_": "test_acronym", 182 | "__APIResponse__": "test_acronym", 183 | "ruby_tuesdays": "ruby_tuesdays", 184 | }, 185 | ] 186 | ) 187 | expected = [ 188 | { 189 | "symbol": "AAL", 190 | "last_price": 31.78, 191 | "change_pct": 2.8146, 192 | "implied_volatality": 0.482, 193 | }, 194 | { 195 | "symbol": "LBTYA", 196 | "last_price": 25.95, 197 | "change_pct": 2.6503, 198 | "implied_volatality": 0.7287, 199 | }, 200 | { 201 | "_symbol": "LBTYK", 202 | "change_pct_": 2.5827, 203 | "_last_price__": 25.42, 204 | "__implied_volatality_": 0.4454, 205 | }, 206 | { 207 | "API": "test_upper", 208 | "_API_": "test_upper", 209 | "__API__": "test_upper", 210 | "api_response": "test_acronym", 211 | "_api_response_": "test_acronym", 212 | "__api_response__": "test_acronym", 213 | "ruby_tuesdays": "ruby_tuesdays", 214 | }, 215 | ] 216 | 217 | assert actual == expected 218 | -------------------------------------------------------------------------------- /humps/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains all the core logic for humps. 3 | """ 4 | import re 5 | 6 | from collections.abc import Mapping # pylint: disable-msg=E0611 7 | 8 | ACRONYM_RE = re.compile(r"([A-Z\d]+)(?=[A-Z\d]|$)") 9 | PASCAL_RE = re.compile(r"([^\-_]+)") 10 | SPLIT_RE = re.compile(r"([\-_]*(?<=[^0-9])(?=[A-Z])[^A-Z]*[\-_]*)") 11 | UNDERSCORE_RE = re.compile(r"(?<=[^\-_])[\-_]+[^\-_]") 12 | 13 | 14 | def pascalize(str_or_iter): 15 | """ 16 | Convert a string, dict, or list of dicts to pascal case. 17 | 18 | :param str_or_iter: 19 | A string or iterable. 20 | :type str_or_iter: Union[list, dict, str] 21 | :rtype: Union[list, dict, str] 22 | :returns: 23 | pascalized string, dictionary, or list of dictionaries. 24 | """ 25 | if isinstance(str_or_iter, (list, Mapping)): 26 | return _process_keys(str_or_iter, pascalize) 27 | 28 | s = _is_none(str_or_iter) 29 | if s.isupper() or s.isnumeric(): 30 | return str_or_iter 31 | 32 | def _replace_fn(match): 33 | """ 34 | :rtype: str 35 | """ 36 | return match.group(1)[0].upper() + match.group(1)[1:] 37 | 38 | s = camelize(PASCAL_RE.sub(_replace_fn, s)) 39 | return s[0].upper() + s[1:] if len(s) != 0 else s 40 | 41 | 42 | def camelize(str_or_iter): 43 | """ 44 | Convert a string, dict, or list of dicts to camel case. 45 | 46 | :param str_or_iter: 47 | A string or iterable. 48 | :type str_or_iter: Union[list, dict, str] 49 | :rtype: Union[list, dict, str] 50 | :returns: 51 | camelized string, dictionary, or list of dictionaries. 52 | """ 53 | if isinstance(str_or_iter, (list, Mapping)): 54 | return _process_keys(str_or_iter, camelize) 55 | 56 | s = _is_none(str_or_iter) 57 | if s.isupper() or s.isnumeric(): 58 | return str_or_iter 59 | 60 | if len(s) != 0 and not s[:2].isupper(): 61 | s = s[0].lower() + s[1:] 62 | 63 | # For string "hello_world", match will contain 64 | # the regex capture group for "_w". 65 | return UNDERSCORE_RE.sub(lambda m: m.group(0)[-1].upper(), s) 66 | 67 | 68 | def kebabize(str_or_iter): 69 | """ 70 | Convert a string, dict, or list of dicts to kebab case. 71 | :param str_or_iter: 72 | A string or iterable. 73 | :type str_or_iter: Union[list, dict, str] 74 | :rtype: Union[list, dict, str] 75 | :returns: 76 | kebabized string, dictionary, or list of dictionaries. 77 | """ 78 | if isinstance(str_or_iter, (list, Mapping)): 79 | return _process_keys(str_or_iter, kebabize) 80 | 81 | s = _is_none(str_or_iter) 82 | if s.isnumeric(): 83 | return str_or_iter 84 | 85 | if not (s.isupper()) and (is_camelcase(s) or is_pascalcase(s)): 86 | return ( 87 | _separate_words( 88 | string=_fix_abbreviations(s), 89 | separator="-" 90 | ).lower() 91 | ) 92 | 93 | return UNDERSCORE_RE.sub(lambda m: "-" + m.group(0)[-1], s) 94 | 95 | 96 | def decamelize(str_or_iter): 97 | """ 98 | Convert a string, dict, or list of dicts to snake case. 99 | 100 | :param str_or_iter: 101 | A string or iterable. 102 | :type str_or_iter: Union[list, dict, str] 103 | :rtype: Union[list, dict, str] 104 | :returns: 105 | snake cased string, dictionary, or list of dictionaries. 106 | """ 107 | if isinstance(str_or_iter, (list, Mapping)): 108 | return _process_keys(str_or_iter, decamelize) 109 | 110 | s = _is_none(str_or_iter) 111 | if s.isupper() or s.isnumeric(): 112 | return str_or_iter 113 | 114 | return _separate_words(_fix_abbreviations(s)).lower() 115 | 116 | 117 | def depascalize(str_or_iter): 118 | """ 119 | Convert a string, dict, or list of dicts to snake case. 120 | 121 | :param str_or_iter: A string or iterable. 122 | :type str_or_iter: Union[list, dict, str] 123 | :rtype: Union[list, dict, str] 124 | :returns: 125 | snake cased string, dictionary, or list of dictionaries. 126 | """ 127 | return decamelize(str_or_iter) 128 | 129 | 130 | def dekebabize(str_or_iter): 131 | """ 132 | Convert a string, dict, or list of dicts to snake case. 133 | :param str_or_iter: 134 | A string or iterable. 135 | :type str_or_iter: Union[list, dict, str] 136 | :rtype: Union[list, dict, str] 137 | :returns: 138 | snake cased string, dictionary, or list of dictionaries. 139 | """ 140 | if isinstance(str_or_iter, (list, Mapping)): 141 | return _process_keys(str_or_iter, dekebabize) 142 | 143 | s = _is_none(str_or_iter) 144 | if s.isnumeric(): 145 | return str_or_iter 146 | 147 | return s.replace("-", "_") 148 | 149 | 150 | def is_camelcase(str_or_iter): 151 | """ 152 | Determine if a string, dict, or list of dicts is camel case. 153 | 154 | :param str_or_iter: 155 | A string or iterable. 156 | :type str_or_iter: Union[list, dict, str] 157 | :rtype: bool 158 | :returns: 159 | True/False whether string or iterable is camel case 160 | """ 161 | return str_or_iter == camelize(str_or_iter) 162 | 163 | 164 | def is_pascalcase(str_or_iter): 165 | """ 166 | Determine if a string, dict, or list of dicts is pascal case. 167 | 168 | :param str_or_iter: A string or iterable. 169 | :type str_or_iter: Union[list, dict, str] 170 | :rtype: bool 171 | :returns: 172 | True/False whether string or iterable is pascal case 173 | """ 174 | return str_or_iter == pascalize(str_or_iter) 175 | 176 | 177 | def is_kebabcase(str_or_iter): 178 | """ 179 | Determine if a string, dict, or list of dicts is camel case. 180 | :param str_or_iter: 181 | A string or iterable. 182 | :type str_or_iter: Union[list, dict, str] 183 | :rtype: bool 184 | :returns: 185 | True/False whether string or iterable is camel case 186 | """ 187 | return str_or_iter == kebabize(str_or_iter) 188 | 189 | 190 | def is_snakecase(str_or_iter): 191 | """ 192 | Determine if a string, dict, or list of dicts is snake case. 193 | 194 | :param str_or_iter: 195 | A string or iterable. 196 | :type str_or_iter: Union[list, dict, str] 197 | :rtype: bool 198 | :returns: 199 | True/False whether string or iterable is snake case 200 | """ 201 | if is_kebabcase(str_or_iter) and not is_camelcase(str_or_iter): 202 | return False 203 | 204 | return str_or_iter == decamelize(str_or_iter) 205 | 206 | 207 | def _is_none(_in): 208 | """ 209 | Determine if the input is None 210 | and returns a string with white-space removed 211 | :param _in: input 212 | :return: 213 | an empty sting if _in is None, 214 | else the input is returned with white-space removed 215 | """ 216 | return "" if _in is None else re.sub(r"\s+", "", str(_in)) 217 | 218 | 219 | def _process_keys(str_or_iter, fn): 220 | if isinstance(str_or_iter, list): 221 | return [_process_keys(k, fn) for k in str_or_iter] 222 | if isinstance(str_or_iter, Mapping): 223 | return {fn(k): _process_keys(v, fn) for k, v in str_or_iter.items()} 224 | return str_or_iter 225 | 226 | 227 | def _fix_abbreviations(string): 228 | """ 229 | Rewrite incorrectly cased acronyms, initialisms, and abbreviations, 230 | allowing them to be decamelized correctly. For example, given the string 231 | "APIResponse", this function is responsible for ensuring the output is 232 | "api_response" instead of "a_p_i_response". 233 | 234 | :param string: A string that may contain an incorrectly cased abbreviation. 235 | :type string: str 236 | :rtype: str 237 | :returns: 238 | A rewritten string that is safe for decamelization. 239 | """ 240 | return ACRONYM_RE.sub(lambda m: m.group(0).title(), string) 241 | 242 | 243 | def _separate_words(string, separator="_"): 244 | """ 245 | Split words that are separated by case differentiation. 246 | :param string: Original string. 247 | :param separator: String by which the individual 248 | words will be put back together. 249 | :returns: 250 | New string. 251 | """ 252 | return separator.join(s for s in SPLIT_RE.split(string) if s) 253 | --------------------------------------------------------------------------------