├── 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 |
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 |
--------------------------------------------------------------------------------