├── inflection ├── py.typed └── __init__.py ├── requirements.txt ├── MANIFEST.in ├── .gitignore ├── setup.py ├── pyproject.toml ├── .editorconfig ├── .travis.yml ├── tox.ini ├── README.rst ├── LICENSE ├── docs ├── index.rst ├── Makefile └── conf.py ├── setup.cfg └── test_inflection.py /inflection/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | isort 3 | pytest 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE test_inflection.py 2 | graft docs 3 | prune docs/_build 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | *.pyc 3 | .Python 4 | .tox/ 5 | __pycache__/ 6 | dist/ 7 | docs/_build/ 8 | .mypy_cache/ 9 | .venv/ 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | if __name__ == "__main__": 7 | setup() 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=44", "wheel", "setuptools_scm[toml]>=3.4.3"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | end_of_line = lf 9 | 10 | [*.{yml,yaml}] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.5 4 | - 3.6 5 | - 3.7 6 | - 3.8 7 | - pypy3 8 | script: 9 | - flake8 . 10 | - isort --recursive --diff . && isort --recursive --check-only . 11 | - pytest 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py35, py36, py37, py38, pypy3 3 | 4 | [testenv] 5 | deps = -r{toxinidir}/requirements.txt 6 | pytest-mypy 7 | commands = pytest --mypy {posargs} 8 | 9 | [pytest] 10 | addopts = --doctest-modules 11 | 12 | [testenv:pypy3] 13 | deps = -r{toxinidir}/requirements.txt 14 | commands = pytest {posargs} 15 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Inflection 2 | ========== 3 | 4 | |build status|_ 5 | 6 | .. |build status| image:: https://travis-ci.org/jpvanhal/inflection.svg?branch=master 7 | :alt: Build Status 8 | .. _build status: http://travis-ci.org/jpvanhal/inflection 9 | 10 | Inflection is a string transformation library. It singularizes and pluralizes 11 | English words, and transforms strings from CamelCase to underscored string. 12 | Inflection is a port of `Ruby on Rails`_' `inflector`_ to Python. 13 | 14 | .. _Ruby on Rails: http://rubyonrails.org 15 | .. _inflector: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html 16 | 17 | Resources 18 | --------- 19 | 20 | - `Documentation `_ 21 | - `Issue Tracker `_ 22 | - `Code `_ 23 | - `Development Version 24 | `_ 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012-2020 Janne Vanhala 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Inflection 2 | ========== 3 | 4 | Inflection is a string transformation library. It singularizes and pluralizes 5 | English words, and transforms strings from CamelCase to underscored_string. 6 | Inflection is a port of `Ruby on Rails`_' `inflector`_ to Python. 7 | 8 | .. _Ruby on Rails: http://rubyonrails.org 9 | .. _inflector: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | Use pip to install from PyPI:: 16 | 17 | pip install inflection 18 | 19 | 20 | Contributing 21 | ------------ 22 | 23 | To contribute to Inflector `create a fork`_ on GitHub. Clone your fork, make 24 | some changes, and submit a pull request. 25 | 26 | .. _create a fork: https://github.com/jpvanhal/inflection/fork_select 27 | 28 | 29 | API Documentation 30 | ----------------- 31 | 32 | .. module:: inflection 33 | 34 | .. autofunction:: camelize 35 | .. autofunction:: dasherize 36 | .. autofunction:: humanize 37 | .. autofunction:: ordinal 38 | .. autofunction:: ordinalize 39 | .. autofunction:: parameterize 40 | .. autofunction:: pluralize 41 | .. autofunction:: singularize 42 | .. autofunction:: tableize 43 | .. autofunction:: titleize 44 | .. autofunction:: transliterate 45 | .. autofunction:: underscore 46 | 47 | 48 | Changelog 49 | --------- 50 | 51 | You you can see the full list of changes between each Inflection release in 52 | `GitHub releases page`_. 53 | 54 | .. _GitHub releases page: https://github.com/jpvanhal/inflection/releases 55 | 56 | 57 | License 58 | ------- 59 | 60 | .. include:: ../LICENSE 61 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | name = inflection 4 | author = Janne Vanhala 5 | author_email = janne.vanhala@gmail.com 6 | license = MIT 7 | description = A port of Ruby on Rails inflector to Python 8 | url = https://github.com/jpvanhal/inflection 9 | long_description = file: README.rst 10 | classifiers = 11 | Development Status :: 4 - Beta 12 | Intended Audience :: Developers 13 | Natural Language :: English 14 | License :: OSI Approved :: MIT License 15 | Programming Language :: Python 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3.5 18 | Programming Language :: Python :: 3.6 19 | Programming Language :: Python :: 3.7 20 | Programming Language :: Python :: 3.8 21 | Programming Language :: Python :: Implementation :: CPython 22 | Programming Language :: Python :: Implementation :: PyPy 23 | 24 | [options] 25 | py_modules = inflection 26 | zip_safe = False 27 | python_requires = >=3.5 28 | 29 | [options.package_data] 30 | inflection = py.typed 31 | 32 | [flake8] 33 | exclude = docs/* 34 | 35 | [isort] 36 | skip = .tox,docs 37 | 38 | [mypy] 39 | disallow_any_decorated = True 40 | disallow_any_explicit = True 41 | warn_unreachable = True 42 | # options from __strict 43 | warn_unused_configs = True 44 | disallow_any_generics = True 45 | disallow_subclassing_any = True 46 | disallow_untyped_calls = True 47 | disallow_untyped_defs = True 48 | disallow_incomplete_defs = True 49 | check_untyped_defs = True 50 | disallow_untyped_decorators = True 51 | no_implicit_optional = True 52 | warn_redundant_casts = True 53 | warn_unused_ignores = True 54 | warn_return_any = True 55 | no_implicit_reexport = True 56 | strict_equality = True 57 | 58 | [mypy-setuptools] 59 | ignore_missing_imports = True 60 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/inflection.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/inflection.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/inflection" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/inflection" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # inflection documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Feb 22 22:51:13 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | from typing import Dict 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('..')) 22 | from inflection import __version__ 23 | 24 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 25 | 26 | 27 | # -- General configuration ----------------------------------------------------- 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | #needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be extensions 33 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 34 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = u'inflection' 50 | copyright = u'2012-2020, Janne Vanhala' 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = __version__ 58 | # The full version, including alpha/beta/rc tags. 59 | release = version 60 | 61 | # The language for content autogenerated by Sphinx. Refer to documentation 62 | # for a list of supported languages. 63 | #language = None 64 | 65 | # There are two options for replacing |today|: either, you set today to some 66 | # non-false value, then it is used: 67 | #today = '' 68 | # Else, today_fmt is used as the format for a strftime call. 69 | #today_fmt = '%B %d, %Y' 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | exclude_patterns = ['_build'] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all documents. 76 | #default_role = None 77 | 78 | # If true, '()' will be appended to :func: etc. cross-reference text. 79 | #add_function_parentheses = True 80 | 81 | # If true, the current module name will be prepended to all description 82 | # unit titles (such as .. function::). 83 | #add_module_names = True 84 | 85 | # If true, sectionauthor and moduleauthor directives will be shown in the 86 | # output. They are ignored by default. 87 | #show_authors = False 88 | 89 | # The name of the Pygments (syntax highlighting) style to use. 90 | pygments_style = 'sphinx' 91 | 92 | # A list of ignored prefixes for module index sorting. 93 | #modindex_common_prefix = [] 94 | 95 | 96 | # -- Options for HTML output --------------------------------------------------- 97 | 98 | # The theme to use for HTML and HTML Help pages. See the documentation for 99 | # a list of builtin themes. 100 | html_theme = 'default' if on_rtd else 'nature' 101 | 102 | # Theme options are theme-specific and customize the look and feel of a theme 103 | # further. For a list of options available for each theme, see the 104 | # documentation. 105 | #html_theme_options = {} 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | #html_theme_path = [] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | #html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | #html_static_path = ['_static'] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | #html_last_updated_fmt = '%b %d, %Y' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | #html_use_smartypants = True 138 | 139 | # Custom sidebar templates, maps document names to template names. 140 | #html_sidebars = {} 141 | 142 | # Additional templates that should be rendered to pages, maps page names to 143 | # template names. 144 | #html_additional_pages = {} 145 | 146 | # If false, no module index is generated. 147 | #html_domain_indices = True 148 | 149 | # If false, no index is generated. 150 | #html_use_index = True 151 | 152 | # If true, the index is split into individual pages for each letter. 153 | #html_split_index = False 154 | 155 | # If true, links to the reST sources are added to the pages. 156 | #html_show_sourcelink = True 157 | 158 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 159 | #html_show_sphinx = True 160 | 161 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 162 | #html_show_copyright = True 163 | 164 | # If true, an OpenSearch description file will be output, and all pages will 165 | # contain a tag referring to it. The value of this option must be the 166 | # base URL from which the finished HTML is served. 167 | #html_use_opensearch = '' 168 | 169 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 170 | #html_file_suffix = None 171 | 172 | # Output file base name for HTML help builder. 173 | htmlhelp_basename = 'inflectiondoc' 174 | 175 | 176 | # -- Options for LaTeX output -------------------------------------------------- 177 | 178 | latex_elements: Dict[str, str] = { 179 | # The paper size ('letterpaper' or 'a4paper'). 180 | #'papersize': 'letterpaper', 181 | 182 | # The font size ('10pt', '11pt' or '12pt'). 183 | #'pointsize': '10pt', 184 | 185 | # Additional stuff for the LaTeX preamble. 186 | #'preamble': '', 187 | } 188 | 189 | # Grouping the document tree into LaTeX files. List of tuples 190 | # (source start file, target name, title, author, documentclass [howto/manual]). 191 | latex_documents = [ 192 | ('index', 'inflection.tex', u'inflection Documentation', 193 | u'Janne Vanhala', 'manual'), 194 | ] 195 | 196 | # The name of an image file (relative to this directory) to place at the top of 197 | # the title page. 198 | #latex_logo = None 199 | 200 | # For "manual" documents, if this is true, then toplevel headings are parts, 201 | # not chapters. 202 | #latex_use_parts = False 203 | 204 | # If true, show page references after internal links. 205 | #latex_show_pagerefs = False 206 | 207 | # If true, show URL addresses after external links. 208 | #latex_show_urls = False 209 | 210 | # Documents to append as an appendix to all manuals. 211 | #latex_appendices = [] 212 | 213 | # If false, no module index is generated. 214 | #latex_domain_indices = True 215 | 216 | 217 | # -- Options for manual page output -------------------------------------------- 218 | 219 | # One entry per manual page. List of tuples 220 | # (source start file, name, description, authors, manual section). 221 | man_pages = [ 222 | ('index', 'inflection', u'inflection Documentation', 223 | [u'Janne Vanhala'], 1) 224 | ] 225 | 226 | # If true, show URL addresses after external links. 227 | #man_show_urls = False 228 | 229 | 230 | # -- Options for Texinfo output ------------------------------------------------ 231 | 232 | # Grouping the document tree into Texinfo files. List of tuples 233 | # (source start file, target name, title, author, 234 | # dir menu entry, description, category) 235 | texinfo_documents = [ 236 | ('index', 'inflection', u'inflection Documentation', 237 | u'Janne Vanhala', 'inflection', 'One line description of project.', 238 | 'Miscellaneous'), 239 | ] 240 | 241 | # Documents to append as an appendix to all manuals. 242 | #texinfo_appendices = [] 243 | 244 | # If false, no module index is generated. 245 | #texinfo_domain_indices = True 246 | 247 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 248 | #texinfo_show_urls = 'footnote' 249 | -------------------------------------------------------------------------------- /inflection/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | inflection 4 | ~~~~~~~~~~~~ 5 | 6 | A port of Ruby on Rails' inflector to Python. 7 | 8 | :copyright: (c) 2012-2020 by Janne Vanhala 9 | 10 | :license: MIT, see LICENSE for more details. 11 | """ 12 | import re 13 | import typing 14 | import unicodedata 15 | 16 | __version__ = '0.5.1' 17 | 18 | RegexReplaceList = typing.List[typing.Tuple[str, str]] 19 | 20 | PLURALS: RegexReplaceList = [ 21 | (r"(?i)(quiz)$", r'\1zes'), 22 | (r"(?i)^(oxen)$", r'\1'), 23 | (r"(?i)^(ox)$", r'\1en'), 24 | (r"(?i)(m|l)ice$", r'\1ice'), 25 | (r"(?i)(m|l)ouse$", r'\1ice'), 26 | (r"(?i)(passer)s?by$", r'\1sby'), 27 | (r"(?i)(matr|vert|ind)(?:ix|ex)$", r'\1ices'), 28 | (r"(?i)(x|ch|ss|sh)$", r'\1es'), 29 | (r"(?i)([^aeiouy]|qu)y$", r'\1ies'), 30 | (r"(?i)(hive)$", r'\1s'), 31 | (r"(?i)([lr])f$", r'\1ves'), 32 | (r"(?i)([^f])fe$", r'\1ves'), 33 | (r"(?i)sis$", 'ses'), 34 | (r"(?i)([ti])a$", r'\1a'), 35 | (r"(?i)([ti])um$", r'\1a'), 36 | (r"(?i)(buffal|potat|tomat)o$", r'\1oes'), 37 | (r"(?i)(bu)s$", r'\1ses'), 38 | (r"(?i)(alias|status)$", r'\1es'), 39 | (r"(?i)(octop|vir)i$", r'\1i'), 40 | (r"(?i)(octop|vir)us$", r'\1i'), 41 | (r"(?i)^(ax|test)is$", r'\1es'), 42 | (r"(?i)s$", 's'), 43 | (r"$", 's'), 44 | ] 45 | 46 | SINGULARS: RegexReplaceList = [ 47 | (r"(?i)(database)s$", r'\1'), 48 | (r"(?i)(quiz)zes$", r'\1'), 49 | (r"(?i)(matr)ices$", r'\1ix'), 50 | (r"(?i)(vert|ind)ices$", r'\1ex'), 51 | (r"(?i)(passer)sby$", r'\1by'), 52 | (r"(?i)^(ox)en", r'\1'), 53 | (r"(?i)(alias|status)(es)?$", r'\1'), 54 | (r"(?i)(octop|vir)(us|i)$", r'\1us'), 55 | (r"(?i)^(a)x[ie]s$", r'\1xis'), 56 | (r"(?i)(cris|test)(is|es)$", r'\1is'), 57 | (r"(?i)(shoe)s$", r'\1'), 58 | (r"(?i)(o)es$", r'\1'), 59 | (r"(?i)(bus)(es)?$", r'\1'), 60 | (r"(?i)(m|l)ice$", r'\1ouse'), 61 | (r"(?i)(x|ch|ss|sh)es$", r'\1'), 62 | (r"(?i)(m)ovies$", r'\1ovie'), 63 | (r"(?i)(s)eries$", r'\1eries'), 64 | (r"(?i)([^aeiouy]|qu)ies$", r'\1y'), 65 | (r"(?i)([lr])ves$", r'\1f'), 66 | (r"(?i)(tive)s$", r'\1'), 67 | (r"(?i)(hive)s$", r'\1'), 68 | (r"(?i)([^f])ves$", r'\1fe'), 69 | (r"(?i)(t)he(sis|ses)$", r"\1hesis"), 70 | (r"(?i)(s)ynop(sis|ses)$", r"\1ynopsis"), 71 | (r"(?i)(p)rogno(sis|ses)$", r"\1rognosis"), 72 | (r"(?i)(p)arenthe(sis|ses)$", r"\1arenthesis"), 73 | (r"(?i)(d)iagno(sis|ses)$", r"\1iagnosis"), 74 | (r"(?i)(b)a(sis|ses)$", r"\1asis"), 75 | (r"(?i)(a)naly(sis|ses)$", r"\1nalysis"), 76 | (r"(?i)([ti])a$", r'\1um'), 77 | (r"(?i)(n)ews$", r'\1ews'), 78 | (r"(?i)(ss)$", r'\1'), 79 | (r"(?i)s$", ''), 80 | ] 81 | 82 | UNCOUNTABLES: typing.Set[str] = { 83 | 'equipment', 84 | 'fish', 85 | 'information', 86 | 'jeans', 87 | 'money', 88 | 'rice', 89 | 'series', 90 | 'sheep', 91 | 'species'} 92 | 93 | 94 | def _irregular(singular: str, plural: str) -> None: 95 | """ 96 | A convenience function to add appropriate rules to plurals and singular 97 | for irregular words. 98 | 99 | :param singular: irregular word in singular form 100 | :param plural: irregular word in plural form 101 | """ 102 | def caseinsensitive(string: str) -> str: 103 | return ''.join('[' + char + char.upper() + ']' for char in string) 104 | 105 | if singular[0].upper() == plural[0].upper(): 106 | PLURALS.insert(0, ( 107 | r"(?i)({}){}$".format(singular[0], singular[1:]), 108 | r'\1' + plural[1:] 109 | )) 110 | PLURALS.insert(0, ( 111 | r"(?i)({}){}$".format(plural[0], plural[1:]), 112 | r'\1' + plural[1:] 113 | )) 114 | SINGULARS.insert(0, ( 115 | r"(?i)({}){}$".format(plural[0], plural[1:]), 116 | r'\1' + singular[1:] 117 | )) 118 | else: 119 | PLURALS.insert(0, ( 120 | r"{}{}$".format(singular[0].upper(), 121 | caseinsensitive(singular[1:])), 122 | plural[0].upper() + plural[1:] 123 | )) 124 | PLURALS.insert(0, ( 125 | r"{}{}$".format(singular[0].lower(), 126 | caseinsensitive(singular[1:])), 127 | plural[0].lower() + plural[1:] 128 | )) 129 | PLURALS.insert(0, ( 130 | r"{}{}$".format(plural[0].upper(), caseinsensitive(plural[1:])), 131 | plural[0].upper() + plural[1:] 132 | )) 133 | PLURALS.insert(0, ( 134 | r"{}{}$".format(plural[0].lower(), caseinsensitive(plural[1:])), 135 | plural[0].lower() + plural[1:] 136 | )) 137 | SINGULARS.insert(0, ( 138 | r"{}{}$".format(plural[0].upper(), caseinsensitive(plural[1:])), 139 | singular[0].upper() + singular[1:] 140 | )) 141 | SINGULARS.insert(0, ( 142 | r"{}{}$".format(plural[0].lower(), caseinsensitive(plural[1:])), 143 | singular[0].lower() + singular[1:] 144 | )) 145 | 146 | 147 | def camelize(string: str, uppercase_first_letter: bool = True) -> str: 148 | """ 149 | Convert strings to CamelCase. 150 | 151 | Examples:: 152 | 153 | >>> camelize("device_type") 154 | 'DeviceType' 155 | >>> camelize("device_type", False) 156 | 'deviceType' 157 | 158 | :func:`camelize` can be thought of as a inverse of :func:`underscore`, 159 | although there are some cases where that does not hold:: 160 | 161 | >>> camelize(underscore("IOError")) 162 | 'IoError' 163 | 164 | :param uppercase_first_letter: if set to `True` :func:`camelize` converts 165 | strings to UpperCamelCase. If set to `False` :func:`camelize` produces 166 | lowerCamelCase. Defaults to `True`. 167 | """ 168 | if uppercase_first_letter: 169 | return re.sub(r"(?:^|_)(.)", lambda m: m.group(1).upper(), string) 170 | else: 171 | return string[0].lower() + camelize(string)[1:] 172 | 173 | 174 | def dasherize(word: str) -> str: 175 | """Replace underscores with dashes in the string. 176 | 177 | Example:: 178 | 179 | >>> dasherize("puni_puni") 180 | 'puni-puni' 181 | 182 | """ 183 | return word.replace('_', '-') 184 | 185 | 186 | def humanize(word: str) -> str: 187 | """ 188 | Capitalize the first word and turn underscores into spaces and strip a 189 | trailing ``"_id"``, if any. Like :func:`titleize`, this is meant for 190 | creating pretty output. 191 | 192 | Examples:: 193 | 194 | >>> humanize("employee_salary") 195 | 'Employee salary' 196 | >>> humanize("author_id") 197 | 'Author' 198 | 199 | """ 200 | word = re.sub(r"_id$", "", word) 201 | word = word.replace('_', ' ') 202 | word = re.sub(r"(?i)([a-z\d]*)", lambda m: m.group(1).lower(), word) 203 | word = re.sub(r"^\w", lambda m: m.group(0).upper(), word) 204 | return word 205 | 206 | 207 | def ordinal(number: int) -> str: 208 | """ 209 | Return the suffix that should be added to a number to denote the position 210 | in an ordered sequence such as 1st, 2nd, 3rd, 4th. 211 | 212 | Examples:: 213 | 214 | >>> ordinal(1) 215 | 'st' 216 | >>> ordinal(2) 217 | 'nd' 218 | >>> ordinal(1002) 219 | 'nd' 220 | >>> ordinal(1003) 221 | 'rd' 222 | >>> ordinal(-11) 223 | 'th' 224 | >>> ordinal(-1021) 225 | 'st' 226 | 227 | """ 228 | number = abs(int(number)) 229 | if number % 100 in (11, 12, 13): 230 | return "th" 231 | else: 232 | return { 233 | 1: "st", 234 | 2: "nd", 235 | 3: "rd", 236 | }.get(number % 10, "th") 237 | 238 | 239 | def ordinalize(number: int) -> str: 240 | """ 241 | Turn a number into an ordinal string used to denote the position in an 242 | ordered sequence such as 1st, 2nd, 3rd, 4th. 243 | 244 | Examples:: 245 | 246 | >>> ordinalize(1) 247 | '1st' 248 | >>> ordinalize(2) 249 | '2nd' 250 | >>> ordinalize(1002) 251 | '1002nd' 252 | >>> ordinalize(1003) 253 | '1003rd' 254 | >>> ordinalize(-11) 255 | '-11th' 256 | >>> ordinalize(-1021) 257 | '-1021st' 258 | 259 | """ 260 | return "{}{}".format(number, ordinal(number)) 261 | 262 | 263 | def parameterize(string: str, separator: str = '-') -> str: 264 | """ 265 | Replace special characters in a string so that it may be used as part of a 266 | 'pretty' URL. 267 | 268 | Example:: 269 | 270 | >>> parameterize(u"Donald E. Knuth") 271 | 'donald-e-knuth' 272 | 273 | """ 274 | string = transliterate(string) 275 | # Turn unwanted chars into the separator 276 | string = re.sub(r"(?i)[^a-z0-9\-_]+", separator, string) 277 | if separator: 278 | re_sep = re.escape(separator) 279 | # No more than one of the separator in a row. 280 | string = re.sub(r'%s{2,}' % re_sep, separator, string) 281 | # Remove leading/trailing separator. 282 | string = re.sub(r"(?i)^{sep}|{sep}$".format(sep=re_sep), '', string) 283 | 284 | return string.lower() 285 | 286 | 287 | def pluralize(word: str) -> str: 288 | """ 289 | Return the plural form of a word. 290 | 291 | Examples:: 292 | 293 | >>> pluralize("posts") 294 | 'posts' 295 | >>> pluralize("octopus") 296 | 'octopi' 297 | >>> pluralize("sheep") 298 | 'sheep' 299 | >>> pluralize("CamelOctopus") 300 | 'CamelOctopi' 301 | 302 | """ 303 | if not word or word.lower() in UNCOUNTABLES: 304 | return word 305 | else: 306 | for rule, replacement in PLURALS: 307 | if re.search(rule, word): 308 | return re.sub(rule, replacement, word) 309 | return word 310 | 311 | 312 | def singularize(word: str) -> str: 313 | """ 314 | Return the singular form of a word, the reverse of :func:`pluralize`. 315 | 316 | Examples:: 317 | 318 | >>> singularize("posts") 319 | 'post' 320 | >>> singularize("octopi") 321 | 'octopus' 322 | >>> singularize("sheep") 323 | 'sheep' 324 | >>> singularize("word") 325 | 'word' 326 | >>> singularize("CamelOctopi") 327 | 'CamelOctopus' 328 | 329 | """ 330 | for inflection in UNCOUNTABLES: 331 | if re.search(r'(?i)\b(%s)\Z' % inflection, word): 332 | return word 333 | 334 | for rule, replacement in SINGULARS: 335 | if re.search(rule, word): 336 | return re.sub(rule, replacement, word) 337 | return word 338 | 339 | 340 | def tableize(word: str) -> str: 341 | """ 342 | Create the name of a table like Rails does for models to table names. This 343 | method uses the :func:`pluralize` method on the last word in the string. 344 | 345 | Examples:: 346 | 347 | >>> tableize('RawScaledScorer') 348 | 'raw_scaled_scorers' 349 | >>> tableize('egg_and_ham') 350 | 'egg_and_hams' 351 | >>> tableize('fancyCategory') 352 | 'fancy_categories' 353 | """ 354 | return pluralize(underscore(word)) 355 | 356 | 357 | def titleize(word: str) -> str: 358 | """ 359 | Capitalize all the words and replace some characters in the string to 360 | create a nicer looking title. :func:`titleize` is meant for creating pretty 361 | output. 362 | 363 | Examples:: 364 | 365 | >>> titleize("man from the boondocks") 366 | 'Man From The Boondocks' 367 | >>> titleize("x-men: the last stand") 368 | 'X Men: The Last Stand' 369 | >>> titleize("TheManWithoutAPast") 370 | 'The Man Without A Past' 371 | >>> titleize("raiders_of_the_lost_ark") 372 | 'Raiders Of The Lost Ark' 373 | 374 | """ 375 | return re.sub( 376 | r"\b('?\w)", 377 | lambda match: match.group(1).capitalize(), 378 | humanize(underscore(word)).title() 379 | ) 380 | 381 | 382 | def transliterate(string: str) -> str: 383 | """ 384 | Replace non-ASCII characters with an ASCII approximation. If no 385 | approximation exists, the non-ASCII character is ignored. The string must 386 | be ``unicode``. 387 | 388 | Examples:: 389 | 390 | >>> transliterate('älämölö') 391 | 'alamolo' 392 | >>> transliterate('Ærøskøbing') 393 | 'rskbing' 394 | 395 | """ 396 | normalized = unicodedata.normalize('NFKD', string) 397 | return normalized.encode('ascii', 'ignore').decode('ascii') 398 | 399 | 400 | def underscore(word: str) -> str: 401 | """ 402 | Make an underscored, lowercase form from the expression in the string. 403 | 404 | Example:: 405 | 406 | >>> underscore("DeviceType") 407 | 'device_type' 408 | 409 | As a rule of thumb you can think of :func:`underscore` as the inverse of 410 | :func:`camelize`, though there are cases where that does not hold:: 411 | 412 | >>> camelize(underscore("IOError")) 413 | 'IoError' 414 | 415 | """ 416 | word = re.sub(r"([A-Z]+)([A-Z][a-z])", r'\1_\2', word) 417 | word = re.sub(r"([a-z\d])([A-Z])", r'\1_\2', word) 418 | word = word.replace("-", "_") 419 | return word.lower() 420 | 421 | 422 | _irregular('person', 'people') 423 | _irregular('man', 'men') 424 | _irregular('human', 'humans') 425 | _irregular('child', 'children') 426 | _irregular('sex', 'sexes') 427 | _irregular('move', 'moves') 428 | _irregular('cow', 'kine') 429 | _irregular('zombie', 'zombies') 430 | -------------------------------------------------------------------------------- /test_inflection.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import typing 3 | 4 | import pytest 5 | 6 | import inflection 7 | 8 | TestParameters = typing.Tuple[typing.Tuple[str, str], ...] 9 | 10 | SINGULAR_TO_PLURAL: TestParameters = ( 11 | ("search", "searches"), 12 | ("switch", "switches"), 13 | ("fix", "fixes"), 14 | ("box", "boxes"), 15 | ("process", "processes"), 16 | ("address", "addresses"), 17 | ("case", "cases"), 18 | ("stack", "stacks"), 19 | ("wish", "wishes"), 20 | ("fish", "fish"), 21 | ("jeans", "jeans"), 22 | ("funky jeans", "funky jeans"), 23 | 24 | ("category", "categories"), 25 | ("query", "queries"), 26 | ("ability", "abilities"), 27 | ("agency", "agencies"), 28 | ("movie", "movies"), 29 | 30 | ("archive", "archives"), 31 | 32 | ("index", "indices"), 33 | 34 | ("wife", "wives"), 35 | ("safe", "saves"), 36 | ("half", "halves"), 37 | 38 | ("move", "moves"), 39 | 40 | ("salesperson", "salespeople"), 41 | ("person", "people"), 42 | 43 | ("spokesman", "spokesmen"), 44 | ("man", "men"), 45 | ("woman", "women"), 46 | 47 | ("basis", "bases"), 48 | ("diagnosis", "diagnoses"), 49 | ("diagnosis_a", "diagnosis_as"), 50 | 51 | ("datum", "data"), 52 | ("medium", "media"), 53 | ("stadium", "stadia"), 54 | ("analysis", "analyses"), 55 | 56 | ("node_child", "node_children"), 57 | ("child", "children"), 58 | 59 | ("experience", "experiences"), 60 | ("day", "days"), 61 | 62 | ("comment", "comments"), 63 | ("foobar", "foobars"), 64 | ("newsletter", "newsletters"), 65 | 66 | ("old_news", "old_news"), 67 | ("news", "news"), 68 | 69 | ("series", "series"), 70 | ("species", "species"), 71 | 72 | ("quiz", "quizzes"), 73 | 74 | ("perspective", "perspectives"), 75 | 76 | ("ox", "oxen"), 77 | ("passerby", "passersby"), 78 | ("photo", "photos"), 79 | ("buffalo", "buffaloes"), 80 | ("tomato", "tomatoes"), 81 | ("potato", "potatoes"), 82 | ("dwarf", "dwarves"), 83 | ("elf", "elves"), 84 | ("information", "information"), 85 | ("equipment", "equipment"), 86 | ("bus", "buses"), 87 | ("status", "statuses"), 88 | ("status_code", "status_codes"), 89 | ("mouse", "mice"), 90 | 91 | ("louse", "lice"), 92 | ("house", "houses"), 93 | ("octopus", "octopi"), 94 | ("virus", "viri"), 95 | ("alias", "aliases"), 96 | ("portfolio", "portfolios"), 97 | 98 | ("vertex", "vertices"), 99 | ("matrix", "matrices"), 100 | ("matrix_fu", "matrix_fus"), 101 | 102 | ("axis", "axes"), 103 | ("testis", "testes"), 104 | ("crisis", "crises"), 105 | 106 | ("rice", "rice"), 107 | ("shoe", "shoes"), 108 | 109 | ("horse", "horses"), 110 | ("prize", "prizes"), 111 | ("edge", "edges"), 112 | 113 | ("cow", "kine"), 114 | ("database", "databases"), 115 | ("human", "humans") 116 | ) 117 | 118 | CAMEL_TO_UNDERSCORE: TestParameters = ( 119 | ("Product", "product"), 120 | ("SpecialGuest", "special_guest"), 121 | ("ApplicationController", "application_controller"), 122 | ("Area51Controller", "area51_controller"), 123 | ) 124 | 125 | CAMEL_TO_UNDERSCORE_WITHOUT_REVERSE: TestParameters = ( 126 | ("HTMLTidy", "html_tidy"), 127 | ("HTMLTidyGenerator", "html_tidy_generator"), 128 | ("FreeBSD", "free_bsd"), 129 | ("HTML", "html"), 130 | ) 131 | 132 | STRING_TO_PARAMETERIZED: TestParameters = ( 133 | (u"Donald E. Knuth", "donald-e-knuth"), 134 | ( 135 | u"Random text with *(bad)* characters", 136 | "random-text-with-bad-characters" 137 | ), 138 | (u"Allow_Under_Scores", "allow_under_scores"), 139 | (u"Trailing bad characters!@#", "trailing-bad-characters"), 140 | (u"!@#Leading bad characters", "leading-bad-characters"), 141 | (u"Squeeze separators", "squeeze-separators"), 142 | (u"Test with + sign", "test-with-sign"), 143 | (u"Test with malformed utf8 \251", "test-with-malformed-utf8"), 144 | ) 145 | 146 | STRING_TO_PARAMETERIZE_WITH_NO_SEPARATOR: TestParameters = ( 147 | (u"Donald E. Knuth", "donaldeknuth"), 148 | (u"With-some-dashes", "with-some-dashes"), 149 | (u"Random text with *(bad)* characters", "randomtextwithbadcharacters"), 150 | (u"Trailing bad characters!@#", "trailingbadcharacters"), 151 | (u"!@#Leading bad characters", "leadingbadcharacters"), 152 | (u"Squeeze separators", "squeezeseparators"), 153 | (u"Test with + sign", "testwithsign"), 154 | (u"Test with malformed utf8 \251", "testwithmalformedutf8"), 155 | ) 156 | 157 | STRING_TO_PARAMETERIZE_WITH_UNDERSCORE: TestParameters = ( 158 | (u"Donald E. Knuth", "donald_e_knuth"), 159 | ( 160 | u"Random text with *(bad)* characters", 161 | "random_text_with_bad_characters" 162 | ), 163 | (u"With-some-dashes", "with-some-dashes"), 164 | (u"Retain_underscore", "retain_underscore"), 165 | (u"Trailing bad characters!@#", "trailing_bad_characters"), 166 | (u"!@#Leading bad characters", "leading_bad_characters"), 167 | (u"Squeeze separators", "squeeze_separators"), 168 | (u"Test with + sign", "test_with_sign"), 169 | (u"Test with malformed utf8 \251", "test_with_malformed_utf8"), 170 | ) 171 | 172 | STRING_TO_PARAMETERIZED_AND_NORMALIZED: TestParameters = ( 173 | (u"Malmö", "malmo"), 174 | (u"Garçons", "garcons"), 175 | (u"Ops\331", "opsu"), 176 | (u"Ærøskøbing", "rskbing"), 177 | (u"Aßlar", "alar"), 178 | (u"Japanese: 日本語", "japanese"), 179 | ) 180 | 181 | UNDERSCORE_TO_HUMAN: TestParameters = ( 182 | ("employee_salary", "Employee salary"), 183 | ("employee_id", "Employee"), 184 | ("underground", "Underground"), 185 | ) 186 | 187 | MIXTURE_TO_TITLEIZED: TestParameters = ( 188 | ('active_record', 'Active Record'), 189 | ('ActiveRecord', 'Active Record'), 190 | ('action web service', 'Action Web Service'), 191 | ('Action Web Service', 'Action Web Service'), 192 | ('Action web service', 'Action Web Service'), 193 | ('actionwebservice', 'Actionwebservice'), 194 | ('Actionwebservice', 'Actionwebservice'), 195 | ("david's code", "David's Code"), 196 | ("David's code", "David's Code"), 197 | ("david's Code", "David's Code"), 198 | ("ana índia", "Ana Índia"), 199 | ("Ana Índia", "Ana Índia"), 200 | ) 201 | 202 | 203 | ORDINAL_NUMBERS: TestParameters = ( 204 | ("-1", "-1st"), 205 | ("-2", "-2nd"), 206 | ("-3", "-3rd"), 207 | ("-4", "-4th"), 208 | ("-5", "-5th"), 209 | ("-6", "-6th"), 210 | ("-7", "-7th"), 211 | ("-8", "-8th"), 212 | ("-9", "-9th"), 213 | ("-10", "-10th"), 214 | ("-11", "-11th"), 215 | ("-12", "-12th"), 216 | ("-13", "-13th"), 217 | ("-14", "-14th"), 218 | ("-20", "-20th"), 219 | ("-21", "-21st"), 220 | ("-22", "-22nd"), 221 | ("-23", "-23rd"), 222 | ("-24", "-24th"), 223 | ("-100", "-100th"), 224 | ("-101", "-101st"), 225 | ("-102", "-102nd"), 226 | ("-103", "-103rd"), 227 | ("-104", "-104th"), 228 | ("-110", "-110th"), 229 | ("-111", "-111th"), 230 | ("-112", "-112th"), 231 | ("-113", "-113th"), 232 | ("-1000", "-1000th"), 233 | ("-1001", "-1001st"), 234 | ("0", "0th"), 235 | ("1", "1st"), 236 | ("2", "2nd"), 237 | ("3", "3rd"), 238 | ("4", "4th"), 239 | ("5", "5th"), 240 | ("6", "6th"), 241 | ("7", "7th"), 242 | ("8", "8th"), 243 | ("9", "9th"), 244 | ("10", "10th"), 245 | ("11", "11th"), 246 | ("12", "12th"), 247 | ("13", "13th"), 248 | ("14", "14th"), 249 | ("20", "20th"), 250 | ("21", "21st"), 251 | ("22", "22nd"), 252 | ("23", "23rd"), 253 | ("24", "24th"), 254 | ("100", "100th"), 255 | ("101", "101st"), 256 | ("102", "102nd"), 257 | ("103", "103rd"), 258 | ("104", "104th"), 259 | ("110", "110th"), 260 | ("111", "111th"), 261 | ("112", "112th"), 262 | ("113", "113th"), 263 | ("1000", "1000th"), 264 | ("1001", "1001st"), 265 | ) 266 | 267 | UNDERSCORES_TO_DASHES: TestParameters = ( 268 | ("street", "street"), 269 | ("street_address", "street-address"), 270 | ("person_street_address", "person-street-address"), 271 | ) 272 | 273 | STRING_TO_TABLEIZE: TestParameters = ( 274 | ("person", "people"), 275 | ("Country", "countries"), 276 | ("ChildToy", "child_toys"), 277 | ("_RecipeIngredient", "_recipe_ingredients"), 278 | ) 279 | 280 | 281 | def test_pluralize_plurals() -> None: 282 | assert "plurals" == inflection.pluralize("plurals") 283 | assert "Plurals" == inflection.pluralize("Plurals") 284 | 285 | 286 | def test_pluralize_empty_string() -> None: 287 | assert "" == inflection.pluralize("") 288 | 289 | 290 | @pytest.mark.parametrize( 291 | ("word", ), 292 | [(word,) for word in inflection.UNCOUNTABLES] 293 | ) 294 | def test_uncountability(word: str) -> None: 295 | assert word == inflection.singularize(word) 296 | assert word == inflection.pluralize(word) 297 | assert inflection.pluralize(word) == inflection.singularize(word) 298 | 299 | 300 | def test_uncountable_word_is_not_greedy() -> None: 301 | uncountable_word = "ors" 302 | countable_word = "sponsor" 303 | 304 | inflection.UNCOUNTABLES.add(uncountable_word) 305 | try: 306 | assert uncountable_word == inflection.singularize(uncountable_word) 307 | assert uncountable_word == inflection.pluralize(uncountable_word) 308 | assert( 309 | inflection.pluralize(uncountable_word) == 310 | inflection.singularize(uncountable_word) 311 | ) 312 | 313 | assert "sponsor" == inflection.singularize(countable_word) 314 | assert "sponsors" == inflection.pluralize(countable_word) 315 | assert ( 316 | "sponsor" == 317 | inflection.singularize(inflection.pluralize(countable_word)) 318 | ) 319 | finally: 320 | inflection.UNCOUNTABLES.remove(uncountable_word) 321 | 322 | 323 | @pytest.mark.parametrize(("singular", "plural"), SINGULAR_TO_PLURAL) 324 | def test_pluralize_singular(singular: str, plural: str) -> None: 325 | assert plural == inflection.pluralize(singular) 326 | assert plural.capitalize() == inflection.pluralize(singular.capitalize()) 327 | 328 | 329 | @pytest.mark.parametrize(("singular", "plural"), SINGULAR_TO_PLURAL) 330 | def test_singularize_plural(singular: str, plural: str) -> None: 331 | assert singular == inflection.singularize(plural) 332 | assert singular.capitalize() == inflection.singularize(plural.capitalize()) 333 | 334 | 335 | @pytest.mark.parametrize(("singular", "plural"), SINGULAR_TO_PLURAL) 336 | def test_pluralize_plural(singular: str, plural: str) -> None: 337 | assert plural == inflection.pluralize(plural) 338 | assert plural.capitalize() == inflection.pluralize(plural.capitalize()) 339 | 340 | 341 | @pytest.mark.parametrize(("before", "titleized"), MIXTURE_TO_TITLEIZED) 342 | def test_titleize(before: str, titleized: str) -> None: 343 | assert titleized == inflection.titleize(before) 344 | 345 | 346 | @pytest.mark.parametrize(("camel", "underscore"), CAMEL_TO_UNDERSCORE) 347 | def test_camelize(camel: str, underscore: str) -> None: 348 | assert camel == inflection.camelize(underscore) 349 | 350 | 351 | def test_camelize_with_lower_downcases_the_first_letter() -> None: 352 | assert 'capital' == inflection.camelize('Capital', False) 353 | 354 | 355 | def test_camelize_with_underscores() -> None: 356 | assert "CamelCase" == inflection.camelize('Camel_Case') 357 | 358 | 359 | @pytest.mark.parametrize( 360 | ("camel", "underscore"), 361 | CAMEL_TO_UNDERSCORE + CAMEL_TO_UNDERSCORE_WITHOUT_REVERSE 362 | ) 363 | def test_underscore(camel: str, underscore: str) -> None: 364 | assert underscore == inflection.underscore(camel) 365 | 366 | 367 | @pytest.mark.parametrize( 368 | ("some_string", "parameterized_string"), 369 | STRING_TO_PARAMETERIZED 370 | ) 371 | def test_parameterize(some_string: str, parameterized_string: str) -> None: 372 | assert parameterized_string == inflection.parameterize(some_string) 373 | 374 | 375 | @pytest.mark.parametrize( 376 | ("some_string", "parameterized_string"), 377 | STRING_TO_PARAMETERIZED_AND_NORMALIZED 378 | ) 379 | def test_parameterize_and_normalize(some_string: str, parameterized_string: str) -> None: 380 | assert parameterized_string == inflection.parameterize(some_string) 381 | 382 | 383 | @pytest.mark.parametrize( 384 | ("some_string", "parameterized_string"), 385 | STRING_TO_PARAMETERIZE_WITH_UNDERSCORE 386 | ) 387 | def test_parameterize_with_custom_separator(some_string: str, parameterized_string: str) -> None: 388 | assert parameterized_string == inflection.parameterize(some_string, '_') 389 | 390 | 391 | @pytest.mark.parametrize( 392 | ("some_string", "parameterized_string"), 393 | STRING_TO_PARAMETERIZED 394 | ) 395 | def test_parameterize_with_multi_character_separator( 396 | some_string: str, 397 | parameterized_string: str 398 | ) -> None: 399 | assert ( 400 | parameterized_string.replace('-', '__sep__') == 401 | inflection.parameterize(some_string, '__sep__') 402 | ) 403 | 404 | 405 | @pytest.mark.parametrize( 406 | ("some_string", "parameterized_string"), 407 | STRING_TO_PARAMETERIZE_WITH_NO_SEPARATOR 408 | ) 409 | def test_parameterize_with_no_separator(some_string: str, parameterized_string: str) -> None: 410 | assert parameterized_string == inflection.parameterize(some_string, '') 411 | 412 | 413 | @pytest.mark.parametrize(("underscore", "human"), UNDERSCORE_TO_HUMAN) 414 | def test_humanize(underscore: str, human: str) -> None: 415 | assert human == inflection.humanize(underscore) 416 | 417 | 418 | @pytest.mark.parametrize(("number", "ordinalized"), ORDINAL_NUMBERS) 419 | def test_ordinal(number: str, ordinalized: str) -> None: 420 | assert ordinalized == number + inflection.ordinal(int(number)) 421 | 422 | 423 | @pytest.mark.parametrize(("number", "ordinalized"), ORDINAL_NUMBERS) 424 | def test_ordinalize(number: str, ordinalized: str) -> None: 425 | assert ordinalized == inflection.ordinalize(int(number)) 426 | 427 | 428 | @pytest.mark.parametrize(("input", "expected"), UNDERSCORES_TO_DASHES) 429 | def test_dasherize(input: str, expected: str) -> None: 430 | assert inflection.dasherize(input) == expected 431 | 432 | 433 | @pytest.mark.parametrize(("string", "tableized"), STRING_TO_TABLEIZE) 434 | def test_tableize(string: str, tableized: str) -> None: 435 | assert inflection.tableize(string) == tableized 436 | --------------------------------------------------------------------------------