Full Code of jazzband/inflect for AI

main 262a247d2d99 cached
39 files
255.2 KB
72.9k tokens
170 symbols
1 requests
Download .txt
Showing preview only (268K chars total). Download the full file or copy to clipboard to get everything.
Repository: jazzband/inflect
Branch: main
Commit: 262a247d2d99
Files: 39
Total size: 255.2 KB

Directory structure:
gitextract_jpl9oo1o/

├── .coveragerc
├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── NEWS.rst
├── README.rst
├── SECURITY.md
├── docs/
│   ├── conf.py
│   ├── history.rst
│   └── index.rst
├── inflect/
│   ├── __init__.py
│   ├── compat/
│   │   ├── __init__.py
│   │   └── py38.py
│   └── py.typed
├── mypy.ini
├── pyproject.toml
├── pytest.ini
├── ruff.toml
├── tea.yaml
├── tests/
│   ├── inflections.txt
│   ├── test_an.py
│   ├── test_classical_all.py
│   ├── test_classical_ancient.py
│   ├── test_classical_herd.py
│   ├── test_classical_names.py
│   ├── test_classical_person.py
│   ├── test_classical_zero.py
│   ├── test_compounds.py
│   ├── test_inflections.py
│   ├── test_join.py
│   ├── test_numwords.py
│   ├── test_pl_si.py
│   ├── test_pwd.py
│   └── test_unicode.py
├── towncrier.toml
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
omit =
	# leading `*/` for pytest-dev/pytest-cov#456
	*/.tox/*
disable_warnings =
	couldnt-parse

[report]
show_missing = True
exclude_also =
	# Exclude common false positives per
	# https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion
	# Ref jaraco/skeleton#97 and jaraco/skeleton#135
	class .*\bProtocol\):
	if TYPE_CHECKING:


================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = tab
indent_size = 4
insert_final_newline = true
end_of_line = lf

[*.py]
indent_style = space
max_line_length = 88

[*.{yml,yaml}]
indent_style = space
indent_size = 2

[*.rst]
indent_style = space


================================================
FILE: .github/FUNDING.yml
================================================
tidelift: pypi/inflect


================================================
FILE: .github/workflows/main.yml
================================================
name: tests

on:
  merge_group:
  push:
    branches-ignore:
    # temporary GH branches relating to merge queues (jaraco/skeleton#93)
    - gh-readonly-queue/**
    tags:
    # required if branches-ignore is supplied (jaraco/skeleton#103)
    - '**'
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

env:
  # Environment variable to support color support (jaraco/skeleton#66)
  FORCE_COLOR: 1

  # Suppress noisy pip warnings
  PIP_DISABLE_PIP_VERSION_CHECK: 'true'
  PIP_NO_WARN_SCRIPT_LOCATION: 'true'

  # Ensure tests can sense settings about the environment
  TOX_OVERRIDE: >-
    testenv.pass_env+=GITHUB_*,FORCE_COLOR


jobs:
  test:
    strategy:
      # https://blog.jaraco.com/efficient-use-of-ci-resources/
      matrix:
        python:
        - "3.10"
        - "3.13"
        platform:
        - ubuntu-latest
        - macos-latest
        - windows-latest
        include:
        - python: "3.11"
          platform: ubuntu-latest
        - python: "3.12"
          platform: ubuntu-latest
        - python: "3.14"
          platform: ubuntu-latest
        - python: "3.15"
          platform: ubuntu-latest
        - python: pypy3.10
          platform: ubuntu-latest
        - python: "3.x"
          platform: ubuntu-latest
    runs-on: ${{ matrix.platform }}
    continue-on-error: ${{ matrix.python == '3.15' }}
    steps:
      - uses: actions/checkout@v4
      - name: Install build dependencies
        # Install dependencies for building packages on pre-release Pythons
        # jaraco/skeleton#161
        if: matrix.python == '3.15' && matrix.platform == 'ubuntu-latest'
        run: |
          sudo apt update
          sudo apt install -y libxml2-dev libxslt-dev
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
          allow-prereleases: true
      - name: Install tox
        run: python -m pip install tox
      - name: Run
        run: tox

  collateral:
    strategy:
      fail-fast: false
      matrix:
        job:
        - diffcov
        - docs
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: 3.x
      - name: Install tox
        run: python -m pip install tox
      - name: Eval ${{ matrix.job }}
        run: tox -e ${{ matrix.job }}

  check:  # This job does nothing and is only used for the branch protection
    if: always()

    needs:
    - test
    - collateral

    runs-on: ubuntu-latest

    steps:
    - name: Decide whether the needed jobs succeeded or failed
      uses: re-actors/alls-green@release/v1
      with:
        jobs: ${{ toJSON(needs) }}

  release:
    permissions:
      contents: write
    needs:
    - check
    if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: 3.x
      - name: Install tox
        run: python -m pip install tox
      - name: Run
        run: tox -e release
        env:
          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
*.pyc
MANIFEST
dist/*
cover/*
htmlcov/*
.tox/*
*.egg-info
.coverage


================================================
FILE: .pre-commit-config.yaml
================================================
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
  rev: v0.12.0
  hooks:
  - id: ruff-check
    args: [--fix, --unsafe-fixes]
  - id: ruff-format


================================================
FILE: .readthedocs.yaml
================================================
version: 2
python:
  install:
  - path: .
    extra_requirements:
      - doc

sphinx:
  configuration: docs/conf.py

# required boilerplate readthedocs/readthedocs.org#10401
build:
  os: ubuntu-lts-latest
  tools:
    python: latest
  # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114
  jobs:
    post_checkout:
    - git fetch --unshallow || true


================================================
FILE: NEWS.rst
================================================
v7.5.0
======

Features
--------

- Updated `ast` classes for Python 3.14 compatibility. (#225)


v7.4.0
======

Features
--------

- Handle a single apostrophe more gracefully. (#218)


v7.3.1
======

Bugfixes
--------

- Set minimum version of more-itertools to 8.5 (#215)


v7.3.0
======

Features
--------

- Restricted typing_extensions to Python 3.8. (#211)


v7.2.1
======

Bugfixes
--------

- Refactored number_to_words toward reduced complexity.


v7.2.0
======

Features
--------

- Replace pydantic with typeguard (#195)


v7.1.0
======

Features
--------

- Now handle 'pair of x' in pl_sb_uninflected_complete (#188)


v7.0.0
======

Features
--------

- Refine type hint for ``singular_noun`` to indicate a literal return type for ``False``. (#186)


Deprecations and Removals
-------------------------

- Removed methods renamed in 0.2.0.


v6.2.0
======

Features
--------

- Project now supports Pydantic 2 while retaining support for Pydantic 1. (#187)


Bugfixes
--------

- Added validation of user-defined words and amended the type declarations to match, allowing for null values but not empty strings. (#187)


v6.1.1
======

Bugfixes
--------

- ``ordinal`` now handles float types correctly without first coercing them to strings. (#178)


v6.1.0
======

Features
--------

- Require Python 3.8 or later.


v6.0.5
======

* #187: Pin to Pydantic 1 to avoid breaking in Pydantic 2.

v6.0.4
======

* Internal cleanup.

v6.0.3
======

* #136: A/an support now more correctly honors leading
  capitalized words and abbreviations.

* #178: Improve support for ordinals for floats.

v6.0.2
======

* #169: Require pydantic 1.9.1 to avoid ``ValueError``.

v6.0.1
======

* Minor tweaks and packaging refresh.

v6.0.0
======

* #157: ``compare`` methods now validate their inputs
  and will raise a more meaningful exception if an
  empty string or None is passed. This expectation is now
  documented.

* Many public methods now perform validation on arguments.
  An empty string is no longer allowed for words or text.
  Callers are expected to pass non-empty text or trap
  the validation errors that are raised. The exceptions
  raised are ``pydantic.error_wrappers.ValidationError``,
  which are currently a subclass of ``ValueError``, but since
  that
  `may change <https://pydantic-docs.helpmanual.io/usage/validation_decorator/#validation-exception>`_,
  tests check for a generic ``Exception``.

v5.6.2
======

* #15: Fixes to plural edge case handling.

v5.6.1
======

* Packaging refresh and docs update.

v5.6.0
======

* #153: Internal refactor to simplify and unify
  ``_plnoun`` and ``_sinoun``.

v5.5.2
======

* Fixed badges.

v5.5.1
======

* #150: Rewrite to satisfy type checkers.

v5.5.0
======

* #147: Enhanced type annotations.

v5.4.0
======

* #133: Add a ``py.typed`` file so mypy recognizes type annotations.
* Misc fixes in #128, #134, #135, #137, #138, #139, #140, #142,
  #143, #144.
* Require Python 3.7 or later.

v5.3.0
======

* #108: Add support for pluralizing open compound nouns.

v5.2.0
======

* #121: Modernized the codebase. Added a lot of type annotations.

v5.1.0
======

* #113: Add support for uncountable nouns.

v5.0.3
======

* Refreshed package metadata.

v5.0.2
======

* #102: Inflect withdraws from `Jazzband <https://jazzband.co>`_
  in order to continue to participate in sustained maintenance
  and enterprise support through `Tidelift <https://tidelift.com>`_.
  The project continues to honor the guidelines and principles
  behind Jazzband and welcomes contributors openly.

v5.0.1
======

* Identical release validating release process.

v5.0.0
======

* Module no longer exposes a ``__version__`` attribute. Instead
  to query the version installed, use
  `importlib.metadata <https://docs.python.org/3/library/importlib.metadata.html>`_
  or `its backport <https://pypi.org/project/importlib_metadata>`_
  to query::

    importlib.metadata.version('inflect')

v4.1.1
======

* Refreshed package metadata.

v4.1.0
======

* #95: Certain operations now allow ignore arbitrary leading words.

v4.0.0
======

* Require Python 3.6 or later.

v3.0.2
======

* #88: Distribution no longer includes root ``tests`` package.

v3.0.1
======

* Project now builds on jaraco/skeleton for shared package
  management.

v3.0.0
======

* #75: Drop support for Python 3.4.

v2.1.0
======

* #29: Relicensed under the more permissive MIT License.

v2.0.1
======

* #57: Fix pluralization of taco.

v2.0.0
======

* #37: fix inconsistencies with the inflect method

  We now build and parse AST to extract function arguments instead of relying
  on regular expressions. This also adds support for keyword arguments and
  built-in constants when calling functions in the string.
  Unfortunately, this is not backwards compatible in some cases:
* Strings should now be wrapped in single or double quotes
  p.inflect("singular_noun(to them)") should now be p.inflect("singular_noun('to them')")
* Empty second argument to a function will now be parsed as None instead of ''.
  p.inflect("num(%d,) eggs" % 2) now prints "2 eggs" instead of " eggs"
  Since None, True and False are now supported, they can be passed explicitly:
  p.inflect("num(%d, False) eggs" % 2) will print " eggs"
  p.inflect("num(%d, True) eggs" % 2) will print "2 eggs"

v1.0.2
======

* #53: Improved unicode handling.
* #5 and #40 via #55: Fix capitalization issues in processes where
  more than one word is involved.
* #56: Handle correctly units containing 'degree' and 'per'.

v1.0.1
======

* #31: fix extraneous close parentheses.

v1.0.0
======

* Dropped support for Python 3.3.

v0.3.1
======

* Fixed badges in readme.

v0.3.0
======

* Moved hosting to the `jazzband project on GitHub <https://github.com/jazzband/inflect>`_.

v0.2.5
======

* Fixed TypeError while parsing compounds (by yavarhusain)
* Fixed encoding issue in setup.py on Python 3


v0.2.4
======

* new maintainer (Alex Grönholm)
* added Python 3 compatibility (by Thorben Krüger)


v0.2.3
======

* fix a/an for dishonor, Honolulu, mpeg, onetime, Ugandan, Ukrainian,
  Unabomber, unanimous, US
* merge in 'subspecies' fix by UltraNurd
* add arboretum to classical plurals
* prevent crash with singular_noun('ys')


v0.2.2
======

* change numwords to number_to_words in strings
* improve some docstrings
* comment out imports for unused .inflectrc
* remove unused exception class


v0.2.1
======

* remove incorrect gnome_sudoku import


v0.2.0
======

* add gender() to select the gender of singular pronouns
* replace short named methods with longer methods. shorted method now print a message and raise DecrecationWarning

  pl -> plural

  plnoun -> plural_noun

  plverb -> plural_verb

  pladj -> plural_adjective

  sinoun -> singular_noun

  prespart -> present_participle

  numwords -> number_to_words

  plequal -> compare

  plnounequal -> compare_nouns

  plverbequal -> compare_verbs

  pladjequal -> compare_adjs

  wordlist -> join
* change classical() to only accept keyword args: only one way to do it
* fix bug in numwords where hundreds was giving the wrong number when group=3


v0.1.8
======

* add line to setup showing that this provides 'inflect' so that
  inflect_dj can require it
* add the rest of the tests from the Perl version


v0.1.7
======

* replace most of the regular expressions in _plnoun and _sinoun. They run several times faster now.


v0.1.6
======

* add method sinoun() to generate the singular of a plural noun. Phew!
* add changes from new Perl version: 1.892
* start adding tests from Perl version
* add test to check sinoun(plnoun(word)) == word
  Can now use word lists to check these methods without needing to have
  a list of plurals. ;-)
* fix die -> dice


================================================
FILE: README.rst
================================================
.. image:: https://img.shields.io/pypi/v/inflect.svg
   :target: https://pypi.org/project/inflect

.. image:: https://img.shields.io/pypi/pyversions/inflect.svg

.. image:: https://github.com/jaraco/inflect/actions/workflows/main.yml/badge.svg
   :target: https://github.com/jaraco/inflect/actions?query=workflow%3A%22tests%22
   :alt: tests

.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
    :target: https://github.com/astral-sh/ruff
    :alt: Ruff

.. image:: https://readthedocs.org/projects/inflect/badge/?version=latest
   :target: https://inflect.readthedocs.io/en/latest/?badge=latest

.. image:: https://img.shields.io/badge/skeleton-2026-informational
   :target: https://blog.jaraco.com/skeleton

.. image:: https://tidelift.com/badges/package/pypi/inflect
   :target: https://tidelift.com/subscription/pkg/pypi-inflect?utm_source=pypi-inflect&utm_medium=readme

NAME
====

inflect.py - Accurately generate plurals, singular nouns, ordinals, indefinite articles, and word-based representations of numbers. This functionality is limited to English.

SYNOPSIS
========

.. code-block:: python
    
    >>> import inflect
    >>> p = inflect.engine()

Simple example with pluralization and word-representation of numbers:

.. code-block:: python
    
    >>> count=1
    >>> print('There', p.plural_verb('was', count), p.number_to_words(count), p.plural_noun('person', count), 'by the door.')
    There was one person by the door.

When ``count=243``, the same code will generate:

.. code-block:: python
    
    There were two hundred and forty-three people by the door.


Methods
=======

- ``plural``, ``plural_noun``, ``plural_verb``, ``plural_adj``, ``singular_noun``, ``no``, ``num``
- ``compare``, ``compare_nouns``, ``compare_nouns``, ``compare_adjs``
- ``a``, ``an``
- ``present_participle``
- ``ordinal``, ``number_to_words``
- ``join``
- ``inflect``, ``classical``, ``gender``
- ``defnoun``, ``defverb``, ``defadj``, ``defa``, ``defan``

Plurality/Singularity
---------------------
Unconditionally Form the Plural
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> "the plural of person is " + p.plural("person")
    'the plural of person is people'

Conditionally Form the Plural
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> "the plural of 1 person is " + p.plural("person", 1)
    'the plural of 1 person is person'

Form Plurals for Specific Parts of Speech
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> p.plural_noun("I", 2)
    'we'
    >>> p.plural_verb("saw", 1)
    'saw'
    >>> p.plural_adj("my", 2)
    'our'
    >>> p.plural_noun("saw", 2)
    'saws'

Form the Singular of Plural Nouns
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> "The singular of people is " + p.singular_noun("people")
    'The singular of people is person'

Select the Gender of Singular Pronouns
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> p.singular_noun("they")
    'it'
    >>> p.gender("feminine")
    >>> p.singular_noun("they")
    'she'

Deal with "0/1/N" -> "no/1/N" Translation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> errors = 1
    >>> "There ", p.plural_verb("was", errors), p.no(" error", errors)
    ('There ', 'was', ' 1 error')
    >>> errors = 2
    >>> "There ", p.plural_verb("was", errors), p.no(" error", errors)
    ('There ', 'were', ' 2 errors')

Use Default Counts
^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> p.num(1, "")
    ''
    >>> p.plural("I")
    'I'
    >>> p.plural_verb(" saw")
    ' saw'
    >>> p.num(2)
    '2'
    >>> p.plural_noun(" saw")
    ' saws'
    >>> "There ", p.num(errors, ""), p.plural_verb("was"), p.no(" error")
    ('There ', '', 'were', ' 2 errors')

Compare Two Words Number-Intensitively
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> p.compare('person', 'person')
    'eq'
    >>> p.compare('person', 'people')
    's:p'
    >>> p.compare_nouns('person', 'people')
    's:p'
    >>> p.compare_verbs('run', 'ran')
    False
    >>> p.compare_verbs('run', 'running')
    False
    >>> p.compare_verbs('run', 'run')
    'eq'
    >>> p.compare_adjs('my', 'mine')
    False
    >>> p.compare_adjs('my', 'our')
    's:p'

Add Correct *a* or *an* for a Given Word
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> "Did you want ", p.a('thing'), " or ", p.a('idea')
    ('Did you want ', 'a thing', ' or ', 'an idea')

Convert Numerals into Ordinals
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> "It was", p.ordinal(1), " from the left"
    ('It was', '1st', ' from the left')
    >>> "It was", p.ordinal(2), " from the left"
    ('It was', '2nd', ' from the left')
    >>> "It was", p.ordinal(3), " from the left"
    ('It was', '3rd', ' from the left')
    >>> "It was", p.ordinal(347), " from the left"
    ('It was', '347th', ' from the left')

Convert Numerals to Words
^^^^^^^^^^^^^^^^^^^^^^^^^
Note: This returns a single string.

.. code-block:: python
    
    >>> p.number_to_words(1)
    'one'
    >>> p.number_to_words(38)
    'thirty-eight'
    >>> p.number_to_words(1234)
    'one thousand, two hundred and thirty-four'
    >>> p.number_to_words(p.ordinal(1234))
    'one thousand, two hundred and thirty-fourth'

Retrieve Words as List of Parts
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> p.number_to_words(1234, wantlist=True)
    ['one thousand', 'two hundred and thirty-four']

Grouping Options
^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> p.number_to_words(12345, group=1)
    'one, two, three, four, five'
    >>> p.number_to_words(12345, group=2)
    'twelve, thirty-four, five'
    >>> p.number_to_words(12345, group=3)
    'one twenty-three, forty-five'
    >>> p.number_to_words(1234, andword="")
    'one thousand, two hundred thirty-four'
    >>> p.number_to_words(1234, andword=", plus")
    'one thousand, two hundred, plus thirty-four'
    >>> p.number_to_words(555_1202, group=1, zero="oh")
    'five, five, five, one, two, oh, two'
    >>> p.number_to_words(555_1202, group=1, one="unity")
    'five, five, five, unity, two, zero, two'
    >>> p.number_to_words(123.456, group=1, decimal="mark")
    'one, two, three, mark, four, five, six'

Apply Threshold for Word-Representation of Numbers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Above provided threshold, numberals will remain numerals

.. code-block:: python
    
    >>> p.number_to_words(9, threshold=10)
    'nine'
    >>> p.number_to_words(10, threshold=10)
    'ten'
    >>> p.number_to_words(11, threshold=10)
    '11'
    >>> p.number_to_words(1000, threshold=10)
    '1,000'

Join Words into a List
^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
    
    >>> p.join(("apple", "banana", "carrot"))
    'apple, banana, and carrot'
    >>> p.join(("apple", "banana"))
    'apple and banana'
    >>> p.join(("apple", "banana", "carrot"), final_sep="")
    'apple, banana and carrot'
    >>> p.join(('apples', 'bananas', 'carrots'), conj='and even')
    'apples, bananas, and even carrots'
    >>> p.join(('apple', 'banana', 'carrot'), sep='/', sep_spaced=False, conj='', conj_spaced=False)
    'apple/banana/carrot'
    
Require Classical Plurals
^^^^^^^^^^^^^^^^^^^^^^^^^
Adhere to conventions from Classical Latin and Classical Greek

.. code-block:: python
        
    >>> p.classical()
    >>> p.plural_noun("focus", 2)
    'foci'
    >>> p.plural_noun("cherubim", 2)
    'cherubims'
    >>> p.plural_noun("cherub", 2)
    'cherubim'

Other options for classical plurals:

.. code-block:: python
    
    p.classical(all=True)  # USE ALL CLASSICAL PLURALS
    p.classical(all=False)  # SWITCH OFF CLASSICAL MODE
    
    p.classical(zero=True)  #  "no error" INSTEAD OF "no errors"
    p.classical(zero=False)  #  "no errors" INSTEAD OF "no error"
    
    p.classical(herd=True)  #  "2 buffalo" INSTEAD OF "2 buffalos"
    p.classical(herd=False)  #  "2 buffalos" INSTEAD OF "2 buffalo"
    
    p.classical(persons=True)  # "2 chairpersons" INSTEAD OF "2 chairpeople"
    p.classical(persons=False)  # "2 chairpeople" INSTEAD OF "2 chairpersons"
    
    p.classical(ancient=True)  # "2 formulae" INSTEAD OF "2 formulas"
    p.classical(ancient=False)  # "2 formulas" INSTEAD OF "2 formulae"


Support for interpolation
^^^^^^^^^^^^^^^^^^^^^^^^^
Supports string interpolation with the following functions: ``plural()``, ``plural_noun()``, ``plural_verb()``, ``plural_adj()``, ``singular_noun()``, ``a()``, ``an()``, ``num()`` and ``ordinal()``.

.. code-block:: python
    
    >>> p.inflect("The plural of {0} is plural('{0}')".format('car'))
    'The plural of car is cars'
    >>> p.inflect("The singular of {0} is singular_noun('{0}')".format('car'))
    'The singular of car is car'
    >>> p.inflect("I saw {0} plural('cat',{0})".format(3))
    'I saw 3 cats'
    >>> p.inflect(
    ...     "plural('I',{0}) "
    ...     "plural_verb('saw',{0}) "
    ...     "plural('a',{1}) "
    ...     "plural_noun('saw',{1})".format(1, 2)
    ... )
    'I saw some saws'
    >>> p.inflect(
    ...     "num({0}, False)plural('I') "
    ...     "plural_verb('saw') "
    ...     "num({1}, False)plural('a') "
    ...     "plural_noun('saw')".format(N1, 1)
    ... )
    'I saw a saw'
    >>> p.inflect(
    ...     "num({0}, False)plural('I') "
    ...     "plural_verb('saw') "
    ...     "num({1}, False)plural('a') "
    ...     "plural_noun('saw')".format(2, 2)
    ... )
    'we saw some saws'
    >>> p.inflect("I saw num({0}) plural('cat')\nnum()".format(cat_count))
    'I saw 3 cats\n'
    >>> p.inflect("There plural_verb('was',{0}) no('error',{0})".format(errors))
    'There were 2 errors'
    >>> p.inflect("There num({0}, False)plural_verb('was') no('error')".format(errors))
    'There were 2 errors'
    >>> p.inflect("Did you want a('{0}') or an('{1}')".format(thing, idea))
    'Did you want a thing or an idea'
    >>> p.inflect("It was ordinal('{0}') from the left".format(2))
    'It was 2nd from the left'

Add User-Defined Inflections
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Allows for overriding default rules.

Override noun defaults:

.. code-block:: python
        
    p.defnoun("VAX", "VAXen")  # SINGULAR => PLURAL

Override Verb defaults:

.. code-block:: python
    
    p.defverb(
        "will",  # 1ST PERSON SINGULAR
        "shall",  # 1ST PERSON PLURAL
        "will",  # 2ND PERSON SINGULAR
        "will",  # 2ND PERSON PLURAL
        "will",  # 3RD PERSON SINGULAR
        "will",  # 3RD PERSON PLURAL
    )

Override adjective defaults:

.. code-block:: python
    
    >>> p.defadj('hir', 'their')
    1
    >>> p.plural_adj('hir', 2)
    'their'

Override the words that use the indefinite articles "a" or "an":

.. code-block:: python
    
    >>> p.a('ape', 1)
    'an ape'
    >>> p.defa('a')
    1
    >>> p.a('ape', 1)
    'an ape'
    >>> p.defa('ape')
    1
    >>> p.a('ape', 1)
    'a ape'
    >>> p.defan('horrendous.*')
    1
    >>> p.a('horrendous affectation', 1)
    'an horrendous affectation'
    >>> 


DESCRIPTION
===========

The methods of the class ``engine`` in module ``inflect.py`` provide plural
inflections, singular noun inflections, "a"/"an" selection for English words,
and manipulation of numbers as words.

Plural forms of all nouns, most verbs, and some adjectives are
provided. Where appropriate, "classical" variants (for example: "brother" ->
"brethren", "dogma" -> "dogmata", etc.) are also provided.

Single forms of nouns are also provided. The gender of singular pronouns
can be chosen (for example "they" -> "it" or "she" or "he" or "they").

Pronunciation-based "a"/"an" selection is provided for all English
words, and most initialisms.

It is also possible to inflect numerals (1,2,3) to ordinals (1st, 2nd, 3rd)
or to English words ("one", "two", "three").

In generating these inflections, ``inflect.py`` follows the Oxford
English Dictionary and the guidelines in Fowler's Modern English
Usage, preferring the former where the two disagree.

The module is built around standard British spelling, but is designed
to cope with common American variants as well. Slang, jargon, and
other English dialects are *not* explicitly catered for.

Where two or more inflected forms exist for a single word (typically a
"classical" form and a "modern" form), ``inflect.py`` prefers the
more common form (typically the "modern" one), unless "classical"
processing has been specified
(see `MODERN VS CLASSICAL INFLECTIONS`).

FORMING PLURALS AND SINGULARS
=============================

Inflecting Plurals and Singulars
--------------------------------

All of the ``plural...`` plural inflection methods take the word to be
inflected as their first argument and return the corresponding inflection.
Note that all such methods expect the *singular* form of the word. The
results of passing a plural form are undefined (and unlikely to be correct).
Similarly, the ``si...`` singular inflection method expects the *plural*
form of the word.

The ``plural...`` methods also take an optional second argument,
which indicates the grammatical "number" of the word (or of another word
with which the word being inflected must agree). If the "number" argument is
supplied and is not ``1`` (or ``"one"`` or ``"a"``, or some other adjective that
implies the singular), the plural form of the word is returned. If the
"number" argument *does* indicate singularity, the (uninflected) word
itself is returned. If the number argument is omitted, the plural form
is returned unconditionally.

The ``si...`` method takes a second argument in a similar fashion. If it is
some form of the number ``1``, or is omitted, the singular form is returned.
Otherwise the plural is returned unaltered.


The various methods of ``inflect.engine`` are:



``plural_noun(word, count=None)``

 The method ``plural_noun()`` takes a *singular* English noun or
 pronoun and returns its plural. Pronouns in the nominative ("I" ->
 "we") and accusative ("me" -> "us") cases are handled, as are
 possessive pronouns ("mine" -> "ours").


``plural_verb(word, count=None)``

 The method ``plural_verb()`` takes the *singular* form of a
 conjugated verb (that is, one which is already in the correct "person"
 and "mood") and returns the corresponding plural conjugation.


``plural_adj(word, count=None)``

 The method ``plural_adj()`` takes the *singular* form of
 certain types of adjectives and returns the corresponding plural form.
 Adjectives that are correctly handled include: "numerical" adjectives
 ("a" -> "some"), demonstrative adjectives ("this" -> "these", "that" ->
 "those"), and possessives ("my" -> "our", "cat's" -> "cats'", "child's"
 -> "childrens'", etc.)


``plural(word, count=None)``

 The method ``plural()`` takes a *singular* English noun,
 pronoun, verb, or adjective and returns its plural form. Where a word
 has more than one inflection depending on its part of speech (for
 example, the noun "thought" inflects to "thoughts", the verb "thought"
 to "thought"), the (singular) noun sense is preferred to the (singular)
 verb sense.

 Hence ``plural("knife")`` will return "knives" ("knife" having been treated
 as a singular noun), whereas ``plural("knifes")`` will return "knife"
 ("knifes" having been treated as a 3rd person singular verb).

 The inherent ambiguity of such cases suggests that,
 where the part of speech is known, ``plural_noun``, ``plural_verb``, and
 ``plural_adj`` should be used in preference to ``plural``.


``singular_noun(word, count=None)``

 The method ``singular_noun()`` takes a *plural* English noun or
 pronoun and returns its singular. Pronouns in the nominative ("we" ->
 "I") and accusative ("us" -> "me") cases are handled, as are
 possessive pronouns ("ours" -> "mine"). When third person
 singular pronouns are returned they take the neuter gender by default
 ("they" -> "it"), not ("they"-> "she") nor ("they" -> "he"). This can be
 changed with ``gender()``.

Note that all these methods ignore any whitespace surrounding the
word being inflected, but preserve that whitespace when the result is
returned. For example, ``plural(" cat  ")`` returns " cats  ".


``gender(genderletter)``

 The third person plural pronoun takes the same form for the female, male and
 neuter (e.g. "they"). The singular however, depends upon gender (e.g. "she",
 "he", "it" and "they" -- "they" being the gender neutral form.) By default
 ``singular_noun`` returns the neuter form, however, the gender can be selected with
 the ``gender`` method. Pass the first letter of the gender to
 ``gender`` to return the f(eminine), m(asculine), n(euter) or t(hey)
 form of the singular. e.g.
 gender('f') followed by singular_noun('themselves') returns 'herself'.

Numbered plurals
----------------

The ``plural...`` methods return only the inflected word, not the count that
was used to inflect it. Thus, in order to produce "I saw 3 ducks", it
is necessary to use:

.. code-block:: python

    print("I saw", N, p.plural_noun(animal, N))

Since the usual purpose of producing a plural is to make it agree with
a preceding count, inflect.py provides a method
(``no(word, count)``) which, given a word and a(n optional) count, returns the
count followed by the correctly inflected word. Hence the previous
example can be rewritten:

.. code-block:: python

    print("I saw ", p.no(animal, N))

In addition, if the count is zero (or some other term which implies
zero, such as ``"zero"``, ``"nil"``, etc.) the count is replaced by the
word "no". Hence, if ``N`` had the value zero, the previous example
would print (the somewhat more elegant)::

    I saw no animals

rather than::

    I saw 0 animals

Note that the name of the method is a pun: the method
returns either a number (a *No.*) or a ``"no"``, in front of the
inflected word.


Reducing the number of counts required
--------------------------------------

In some contexts, the need to supply an explicit count to the various
``plural...`` methods makes for tiresome repetition. For example:

.. code-block:: python

    print(
        plural_adj("This", errors),
        plural_noun(" error", errors),
        plural_verb(" was", errors),
        " fatal.",
    )

inflect.py therefore provides a method
(``num(count=None, show=None)``) which may be used to set a persistent "default number"
value. If such a value is set, it is subsequently used whenever an
optional second "number" argument is omitted. The default value thus set
can subsequently be removed by calling ``num()`` with no arguments.
Hence we could rewrite the previous example:

.. code-block:: python

    p.num(errors)
    print(p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal.")
    p.num()

Normally, ``num()`` returns its first argument, so that it may also
be "inlined" in contexts like:

.. code-block:: python

    print(p.num(errors), p.plural_noun(" error"), p.plural_verb(" was"), " detected.")
    if severity > 1:
        print(
            p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal."
        )

However, in certain contexts (see `INTERPOLATING INFLECTIONS IN STRINGS`)
it is preferable that ``num()`` return an empty string. Hence ``num()``
provides an optional second argument. If that argument is supplied (that is, if
it is defined) and evaluates to false, ``num`` returns an empty string
instead of its first argument. For example:

.. code-block:: python

    print(p.num(errors, 0), p.no("error"), p.plural_verb(" was"), " detected.")
    if severity > 1:
        print(
            p.plural_adj("This"), p.plural_noun(" error"), p.plural_verb(" was"), "fatal."
        )



Number-insensitive equality
---------------------------

inflect.py also provides a solution to the problem
of comparing words of differing plurality through the methods
``compare(word1, word2)``, ``compare_nouns(word1, word2)``,
``compare_verbs(word1, word2)``, and ``compare_adjs(word1, word2)``.
Each  of these methods takes two strings, and  compares them
using the corresponding plural-inflection method (``plural()``, ``plural_noun()``,
``plural_verb()``, and ``plural_adj()`` respectively).

The comparison returns true if:

- the strings are equal, or
- one string is equal to a plural form of the other, or
- the strings are two different plural forms of the one word.


Hence all of the following return true:

.. code-block:: python

    p.compare("index", "index")  # RETURNS "eq"
    p.compare("index", "indexes")  # RETURNS "s:p"
    p.compare("index", "indices")  # RETURNS "s:p"
    p.compare("indexes", "index")  # RETURNS "p:s"
    p.compare("indices", "index")  # RETURNS "p:s"
    p.compare("indices", "indexes")  # RETURNS "p:p"
    p.compare("indexes", "indices")  # RETURNS "p:p"
    p.compare("indices", "indices")  # RETURNS "eq"

As indicated by the comments in the previous example, the actual value
returned by the various ``compare`` methods encodes which of the
three equality rules succeeded: "eq" is returned if the strings were
identical, "s:p" if the strings were singular and plural respectively,
"p:s" for plural and singular, and "p:p" for two distinct plurals.
Inequality is indicated by returning an empty string.

It should be noted that two distinct singular words which happen to take
the same plural form are *not* considered equal, nor are cases where
one (singular) word's plural is the other (plural) word's singular.
Hence all of the following return false:

.. code-block:: python

    p.compare("base", "basis")  # ALTHOUGH BOTH -> "bases"
    p.compare("syrinx", "syringe")  # ALTHOUGH BOTH -> "syringes"
    p.compare("she", "he")  # ALTHOUGH BOTH -> "they"

    p.compare("opus", "operas")  # ALTHOUGH "opus" -> "opera" -> "operas"
    p.compare("taxi", "taxes")  # ALTHOUGH "taxi" -> "taxis" -> "taxes"

Note too that, although the comparison is "number-insensitive" it is *not*
case-insensitive (that is, ``plural("time","Times")`` returns false. To obtain
both number and case insensitivity, use the ``lower()`` method on both strings
(that is, ``plural("time".lower(), "Times".lower())`` returns true).

Related Functionality
=====================

Shout out to these libraries that provide related functionality:

* `WordSet <https://jaracotext.readthedocs.io/en/latest/#jaraco.text.WordSet>`_
  parses identifiers like variable names into sets of words suitable for re-assembling
  in another form.

* `word2number <https://pypi.org/project/word2number/>`_ converts words to
  a number.


For Enterprise
==============

Available as part of the Tidelift Subscription.

This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use.

`Learn more <https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=referral&utm_campaign=github>`_.


================================================
FILE: SECURITY.md
================================================
# Security Contact

To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.


================================================
FILE: docs/conf.py
================================================
from __future__ import annotations

extensions = [
    'sphinx.ext.autodoc',
    'jaraco.packaging.sphinx',
]

master_doc = "index"
html_theme = "furo"

# Link dates and other references in the changelog
extensions += ['rst.linker']
link_files = {
    '../NEWS.rst': dict(
        using=dict(GH='https://github.com'),
        replace=[
            dict(
                pattern=r'(Issue #|\B#)(?P<issue>\d+)',
                url='{package_url}/issues/{issue}',
            ),
            dict(
                pattern=r'(?m:^((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n)',
                with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n',
            ),
            dict(
                pattern=r'PEP[- ](?P<pep_number>\d+)',
                url='https://peps.python.org/pep-{pep_number:0>4}/',
            ),
        ],
    )
}

# Be strict about any broken references
nitpicky = True
nitpick_ignore: list[tuple[str, str]] = []

# Include Python intersphinx mapping to prevent failures
# jaraco/skeleton#51
extensions += ['sphinx.ext.intersphinx']
intersphinx_mapping = {
    'python': ('https://docs.python.org/3', None),
}

# Preserve authored syntax for defaults
autodoc_preserve_defaults = True

# Add support for linking usernames, PyPI projects, Wikipedia pages
github_url = 'https://github.com/'
extlinks = {
    'user': (f'{github_url}%s', '@%s'),
    'pypi': ('https://pypi.org/project/%s', '%s'),
    'wiki': ('https://wikipedia.org/wiki/%s', '%s'),
}
extensions += ['sphinx.ext.extlinks']

# local

extensions += ['jaraco.tidelift']


================================================
FILE: docs/history.rst
================================================
:tocdepth: 2

.. _changes:

History
*******

.. include:: ../NEWS (links).rst


================================================
FILE: docs/index.rst
================================================
Welcome to |project| documentation!
===================================

.. sidebar-links::
   :home:
   :pypi:

.. toctree::
   :maxdepth: 1

   history

.. tidelift-referral-banner::

.. automodule:: inflect
    :members:
    :undoc-members:
    :show-inheritance:


Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`


================================================
FILE: inflect/__init__.py
================================================
"""
inflect: english language inflection
 - correctly generate plurals, ordinals, indefinite articles
 - convert numbers to words

Copyright (C) 2010 Paul Dyson

Based upon the Perl module
`Lingua::EN::Inflect <https://metacpan.org/pod/Lingua::EN::Inflect>`_.

methods:
    classical inflect
    plural plural_noun plural_verb plural_adj singular_noun no num a an
    compare compare_nouns compare_verbs compare_adjs
    present_participle
    ordinal
    number_to_words
    join
    defnoun defverb defadj defa defan

INFLECTIONS:
    classical inflect
    plural plural_noun plural_verb plural_adj singular_noun compare
    no num a an present_participle

PLURALS:
    classical inflect
    plural plural_noun plural_verb plural_adj singular_noun no num
    compare compare_nouns compare_verbs compare_adjs

COMPARISONS:
    classical
    compare compare_nouns compare_verbs compare_adjs

ARTICLES:
    classical inflect num a an

NUMERICAL:
    ordinal number_to_words

USER_DEFINED:
    defnoun defverb defadj defa defan

Exceptions:
 UnknownClassicalModeError
 BadNumValueError
 BadChunkingOptionError
 NumOutOfRangeError
 BadUserDefinedPatternError
 BadRcFileError
 BadGenderError

"""

from __future__ import annotations

import ast
import collections
import contextlib
import functools
import itertools
import re
from numbers import Number
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Dict,
    Iterable,
    List,
    Literal,
    Match,
    Optional,
    Sequence,
    Tuple,
    Union,
    cast,
)

from more_itertools import windowed_complete
from typeguard import typechecked

from .compat.py38 import Annotated


class UnknownClassicalModeError(Exception):
    pass


class BadNumValueError(Exception):
    pass


class BadChunkingOptionError(Exception):
    pass


class NumOutOfRangeError(Exception):
    pass


class BadUserDefinedPatternError(Exception):
    pass


class BadRcFileError(Exception):
    pass


class BadGenderError(Exception):
    pass


def enclose(s: str) -> str:
    return f"(?:{s})"


def joinstem(cutpoint: Optional[int] = 0, words: Optional[Iterable[str]] = None) -> str:
    """
    Join stem of each word in words into a string for regex.

    Each word is truncated at cutpoint.

    Cutpoint is usually negative indicating the number of letters to remove
    from the end of each word.

    >>> joinstem(-2, ["ephemeris", "iris", ".*itis"])
    '(?:ephemer|ir|.*it)'

    >>> joinstem(None, ["ephemeris"])
    '(?:ephemeris)'

    >>> joinstem(5, None)
    '(?:)'
    """
    return enclose("|".join(w[:cutpoint] for w in words or []))


def bysize(words: Iterable[str]) -> Dict[int, set]:
    """
    From a list of words, return a dict of sets sorted by word length.

    >>> words = ['ant', 'cat', 'dog', 'pig', 'frog', 'goat', 'horse', 'elephant']
    >>> ret = bysize(words)
    >>> sorted(ret[3])
    ['ant', 'cat', 'dog', 'pig']
    >>> ret[5]
    {'horse'}
    """
    res: Dict[int, set] = collections.defaultdict(set)
    for w in words:
        res[len(w)].add(w)
    return res


def make_pl_si_lists(
    lst: Iterable[str],
    plending: str,
    siendingsize: Optional[int],
    dojoinstem: bool = True,
):
    """
    given a list of singular words: lst

    an ending to append to make the plural: plending

    the number of characters to remove from the singular
    before appending plending: siendingsize

    a flag whether to create a joinstem: dojoinstem

    return:
    a list of pluralised words: si_list (called si because this is what you need to
    look for to make the singular)

    the pluralised words as a dict of sets sorted by word length: si_bysize
    the singular words as a dict of sets sorted by word length: pl_bysize
    if dojoinstem is True: a regular expression that matches any of the stems: stem
    """
    if siendingsize is not None:
        siendingsize = -siendingsize
    si_list = [w[:siendingsize] + plending for w in lst]
    pl_bysize = bysize(lst)
    si_bysize = bysize(si_list)
    if dojoinstem:
        stem = joinstem(siendingsize, lst)
        return si_list, si_bysize, pl_bysize, stem
    else:
        return si_list, si_bysize, pl_bysize


# 1. PLURALS

pl_sb_irregular_s = {
    "corpus": "corpuses|corpora",
    "opus": "opuses|opera",
    "genus": "genera",
    "mythos": "mythoi",
    "penis": "penises|penes",
    "testis": "testes",
    "atlas": "atlases|atlantes",
    "yes": "yeses",
}

pl_sb_irregular = {
    "child": "children",
    "chili": "chilis|chilies",
    "brother": "brothers|brethren",
    "infinity": "infinities|infinity",
    "loaf": "loaves",
    "lore": "lores|lore",
    "hoof": "hoofs|hooves",
    "beef": "beefs|beeves",
    "thief": "thiefs|thieves",
    "money": "monies",
    "mongoose": "mongooses",
    "ox": "oxen",
    "cow": "cows|kine",
    "graffito": "graffiti",
    "octopus": "octopuses|octopodes",
    "genie": "genies|genii",
    "ganglion": "ganglions|ganglia",
    "trilby": "trilbys",
    "turf": "turfs|turves",
    "numen": "numina",
    "atman": "atmas",
    "occiput": "occiputs|occipita",
    "sabretooth": "sabretooths",
    "sabertooth": "sabertooths",
    "lowlife": "lowlifes",
    "flatfoot": "flatfoots",
    "tenderfoot": "tenderfoots",
    "romany": "romanies",
    "jerry": "jerries",
    "mary": "maries",
    "talouse": "talouses",
    "rom": "roma",
    "carmen": "carmina",
}

pl_sb_irregular.update(pl_sb_irregular_s)
# pl_sb_irregular_keys = enclose('|'.join(pl_sb_irregular.keys()))

pl_sb_irregular_caps = {
    "Romany": "Romanies",
    "Jerry": "Jerrys",
    "Mary": "Marys",
    "Rom": "Roma",
}

pl_sb_irregular_compound = {"prima donna": "prima donnas|prime donne"}

si_sb_irregular = {v: k for (k, v) in pl_sb_irregular.items()}
for k in list(si_sb_irregular):
    if "|" in k:
        k1, k2 = k.split("|")
        si_sb_irregular[k1] = si_sb_irregular[k2] = si_sb_irregular[k]
        del si_sb_irregular[k]
si_sb_irregular_caps = {v: k for (k, v) in pl_sb_irregular_caps.items()}
si_sb_irregular_compound = {v: k for (k, v) in pl_sb_irregular_compound.items()}
for k in list(si_sb_irregular_compound):
    if "|" in k:
        k1, k2 = k.split("|")
        si_sb_irregular_compound[k1] = si_sb_irregular_compound[k2] = (
            si_sb_irregular_compound[k]
        )
        del si_sb_irregular_compound[k]

# si_sb_irregular_keys = enclose('|'.join(si_sb_irregular.keys()))

# Z's that don't double

pl_sb_z_zes_list = ("quartz", "topaz")
pl_sb_z_zes_bysize = bysize(pl_sb_z_zes_list)

pl_sb_ze_zes_list = ("snooze",)
pl_sb_ze_zes_bysize = bysize(pl_sb_ze_zes_list)


# CLASSICAL "..is" -> "..ides"

pl_sb_C_is_ides_complete = [
    # GENERAL WORDS...
    "ephemeris",
    "iris",
    "clitoris",
    "chrysalis",
    "epididymis",
]

pl_sb_C_is_ides_endings = [
    # INFLAMATIONS...
    "itis"
]

pl_sb_C_is_ides = joinstem(
    -2, pl_sb_C_is_ides_complete + [f".*{w}" for w in pl_sb_C_is_ides_endings]
)

pl_sb_C_is_ides_list = pl_sb_C_is_ides_complete + pl_sb_C_is_ides_endings

(
    si_sb_C_is_ides_list,
    si_sb_C_is_ides_bysize,
    pl_sb_C_is_ides_bysize,
) = make_pl_si_lists(pl_sb_C_is_ides_list, "ides", 2, dojoinstem=False)


# CLASSICAL "..a" -> "..ata"

pl_sb_C_a_ata_list = (
    "anathema",
    "bema",
    "carcinoma",
    "charisma",
    "diploma",
    "dogma",
    "drama",
    "edema",
    "enema",
    "enigma",
    "lemma",
    "lymphoma",
    "magma",
    "melisma",
    "miasma",
    "oedema",
    "sarcoma",
    "schema",
    "soma",
    "stigma",
    "stoma",
    "trauma",
    "gumma",
    "pragma",
)

(
    si_sb_C_a_ata_list,
    si_sb_C_a_ata_bysize,
    pl_sb_C_a_ata_bysize,
    pl_sb_C_a_ata,
) = make_pl_si_lists(pl_sb_C_a_ata_list, "ata", 1)

# UNCONDITIONAL "..a" -> "..ae"

pl_sb_U_a_ae_list = (
    "alumna",
    "alga",
    "vertebra",
    "persona",
    "vita",
)
(
    si_sb_U_a_ae_list,
    si_sb_U_a_ae_bysize,
    pl_sb_U_a_ae_bysize,
    pl_sb_U_a_ae,
) = make_pl_si_lists(pl_sb_U_a_ae_list, "e", None)

# CLASSICAL "..a" -> "..ae"

pl_sb_C_a_ae_list = (
    "amoeba",
    "antenna",
    "formula",
    "hyperbola",
    "medusa",
    "nebula",
    "parabola",
    "abscissa",
    "hydra",
    "nova",
    "lacuna",
    "aurora",
    "umbra",
    "flora",
    "fauna",
)
(
    si_sb_C_a_ae_list,
    si_sb_C_a_ae_bysize,
    pl_sb_C_a_ae_bysize,
    pl_sb_C_a_ae,
) = make_pl_si_lists(pl_sb_C_a_ae_list, "e", None)


# CLASSICAL "..en" -> "..ina"

pl_sb_C_en_ina_list = ("stamen", "foramen", "lumen")

(
    si_sb_C_en_ina_list,
    si_sb_C_en_ina_bysize,
    pl_sb_C_en_ina_bysize,
    pl_sb_C_en_ina,
) = make_pl_si_lists(pl_sb_C_en_ina_list, "ina", 2)


# UNCONDITIONAL "..um" -> "..a"

pl_sb_U_um_a_list = (
    "bacterium",
    "agendum",
    "desideratum",
    "erratum",
    "stratum",
    "datum",
    "ovum",
    "extremum",
    "candelabrum",
)
(
    si_sb_U_um_a_list,
    si_sb_U_um_a_bysize,
    pl_sb_U_um_a_bysize,
    pl_sb_U_um_a,
) = make_pl_si_lists(pl_sb_U_um_a_list, "a", 2)

# CLASSICAL "..um" -> "..a"

pl_sb_C_um_a_list = (
    "maximum",
    "minimum",
    "momentum",
    "optimum",
    "quantum",
    "cranium",
    "curriculum",
    "dictum",
    "phylum",
    "aquarium",
    "compendium",
    "emporium",
    "encomium",
    "gymnasium",
    "honorarium",
    "interregnum",
    "lustrum",
    "memorandum",
    "millennium",
    "rostrum",
    "spectrum",
    "speculum",
    "stadium",
    "trapezium",
    "ultimatum",
    "medium",
    "vacuum",
    "velum",
    "consortium",
    "arboretum",
)

(
    si_sb_C_um_a_list,
    si_sb_C_um_a_bysize,
    pl_sb_C_um_a_bysize,
    pl_sb_C_um_a,
) = make_pl_si_lists(pl_sb_C_um_a_list, "a", 2)


# UNCONDITIONAL "..us" -> "i"

pl_sb_U_us_i_list = (
    "alumnus",
    "alveolus",
    "bacillus",
    "bronchus",
    "locus",
    "nucleus",
    "stimulus",
    "meniscus",
    "sarcophagus",
)
(
    si_sb_U_us_i_list,
    si_sb_U_us_i_bysize,
    pl_sb_U_us_i_bysize,
    pl_sb_U_us_i,
) = make_pl_si_lists(pl_sb_U_us_i_list, "i", 2)

# CLASSICAL "..us" -> "..i"

pl_sb_C_us_i_list = (
    "focus",
    "radius",
    "genius",
    "incubus",
    "succubus",
    "nimbus",
    "fungus",
    "nucleolus",
    "stylus",
    "torus",
    "umbilicus",
    "uterus",
    "hippopotamus",
    "cactus",
)

(
    si_sb_C_us_i_list,
    si_sb_C_us_i_bysize,
    pl_sb_C_us_i_bysize,
    pl_sb_C_us_i,
) = make_pl_si_lists(pl_sb_C_us_i_list, "i", 2)


# CLASSICAL "..us" -> "..us"  (ASSIMILATED 4TH DECLENSION LATIN NOUNS)

pl_sb_C_us_us = (
    "status",
    "apparatus",
    "prospectus",
    "sinus",
    "hiatus",
    "impetus",
    "plexus",
)
pl_sb_C_us_us_bysize = bysize(pl_sb_C_us_us)

# UNCONDITIONAL "..on" -> "a"

pl_sb_U_on_a_list = (
    "criterion",
    "perihelion",
    "aphelion",
    "phenomenon",
    "prolegomenon",
    "noumenon",
    "organon",
    "asyndeton",
    "hyperbaton",
)
(
    si_sb_U_on_a_list,
    si_sb_U_on_a_bysize,
    pl_sb_U_on_a_bysize,
    pl_sb_U_on_a,
) = make_pl_si_lists(pl_sb_U_on_a_list, "a", 2)

# CLASSICAL "..on" -> "..a"

pl_sb_C_on_a_list = ("oxymoron",)

(
    si_sb_C_on_a_list,
    si_sb_C_on_a_bysize,
    pl_sb_C_on_a_bysize,
    pl_sb_C_on_a,
) = make_pl_si_lists(pl_sb_C_on_a_list, "a", 2)


# CLASSICAL "..o" -> "..i"  (BUT NORMALLY -> "..os")

pl_sb_C_o_i = [
    "solo",
    "soprano",
    "basso",
    "alto",
    "contralto",
    "tempo",
    "piano",
    "virtuoso",
]  # list not tuple so can concat for pl_sb_U_o_os

pl_sb_C_o_i_bysize = bysize(pl_sb_C_o_i)
si_sb_C_o_i_bysize = bysize([f"{w[:-1]}i" for w in pl_sb_C_o_i])

pl_sb_C_o_i_stems = joinstem(-1, pl_sb_C_o_i)

# ALWAYS "..o" -> "..os"

pl_sb_U_o_os_complete = {"ado", "ISO", "NATO", "NCO", "NGO", "oto"}
si_sb_U_o_os_complete = {f"{w}s" for w in pl_sb_U_o_os_complete}


pl_sb_U_o_os_endings = [
    "aficionado",
    "aggro",
    "albino",
    "allegro",
    "ammo",
    "Antananarivo",
    "archipelago",
    "armadillo",
    "auto",
    "avocado",
    "Bamako",
    "Barquisimeto",
    "bimbo",
    "bingo",
    "Biro",
    "bolero",
    "Bolzano",
    "bongo",
    "Boto",
    "burro",
    "Cairo",
    "canto",
    "cappuccino",
    "casino",
    "cello",
    "Chicago",
    "Chimango",
    "cilantro",
    "cochito",
    "coco",
    "Colombo",
    "Colorado",
    "commando",
    "concertino",
    "contango",
    "credo",
    "crescendo",
    "cyano",
    "demo",
    "ditto",
    "Draco",
    "dynamo",
    "embryo",
    "Esperanto",
    "espresso",
    "euro",
    "falsetto",
    "Faro",
    "fiasco",
    "Filipino",
    "flamenco",
    "furioso",
    "generalissimo",
    "Gestapo",
    "ghetto",
    "gigolo",
    "gizmo",
    "Greensboro",
    "gringo",
    "Guaiabero",
    "guano",
    "gumbo",
    "gyro",
    "hairdo",
    "hippo",
    "Idaho",
    "impetigo",
    "inferno",
    "info",
    "intermezzo",
    "intertrigo",
    "Iquico",
    "jumbo",
    "junto",
    "Kakapo",
    "kilo",
    "Kinkimavo",
    "Kokako",
    "Kosovo",
    "Lesotho",
    "libero",
    "libido",
    "libretto",
    "lido",
    "Lilo",
    "limbo",
    "limo",
    "lineno",
    "lingo",
    "lino",
    "livedo",
    "loco",
    "logo",
    "lumbago",
    "macho",
    "macro",
    "mafioso",
    "magneto",
    "magnifico",
    "Majuro",
    "Malabo",
    "manifesto",
    "Maputo",
    "Maracaibo",
    "medico",
    "memo",
    "metro",
    "Mexico",
    "micro",
    "Milano",
    "Monaco",
    "mono",
    "Montenegro",
    "Morocco",
    "Muqdisho",
    "myo",
    "neutrino",
    "Ningbo",
    "octavo",
    "oregano",
    "Orinoco",
    "Orlando",
    "Oslo",
    "panto",
    "Paramaribo",
    "Pardusco",
    "pedalo",
    "photo",
    "pimento",
    "pinto",
    "pleco",
    "Pluto",
    "pogo",
    "polo",
    "poncho",
    "Porto-Novo",
    "Porto",
    "pro",
    "psycho",
    "pueblo",
    "quarto",
    "Quito",
    "repo",
    "rhino",
    "risotto",
    "rococo",
    "rondo",
    "Sacramento",
    "saddo",
    "sago",
    "salvo",
    "Santiago",
    "Sapporo",
    "Sarajevo",
    "scherzando",
    "scherzo",
    "silo",
    "sirocco",
    "sombrero",
    "staccato",
    "sterno",
    "stucco",
    "stylo",
    "sumo",
    "Taiko",
    "techno",
    "terrazzo",
    "testudo",
    "timpano",
    "tiro",
    "tobacco",
    "Togo",
    "Tokyo",
    "torero",
    "Torino",
    "Toronto",
    "torso",
    "tremolo",
    "typo",
    "tyro",
    "ufo",
    "UNESCO",
    "vaquero",
    "vermicello",
    "verso",
    "vibrato",
    "violoncello",
    "Virgo",
    "weirdo",
    "WHO",
    "WTO",
    "Yamoussoukro",
    "yo-yo",
    "zero",
    "Zibo",
] + pl_sb_C_o_i

pl_sb_U_o_os_bysize = bysize(pl_sb_U_o_os_endings)
si_sb_U_o_os_bysize = bysize([f"{w}s" for w in pl_sb_U_o_os_endings])


# UNCONDITIONAL "..ch" -> "..chs"

pl_sb_U_ch_chs_list = ("czech", "eunuch", "stomach")

(
    si_sb_U_ch_chs_list,
    si_sb_U_ch_chs_bysize,
    pl_sb_U_ch_chs_bysize,
    pl_sb_U_ch_chs,
) = make_pl_si_lists(pl_sb_U_ch_chs_list, "s", None)


# UNCONDITIONAL "..[ei]x" -> "..ices"

pl_sb_U_ex_ices_list = ("codex", "murex", "silex")
(
    si_sb_U_ex_ices_list,
    si_sb_U_ex_ices_bysize,
    pl_sb_U_ex_ices_bysize,
    pl_sb_U_ex_ices,
) = make_pl_si_lists(pl_sb_U_ex_ices_list, "ices", 2)

pl_sb_U_ix_ices_list = ("radix", "helix")
(
    si_sb_U_ix_ices_list,
    si_sb_U_ix_ices_bysize,
    pl_sb_U_ix_ices_bysize,
    pl_sb_U_ix_ices,
) = make_pl_si_lists(pl_sb_U_ix_ices_list, "ices", 2)

# CLASSICAL "..[ei]x" -> "..ices"

pl_sb_C_ex_ices_list = (
    "vortex",
    "vertex",
    "cortex",
    "latex",
    "pontifex",
    "apex",
    "index",
    "simplex",
)

(
    si_sb_C_ex_ices_list,
    si_sb_C_ex_ices_bysize,
    pl_sb_C_ex_ices_bysize,
    pl_sb_C_ex_ices,
) = make_pl_si_lists(pl_sb_C_ex_ices_list, "ices", 2)


pl_sb_C_ix_ices_list = ("appendix",)

(
    si_sb_C_ix_ices_list,
    si_sb_C_ix_ices_bysize,
    pl_sb_C_ix_ices_bysize,
    pl_sb_C_ix_ices,
) = make_pl_si_lists(pl_sb_C_ix_ices_list, "ices", 2)


# ARABIC: ".." -> "..i"

pl_sb_C_i_list = ("afrit", "afreet", "efreet")

(si_sb_C_i_list, si_sb_C_i_bysize, pl_sb_C_i_bysize, pl_sb_C_i) = make_pl_si_lists(
    pl_sb_C_i_list, "i", None
)


# HEBREW: ".." -> "..im"

pl_sb_C_im_list = ("goy", "seraph", "cherub")

(si_sb_C_im_list, si_sb_C_im_bysize, pl_sb_C_im_bysize, pl_sb_C_im) = make_pl_si_lists(
    pl_sb_C_im_list, "im", None
)


# UNCONDITIONAL "..man" -> "..mans"

pl_sb_U_man_mans_list = """
    ataman caiman cayman ceriman
    desman dolman farman harman hetman
    human leman ottoman shaman talisman
""".split()
pl_sb_U_man_mans_caps_list = """
    Alabaman Bahaman Burman German
    Hiroshiman Liman Nakayaman Norman Oklahoman
    Panaman Roman Selman Sonaman Tacoman Yakiman
    Yokohaman Yuman
""".split()

(
    si_sb_U_man_mans_list,
    si_sb_U_man_mans_bysize,
    pl_sb_U_man_mans_bysize,
) = make_pl_si_lists(pl_sb_U_man_mans_list, "s", None, dojoinstem=False)
(
    si_sb_U_man_mans_caps_list,
    si_sb_U_man_mans_caps_bysize,
    pl_sb_U_man_mans_caps_bysize,
) = make_pl_si_lists(pl_sb_U_man_mans_caps_list, "s", None, dojoinstem=False)

# UNCONDITIONAL "..louse" -> "..lice"
pl_sb_U_louse_lice_list = ("booklouse", "grapelouse", "louse", "woodlouse")

(
    si_sb_U_louse_lice_list,
    si_sb_U_louse_lice_bysize,
    pl_sb_U_louse_lice_bysize,
) = make_pl_si_lists(pl_sb_U_louse_lice_list, "lice", 5, dojoinstem=False)

pl_sb_uninflected_s_complete = [
    # PAIRS OR GROUPS SUBSUMED TO A SINGULAR...
    "breeches",
    "britches",
    "pajamas",
    "pyjamas",
    "clippers",
    "gallows",
    "hijinks",
    "headquarters",
    "pliers",
    "scissors",
    "testes",
    "herpes",
    "pincers",
    "shears",
    "proceedings",
    "trousers",
    # UNASSIMILATED LATIN 4th DECLENSION
    "cantus",
    "coitus",
    "nexus",
    # RECENT IMPORTS...
    "contretemps",
    "corps",
    "debris",
    "siemens",
    # DISEASES
    "mumps",
    # MISCELLANEOUS OTHERS...
    "diabetes",
    "jackanapes",
    "series",
    "species",
    "subspecies",
    "rabies",
    "chassis",
    "innings",
    "news",
    "mews",
    "haggis",
]

pl_sb_uninflected_s_endings = [
    # RECENT IMPORTS...
    "ois",
    # DISEASES
    "measles",
]

pl_sb_uninflected_s = pl_sb_uninflected_s_complete + [
    f".*{w}" for w in pl_sb_uninflected_s_endings
]

pl_sb_uninflected_herd = (
    # DON'T INFLECT IN CLASSICAL MODE, OTHERWISE NORMAL INFLECTION
    "wildebeest",
    "swine",
    "eland",
    "bison",
    "buffalo",
    "cattle",
    "elk",
    "rhinoceros",
    "zucchini",
    "caribou",
    "dace",
    "grouse",
    "guinea fowl",
    "guinea-fowl",
    "haddock",
    "hake",
    "halibut",
    "herring",
    "mackerel",
    "pickerel",
    "pike",
    "roe",
    "seed",
    "shad",
    "snipe",
    "teal",
    "turbot",
    "water fowl",
    "water-fowl",
)

pl_sb_uninflected_complete = [
    # SOME FISH AND HERD ANIMALS
    "tuna",
    "salmon",
    "mackerel",
    "trout",
    "bream",
    "sea-bass",
    "sea bass",
    "carp",
    "cod",
    "flounder",
    "whiting",
    "moose",
    # OTHER ODDITIES
    "graffiti",
    "djinn",
    "samuri",
    "offspring",
    "pence",
    "quid",
    "hertz",
] + pl_sb_uninflected_s_complete
# SOME WORDS ENDING IN ...s (OFTEN PAIRS TAKEN AS A WHOLE)

pl_sb_uninflected_caps = [
    # ALL NATIONALS ENDING IN -ese
    "Portuguese",
    "Amoyese",
    "Borghese",
    "Congoese",
    "Faroese",
    "Foochowese",
    "Genevese",
    "Genoese",
    "Gilbertese",
    "Hottentotese",
    "Kiplingese",
    "Kongoese",
    "Lucchese",
    "Maltese",
    "Nankingese",
    "Niasese",
    "Pekingese",
    "Piedmontese",
    "Pistoiese",
    "Sarawakese",
    "Shavese",
    "Vermontese",
    "Wenchowese",
    "Yengeese",
]


pl_sb_uninflected_endings = [
    # UNCOUNTABLE NOUNS
    "butter",
    "cash",
    "furniture",
    "information",
    # SOME FISH AND HERD ANIMALS
    "fish",
    "deer",
    "sheep",
    # ALL NATIONALS ENDING IN -ese
    "nese",
    "rese",
    "lese",
    "mese",
    # DISEASES
    "pox",
    # OTHER ODDITIES
    "craft",
] + pl_sb_uninflected_s_endings
# SOME WORDS ENDING IN ...s (OFTEN PAIRS TAKEN AS A WHOLE)


pl_sb_uninflected_bysize = bysize(pl_sb_uninflected_endings)


# SINGULAR WORDS ENDING IN ...s (ALL INFLECT WITH ...es)

pl_sb_singular_s_complete = [
    "acropolis",
    "aegis",
    "alias",
    "asbestos",
    "bathos",
    "bias",
    "bronchitis",
    "bursitis",
    "caddis",
    "cannabis",
    "canvas",
    "chaos",
    "cosmos",
    "dais",
    "digitalis",
    "epidermis",
    "ethos",
    "eyas",
    "gas",
    "glottis",
    "hubris",
    "ibis",
    "lens",
    "mantis",
    "marquis",
    "metropolis",
    "pathos",
    "pelvis",
    "polis",
    "rhinoceros",
    "sassafras",
    "trellis",
] + pl_sb_C_is_ides_complete


pl_sb_singular_s_endings = ["ss", "us"] + pl_sb_C_is_ides_endings

pl_sb_singular_s_bysize = bysize(pl_sb_singular_s_endings)

si_sb_singular_s_complete = [f"{w}es" for w in pl_sb_singular_s_complete]
si_sb_singular_s_endings = [f"{w}es" for w in pl_sb_singular_s_endings]
si_sb_singular_s_bysize = bysize(si_sb_singular_s_endings)

pl_sb_singular_s_es = ["[A-Z].*es"]

pl_sb_singular_s = enclose(
    "|".join(
        pl_sb_singular_s_complete
        + [f".*{w}" for w in pl_sb_singular_s_endings]
        + pl_sb_singular_s_es
    )
)


# PLURALS ENDING IN uses -> use


si_sb_ois_oi_case = ("Bolshois", "Hanois")

si_sb_uses_use_case = ("Betelgeuses", "Duses", "Meuses", "Syracuses", "Toulouses")

si_sb_uses_use = (
    "abuses",
    "applauses",
    "blouses",
    "carouses",
    "causes",
    "chartreuses",
    "clauses",
    "contuses",
    "douses",
    "excuses",
    "fuses",
    "grouses",
    "hypotenuses",
    "masseuses",
    "menopauses",
    "misuses",
    "muses",
    "overuses",
    "pauses",
    "peruses",
    "profuses",
    "recluses",
    "reuses",
    "ruses",
    "souses",
    "spouses",
    "suffuses",
    "transfuses",
    "uses",
)

si_sb_ies_ie_case = (
    "Addies",
    "Aggies",
    "Allies",
    "Amies",
    "Angies",
    "Annies",
    "Annmaries",
    "Archies",
    "Arties",
    "Aussies",
    "Barbies",
    "Barries",
    "Basies",
    "Bennies",
    "Bernies",
    "Berties",
    "Bessies",
    "Betties",
    "Billies",
    "Blondies",
    "Bobbies",
    "Bonnies",
    "Bowies",
    "Brandies",
    "Bries",
    "Brownies",
    "Callies",
    "Carnegies",
    "Carries",
    "Cassies",
    "Charlies",
    "Cheries",
    "Christies",
    "Connies",
    "Curies",
    "Dannies",
    "Debbies",
    "Dixies",
    "Dollies",
    "Donnies",
    "Drambuies",
    "Eddies",
    "Effies",
    "Ellies",
    "Elsies",
    "Eries",
    "Ernies",
    "Essies",
    "Eugenies",
    "Fannies",
    "Flossies",
    "Frankies",
    "Freddies",
    "Gillespies",
    "Goldies",
    "Gracies",
    "Guthries",
    "Hallies",
    "Hatties",
    "Hetties",
    "Hollies",
    "Jackies",
    "Jamies",
    "Janies",
    "Jannies",
    "Jeanies",
    "Jeannies",
    "Jennies",
    "Jessies",
    "Jimmies",
    "Jodies",
    "Johnies",
    "Johnnies",
    "Josies",
    "Julies",
    "Kalgoorlies",
    "Kathies",
    "Katies",
    "Kellies",
    "Kewpies",
    "Kristies",
    "Laramies",
    "Lassies",
    "Lauries",
    "Leslies",
    "Lessies",
    "Lillies",
    "Lizzies",
    "Lonnies",
    "Lories",
    "Lorries",
    "Lotties",
    "Louies",
    "Mackenzies",
    "Maggies",
    "Maisies",
    "Mamies",
    "Marcies",
    "Margies",
    "Maries",
    "Marjories",
    "Matties",
    "McKenzies",
    "Melanies",
    "Mickies",
    "Millies",
    "Minnies",
    "Mollies",
    "Mounties",
    "Nannies",
    "Natalies",
    "Nellies",
    "Netties",
    "Ollies",
    "Ozzies",
    "Pearlies",
    "Pottawatomies",
    "Reggies",
    "Richies",
    "Rickies",
    "Robbies",
    "Ronnies",
    "Rosalies",
    "Rosemaries",
    "Rosies",
    "Roxies",
    "Rushdies",
    "Ruthies",
    "Sadies",
    "Sallies",
    "Sammies",
    "Scotties",
    "Selassies",
    "Sherries",
    "Sophies",
    "Stacies",
    "Stefanies",
    "Stephanies",
    "Stevies",
    "Susies",
    "Sylvies",
    "Tammies",
    "Terries",
    "Tessies",
    "Tommies",
    "Tracies",
    "Trekkies",
    "Valaries",
    "Valeries",
    "Valkyries",
    "Vickies",
    "Virgies",
    "Willies",
    "Winnies",
    "Wylies",
    "Yorkies",
)

si_sb_ies_ie = (
    "aeries",
    "baggies",
    "belies",
    "biggies",
    "birdies",
    "bogies",
    "bonnies",
    "boogies",
    "bookies",
    "bourgeoisies",
    "brownies",
    "budgies",
    "caddies",
    "calories",
    "camaraderies",
    "cockamamies",
    "collies",
    "cookies",
    "coolies",
    "cooties",
    "coteries",
    "crappies",
    "curies",
    "cutesies",
    "dogies",
    "eyries",
    "floozies",
    "footsies",
    "freebies",
    "genies",
    "goalies",
    "groupies",
    "hies",
    "jalousies",
    "junkies",
    "kiddies",
    "laddies",
    "lassies",
    "lies",
    "lingeries",
    "magpies",
    "menageries",
    "mommies",
    "movies",
    "neckties",
    "newbies",
    "nighties",
    "oldies",
    "organdies",
    "overlies",
    "pies",
    "pinkies",
    "pixies",
    "potpies",
    "prairies",
    "quickies",
    "reveries",
    "rookies",
    "rotisseries",
    "softies",
    "sorties",
    "species",
    "stymies",
    "sweeties",
    "ties",
    "underlies",
    "unties",
    "veggies",
    "vies",
    "yuppies",
    "zombies",
)


si_sb_oes_oe_case = (
    "Chloes",
    "Crusoes",
    "Defoes",
    "Faeroes",
    "Ivanhoes",
    "Joes",
    "McEnroes",
    "Moes",
    "Monroes",
    "Noes",
    "Poes",
    "Roscoes",
    "Tahoes",
    "Tippecanoes",
    "Zoes",
)

si_sb_oes_oe = (
    "aloes",
    "backhoes",
    "canoes",
    "does",
    "floes",
    "foes",
    "hoes",
    "mistletoes",
    "oboes",
    "pekoes",
    "roes",
    "sloes",
    "throes",
    "tiptoes",
    "toes",
    "woes",
)

si_sb_z_zes = ("quartzes", "topazes")

si_sb_zzes_zz = ("buzzes", "fizzes", "frizzes", "razzes")

si_sb_ches_che_case = (
    "Andromaches",
    "Apaches",
    "Blanches",
    "Comanches",
    "Nietzsches",
    "Porsches",
    "Roches",
)

si_sb_ches_che = (
    "aches",
    "avalanches",
    "backaches",
    "bellyaches",
    "caches",
    "cloches",
    "creches",
    "douches",
    "earaches",
    "fiches",
    "headaches",
    "heartaches",
    "microfiches",
    "niches",
    "pastiches",
    "psyches",
    "quiches",
    "stomachaches",
    "toothaches",
    "tranches",
)

si_sb_xes_xe = ("annexes", "axes", "deluxes", "pickaxes")

si_sb_sses_sse_case = ("Hesses", "Jesses", "Larousses", "Matisses")
si_sb_sses_sse = (
    "bouillabaisses",
    "crevasses",
    "demitasses",
    "impasses",
    "mousses",
    "posses",
)

si_sb_ves_ve_case = (
    # *[nwl]ives -> [nwl]live
    "Clives",
    "Palmolives",
)
si_sb_ves_ve = (
    # *[^d]eaves -> eave
    "interweaves",
    "weaves",
    # *[nwl]ives -> [nwl]live
    "olives",
    # *[eoa]lves -> [eoa]lve
    "bivalves",
    "dissolves",
    "resolves",
    "salves",
    "twelves",
    "valves",
)


plverb_special_s = enclose(
    "|".join(
        [pl_sb_singular_s]
        + pl_sb_uninflected_s
        + list(pl_sb_irregular_s)
        + ["(.*[csx])is", "(.*)ceps", "[A-Z].*s"]
    )
)

_pl_sb_postfix_adj_defn = (
    ("general", enclose(r"(?!major|lieutenant|brigadier|adjutant|.*star)\S+")),
    ("martial", enclose("court")),
    ("force", enclose("pound")),
)

pl_sb_postfix_adj: Iterable[str] = (
    enclose(val + f"(?=(?:-|\\s+){key})") for key, val in _pl_sb_postfix_adj_defn
)

pl_sb_postfix_adj_stems = f"({'|'.join(pl_sb_postfix_adj)})(.*)"


# PLURAL WORDS ENDING IS es GO TO SINGULAR is

si_sb_es_is = (
    "amanuenses",
    "amniocenteses",
    "analyses",
    "antitheses",
    "apotheoses",
    "arterioscleroses",
    "atheroscleroses",
    "axes",
    # 'bases', # bases -> basis
    "catalyses",
    "catharses",
    "chasses",
    "cirrhoses",
    "cocces",
    "crises",
    "diagnoses",
    "dialyses",
    "diereses",
    "electrolyses",
    "emphases",
    "exegeses",
    "geneses",
    "halitoses",
    "hydrolyses",
    "hypnoses",
    "hypotheses",
    "hystereses",
    "metamorphoses",
    "metastases",
    "misdiagnoses",
    "mitoses",
    "mononucleoses",
    "narcoses",
    "necroses",
    "nemeses",
    "neuroses",
    "oases",
    "osmoses",
    "osteoporoses",
    "paralyses",
    "parentheses",
    "parthenogeneses",
    "periphrases",
    "photosyntheses",
    "probosces",
    "prognoses",
    "prophylaxes",
    "prostheses",
    "preces",
    "psoriases",
    "psychoanalyses",
    "psychokineses",
    "psychoses",
    "scleroses",
    "scolioses",
    "sepses",
    "silicoses",
    "symbioses",
    "synopses",
    "syntheses",
    "taxes",
    "telekineses",
    "theses",
    "thromboses",
    "tuberculoses",
    "urinalyses",
)

pl_prep_list = """
    about above across after among around at athwart before behind
    below beneath beside besides between betwixt beyond but by
    during except for from in into near of off on onto out over
    since till to under until unto upon with""".split()

pl_prep_list_da = pl_prep_list + ["de", "du", "da"]

pl_prep_bysize = bysize(pl_prep_list_da)

pl_prep = enclose("|".join(pl_prep_list_da))

pl_sb_prep_dual_compound = rf"(.*?)((?:-|\s+)(?:{pl_prep})(?:-|\s+))a(?:-|\s+)(.*)"


singular_pronoun_genders = {
    "neuter",
    "feminine",
    "masculine",
    "gender-neutral",
    "feminine or masculine",
    "masculine or feminine",
}

pl_pron_nom = {
    # NOMINATIVE    REFLEXIVE
    "i": "we",
    "myself": "ourselves",
    "you": "you",
    "yourself": "yourselves",
    "she": "they",
    "herself": "themselves",
    "he": "they",
    "himself": "themselves",
    "it": "they",
    "itself": "themselves",
    "they": "they",
    "themself": "themselves",
    #   POSSESSIVE
    "mine": "ours",
    "yours": "yours",
    "hers": "theirs",
    "his": "theirs",
    "its": "theirs",
    "theirs": "theirs",
}

si_pron: Dict[str, Dict[str, Union[str, Dict[str, str]]]] = {
    "nom": {v: k for (k, v) in pl_pron_nom.items()}
}
si_pron["nom"]["we"] = "I"


pl_pron_acc = {
    # ACCUSATIVE    REFLEXIVE
    "me": "us",
    "myself": "ourselves",
    "you": "you",
    "yourself": "yourselves",
    "her": "them",
    "herself": "themselves",
    "him": "them",
    "himself": "themselves",
    "it": "them",
    "itself": "themselves",
    "them": "them",
    "themself": "themselves",
}

pl_pron_acc_keys = enclose("|".join(pl_pron_acc))
pl_pron_acc_keys_bysize = bysize(pl_pron_acc)

si_pron["acc"] = {v: k for (k, v) in pl_pron_acc.items()}

for _thecase, _plur, _gend_sing in (
    (
        "nom",
        "they",
        {
            "neuter": "it",
            "feminine": "she",
            "masculine": "he",
            "gender-neutral": "they",
            "feminine or masculine": "she or he",
            "masculine or feminine": "he or she",
        },
    ),
    (
        "nom",
        "themselves",
        {
            "neuter": "itself",
            "feminine": "herself",
            "masculine": "himself",
            "gender-neutral": "themself",
            "feminine or masculine": "herself or himself",
            "masculine or feminine": "himself or herself",
        },
    ),
    (
        "nom",
        "theirs",
        {
            "neuter": "its",
            "feminine": "hers",
            "masculine": "his",
            "gender-neutral": "theirs",
            "feminine or masculine": "hers or his",
            "masculine or feminine": "his or hers",
        },
    ),
    (
        "acc",
        "them",
        {
            "neuter": "it",
            "feminine": "her",
            "masculine": "him",
            "gender-neutral": "them",
            "feminine or masculine": "her or him",
            "masculine or feminine": "him or her",
        },
    ),
    (
        "acc",
        "themselves",
        {
            "neuter": "itself",
            "feminine": "herself",
            "masculine": "himself",
            "gender-neutral": "themself",
            "feminine or masculine": "herself or himself",
            "masculine or feminine": "himself or herself",
        },
    ),
):
    si_pron[_thecase][_plur] = _gend_sing


si_pron_acc_keys = enclose("|".join(si_pron["acc"]))
si_pron_acc_keys_bysize = bysize(si_pron["acc"])


def get_si_pron(thecase, word, gender) -> str:
    try:
        sing = si_pron[thecase][word]
    except KeyError:
        raise  # not a pronoun
    try:
        return sing[gender]  # has several types due to gender
    except TypeError:
        return cast(str, sing)  # answer independent of gender


# These dictionaries group verbs by first, second and third person
# conjugations.

plverb_irregular_pres = {
    "am": "are",
    "are": "are",
    "is": "are",
    "was": "were",
    "were": "were",
    "have": "have",
    "has": "have",
    "do": "do",
    "does": "do",
}

plverb_ambiguous_pres = {
    "act": "act",
    "acts": "act",
    "blame": "blame",
    "blames": "blame",
    "can": "can",
    "must": "must",
    "fly": "fly",
    "flies": "fly",
    "copy": "copy",
    "copies": "copy",
    "drink": "drink",
    "drinks": "drink",
    "fight": "fight",
    "fights": "fight",
    "fire": "fire",
    "fires": "fire",
    "like": "like",
    "likes": "like",
    "look": "look",
    "looks": "look",
    "make": "make",
    "makes": "make",
    "reach": "reach",
    "reaches": "reach",
    "run": "run",
    "runs": "run",
    "sink": "sink",
    "sinks": "sink",
    "sleep": "sleep",
    "sleeps": "sleep",
    "view": "view",
    "views": "view",
}

plverb_ambiguous_pres_keys = re.compile(
    rf"^({enclose('|'.join(plverb_ambiguous_pres))})((\s.*)?)$", re.IGNORECASE
)


plverb_irregular_non_pres = (
    "did",
    "had",
    "ate",
    "made",
    "put",
    "spent",
    "fought",
    "sank",
    "gave",
    "sought",
    "shall",
    "could",
    "ought",
    "should",
)

plverb_ambiguous_non_pres = re.compile(
    r"^((?:thought|saw|bent|will|might|cut))((\s.*)?)$", re.IGNORECASE
)

# "..oes" -> "..oe" (the rest are "..oes" -> "o")

pl_v_oes_oe = ("canoes", "floes", "oboes", "roes", "throes", "woes")
pl_v_oes_oe_endings_size4 = ("hoes", "toes")
pl_v_oes_oe_endings_size5 = ("shoes",)


pl_count_zero = ("0", "no", "zero", "nil")


pl_count_one = ("1", "a", "an", "one", "each", "every", "this", "that")

pl_adj_special = {"a": "some", "an": "some", "this": "these", "that": "those"}

pl_adj_special_keys = re.compile(
    rf"^({enclose('|'.join(pl_adj_special))})$", re.IGNORECASE
)

pl_adj_poss = {
    "my": "our",
    "your": "your",
    "its": "their",
    "her": "their",
    "his": "their",
    "their": "their",
}

pl_adj_poss_keys = re.compile(rf"^({enclose('|'.join(pl_adj_poss))})$", re.IGNORECASE)


# 2. INDEFINITE ARTICLES

# THIS PATTERN MATCHES STRINGS OF CAPITALS STARTING WITH A "VOWEL-SOUND"
# CONSONANT FOLLOWED BY ANOTHER CONSONANT, AND WHICH ARE NOT LIKELY
# TO BE REAL WORDS (OH, ALL RIGHT THEN, IT'S JUST MAGIC!)

A_abbrev = re.compile(
    r"""
^(?! FJO | [HLMNS]Y.  | RY[EO] | SQU
  | ( F[LR]? | [HL] | MN? | N | RH? | S[CHKLMNPTVW]? | X(YL)?) [AEIOU])
[FHLMNRSX][A-Z]
""",
    re.VERBOSE,
)

# THIS PATTERN CODES THE BEGINNINGS OF ALL ENGLISH WORDS BEGINING WITH A
# 'y' FOLLOWED BY A CONSONANT. ANY OTHER Y-CONSONANT PREFIX THEREFORE
# IMPLIES AN ABBREVIATION.

A_y_cons = re.compile(r"^(y(b[lor]|cl[ea]|fere|gg|p[ios]|rou|tt))", re.IGNORECASE)

# EXCEPTIONS TO EXCEPTIONS

A_explicit_a = re.compile(r"^((?:unabomber|unanimous|US))", re.IGNORECASE)

A_explicit_an = re.compile(
    r"^((?:euler|hour(?!i)|heir|honest|hono[ur]|mpeg))", re.IGNORECASE
)

A_ordinal_an = re.compile(r"^([aefhilmnorsx]-?th)", re.IGNORECASE)

A_ordinal_a = re.compile(r"^([bcdgjkpqtuvwyz]-?th)", re.IGNORECASE)


# NUMERICAL INFLECTIONS

nth = {
    0: "th",
    1: "st",
    2: "nd",
    3: "rd",
    4: "th",
    5: "th",
    6: "th",
    7: "th",
    8: "th",
    9: "th",
    11: "th",
    12: "th",
    13: "th",
}
nth_suff = set(nth.values())

ordinal = dict(
    ty="tieth",
    one="first",
    two="second",
    three="third",
    five="fifth",
    eight="eighth",
    nine="ninth",
    twelve="twelfth",
)

ordinal_suff = re.compile(rf"({'|'.join(ordinal)})\Z")


# NUMBERS

unit = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
teen = [
    "ten",
    "eleven",
    "twelve",
    "thirteen",
    "fourteen",
    "fifteen",
    "sixteen",
    "seventeen",
    "eighteen",
    "nineteen",
]
ten = [
    "",
    "",
    "twenty",
    "thirty",
    "forty",
    "fifty",
    "sixty",
    "seventy",
    "eighty",
    "ninety",
]
mill = [
    " ",
    " thousand",
    " million",
    " billion",
    " trillion",
    " quadrillion",
    " quintillion",
    " sextillion",
    " septillion",
    " octillion",
    " nonillion",
    " decillion",
]


# SUPPORT CLASSICAL PLURALIZATIONS

def_classical = dict(
    all=False, zero=False, herd=False, names=True, persons=False, ancient=False
)

all_classical = {k: True for k in def_classical}
no_classical = {k: False for k in def_classical}


# Maps strings to built-in constant types
string_to_constant = {"True": True, "False": False, "None": None}


# Pre-compiled regular expression objects
DOLLAR_DIGITS = re.compile(r"\$(\d+)")
FUNCTION_CALL = re.compile(r"((\w+)\([^)]*\)*)", re.IGNORECASE)
PARTITION_WORD = re.compile(r"\A(\s*)(.+?)(\s*)\Z")
PL_SB_POSTFIX_ADJ_STEMS_RE = re.compile(
    rf"^(?:{pl_sb_postfix_adj_stems})$", re.IGNORECASE
)
PL_SB_PREP_DUAL_COMPOUND_RE = re.compile(
    rf"^(?:{pl_sb_prep_dual_compound})$", re.IGNORECASE
)
DENOMINATOR = re.compile(r"(?P<denominator>.+)( (per|a) .+)")
PLVERB_SPECIAL_S_RE = re.compile(rf"^({plverb_special_s})$")
WHITESPACE = re.compile(r"\s")
ENDS_WITH_S = re.compile(r"^(.*[^s])s$", re.IGNORECASE)
ENDS_WITH_APOSTROPHE_S = re.compile(r"^(.+)'s?$")
INDEFINITE_ARTICLE_TEST = re.compile(r"\A(\s*)(?:an?\s+)?(.+?)(\s*)\Z", re.IGNORECASE)
SPECIAL_AN = re.compile(r"^[aefhilmnorsx]$", re.IGNORECASE)
SPECIAL_A = re.compile(r"^[bcdgjkpqtuvwyz]$", re.IGNORECASE)
SPECIAL_ABBREV_AN = re.compile(r"^[aefhilmnorsx][.-]", re.IGNORECASE)
SPECIAL_ABBREV_A = re.compile(r"^[a-z][.-]", re.IGNORECASE)
CONSONANTS = re.compile(r"^[^aeiouy]", re.IGNORECASE)
ARTICLE_SPECIAL_EU = re.compile(r"^e[uw]", re.IGNORECASE)
ARTICLE_SPECIAL_ONCE = re.compile(r"^onc?e\b", re.IGNORECASE)
ARTICLE_SPECIAL_ONETIME = re.compile(r"^onetime\b", re.IGNORECASE)
ARTICLE_SPECIAL_UNIT = re.compile(r"^uni([^nmd]|mo)", re.IGNORECASE)
ARTICLE_SPECIAL_UBA = re.compile(r"^u[bcfghjkqrst][aeiou]", re.IGNORECASE)
ARTICLE_SPECIAL_UKR = re.compile(r"^ukr", re.IGNORECASE)
SPECIAL_CAPITALS = re.compile(r"^U[NK][AIEO]?")
VOWELS = re.compile(r"^[aeiou]", re.IGNORECASE)

DIGIT_GROUP = re.compile(r"(\d)")
TWO_DIGITS = re.compile(r"(\d)(\d)")
THREE_DIGITS = re.compile(r"(\d)(\d)(\d)")
THREE_DIGITS_WORD = re.compile(r"(\d)(\d)(\d)(?=\D*\Z)")
TWO_DIGITS_WORD = re.compile(r"(\d)(\d)(?=\D*\Z)")
ONE_DIGIT_WORD = re.compile(r"(\d)(?=\D*\Z)")

FOUR_DIGIT_COMMA = re.compile(r"(\d)(\d{3}(?:,|\Z))")
NON_DIGIT = re.compile(r"\D")
WHITESPACES_COMMA = re.compile(r"\s+,")
COMMA_WORD = re.compile(r", (\S+)\s+\Z")
WHITESPACES = re.compile(r"\s+")


PRESENT_PARTICIPLE_REPLACEMENTS = (
    (re.compile(r"ie$"), r"y"),
    (
        re.compile(r"ue$"),
        r"u",
    ),  # TODO: isn't ue$ -> u encompassed in the following rule?
    (re.compile(r"([auy])e$"), r"\g<1>"),
    (re.compile(r"ski$"), r"ski"),
    (re.compile(r"[^b]i$"), r""),
    (re.compile(r"^(are|were)$"), r"be"),
    (re.compile(r"^(had)$"), r"hav"),
    (re.compile(r"^(hoe)$"), r"\g<1>"),
    (re.compile(r"([^e])e$"), r"\g<1>"),
    (re.compile(r"er$"), r"er"),
    (re.compile(r"([^aeiou][aeiouy]([bdgmnprst]))$"), r"\g<1>\g<2>"),
)

DIGIT = re.compile(r"\d")


class Words(str):
    lowered: str
    split_: List[str]
    first: str
    last: str

    def __init__(self, orig) -> None:
        self.lowered = self.lower()
        self.split_ = self.split()
        self.first = self.split_[0]
        self.last = self.split_[-1]


Falsish = Any  # ideally, falsish would only validate on bool(value) is False


_STATIC_TYPE_CHECKING = TYPE_CHECKING
# ^-- Workaround for typeguard AST manipulation:
#     https://github.com/agronholm/typeguard/issues/353#issuecomment-1556306554

if _STATIC_TYPE_CHECKING:  # pragma: no cover
    Word = Annotated[str, "String with at least 1 character"]
else:

    class _WordMeta(type):  # Too dynamic to be supported by mypy...
        def __instancecheck__(self, instance: Any) -> bool:
            return isinstance(instance, str) and len(instance) >= 1

    class Word(metaclass=_WordMeta):  # type: ignore[no-redef]
        """String with at least 1 character"""


class engine:
    def __init__(self) -> None:
        self.classical_dict = def_classical.copy()
        self.persistent_count: Optional[int] = None
        self.mill_count = 0
        self.pl_sb_user_defined: List[Optional[Word]] = []
        self.pl_v_user_defined: List[Optional[Word]] = []
        self.pl_adj_user_defined: List[Optional[Word]] = []
        self.si_sb_user_defined: List[Optional[Word]] = []
        self.A_a_user_defined: List[Optional[Word]] = []
        self.thegender = "neuter"
        self.__number_args: Optional[Dict[str, str]] = None

    @property
    def _number_args(self):
        return cast(Dict[str, str], self.__number_args)

    @_number_args.setter
    def _number_args(self, val):
        self.__number_args = val

    @typechecked
    def defnoun(self, singular: Optional[Word], plural: Optional[Word]) -> int:
        """
        Set the noun plural of singular to plural.

        """
        self.checkpat(singular)
        self.checkpatplural(plural)
        self.pl_sb_user_defined.extend((singular, plural))
        self.si_sb_user_defined.extend((plural, singular))
        return 1

    @typechecked
    def defverb(
        self,
        s1: Optional[Word],
        p1: Optional[Word],
        s2: Optional[Word],
        p2: Optional[Word],
        s3: Optional[Word],
        p3: Optional[Word],
    ) -> int:
        """
        Set the verb plurals for s1, s2 and s3 to p1, p2 and p3 respectively.

        Where 1, 2 and 3 represent the 1st, 2nd and 3rd person forms of the verb.

        """
        self.checkpat(s1)
        self.checkpat(s2)
        self.checkpat(s3)
        self.checkpatplural(p1)
        self.checkpatplural(p2)
        self.checkpatplural(p3)
        self.pl_v_user_defined.extend((s1, p1, s2, p2, s3, p3))
        return 1

    @typechecked
    def defadj(self, singular: Optional[Word], plural: Optional[Word]) -> int:
        """
        Set the adjective plural of singular to plural.

        """
        self.checkpat(singular)
        self.checkpatplural(plural)
        self.pl_adj_user_defined.extend((singular, plural))
        return 1

    @typechecked
    def defa(self, pattern: Optional[Word]) -> int:
        """
        Define the indefinite article as 'a' for words matching pattern.

        """
        self.checkpat(pattern)
        self.A_a_user_defined.extend((pattern, "a"))
        return 1

    @typechecked
    def defan(self, pattern: Optional[Word]) -> int:
        """
        Define the indefinite article as 'an' for words matching pattern.

        """
        self.checkpat(pattern)
        self.A_a_user_defined.extend((pattern, "an"))
        return 1

    def checkpat(self, pattern: Optional[Word]) -> None:
        """
        check for errors in a regex pattern
        """
        if pattern is None:
            return
        try:
            re.match(pattern, "")
        except re.error as err:
            raise BadUserDefinedPatternError(pattern) from err

    def checkpatplural(self, pattern: Optional[Word]) -> None:
        """
        check for errors in a regex replace pattern
        """
        return

    @typechecked
    def ud_match(self, word: Word, wordlist: Sequence[Optional[Word]]) -> Optional[str]:
        for i in range(len(wordlist) - 2, -2, -2):  # backwards through even elements
            mo = re.search(rf"^{wordlist[i]}$", word, re.IGNORECASE)
            if mo:
                if wordlist[i + 1] is None:
                    return None
                pl = DOLLAR_DIGITS.sub(
                    r"\\1", cast(Word, wordlist[i + 1])
                )  # change $n to \n for expand
                return mo.expand(pl)
        return None

    def classical(self, **kwargs) -> None:
        """
        turn classical mode on and off for various categories

        turn on all classical modes:
        classical()
        classical(all=True)

        turn on or off specific claassical modes:
        e.g.
        classical(herd=True)
        classical(names=False)

        By default all classical modes are off except names.

        unknown value in args or key in kwargs raises
        exception: UnknownClasicalModeError

        """
        if not kwargs:
            self.classical_dict = all_classical.copy()
            return
        if "all" in kwargs:
            if kwargs["all"]:
                self.classical_dict = all_classical.copy()
            else:
                self.classical_dict = no_classical.copy()

        for k, v in kwargs.items():
            if k in def_classical:
                self.classical_dict[k] = v
            else:
                raise UnknownClassicalModeError

    def num(
        self, count: Optional[int] = None, show: Optional[int] = None
    ) -> str:  # (;$count,$show)
        """
        Set the number to be used in other method calls.

        Returns count.

        Set show to False to return '' instead.

        """
        if count is not None:
            try:
                self.persistent_count = int(count)
            except ValueError as err:
                raise BadNumValueError from err
            if (show is None) or show:
                return str(count)
        else:
            self.persistent_count = None
        return ""

    def gender(self, gender: str) -> None:
        """
        set the gender for the singular of plural pronouns

        can be one of:
        'neuter'                ('they' -> 'it')
        'feminine'              ('they' -> 'she')
        'masculine'             ('they' -> 'he')
        'gender-neutral'        ('they' -> 'they')
        'feminine or masculine' ('they' -> 'she or he')
        'masculine or feminine' ('they' -> 'he or she')
        """
        if gender in singular_pronoun_genders:
            self.thegender = gender
        else:
            raise BadGenderError

    def _get_value_from_ast(self, obj):
        """
        Return the value of the ast object.
        """
        if isinstance(obj, ast.Constant):
            return obj.value
        elif isinstance(obj, ast.List):
            return [self._get_value_from_ast(e) for e in obj.elts]
        elif isinstance(obj, ast.Tuple):
            return tuple([self._get_value_from_ast(e) for e in obj.elts])

        # Probably passed a variable name.
        # Or passed a single word without wrapping it in quotes as an argument
        # ex: p.inflect("I plural(see)") instead of p.inflect("I plural('see')")
        raise NameError(f"name '{obj.id}' is not defined")

    def _string_to_substitute(
        self, mo: Match, methods_dict: Dict[str, Callable]
    ) -> str:
        """
        Return the string to be substituted for the match.
        """
        matched_text, f_name = mo.groups()
        # matched_text is the complete match string. e.g. plural_noun(cat)
        # f_name is the function name. e.g. plural_noun

        # Return matched_text if function name is not in methods_dict
        if f_name not in methods_dict:
            return matched_text

        # Parse the matched text
        a_tree = ast.parse(matched_text)

        # get the args and kwargs from ast objects
        args_list = [
            self._get_value_from_ast(a)
            for a in a_tree.body[0].value.args  # type: ignore[attr-defined]
        ]
        kwargs_list = {
            kw.arg: self._get_value_from_ast(kw.value)
            for kw in a_tree.body[0].value.keywords  # type: ignore[attr-defined]
        }

        # Call the corresponding function
        return methods_dict[f_name](*args_list, **kwargs_list)

    # 0. PERFORM GENERAL INFLECTIONS IN A STRING

    @typechecked
    def inflect(self, text: Word) -> str:
        """
        Perform inflections in a string.

        e.g. inflect('The plural of cat is plural(cat)') returns
        'The plural of cat is cats'

        can use plural, plural_noun, plural_verb, plural_adj,
        singular_noun, a, an, no, ordinal, number_to_words,
        and prespart

        """
        save_persistent_count = self.persistent_count

        # Dictionary of allowed methods
        methods_dict: Dict[str, Callable] = {
            "plural": self.plural,
            "plural_adj": self.plural_adj,
            "plural_noun": self.plural_noun,
            "plural_verb": self.plural_verb,
            "singular_noun": self.singular_noun,
            "a": self.a,
            "an": self.a,
            "no": self.no,
            "ordinal": self.ordinal,
            "number_to_words": self.number_to_words,
            "present_participle": self.present_participle,
            "num": self.num,
        }

        # Regular expression to find Python's function call syntax
        output = FUNCTION_CALL.sub(
            lambda mo: self._string_to_substitute(mo, methods_dict), text
        )
        self.persistent_count = save_persistent_count
        return output

    # ## PLURAL SUBROUTINES

    def postprocess(self, orig: str, inflected) -> str:
        inflected = str(inflected)
        if "|" in inflected:
            word_options = inflected.split("|")
            # When two parts of a noun need to be pluralized
            if len(word_options[0].split(" ")) == len(word_options[1].split(" ")):
                result = inflected.split("|")[self.classical_dict["all"]].split(" ")
            # When only the last part of the noun needs to be pluralized
            else:
                result = inflected.split(" ")
                for index, word in enumerate(result):
                    if "|" in word:
                        result[index] = word.split("|")[self.classical_dict["all"]]
        else:
            result = inflected.split(" ")

        # Try to fix word wise capitalization
        for index, word in enumerate(orig.split(" ")):
            if word == "I":
                # Is this the only word for exceptions like this
                # Where the original is fully capitalized
                # without 'meaning' capitalization?
                # Also this fails to handle a capitalizaion in context
                continue
            if word.capitalize() == word:
                result[index] = result[index].capitalize()
            if word == word.upper():
                result[index] = result[index].upper()
        return " ".join(result)

    def partition_word(self, text: str) -> Tuple[str, str, str]:
        mo = PARTITION_WORD.search(text)
        if mo:
            return mo.group(1), mo.group(2), mo.group(3)
        else:
            return "", "", ""

    @typechecked
    def plural(self, text: Word, count: Optional[Union[str, int, Any]] = None) -> str:
        """
        Return the plural of text.

        If count supplied, then return text if count is one of:
            1, a, an, one, each, every, this, that

        otherwise return the plural.

        Whitespace at the start and end is preserved.

        """
        pre, word, post = self.partition_word(text)
        if not word:
            return text
        plural = self.postprocess(
            word,
            self._pl_special_adjective(word, count)
            or self._pl_special_verb(word, count)
            or self._plnoun(word, count),
        )
        return f"{pre}{plural}{post}"

    @typechecked
    def plural_noun(
        self, text: Word, count: Optional[Union[str, int, Any]] = None
    ) -> str:
        """
        Return the plural of text, where text is a noun.

        If count supplied, then return text if count is one of:
            1, a, an, one, each, every, this, that

        otherwise return the plural.

        Whitespace at the start and end is preserved.

        """
        pre, word, post = self.partition_word(text)
        if not word:
            return text
        plural = self.postprocess(word, self._plnoun(word, count))
        return f"{pre}{plural}{post}"

    @typechecked
    def plural_verb(
        self, text: Word, count: Optional[Union[str, int, Any]] = None
    ) -> str:
        """
        Return the plural of text, where text is a verb.

        If count supplied, then return text if count is one of:
            1, a, an, one, each, every, this, that

        otherwise return the plural.

        Whitespace at the start and end is preserved.

        """
        pre, word, post = self.partition_word(text)
        if not word:
            return text
        plural = self.postprocess(
            word,
            self._pl_special_verb(word, count) or self._pl_general_verb(word, count),
        )
        return f"{pre}{plural}{post}"

    @typechecked
    def plural_adj(
        self, text: Word, count: Optional[Union[str, int, Any]] = None
    ) -> str:
        """
        Return the plural of text, where text is an adjective.

        If count supplied, then return text if count is one of:
            1, a, an, one, each, every, this, that

        otherwise return the plural.

        Whitespace at the start and end is preserved.

        """
        pre, word, post = self.partition_word(text)
        if not word:
            return text
        plural = self.postprocess(word, self._pl_special_adjective(word, count) or word)
        return f"{pre}{plural}{post}"

    @typechecked
    def compare(self, word1: Word, word2: Word) -> Union[str, bool]:
        """
        compare word1 and word2 for equality regardless of plurality

        return values:
        eq - the strings are equal
        p:s - word1 is the plural of word2
        s:p - word2 is the plural of word1
        p:p - word1 and word2 are two different plural forms of the one word
        False - otherwise

        >>> compare = engine().compare
        >>> compare("egg", "eggs")
        's:p'
        >>> compare('egg', 'egg')
        'eq'

        Words should not be empty.

        >>> compare('egg', '')
        Traceback (most recent call last):
        ...
        typeguard.TypeCheckError:...is not an instance of inflect.Word
        """
        norms = self.plural_noun, self.plural_verb, self.plural_adj
        results = (self._plequal(word1, word2, norm) for norm in norms)
        return next(filter(None, results), False)

    @typechecked
    def compare_nouns(self, word1: Word, word2: Word) -> Union[str, bool]:
        """
        compare word1 and word2 for equality regardless of plurality
        word1 and word2 are to be treated as nouns

        return values:
        eq - the strings are equal
        p:s - word1 is the plural of word2
        s:p - word2 is the plural of word1
        p:p - word1 and word2 are two different plural forms of the one word
        False - otherwise

        """
        return self._plequal(word1, word2, self.plural_noun)

    @typechecked
    def compare_verbs(self, word1: Word, word2: Word) -> Union[str, bool]:
        """
        compare word1 and word2 for equality regardless of plurality
        word1 and word2 are to be treated as verbs

        return values:
        eq - the strings are equal
        p:s - word1 is the plural of word2
        s:p - word2 is the plural of word1
        p:p - word1 and word2 are two different plural forms of the one word
        False - otherwise

        """
        return self._plequal(word1, word2, self.plural_verb)

    @typechecked
    def compare_adjs(self, word1: Word, word2: Word) -> Union[str, bool]:
        """
        compare word1 and word2 for equality regardless of plurality
        word1 and word2 are to be treated as adjectives

        return values:
        eq - the strings are equal
        p:s - word1 is the plural of word2
        s:p - word2 is the plural of word1
        p:p - word1 and word2 are two different plural forms of the one word
        False - otherwise

        """
        return self._plequal(word1, word2, self.plural_adj)

    @typechecked
    def singular_noun(
        self,
        text: Word,
        count: Optional[Union[int, str, Any]] = None,
        gender: Optional[str] = None,
    ) -> Union[str, Literal[False]]:
        """
        Return the singular of text, where text is a plural noun.

        If count supplied, then return the singular if count is one of:
            1, a, an, one, each, every, this, that or if count is None

        otherwise return text unchanged.

        Whitespace at the start and end is preserved.

        >>> p = engine()
        >>> p.singular_noun('horses')
        'horse'
        >>> p.singular_noun('knights')
        'knight'

        Returns False when a singular noun is passed.

        >>> p.singular_noun('horse')
        False
        >>> p.singular_noun('knight')
        False
        >>> p.singular_noun('soldier')
        False

        """
        pre, word, post = self.partition_word(text)
        if not word:
            return text
        sing = self._sinoun(word, count=count, gender=gender)
        if sing is not False:
            plural = self.postprocess(word, sing)
            return f"{pre}{plural}{post}"
        return False

    def _plequal(self, word1: str, word2: str, pl) -> Union[str, bool]:
        classval = self.classical_dict.copy()
        for dictionary in (all_classical, no_classical):
            self.classical_dict = dictionary.copy()
            if word1 == word2:
                return "eq"
            if word1 == pl(word2):
                return "p:s"
            if pl(word1) == word2:
                return "s:p"
        self.classical_dict = classval.copy()

        if pl == self.plural or pl == self.plural_noun:
            if self._pl_check_plurals_N(word1, word2):
                return "p:p"
            if self._pl_check_plurals_N(word2, word1):
                return "p:p"
        if pl == self.plural or pl == self.plural_adj:
            if self._pl_check_plurals_adj(word1, word2):
                return "p:p"
        return False

    def _pl_reg_plurals(self, pair: str, stems: str, end1: str, end2: str) -> bool:
        pattern = rf"({stems})({end1}\|\1{end2}|{end2}\|\1{end1})"
        return bool(re.search(pattern, pair))

    def _pl_check_plurals_N(self, word1: str, word2: str) -> bool:
        stem_endings = (
            (pl_sb_C_a_ata, "as", "ata"),
            (pl_sb_C_is_ides, "is", "ides"),
            (pl_sb_C_a_ae, "s", "e"),
            (pl_sb_C_en_ina, "ens", "ina"),
            (pl_sb_C_um_a, "ums", "a"),
            (pl_sb_C_us_i, "uses", "i"),
            (pl_sb_C_on_a, "ons", "a"),
            (pl_sb_C_o_i_stems, "os", "i"),
            (pl_sb_C_ex_ices, "exes", "ices"),
            (pl_sb_C_ix_ices, "ixes", "ices"),
            (pl_sb_C_i, "s", "i"),
            (pl_sb_C_im, "s", "im"),
            (".*eau", "s", "x"),
            (".*ieu", "s", "x"),
            (".*tri", "xes", "ces"),
            (".{2,}[yia]n", "xes", "ges"),
        )

        words = map(Words, (word1, word2))
        pair = "|".join(word.last for word in words)

        return (
            pair in pl_sb_irregular_s.values()
            or pair in pl_sb_irregular.values()
            or pair in pl_sb_irregular_caps.values()
            or any(
                self._pl_reg_plurals(pair, stems, end1, end2)
                for stems, end1, end2 in stem_endings
            )
        )

    def _pl_check_plurals_adj(self, word1: str, word2: str) -> bool:
        word1a = word1[: word1.rfind("'")] if word1.endswith(("'s", "'")) else ""
        word2a = word2[: word2.rfind("'")] if word2.endswith(("'s", "'")) else ""

        return (
            bool(word1a)
            and bool(word2a)
            and (
                self._pl_check_plurals_N(word1a, word2a)
                or self._pl_check_plurals_N(word2a, word1a)
            )
        )

    def get_count(self, count: Optional[Union[str, int]] = None) -> Union[str, int]:
        if count is None and self.persistent_count is not None:
            count = self.persistent_count

        if count is not None:
            count = (
                1
                if (
                    (str(count) in pl_count_one)
                    or (
                        self.classical_dict["zero"]
                        and str(count).lower() in pl_count_zero
                    )
                )
                else 2
            )
        else:
            count = ""
        return count

    # @profile
    def _plnoun(  # noqa: C901
        self, word: str, count: Optional[Union[str, int]] = None
    ) -> str:
        count = self.get_count(count)

        # DEFAULT TO PLURAL

        if count == 1:
            return word

        # HANDLE USER-DEFINED NOUNS

        value = self.ud_match(word, self.pl_sb_user_defined)
        if value is not None:
            return value

        # HANDLE EMPTY WORD, SINGULAR COUNT AND UNINFLECTED PLURALS

        if word == "":
            return word

        word = Words(word)

        if word.last.lower() in pl_sb_uninflected_complete:
            if len(word.split_) >= 3:
                return self._handle_long_compounds(word, count=2) or word
            return word

        if word in pl_sb_uninflected_caps:
            return word

        for k, v in pl_sb_uninflected_bysize.items():
            if word.lowered[-k:] in v:
                return word

        if self.classical_dict["herd"] and word.last.lower() in pl_sb_uninflected_herd:
            return word

        # HANDLE COMPOUNDS ("Governor General", "mother-in-law", "aide-de-camp", ETC.)

        mo = PL_SB_POSTFIX_ADJ_STEMS_RE.search(word)
        if mo and mo.group(2) != "":
            return f"{self._plnoun(mo.group(1), 2)}{mo.group(2)}"

        if " a " in word.lowered or "-a-" in word.lowered:
            mo = PL_SB_PREP_DUAL_COMPOUND_RE.search(word)
            if mo and mo.group(2) != "" and mo.group(3) != "":
                return (
                    f"{self._plnoun(mo.group(1), 2)}"
                    f"{mo.group(2)}"
                    f"{self._plnoun(mo.group(3))}"
                )

        if len(word.split_) >= 3:
            handled_words = self._handle_long_compounds(word, count=2)
            if handled_words is not None:
                return handled_words

        # only pluralize denominators in units
        mo = DENOMINATOR.search(word.lowered)
        if mo:
            index = len(mo.group("denominator"))
            return f"{self._plnoun(word[:index])}{word[index:]}"

        # handle units given in degrees (only accept if
        # there is no more than one word following)
        # degree Celsius => degrees Celsius but degree
        # fahrenheit hour => degree fahrenheit hours
        if len(word.split_) >= 2 and word.split_[-2] == "degree":
            return " ".join([self._plnoun(word.first)] + word.split_[1:])

        with contextlib.suppress(ValueError):
            return self._handle_prepositional_phrase(
                word.lowered,
                functools.partial(self._plnoun, count=2),
                '-',
            )

        # HANDLE PRONOUNS

        for k, v in pl_pron_acc_keys_bysize.items():
            if word.lowered[-k:] in v:  # ends with accusative pronoun
                for pk, pv in pl_prep_bysize.items():
                    if word.lowered[:pk] in pv:  # starts with a prep
                        if word.lowered.split() == [
                            word.lowered[:pk],
                            word.lowered[-k:],
                        ]:
                            # only whitespace in between
                            return word.lowered[:-k] + pl_pron_acc[word.lowered[-k:]]

        try:
            return pl_pron_nom[word.lowered]
        except KeyError:
            pass

        try:
            return pl_pron_acc[word.lowered]
        except KeyError:
            pass

        # HANDLE ISOLATED IRREGULAR PLURALS

        if word.last in pl_sb_irregular_caps:
            llen = len(word.last)
            return f"{word[:-llen]}{pl_sb_irregular_caps[word.last]}"

        lowered_last = word.last.lower()
        if lowered_last in pl_sb_irregular:
            llen = len(lowered_last)
            return f"{word[:-llen]}{pl_sb_irregular[lowered_last]}"

        dash_split = word.lowered.split('-')
        if (" ".join(dash_split[-2:])).lower() in pl_sb_irregular_compound:
            llen = len(
                " ".join(dash_split[-2:])
            )  # TODO: what if 2 spaces between these words?
            return (
                f"{word[:-llen]}"
                f"{pl_sb_irregular_compound[(' '.join(dash_split[-2:])).lower()]}"
            )

        if word.lowered[-3:] == "quy":
            return f"{word[:-1]}ies"

        if word.lowered[-6:] == "person":
            if self.classical_dict["persons"]:
                return f"{word}s"
            else:
                return f"{word[:-4]}ople"

        # HANDLE FAMILIES OF IRREGULAR PLURALS

        if word.lowered[-3:] == "man":
            for k, v in pl_sb_U_man_mans_bysize.items():
                if word.lowered[-k:] in v:
                    return f"{word}s"
            for k, v in pl_sb_U_man_mans_caps_bysize.items():
                if word[-k:] in v:
                    return f"{word}s"
            return f"{word[:-3]}men"
        if word.lowered[-5:] == "mouse":
            return f"{word[:-5]}mice"
        if word.lowered[-5:] == "louse":
            v = pl_sb_U_louse_lice_bysize.get(len(word))
            if v and word.lowered in v:
                return f"{word[:-5]}lice"
            return f"{word}s"
        if word.lowered[-5:] == "goose":
            return f"{word[:-5]}geese"
        if word.lowered[-5:] == "tooth":
            return f"{word[:-5]}teeth"
        if word.lowered[-4:] == "foot":
            return f"{word[:-4]}feet"
        if word.lowered[-4:] == "taco":
            return f"{word[:-5]}tacos"

        if word.lowered == "die":
            return "dice"

        # HANDLE UNASSIMILATED IMPORTS

        if word.lowered[-4:] == "ceps":
            return word
        if word.lowered[-4:] == "zoon":
            return f"{word[:-2]}a"
        if word.lowered[-3:] in ("cis", "sis", "xis"):
            return f"{word[:-2]}es"

        for lastlet, d, numend, post in (
            ("h", pl_sb_U_ch_chs_bysize, None, "s"),
            ("x", pl_sb_U_ex_ices_bysize, -2, "ices"),
            ("x", pl_sb_U_ix_ices_bysize, -2, "ices"),
            ("m", pl_sb_U_um_a_bysize, -2, "a"),
            ("s", pl_sb_U_us_i_bysize, -2, "i"),
            ("n", pl_sb_U_on_a_bysize, -2, "a"),
            ("a", pl_sb_U_a_ae_bysize, None, "e"),
        ):
            if word.lowered[-1] == lastlet:  # this test to add speed
                for k, v in d.items():
                    if word.lowered[-k:] in v:
                        return word[:numend] + post

        # HANDLE INCOMPLETELY ASSIMILATED IMPORTS

        if self.classical_dict["ancient"]:
            if word.lowered[-4:] == "trix":
                return f"{word[:-1]}ces"
            if word.lowered[-3:] in ("eau", "ieu"):
                return f"{word}x"
            if word.lowered[-3:] in ("ynx", "inx", "anx") and len(word) > 4:
                return f"{word[:-1]}ges"

            for lastlet, d, numend, post in (
                ("n", pl_sb_C_en_ina_bysize, -2, "ina"),
                ("x", pl_sb_C_ex_ices_bysize, -2, "ices"),
                ("x", pl_sb_C_ix_ices_bysize, -2, "ices"),
                ("m", pl_sb_C_um_a_bysize, -2, "a"),
                ("s", pl_sb_C_us_i_bysize, -2, "i"),
                ("s", pl_sb_C_us_us_bysize, None, ""),
                ("a", pl_sb_C_a_ae_bysize, None, "e"),
                ("a", pl_sb_C_a_ata_bysize, None, "ta"),
                ("s", pl_sb_C_is_ides_bysize, -1, "des"),
                ("o", pl_sb_C_o_i_bysize, -1, "i"),
                ("n", pl_sb_C_on_a_bysize, -2, "a"),
            ):
                if word.lowered[-1] == lastlet:  # this test to add speed
                    for k, v in d.items():
                        if word.lowered[-k:] in v:
                            return word[:numend] + post

            for d, numend, post in (
                (pl_sb_C_i_bysize, None, "i"),
                (pl_sb_C_im_bysize, None, "im"),
            ):
                for k, v in d.items():
                    if word.lowered[-k:] in v:
                        return word[:numend] + post

        # HANDLE SINGULAR NOUNS ENDING IN ...s OR OTHER SILIBANTS

        if lowered_last in pl_sb_singular_s_complete:
            return f"{word}es"

        for k, v in pl_sb_singular_s_bysize.items():
            if word.lowered[-k:] in v:
                return f"{word}es"

        if word.lowered[-2:] == "es" and word[0] == word[0].upper():
            return f"{word}es"

        if word.lowered[-1] == "z":
            for k, v in pl_sb_z_zes_bysize.items():
                if word.lowered[-k:] in v:
                    return f"{word}es"

            if word.lowered[-2:-1] != "z":
                return f"{word}zes"

        if word.lowered[-2:] == "ze":
            for k, v in pl_sb_ze_zes_bysize.items():
                if word.lowered[-k:] in v:
                    return f"{word}s"

        if word.lowered[-2:] in ("ch", "sh", "zz", "ss") or word.lowered[-1] == "x":
            return f"{word}es"

        # HANDLE ...f -> ...ves

        if word.lowered[-3:] in ("elf", "alf", "olf"):
            return f"{word[:-1]}ves"
        if word.lowered[-3:] == "eaf" and word.lowered[-4:-3] != "d":
            return f"{word[:-1]}ves"
        if word.lowered[-4:] in ("nife", "life", "wife"):
            return f"{word[:-2]}ves"
        if word.lowered[-3:] == "arf":
            return f"{word[:-1]}ves"

        # HANDLE ...y

        if word.lowered[-1] == "y":
            if word.lowered[-2:-1] in "aeiou" or len(word) == 1:
                return f"{word}s"

            if self.classical_dict["names"]:
                if word.lowered[-1] == "y" and word[0] == word[0].upper():
                    return f"{word}s"

            return f"{word[:-1]}ies"

        # HANDLE ...o

        if lowered_last in pl_sb_U_o_os_complete:
            return f"{word}s"

        for k, v in pl_sb_U_o_os_bysize.items():
            if word.lowered[-k:] in v:
                return f"{word}s"

        if word.lowered[-2:] in ("ao", "eo", "io", "oo", "uo"):
            return f"{word}s"

        if word.lowered[-1] == "o":
            return f"{word}es"

        # OTHERWISE JUST ADD ...s

        return f"{word}s"

    @classmethod
    def _handle_prepositional_phrase(cls, phrase, transform, sep):
        """
        Given a word or phrase possibly separated by sep, parse out
        the prepositional phrase and apply the transform to the word
        preceding the prepositional phrase.

        Raise ValueError if the pivot is not found or if at least two
        separators are not found.

        >>> engine._handle_prepositional_phrase("man-of-war", str.upper, '-')
        'MAN-of-war'
        >>> engine._handle_prepositional_phrase("man of war", str.upper, ' ')
        'MAN of war'
        """
        parts = phrase.split(sep)
        if len(parts) < 3:
            raise ValueError("Cannot handle words with fewer than two separators")

        pivot = cls._find_pivot(parts, pl_prep_list_da)

        transformed = transform(parts[pivot - 1]) or parts[pivot - 1]
        return " ".join(
            parts[: pivot - 1] + [sep.join([transformed, parts[pivot], ''])]
        ) + " ".join(parts[(pivot + 1) :])

    def _handle_long_compounds(self, word: Words, count: int) -> Union[str, None]:
        """
        Handles the plural and singular for compound `Words` that
        have three or more words, based on the given count.

        >>> engine()._handle_long_compounds(Words("pair of scissors"), 2)
        'pairs of scissors'
        >>> engine()._handle_long_compounds(Words("men beyond hills"), 1)
        'man beyond hills'
        """
        inflection = self._sinoun if count == 1 else self._plnoun
        solutions = (
            " ".join(
                itertools.chain(
                    leader,
                    [inflection(cand, count), prep],  # type: ignore[operator]
                    trailer,
                )
            )
            for leader, (cand, prep), trailer in windowed_complete(word.split_, 2)
            if prep in pl_prep_list_da
        )
        return next(solutions, None)

    @staticmethod
    def _find_pivot(words, candidates):
        pivots = (
            index for index in range(1, len(words) - 1) if words[index] in candidates
        )
        try:
            return next(pivots)
        except StopIteration:
            raise ValueError("No pivot found") from None

    def _pl_special_verb(  # noqa: C901
        self, word: str, count: Optional[Union[str, int]] = None
    ) -> Union[str, bool]:
        if self.classical_dict["zero"] and str(count).lower() in pl_count_zero:
            return False
        count = self.get_count(count)

        if count == 1:
            return word

        # HANDLE USER-DEFINED VERBS

        value = self.ud_match(word, self.pl_v_user_defined)
        if value is not None:
            return value

        # HANDLE IRREGULAR PRESENT TENSE (SIMPLE AND COMPOUND)

        try:
            words = Words(word)
        except IndexError:
            return False  # word is ''

        if words.first in plverb_irregular_pres:
            return f"{plverb_irregular_pres[words.first]}{words[len(words.first) :]}"

        # HANDLE IRREGULAR FUTURE, PRETERITE AND PERFECT TENSES

        if words.first in plverb_irregular_non_pres:
            return word

        # HANDLE PRESENT NEGATIONS (SIMPLE AND COMPOUND)

        if words.first.endswith("n't") and words.first[:-3] in plverb_irregular_pres:
            return (
                f"{plverb_irregular_pres[words.first[:-3]]}n't"
                f"{words[len(words.first) :]}"
            )

        if words.first.endswith("n't"):
            return word

        # HANDLE SPECIAL CASES

        mo = PLVERB_SPECIAL_S_RE.search(word)
        if mo:
            return False
        if WHITESPACE.search(word):
            return False

        if words.lowered == "quizzes":
            return "quiz"

        # HANDLE STANDARD 3RD PERSON (CHOP THE ...(e)s OFF SINGLE WORDS)

        if (
            words.lowered[-4:] in ("ches", "shes", "zzes", "sses")
            or words.lowered[-3:] == "xes"
        ):
            return words[:-2]

        if words.lowered[-3:] == "ies" and len(words) > 3:
            return words.lowered[:-3] + "y"

        if (
            words.last.lower() in pl_v_oes_oe
            or words.lowered[-4:] in pl_v_oes_oe_endings_size4
            or words.lowered[-5:] in pl_v_oes_oe_endings_size5
        ):
            return words[:-1]

        if words.lowered.endswith("oes") and len(words) > 3:
            return words.lowered[:-2]

        mo = ENDS_WITH_S.search(words)
        if mo:
            return mo.group(1)

        # OTHERWISE, A REGULAR VERB (HANDLE ELSEWHERE)

        return False

    def _pl_general_verb(
        self, word: str, count: Optional[Union[str, int]] = None
    ) -> str:
        count = self.get_count(count)

        if count == 1:
            return word

        # HANDLE AMBIGUOUS PRESENT TENSES  (SIMPLE AND COMPOUND)

        mo = plverb_ambiguous_pres_keys.search(word)
        if mo:
            return f"{plverb_ambiguous_pres[mo.group(1).lower()]}{mo.group(2)}"

        # HANDLE AMBIGUOUS PRETERITE AND PERFECT TENSES

        mo = plverb_ambiguous_non_pres.search(word)
        if mo:
            return word

        # OTHERWISE, 1st OR 2ND PERSON IS UNINFLECTED

        return word

    def _pl_special_adjective(
        self, word: str, count: Optional[Union[str, int]] = None
    ) -> Union[str, bool]:
        count = self.get_count(count)

        if count == 1:
            return word

        # HANDLE USER-DEFINED ADJECTIVES

        value = self.ud_match(word, self.pl_adj_user_defined)
        if value is not None:
            return value

        # HANDLE KNOWN CASES

        mo = pl_adj_special_keys.search(word)
        if mo:
            return pl_adj_special[mo.group(1).lower()]

        # HANDLE POSSESSIVES

        mo = pl_adj_poss_keys.search(word)
        if mo:
            return pl_adj_poss[mo.group(1).lower()]

        mo = ENDS_WITH_APOSTROPHE_S.search(word)
        if mo:
            pl = self.plural_noun(mo.group(1))
            trailing_s = "" if pl[-1] == "s" else "s"
            return f"{pl}'{trailing_s}"

        # OTHERWISE, NO IDEA

        return False

    # @profile
    def _sinoun(  # noqa: C901
        self,
        word: str,
        count: Optional[Union[str, int]] = None,
        gender: Optional[str] = None,
    ) -> Union[str, bool]:
        count = self.get_count(count)

        # DEFAULT TO PLURAL

        if count == 2:
            return word

        # SET THE GENDER

        try:
            if gender is None:
                gender = self.thegender
            elif gender not in singular_pronoun_genders:
                raise BadGenderError
        except (TypeError, IndexError) as err:
            raise BadGenderError from err

        # HANDLE USER-DEFINED NOUNS

        value = self.ud_match(word, self.si_sb_user_defined)
        if value is not None:
            return value

        # HANDLE EMPTY WORD, SINGULAR COUNT AND UNINFLECTED PLURALS

        if word == "":
            return word

        if word in si_sb_ois_oi_case:
            return word[:-1]

        words = Words(word)

        if words.last.lower() in pl_sb_uninflected_complete:
            if len(words.split_) >= 3:
                return self._handle_long_compounds(words, count=1) or word
            return word

        if word in pl_sb_uninflected_caps:
            return word

        for k, v in pl_sb_uninflected_bysize.items():
            if words.lowered[-k:] in v:
                return word

        if self.classical_dict["herd"] and words.last.lower() in pl_sb_uninflected_herd:
            return word

        if words.last.lower() in pl_sb_C_us_us:
            return word if self.classical_dict["ancient"] else False

        # HANDLE COMPOUNDS ("Governor General", "mother-in-law", "aide-de-camp", ETC.)

        mo = PL_SB_POSTFIX_ADJ_STEMS_RE.search(word)
        if mo and mo.group(2) != "":
            return f"{self._sinoun(mo.group(1), 1, gender=gender)}{mo.group(2)}"

        with contextlib.suppress(ValueError):
            return self._handle_prepositional_phrase(
                words.lowered,
                functools.partial(self._sinoun, count=1, gender=gender),
                ' ',
            )

        with contextlib.suppress(ValueError):
            return self._handle_prepositional_phrase(
                words.lowered,
                functools.partial(self._sinoun, count=1, gender=gender),
                '-',
            )

        # HANDLE PRONOUNS

        for k, v in si_pron_acc_keys_bysize.items():
            if words.lowered[-k:] in v:  # ends with accusative pronoun
                for pk, pv in pl_prep_bysize.items():
                    if words.lowered[:pk] in pv:  # starts with a prep
                        if words.lowered.split() == [
                            words.lowered[:pk],
                            words.lowered[-k:],
                        ]:
                            # only whitespace in between
                            return words.lowered[:-k] + get_si_pron(
                                "acc", words.lowered[-k:], gender
                            )

        try:
            return get_si_pron("nom", words.lowered, gender)
        except KeyError:
            pass

        try:
            return get_si_pron("acc", words.lowered, gender)
        except KeyError:
            pass

        # HANDLE ISOLATED IRREGULAR PLURALS

        if words.last in si_sb_irregular_caps:
            llen = len(words.last)
            return f"{word[:-llen]}{si_sb_irregular_caps[words.last]}"

        if words.last.lower() in si_sb_irregular:
            llen = len(words.last.lower())
            return f"{word[:-llen]}{si_sb_irregular[words.last.lower()]}"

        dash_split = words.lowered.split("-")
        if (" ".join(dash_split[-2:])).lower() in si_sb_irregular_compound:
            llen = len(
                " ".join(dash_split[-2:])
            )  # TODO: what if 2 spaces between these words?
            return "{}{}".format(
                word[:-llen],
                si_sb_irregular_compound[(" ".join(dash_split[-2:])).lower()],
            )

        if words.lowered[-5:] == "quies":
            return word[:-3] + "y"

        if words.lowered[-7:] == "persons":
            return word[:-1]
        if words.lowered[-6:] == "people":
            return word[:-4] + "rson"

        # HANDLE FAMILIES OF IRREGULAR PLURALS

        if words.lowered[-4:] == "mans":
            for k, v in si_sb_U_man_mans_bysize.items():
                if words.lowered[-k:] in v:
                    return word[:-1]
            for k, v in si_sb_U_man_mans_caps_bysize.items():
                if word[-k:] in v:
                    return word[:-1]
        if words.lowered[-3:] == "men":
            return word[:-3] + "man"
        if words.lowered[-4:] == "mice":
            return word[:-4] + "mouse"
        if words.lowered[-4:] == "lice":
            v = si_sb_U_louse_lice_bysize.get(len(word))
            if v and words.lowered in v:
                return word[:-4] + "louse"
        if words.lowered[-5:] == "geese":
            return word[:-5] + "goose"
        if words.lowered[-5:] == "teeth":
            return word[:-5] + "tooth"
        if words.lowered[-4:] == "feet":
            return word[:-4] + "foot"

        if words.lowered == "dice":
            return "die"

        # HANDLE UNASSIMILATED IMPORTS

        if words.lowered[-4:] == "ceps":
            return word
        if words.lowered[-3:] == "zoa":
            return word[:-1] + "on"

        for lastlet, d, unass_numend, post in (
            ("s", si_sb_U_ch_chs_bysize, -1, ""),
            ("s", si_sb_U_ex_ices_bysize, -4, "ex"),
            ("s", si_sb_U_ix_ices_bysize, -4, "ix"),
            ("a", si_sb_U_um_a_bysize, -1, "um"),
            ("i", si_sb_U_us_i_bysize, -1, "us"),
            ("a", si_sb_U_on_a_bysize, -1, "on"),
            ("e", si_sb_U_a_ae_bysize, -1, ""),
        ):
            if words.lowered[-1] == lastlet:  # this test to add speed
                for k, v in d.items():
                    if words.lowered[-k:] in v:
                        return word[:unass_numend] + post

        # HANDLE INCOMPLETELY ASSIMILATED IMPORTS

        if self.classical_dict["ancient"]:
            if words.lowered[-6:] == "trices":
                return word[:-3] + "x"
            if words.lowered[-4:] in ("eaux", "ieux"):
                return word[:-1]
            if words.lowered[-5:] in ("ynges", "inges", "anges") and len(word) > 6:
                return word[:-3] + "x"

            for lastlet, d, class_numend, post in (
                ("a", si_sb_C_en_ina_bysize, -3, "en"),
                ("s", si_sb_C_ex_ices_bysize, -4, "ex"),
                ("s", si_sb_C_ix_ices_bysize, -4, "ix"),
                ("a", si_sb_C_um_a_bysize, -1, "um"),
                ("i", si_sb_C_us_i_bysize, -1, "us"),
                ("s", pl_sb_C_us_us_bysize, None, ""),
                ("e", si_sb_C_a_ae_bysize, -1, ""),
                ("a", si_sb_C_a_ata_bysize, -2, ""),
                ("s", si_sb_C_is_ides_bysize, -3, "s"),
                ("i", si_sb_C_o_i_bysize, -1, "o"),
                ("a", si_sb_C_on_a_bysize, -1, "on"),
                ("m", si_sb_C_im_bysize, -2, ""),
                ("i", si_sb_C_i_bysize, -1, ""),
            ):
                if words.lowered[-1] == lastlet:  # this test to add speed
                    for k, v in d.items():
                        if words.lowered[-k:] in v:
                            return word[:class_numend] + post

        # HANDLE PLURLS ENDING IN uses -> use

        if (
            words.lowered[-6:] == "houses"
            or word in si_sb_uses_use_case
            or words.last.lower() in si_sb_uses_use
        ):
            return word[:-1]

        # HANDLE PLURLS ENDING IN ies -> ie

        if word in si_sb_ies_ie_case or words.last.lower() in si_sb_ies_ie:
            return word[:-1]

        # HANDLE PLURLS ENDING IN oes -> oe

        if (
            words.lowered[-5:] == "shoes"
            or word in si_sb_oes_oe_case
            or words.last.lower() in si_sb_oes_oe
        ):
            return word[:-1]

        # HANDLE SINGULAR NOUNS ENDING IN ...s OR OTHER SILIBANTS

        if word in si_sb_sses_sse_case or words.last.lower() in si_sb_sses_sse:
            return word[:-1]

        if words.last.lower() in si_sb_singular_s_complete:
            return word[:-2]

        for k, v in si_sb_singular_s_bysize.items():
            if words.lowered[-k:] in v:
                return word[:-2]

        if words.lowered[-4:] == "eses" and word[0] == word[0].upper():
            return word[:-2]

        if words.last.lower() in si_sb_z_zes:
            return word[:-2]

        if words.last.lower() in si_sb_zzes_zz:
            return word[:-2]

        if words.lowered[-4:] == "zzes":
            return word[:-3]

        if word in si_sb_ches_che_case or words.last.lower() in si_sb_ches_che:
            return word[:-1]

        if words.lowered[-4:] in ("ches", "shes"):
            return word[:-2]

        if words.last.lower() in si_sb_xes_xe:
            return word[:-1]

        if words.lowered[-3:] == "xes":
            return word[:-2]

        # HANDLE ...f -> ...ves

        if word in si_sb_ves_ve_case or words.last.lower() in si_sb_ves_ve:
            return word[:-1]

        if words.lowered[-3:] == "ves":
            if words.lowered[-5:-3] in ("el", "al", "ol"):
                return word[:-3] + "f"
            if words.lowered[-5:-3] == "ea" and word[-6:-5] != "d":
                return word[:-3] + "f"
            if words.lowered[-5:-3] in ("ni", "li", "wi"):
                return word[:-3] + "fe"
            if words.lowered[-5:-3] == "ar":
                return word[:-3] + "f"

        # HANDLE ...y

        if words.lowered[-2:] == "ys":
            if len(words.lowered) > 2 and words.lowered[-3] in "aeiou":
                return word[:-1]

            if self.classical_dict["names"]:
                if words.lowered[-2:] == "ys" and word[0] == word[0].upper():
                    return word[:-1]

        if words.lowered[-3:] == "ies":
            return word[:-3] + "y"

        # HANDLE ...o

        if words.lowered[-2:] == "os":
            if words.last.lower() in si_sb_U_o_os_complete:
                return word[:-1]

            for k, v in si_sb_U_o_os_bysize.items():
                if words.lowered[-k:] in v:
                    return word[:-1]

            if words.lowered[-3:] in ("aos", "eos", "ios", "oos", "uos"):
                return word[:-1]

        if words.lowered[-3:] == "oes":
            return word[:-2]

        # UNASSIMILATED IMPORTS FINAL RULE

        if word in si_sb_es_is:
            return word[:-2] + "is"

        # OTHERWISE JUST REMOVE ...s

        if words.lowered[-1] == "s":
            return word[:-1]

        # COULD NOT FIND SINGULAR

        return False

    # ADJECTIVES

    @typechecked
    def a(self, text: Word, count: Optional[Union[int, str, Any]] = 1) -> str:
        """
        Return the appropriate indefinite article followed by text.

        The indefinite article is either 'a' or 'an'.

        If count is not one, then return count followed by text
        instead of 'a' or 'an'.

        Whitespace at the start and end is preserved.

        """
        mo = INDEFINITE_ARTICLE_TEST.search(text)
        if mo:
            word = mo.group(2)
            if not word:
                return text
            pre = mo.group(1)
            post = mo.group(3)
            result = self._indef_article(word, count)
            return f"{pre}{result}{post}"
        return ""

    an = a

    _indef_article_cases = (
        # HANDLE ORDINAL FORMS
        (A_ordinal_a, "a"),
        (A_ordinal_an, "an"),
        # HANDLE SPECIAL CASES
        (A_explicit_an, "an"),
        (SPECIAL_AN, "an"),
        (SPECIAL_A, "a"),
        # HANDLE ABBREVIATIONS
        (A_abbrev, "an"),
        (SPECIAL_ABBREV_AN, "an"),
        (SPECIAL_ABBREV_A, "a"),
        # HANDLE CONSONANTS
        (CONSONANTS, "a"),
        # HANDLE SPECIAL VOWEL-FORMS
        (ARTICLE_SPECIAL_EU, "a"),
        (ARTICLE_SPECIAL_ONCE, "a"),
        (ARTICLE_SPECIAL_ONETIME, "a"),
        (ARTICLE_SPECIAL_UNIT, "a"),
        (ARTICLE_SPECIAL_UBA, "a"),
        (ARTICLE_SPECIAL_UKR, "a"),
        (A_explicit_a, "a"),
        # HANDLE SPECIAL CAPITALS
        (SPECIAL_CAPITALS, "a"),
        # HANDLE VOWELS
        (VOWELS, "an"),
        # HANDLE y...
        # (BEFORE CERTAIN CONSONANTS IMPLIES (UNNATURALIZED) "i.." SOUND)
        (A_y_cons, "an"),
    )

    def _indef_article(self, word: str, count: Union[int, str, Any]) -> str:
        mycount = self.get_count(count)

        if mycount != 1:
            return f"{count} {word}"

        # HANDLE USER-DEFINED VARIANTS

        value = self.ud_match(word, self.A_a_user_defined)
        if value is not None:
            return f"{value} {word}"

        matches = (
            f'{article} {word}'
            for regexen, article in self._indef_article_cases
            if regexen.search(word)
        )

        # OTHERWISE, GUESS "a"
        fallback = f'a {word}'
        return next(matches, fallback)

    # 2. TRANSLATE ZERO-QUANTIFIED $word TO "no plural($word)"

    @typechecked
    def no(self, text: Word, count: Optional[Union[int, str]] = None) -> str:
        """
        If count is 0, no, zero or nil, return 'no' followed by the plural
        of text.

        If count is one of:
            1, a, an, one, each, every, this, that
            return count followed by text.

        Otherwise return count follow by the plural of text.

        In the return value count is always followed by a space.

        Whitespace at the start and end is preserved.

        """
        if count is None and self.persistent_count is not None:
            count = self.persistent_count

        if count is None:
            count = 0
        mo = PARTITION_WORD.search(text)
        if mo:
            pre = mo.group(1)
            word = mo.group(2)
            post = mo.group(3)
        else:
            pre = ""
            word = ""
            post = ""

        if str(count).lower() in pl_count_zero:
            count = 'no'
        return f"{pre}{count} {self.plural(word, count)}{post}"

    # PARTICIPLES

    @typechecked
    def present_participle(self, word: Word) -> str:
        """
        Return the present participle for word.

        word is the 3rd person singular verb.

        """
        plv = self.plural_verb(word, 2)
        ans = plv

        for regexen, repl in PRESENT_PARTICIPLE_REPLACEMENTS:
            ans, num = regexen.subn(repl, plv)
            if num:
                return f"{ans}ing"
        return f"{ans}ing"

    # NUMERICAL INFLECTIONS

    @typechecked
    def ordinal(self, num: Union[Number, Word]) -> str:
        """
        Return the ordinal of num.

        >>> ordinal = engine().ordinal
        >>> ordinal(1)
        '1st'
        >>> ordinal('one')
        'first'
        """
        if DIGIT.match(str(num)):
            if isinstance(num, (float, int)) and int(num) == num:
                n = int(num)
            else:
                if "." in str(num):
                    try:
                        # numbers after decimal,
                        # so only need last one for ordinal
                        n = int(str(num)[-1])

                    except ValueError:
                        # ends with '.', so need to use whole string
                        n = int(str(num)[:-1])
                else:
                    n = int(num)  # type: ignore[arg-type]
            try:
                post = nth[n % 100]
            except KeyError:
                post = nth[n % 10]
            return f"{num}{post}"
        else:
            return self._sub_ord(num)

    def millfn(self, ind: int = 0) -> str:
        if ind > len(mill) - 1:
            raise NumOutOfRangeError
        return mill[ind]

    def unitfn(self, units: int, mindex: int = 0) -> str:
        return f"{unit[units]}{self.millfn(mindex)}"

    def tenfn(self, tens, units, mindex=0) -> str:
        if tens != 1:
            tens_part = ten[tens]
            if tens and units:
                hyphen = "-"
            else:
                hyphen = ""
            unit_part = unit[units]
            mill_part = self.millfn(mindex)
            return f"{tens_part}{hyphen}{unit_part}{mill_part}"
        return f"{teen[units]}{mill[mindex]}"

    def hundfn(self, hundreds: int, tens: int, units: int, mindex: int) -> str:
        if hundreds:
            andword = f" {self._number_args['andword']} " if tens or units else ""
            # use unit not unitfn as simpler
            return (
                f"{unit[hundreds]} hundred{andword}"
                f"{self.tenfn(tens, units)}{self.millfn(mindex)}, "
            )
        if tens or units:
            return f"{self.tenfn(tens, units)}{self.millfn(mindex)}, "
        return ""

    def group1sub(self, mo: Match) -> str:
        units = int(mo.group(1))
        if units == 1:
            return f" {self._number_args['one']}, "
        elif units:
            return f"{unit[units]}, "
        else:
            return f" {self._number_args['zero']}, "

    def group1bsub(self, mo: Match) -> str:
        units = int(mo.group(1))
        if units:
            return f"{unit[units]}, "
        else:
            return f" {self._number_args['zero']}, "

    def group2sub(self, mo: Match) -> str:
        tens = int(mo.group(1))
        units = int(mo.group(2))
        if tens:
            return f"{self.tenfn(tens, units)}, "
        if units:
            return f" {self._number_args['zero']} {unit[units]}, "
        return f" {self._number_args['zero']} {self._number_args['zero']}, "

    def group3sub(self, mo: Match) -> str:
        hundreds = int(mo.group(1))
        tens = int(mo.group(2))
        units = int(mo.group(3))
        if hundreds == 1:
            hunword = f" {self._number_args['one']}"
        elif hundreds:
            hunword = str(unit[hundreds])
        else:
            hunword = f" {self._number_args['zero']}"
        if tens:
            tenword = self.tenfn(tens, units)
        elif units:
            tenword = f" {self._number_args['zero']} {unit[units]}"
        else:
            tenword = f" {self._number_args['zero']} {self._number_args['zero']}"
        return f"{hunword} {tenword}, "

    def hundsub(self, mo: Match) -> str:
        ret = self.hundfn(
            int(mo.group(1)), int(mo.group(2)), int(mo.group(3)), self.mill_count
        )
        self.mill_count += 1
        return ret

    def tensub(self, mo: Match) -> str:
        return f"{self.tenfn(int(mo.group(1)), int(mo.group(2)), self.mill_count)}, "

    def unitsub(self, mo: Match) -> str:
        return f"{self.unitfn(int(mo.group(1)), self.mill_count)}, "

    def enword(self, num: str, group: int) -> str:
        # import pdb
        # pdb.set_trace()

        if group == 1:
            num = DIGIT_GROUP.sub(self.group1sub, num)
        elif group == 2:
            num = TWO_DIGITS.sub(self.group2sub, num)
            num = DIGIT_GROUP.sub(self.group1bsub, num, 1)
        elif group == 3:
            num = THREE_DIGITS.sub(self.group3sub, num)
            num = TWO_DIGITS.sub(self.group2sub, num, 1)
            num = DIGIT_GROUP.sub(self.group1sub, num, 1)
        elif int(num) == 0:
            num = self._number_args["zero"]
        elif int(num) == 1:
            num = self._number_args["one"]
        else:
            num = num.lstrip().lstrip("0")
            self.mill_count = 0
            # surely there's a better way to do the next bit
            mo = THREE_DIGITS_WORD.search(num)
            while mo:
                num = THREE_DIGITS_WORD.sub(self.hundsub, num, 1)
                mo = THREE_DIGITS_WORD.search(num)
            num = TWO_DIGITS_WORD.sub(self.tensub, num, 1)
            num = ONE_DIGIT_WORD.sub(self.unitsub, num, 1)
        return num

    @staticmethod
    def _sub_ord(val):
        new = ordinal_suff.sub(lambda match: ordinal[match.group(1)], val)
        return new + "th" * (new == val)

    @classmethod
    def _chunk_num(cls, num, decimal, group):
        if decimal:
            max_split = -1 if group != 0 else 1
            chunks = num.split(".", max_split)
        else:
            chunks = [num]
        return cls._remove_last_blank(chunks)

    @staticmethod
    def _remove_last_blank(chunks):
        """
        Remove the last item from chunks if it's a blank string.

        Return the resultant chunks and whether the last item was removed.
        """
        removed = chunks[-1] == ""
        result = chunks[:-1] if removed else chunks
        return result, removed

    @staticmethod
    def _get_sign(num):
        return {'+': 'plus', '-': 'minus'}.get(num.lstrip()[0], '')

    @typechecked
    def number_to_words(  # noqa: C901
        self,
        num: Union[Number, Word],
        wantlist: bool = False,
        group: int = 0,
        comma: Union[Falsish, str] = ",",
        andword: str = "and",
        zero: str = "zero",
        one: str = "one",
        decimal: Union[Falsish, str] = "point",
        threshold: Optional[int] = None,
    ) -> Union[str, List[str]]:
        """
        Return a number in words.

        group = 1, 2 or 3 to group numbers before turning into words
        comma: define comma

        andword:
            word for 'and'. Can be set to ''.
            e.g. "one hundred and one" vs "one hundred one"

        zero: word for '0'
        one: word for '1'
        decimal: word for decimal point
        threshold: numbers above threshold not turned into words

        parameters not remembered from last call. Departure from Perl version.
        """
        self._number_args = {"andword": andword, "zero": zero, "one": one}
        num = str(num)

        # Handle "stylistic" conversions (up to a given threshold)...
        if threshold is not None and float(num) > threshold:
            spnum = num.split(".", 1)
            while comma:
                (spnum[0], n) = FOUR_DIGIT_COMMA.subn(r"\1,\2", spnum[0])
                if n == 0:
                    break
            try:
                return f"{spnum[0]}.{spnum[1]}"
            except IndexError:
                return str(spnum[0])

        if group < 0 or group > 3:
            raise BadChunkingOptionError

        sign = self._get_sign(num)

        if num in nth_suff:
            num = zero

        myord = num[-2:] in nth_suff
        if myord:
            num = num[:-2]

        chunks, finalpoint = self._chunk_num(num, decimal, group)

        loopstart = chunks[0] == ""
        first: bool | None = not loopstart

        def _handle_chunk(chunk):
            nonlocal first

            # remove all non numeric \D
            chunk = NON_DIGIT.sub("", chunk)
            if chunk == "":
                chunk = "0"

            if group == 0 and not first:
                chunk = self.enword(chunk, 1)
            else:
                chunk = self.enword(chunk, group)

            if chunk[-2:] == ", ":
                chunk = chunk[:-2]
            chunk = WHITESPACES_COMMA.sub(",", chunk)

            if group == 0 and first:
                chunk = COMMA_WORD.sub(f" {andword} \\1", chunk)
            chunk = WHITESPACES.sub(" ", chunk)
            # chunk = re.sub(r"(\A\s|\s\Z)", self.blankfn, chunk)
            chunk = chunk.strip()
            if first:
                first = None
            return chunk

        chunks[loopstart:] = map(_handle_chunk, chunks[loopstart:])

        numchunks = []
        if first != 0:
            numchunks = chunks[0].split(f"{comma} ")

        if myord and numchunks:
            numchunks[-1] = self._sub_ord(numchunks[-1])

        for chunk in chunks[1:]:
            numchunks.append(decimal)
            numchunks.extend(chunk.split(f"{comma} "))

        if finalpoint:
            numchunks.append(decimal)

        if wantlist:
            return [sign] * bool(sign) + numchunks

        signout = f"{sign} " if sign else ""
        valout = (
            ', '.join(numchunks)
            if group
            else ''.join(self._render(numchunks, decimal, comma))
        )
        return signout + valout

    @staticmethod
    def _render(chunks, decimal, comma):
        first_item = chunks.pop(0)
        yield first_item
        first = decimal is None or not first_item.endswith(decimal)
        for nc in chunks:
            if nc == decimal:
                first = False
            elif first:
                yield comma
            yield f" {nc}"

    @typechecked
    def join(
        self,
        words: Optional[Sequence[Word]],
        sep: Optional[str] = None,
        sep_spaced: bool = True,
        final_sep: Optional[str] = None,
        conj: str = "and",
        conj_spaced: bool = True,
    ) -> str:
        """
        Join words into a list.

        e.g. join(['ant', 'bee', 'fly']) returns 'ant, bee, and fly'

        options:
        conj: replacement for 'and'
        sep: separator. default ',', unless ',' is in the list then ';'
        final_sep: final separator. default ',', unless ',' is in the list then ';'
        conj_spaced: boolean. Should conj have spaces around it

        """
        if not words:
            return ""
        if len(words) == 1:
            return words[0]

        if conj_spaced:
            if conj == "":
                conj = " "
            else:
                conj = f" {conj} "

        if len(words) == 2:
            return f"{words[0]}{conj}{words[1]}"

        if sep is None:
            if "," in "".join(words):
                sep = ";"
            else:
                sep = ","
        if final_sep is None:
            final_sep = sep

        final_sep = f"{final_sep}{conj}"

        if sep_spaced:
            sep += " "

        return f"{sep.join(words[0:-1])}{final_sep}{words[-1]}"


================================================
FILE: inflect/compat/__init__.py
================================================


================================================
FILE: inflect/compat/py38.py
================================================
import sys


if sys.version_info > (3, 9):
    from typing import Annotated
else:  # pragma: no cover
    from typing_extensions import Annotated  # noqa: F401


================================================
FILE: inflect/py.typed
================================================


================================================
FILE: mypy.ini
================================================
[mypy]
# Is the project well-typed?
strict = False

# Early opt-in even when strict = False
warn_unused_ignores = True
warn_redundant_casts = True
enable_error_code = ignore-without-code

# Support namespace packages per https://github.com/python/mypy/issues/14057
explicit_package_bases = True

disable_error_code =
	# Disable due to many false positives
	overload-overlap,


================================================
FILE: pyproject.toml
================================================
[build-system]
requires = [
	"setuptools>=77",
	"setuptools_scm[toml]>=3.4.1",
	# jaraco/skeleton#174
	"coherent.licensed",
]
build-backend = "setuptools.build_meta"

[project]
name = "inflect"
authors = [
	{ name = "Paul Dyson", email = "pwdyson@yahoo.com" },
]
maintainers = [
	{ name = "Jason R. Coombs", email = "jaraco@jaraco.com" },
]
description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles" # convert numbers to words
readme = "README.rst"
classifiers = [
	"Development Status :: 5 - Production/Stable",
	"Intended Audience :: Developers",
	"Programming Language :: Python :: 3",
	"Programming Language :: Python :: 3 :: Only",
	"Natural Language :: English",
	"Operating System :: OS Independent",
	"Topic :: Software Development :: Libraries :: Python Modules",
	"Topic :: Text Processing :: Linguistic",
]
requires-python = ">=3.10"
license = "MIT"
dependencies = [
	"more_itertools >= 8.5.0",
	"typeguard >= 4.0.1",
	"typing_extensions ; python_version<'3.9'",
]
keywords = [
	"plural",
	"inflect",
	"participle",
]
dynamic = ["version"]

[project.urls]
Source = "https://github.com/jaraco/inflect"

[project.optional-dependencies]
test = [
	# upstream
	"pytest >= 6, != 8.1.*",

	# local
	"pygments",
]

doc = [
	# upstream
	"sphinx >= 3.5",
	"jaraco.packaging >= 9.3",
	"rst.linker >= 1.9",
	"furo",
	"sphinx-lint",

	# tidelift
	"jaraco.tidelift >= 1.4",

	# local
]

check = [
	"pytest-checkdocs >= 2.14",
	"pytest-ruff >= 0.2.1; sys_platform != 'cygwin'",
]

cover = [
	"pytest-cov",
]

enabler = [
	"pytest-enabler >= 3.4",
]

type = [
	# upstream
	
    # Exclude PyPy from type checks (python/mypy#20454 jaraco/skeleton#187)
	"pytest-mypy >= 1.0.1; platform_python_implementation != 'PyPy'",

	# local
]


[tool.setuptools_scm]


[tool.pytest-enabler.mypy]
# Disabled due to jaraco/skeleton#143


================================================
FILE: pytest.ini
================================================
[pytest]
norecursedirs=dist build .tox .eggs
addopts=
	--doctest-modules
	--import-mode importlib
consider_namespace_packages=true
filterwarnings=
	## upstream

	# Ensure ResourceWarnings are emitted
	default::ResourceWarning

	# realpython/pytest-mypy#152
	ignore:'encoding' argument not specified::pytest_mypy

	# python/cpython#100750
	ignore:'encoding' argument not specified::platform

	# pypa/build#615
	ignore:'encoding' argument not specified::build.env

	# dateutil/dateutil#1284
	ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz

	## end upstream


================================================
FILE: ruff.toml
================================================
[lint]
extend-select = [
	# upstream

	"C901", # complex-structure
	"I", # isort
	"PERF401", # manual-list-comprehension

	# Ensure modern type annotation syntax and best practices
	# Not including those covered by type-checkers or exclusive to Python 3.11+
	"FA", # flake8-future-annotations
	"F404", # late-future-import
	"PYI", # flake8-pyi
	"UP006", # non-pep585-annotation
	"UP007", # non-pep604-annotation
	"UP010", # unnecessary-future-import
	"UP035", # deprecated-import
	"UP037", # quoted-annotation
	"UP043", # unnecessary-default-type-args

	# local
]
ignore = [
	# upstream

	# Typeshed rejects complex or non-literal defaults for maintenance and testing reasons,
	# irrelevant to this project.
	"PYI011", # typed-argument-default-in-stub
	# https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
	"W191",
	"E111",
	"E114",
	"E117",
	"D206",
	"D300",
	"Q000",
	"Q001",
	"Q002",
	"Q003",
	"COM812",
	"COM819",

	# local
]

[format]
# Enable preview to get hugged parenthesis unwrapping and other nice surprises
# See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373
preview = true
# https://docs.astral.sh/ruff/settings/#format_quote-style
quote-style = "preserve"


================================================
FILE: tea.yaml
================================================
# https://tea.xyz/what-is-this-file
---
version: 1.0.0
codeOwners:
  - '0x32392EaEA1FDE87733bEEc3b184C9006501c4A82'
quorum: 1


================================================
FILE: tests/inflections.txt
================================================
                    a  ->  as                             # NOUN FORM
      TODO:sing              a  ->  some                           # INDEFINITE ARTICLE
      TODO:  A.C.R.O.N.Y.M.  ->  A.C.R.O.N.Y.M.s
             abscissa  ->  abscissas|abscissae
           accomplice  ->  accomplices
             Achinese  ->  Achinese
            acropolis  ->  acropolises
                adieu  ->  adieus|adieux
     adjutant general  ->  adjutant generals
                aegis  ->  aegises
             afflatus  ->  afflatuses
               afreet  ->  afreets|afreeti
                afrit  ->  afrits|afriti
              agendum  ->  agenda
         aide-de-camp  ->  aides-de-camp
             Alabaman  ->  Alabamans
               albino  ->  albinos
                album  ->  albums
             Alfurese  ->  Alfurese
                 alga  ->  algae
                alias  ->  aliases
                 alto  ->  altos|alti
               alumna  ->  alumnae
              alumnus  ->  alumni
             alveolus  ->  alveoli
   TODO:siverb                am  ->  are
   TODO:siverb          am going  ->  are going
  ambassador-at-large  ->  ambassadors-at-large
            Amboinese  ->  Amboinese
          Americanese  ->  Americanese
               amoeba  ->  amoebas|amoebae
              Amoyese  ->  Amoyese
   TODO:siadj                an  ->  some                           # INDEFINITE ARTICLE
             analysis  ->  analyses
             anathema  ->  anathemas|anathemata
           Andamanese  ->  Andamanese
             Angolese  ->  Angolese
             Annamese  ->  Annamese
              antenna  ->  antennas|antennae
                 anus  ->  anuses
                 apex  ->  apexes|apices
   TODO:siadj            apex's  ->  apexes'|apices'                # POSSESSIVE FORM
             aphelion  ->  aphelia
            apparatus  ->  apparatuses|apparatus
             appendix  ->  appendixes|appendices
                apple  ->  apples
             aquarium  ->  aquariums|aquaria
            Aragonese  ->  Aragonese
            Arakanese  ->  Arakanese
          archipelago  ->  archipelagos
   TODO:siverb               are  ->  are
   TODO:siverb          are made  ->  are made
            armadillo  ->  armadillos
             arpeggio  ->  arpeggios
            arthritis  ->  arthritises|arthritides
             asbestos  ->  asbestoses
            asparagus  ->  asparaguses
                  ass  ->  asses
             Assamese  ->  Assamese
               asylum  ->  asylums
            asyndeton  ->  asyndeta
                at it  ->  at them                        # ACCUSATIVE
               ataman  ->  atamans
   TODO:siverb               ate  ->  ate
                atlas  ->  atlases|atlantes
                atman  ->  atmas
  TODO:singular_noun   attorney general  ->  attorneys general
   attorney of record  ->  attorneys of record
               aurora  ->  auroras|aurorae
                 auto  ->  autos
           auto-da-fe  ->  autos-da-fe
             aviatrix  ->  aviatrixes|aviatrices
    TODO:siadj       aviatrix's  ->  aviatrixes'|aviatrices'
           Avignonese  ->  Avignonese
                  axe  ->  axes
    TODO:singular_noun 2 anwers!            axis  ->  axes
                axman  ->  axmen
        Azerbaijanese  ->  Azerbaijanese
             bacillus  ->  bacilli
            bacterium  ->  bacteria
              Bahaman  ->  Bahamans
             Balinese  ->  Balinese
               bamboo  ->  bamboos
                banjo  ->  banjoes
                 bass  ->  basses                         # INSTRUMENT, NOT FISH
                basso  ->  bassos|bassi
               bathos  ->  bathoses
                 beau  ->  beaus|beaux
                 beef  ->  beefs|beeves
           beneath it  ->  beneath them                   # ACCUSATIVE
            Bengalese  ->  Bengalese
                 bent  ->  bent                           # VERB FORM
                 bent  ->  bents                          # NOUN FORM
              Bernese  ->  Bernese
            Bhutanese  ->  Bhutanese
                 bias  ->  biases
               biceps  ->  biceps
                bison  ->  bisons|bison
          black olive  ->  black olives
               blouse  ->  blouses
            Bolognese  ->  Bolognese
                bonus  ->  bonuses
             Borghese  ->  Borghese
                 boss  ->  bosses
            Bostonese  ->  Bostonese
                  box  ->  boxes
                  boy  ->  boys
                bravo  ->  bravoes
                bream  ->  bream
             breeches  ->  breeches
          bride-to-be  ->  brides-to-be
    Brigadier General  ->  Brigadier Generals
             britches  ->  britches
           bronchitis  ->  bronchitises|bronchitides
             bronchus  ->  bronchi
              brother  ->  brothers|brethren
   TODO:         brother's  ->  brothers'|brethren's
              buffalo  ->  buffaloes|buffalo
             Buginese  ->  Buginese
                 buoy  ->  buoys
               bureau  ->  bureaus|bureaux
               Burman  ->  Burmans
              Burmese  ->  Burmese
             bursitis  ->  bursitises|bursitides
                  bus  ->  buses
               butter  ->  butter
                 buzz  ->  buzzes
               buzzes  ->  buzz                           # VERB FORM
                by it  ->  by them                        # ACCUSATIVE
               caddis  ->  caddises
               caiman  ->  caimans
                 cake  ->  cakes
            Calabrese  ->  Calabrese
                 calf  ->  calves
               callus  ->  calluses
          Camaldolese  ->  Camaldolese
                cameo  ->  cameos
               campus  ->  campuses
                  can  ->  cans                           # NOUN FORM
                  can  ->  can                            # VERB FORM (all pers.)
                can't  ->  can't                          # VERB FORM
          candelabrum  ->  candelabra
             cannabis  ->  cannabises
      TODO:siverb         canoes  ->  canoe
                canto  ->  cantos
            Cantonese  ->  Cantonese
               cantus  ->  cantus
               canvas  ->  canvases
              CAPITAL  ->  CAPITALS
            carcinoma  ->  carcinomas|carcinomata
                 care  ->  cares
                cargo  ->  cargoes
              caribou  ->  caribous|caribou
            Carlylese  ->  Carlylese
               carmen  ->  carmina
                 carp  ->  carp
                 cash  ->  cash
            Cassinese  ->  Cassinese
                  cat  ->  cats
              catfish  ->  catfish
			   cattle  ->  cattles|cattle
               cayman  ->  caymans
             Celanese  ->  Celanese
              ceriman  ->  cerimans
               cervid  ->  cervids
            Ceylonese  ->  Ceylonese
             chairman  ->  chairmen
              chamois  ->  chamois
                chaos  ->  chaoses
              chapeau  ->  chapeaus|chapeaux
             charisma  ->  charismas|charismata
    TODO:siverb           chases  ->  chase
              chassis  ->  chassis
              chateau  ->  chateaus|chateaux
               cherub  ->  cherubs|cherubim
           chickenpox  ->  chickenpox
                chief  ->  chiefs
                child  ->  children
                chili  ->  chilis|chilies
              Chinese  ->  Chinese
       oatmeal cookie  ->  oatmeal cookies
               chorus  ->  choruses
            chrysalis  ->  chrysalises|chrysalides
               church  ->  churches
             cicatrix  ->  cicatrixes|cicatrices
               circus  ->  circuses
                class  ->  classes
              classes  ->  class                          # VERB FORM
             clippers  ->  clippers
             clitoris  ->  clitorises|clitorides
                  cod  ->  cod
                codex  ->  codices
               coitus  ->  coitus
             commando  ->  commandos
           compendium  ->  compendiums|compendia
                coney  ->  coneys
             Congoese  ->  Congoese
            Congolese  ->  Congolese
           conspectus  ->  conspectuses
            contralto  ->  contraltos|contralti
          contretemps  ->  contretemps
            conundrum  ->  conundrums
                corps  ->  corps
               corpus  ->  corpuses|corpora
               cortex  ->  cortexes|cortices
               cosmos  ->  cosmoses
     TODO:singular_noun   court martial  ->  courts martial
                  cow  ->  cows|kine
              cranium  ->  craniums|crania
            crescendo  ->  crescendos
            criterion  ->  criteria
           curriculum  ->  curriculums|curricula
                czech  ->  czechs
                 dais  ->  daises
           data point  ->  data points
                datum  ->  data
               debris  ->  debris
              decorum  ->  decorums
                 deer  ->  deer
           delphinium  ->  delphiniums
          desideratum  ->  desiderata
               desman  ->  desmans
             diabetes  ->  diabetes
               dictum  ->  dictums|dicta
    TODO:siverb              did  ->  did
    TODO:siverb         did need  ->  did need
            digitalis  ->  digitalises
                dingo  ->  dingoes
              diploma  ->  diplomas|diplomata
               discus  ->  discuses
                 dish  ->  dishes
                ditto  ->  dittos
                djinn  ->  djinn
     TODO:siverb            does  ->  do
     TODO:siverb         doesn't  ->  don't                          # VERB FORM
                  dog  ->  dogs
                dogma  ->  dogmas|dogmata
               dolman  ->  dolmans
           dominatrix  ->  dominatrixes|dominatrices
               domino  ->  dominoes
            Dongolese  ->  Dongolese
             dormouse  ->  dormice
                drama  ->  dramas|dramata
                 drum  ->  drums
                dwarf  ->  dwarves
               dynamo  ->  dynamos
                edema  ->  edemas|edemata
                eland  ->  elands|eland
                  elf  ->  elves
                  elk  ->  elks|elk
               embryo  ->  embryos
             emporium  ->  emporiums|emporia
         encephalitis  ->  encephalitises|encephalitides
             encomium  ->  encomiums|encomia
                enema  ->  enemas|enemata
               enigma  ->  enigmas|enigmata
            epidermis  ->  epidermises
           epididymis  ->  epididymises|epididymides
              erratum  ->  errata
                ethos  ->  ethoses
           eucalyptus  ->  eucalyptuses
               eunuch  ->  eunuchs
             extremum  ->  extrema
                 eyas  ->  eyases
             factotum  ->  factotums
               farman  ->  farmans
              Faroese  ->  Faroese
                fauna  ->  faunas|faunae
                  fax  ->  faxes
            Ferrarese  ->  Ferrarese
                ferry  ->  ferries
                fetus  ->  fetuses
               fiance  ->  fiances
              fiancee  ->  fiancees
               fiasco  ->  fiascos
                 fish  ->  fish
                 fizz  ->  fizzes
             flamingo  ->  flamingoes
         flittermouse  ->  flittermice
     TODO:siverb           floes  ->  floe
                flora  ->  floras|florae
             flounder  ->  flounder
                focus  ->  focuses|foci
               foetus  ->  foetuses
                folio  ->  folios
           Foochowese  ->  Foochowese
                 foot  ->  feet
     TODO:siadj          foot's  ->  feet's                         # POSSESSIVE FORM
              foramen  ->  foramens|foramina
     TODO:siverb       foreshoes  ->  foreshoe
              formula  ->  formulas|formulae
                forum  ->  forums
     TODO:siverb          fought  ->  fought
                  fox  ->  foxes
     TODO:singular_noun 2 different returns        from him  ->  from them
              from it  ->  from them                      # ACCUSATIVE
               fungus  ->  funguses|fungi
             Gabunese  ->  Gabunese
              gallows  ->  gallows
             ganglion  ->  ganglions|ganglia
                  gas  ->  gases
               gateau  ->  gateaus|gateaux
    TODO:siverb             gave  ->  gave
              general  ->  generals
        generalissimo  ->  generalissimos
             Genevese  ->  Genevese
                genie  ->  genies|genii
    TODO:singular_noun 2 diff return values!           genius  ->  geniuses|genii
              Genoese  ->  Genoese
                genus  ->  genera
               German  ->  Germans
               ghetto  ->  ghettos
           Gilbertese  ->  Gilbertese
              glottis  ->  glottises
              Goanese  ->  Goanese
                 goat  ->  goats
                goose  ->  geese
    TODO:singular_noun Governor General  ->  Governors General
                  goy  ->  goys|goyim
             graffiti  ->  graffiti
    TODO:singular_noun 2 diff ret values         graffito  ->  graffiti
              grizzly  ->  grizzlies
                guano  ->  guanos
            guardsman  ->  guardsmen
             Guianese  ->  Guianese
                gumma  ->  gummas|gummata
    TODO:siverb         gumshoes  ->  gumshoe
               gunman  ->  gunmen
            gymnasium  ->  gymnasiums|gymnasia
    TODO:siverb              had  ->  had
    TODO:siverb      had thought  ->  had thought
            Hainanese  ->  Hainanese
    TODO:siverb       hammertoes  ->  hammertoe
         handkerchief  ->  handkerchiefs
             Hararese  ->  Hararese
            Harlemese  ->  Harlemese
               harman  ->  harmans
            harmonium  ->  harmoniums
    TODO:siverb              has  ->  have
    TODO:siverb       has become  ->  have become
    TODO:siverb         has been  ->  have been
    TODO:siverb         has-been  ->  has-beens
               hasn't  ->  haven't                        # VERB FORM
             Havanese  ->  Havanese
    TODO:siverb             have  ->  have
    TODO:siverb    have conceded  ->  have conceded
    TODO:singular_noun 2 values               he  ->  they
         headquarters  ->  headquarters
            Heavenese  ->  Heavenese
                helix  ->  helices
            hepatitis  ->  hepatitises|hepatitides
    TODO:singular_noun 2 values              her  ->  them                           # PRONOUN
    TODO:singular_noun 2 values              her  ->  their
         # POSSESSIVE ADJ
                 hero  ->  heroes
               herpes  ->  herpes
    TODO:singular_noun 2 values             hers  ->  theirs
         # POSSESSIVE NOUN
    TODO:singular_noun 2 values          herself  ->  themselves
               hetman  ->  hetmans
               hiatus  ->  hiatuses|hiatus
            highlight  ->  highlights
              hijinks  ->  hijinks
    TODO:singular_noun 2 values              him  ->  them
    TODO:singular_noun 2 values          himself  ->  themselves
         hippopotamus  ->  hippopotamuses|hippopotami
           Hiroshiman  ->  Hiroshimans
    TODO:singular_noun 2 values              his  ->  their
         # POSSESSIVE ADJ
    TODO:singular_noun 2 values              his  ->  theirs
         # POSSESSIVE NOUN
    TODO:siverb             hoes  ->  hoe
           honorarium  ->  honorariums|honoraria
                 hoof  ->  hoofs|hooves
           Hoosierese  ->  Hoosierese
    TODO:siverb       horseshoes  ->  horseshoe
         Hottentotese  ->  Hottentotese
                house  ->  houses
            housewife  ->  housewives
               hubris  ->  hubrises
                human  ->  humans
             Hunanese  ->  Hunanese
                hydra  ->  hydras|hydrae
           hyperbaton  ->  hyperbata
            hyperbola  ->  hyperbolas|hyperbolae
                    I  ->  we
                 ibis  ->  ibises
            ignoramus  ->  ignoramuses
              impetus  ->  impetuses|impetus
              incubus  ->  incubuses|incubi
                index  ->  indexes|indices
          Indochinese  ->  Indochinese
              inferno  ->  infernos
             infinity  ->  infinities|infinity
          information  ->  information
              innings  ->  innings
   TODO:singular_noun Inspector General  ->  Inspectors General
          interregnum  ->  interregnums|interregna
                 iris  ->  irises|irides
       TODO:siverb            is  ->  are
       TODO:siverb      is eaten  ->  are eaten
                isn't  ->  aren't                         # VERB FORM
                   it  ->  they                           # NOMINATIVE
       TODO:siadj           its  ->  their                          # POSSESSIVE FORM
               itself  ->  themselves
           jackanapes  ->  jackanapes
             Japanese  ->  Japanese
             Javanese  ->  Javanese
                Jerry  ->  Jerrys
                jerry  ->  jerries
                 jinx  ->  jinxes
               jinxes  ->  jinx                           # VERB FORM
           Johnsonese  ->  Johnsonese
                Jones  ->  Joneses
                jumbo  ->  jumbos
             Kanarese  ->  Kanarese
           Kiplingese  ->  Kiplingese
                knife  ->  knives                         # NOUN FORM
                knife  ->  knife                          # VERB FORM (1st/2nd pers.)
               knifes  ->  knife                          # VERB FORM (3rd pers.)
             Kongoese  ->  Kongoese
            Kongolese  ->  Kongolese
               lacuna  ->  lacunas|lacunae
      lady in waiting  ->  ladies in waiting
            Lapponese  ->  Lapponese
               larynx  ->  larynxes|larynges
                latex  ->  latexes|latices
               lawman  ->  lawmen
               layman  ->  laymen
                 leaf  ->  leaves                         # NOUN FORM
                 leaf  ->  leaf                           # VERB FORM (1st/2nd pers.)
                leafs  ->  leaf                           # VERB FORM (3rd pers.)
             Lebanese  ->  Lebanese
                leman  ->  lemans
                lemma  ->  lemmas|lemmata
                 lens  ->  lenses
              Leonese  ->  Leonese
      lick of the cat  ->  licks of the cat
   Lieutenant General  ->  Lieutenant Generals
                  lie  ->  lies
                 life  ->  lives
                Liman  ->  Limans
                lingo  ->  lingos
                 loaf  ->  loaves
                locus  ->  loci
            Londonese  ->  Londonese
                 lore  ->  lores|lore
           Lorrainese  ->  Lorrainese
             lothario  ->  lotharios
                louse  ->  lice
             Lucchese  ->  Lucchese
              lumbago  ->  lumbagos
                lumen  ->  lumens|lumina
               lummox  ->  lummoxes
              lustrum  ->  lustrums|lustra
               lyceum  ->  lyceums
             lymphoma  ->  lymphomas|lymphomata
                 lynx  ->  lynxes
              Lyonese  ->  Lyonese
   TODO:            M.I.A.  ->  M.I.A.s
             Macanese  ->  Macanese
          Macassarese  ->  Macassarese
             mackerel  ->  mackerel
                macro  ->  macros
     TODO:siverb            made  ->  made
               madman  ->  madmen
             Madurese  ->  Madurese
                magma  ->  magmas|magmata
              magneto  ->  magnetos
        Major General  ->  Major Generals
           Malabarese  ->  Malabarese
              Maltese  ->  Maltese
                  man  ->  men
             mandamus  ->  mandamuses
            manifesto  ->  manifestos
               mantis  ->  mantises
              marquis  ->  marquises
                 Mary  ->  Marys
              maximum  ->  maximums|maxima
              measles  ->  measles
               medico  ->  medicos
               medium  ->  mediums|media
   TODO:siadj          medium's  ->  mediums'|media's
               medusa  ->  medusas|medusae
           memorandum  ->  memorandums|memoranda
             meniscus  ->  menisci
               merman  ->  mermen
            Messinese  ->  Messinese
        metamorphosis  ->  metamorphoses
           metropolis  ->  metropolises
                 mews  ->  mews
               miasma  ->  miasmas|miasmata
             Milanese  ->  Milanese
               milieu  ->  milieus|milieux
           millennium  ->  millenniums|millennia
              minimum  ->  minimums|minima
                 minx  ->  minxes
                 miss  ->  miss                           # VERB FORM (1st/2nd pers.)
                 miss  ->  misses                         # NOUN FORM
               misses  ->  miss                           # VERB FORM (3rd pers.)
    TODO:siverb       mistletoes  ->  mistletoe
             mittamus  ->  mittamuses
             Modenese  ->  Modenese
             momentum  ->  momentums|momenta
                money  ->  monies
             mongoose  ->  mongooses
                moose  ->  moose
        mother-in-law  ->  mothers-in-law
                mouse  ->  mice
                mumps  ->  mumps
             Muranese  ->  Muranese
                murex  ->  murices
               museum  ->  museums
            mustachio  ->  mustachios
   TODO:siadj                my  ->  our                            # POSSESSIVE FORM
               myself  ->  ourselves
               mythos  ->  mythoi
            Nakayaman  ->  Nakayamans
           Nankingese  ->  Nankingese
           nasturtium  ->  nasturtiums
            Navarrese  ->  Navarrese
               nebula  ->  nebulas|nebulae
             Nepalese  ->  Nepalese
             neuritis  ->  neuritises|neuritides
             neurosis  ->  neuroses
                 news  ->  news
                nexus  ->  nexus
              Niasese  ->  Niasese
           Nicobarese  ->  Nicobarese
               nimbus  ->  nimbuses|nimbi
            Nipponese  ->  Nipponese
                   no  ->  noes
               Norman  ->  Normans
              nostrum  ->  nostrums
             noumenon  ->  noumena
                 nova  ->  novas|novae
            nucleolus  ->  nucleoluses|nucleoli
              nucleus  ->  nuclei
                numen  ->  numina
                  oaf  ->  oafs
    TODO:siverb            oboes  ->  oboe
              occiput  ->  occiputs|occipita
               octavo  ->  octavos
              octopus  ->  octopuses|octopodes
               oedema  ->  oedemas|oedemata
            Oklahoman  ->  Oklahomans
              omnibus  ->  omnibuses
                on it  ->  on them                        # ACCUSATIVE
                 onus  ->  onuses
                opera  ->  operas
              optimum  ->  optimums|optima
                 opus  ->  opuses|opera
              organon  ->  organa
              ottoman  ->  ottomans
          ought to be  ->  ought to be                    # VERB (UNLIKE bride to be)
     TODO:siverb       overshoes  ->  overshoe
     TODO:siverb        overtoes  ->  overtoe
                 ovum  ->  ova
                   ox  ->  oxen
     TODO:siadj            ox's  ->  oxen's                         # POSSESSIVE FORM
                oxman  ->  oxmen
             oxymoron  ->  oxymorons|oxymora
              Panaman  ->  Panamans
             parabola  ->  parabolas|parabolae
              Parmese  ->  Parmese
               pathos  ->  pathoses
              pegasus  ->  pegasuses
            Pekingese  ->  Pekingese
               pelvis  ->  pelvises
             pendulum  ->  pendulums
                penis  ->  penises|penes
             penumbra  ->  penumbras|penumbrae
           perihelion  ->  perihelia
               person  ->  people|persons
              persona  ->  personae
            petroleum  ->  petroleums
              phalanx  ->  phalanxes|phalanges
                  PhD  ->  PhDs
           phenomenon  ->  phenomena
             philtrum  ->  philtrums
                photo  ->  photos
               phylum  ->  phylums|phyla
                piano  ->  pianos|piani
          Piedmontese  ->  Piedmontese
                 pika  ->  pikas
   TODO:singular_noun ret mul value            pincer  ->  pincers
              pincers  ->  pincers
            Pistoiese  ->  Pistoiese
              plateau  ->  plateaus|plateaux
                 play  ->  plays
               plexus  ->  plexuses|plexus
               pliers  ->  pliers
                plies  ->  ply                            # VERB FORM
                polis  ->  polises
             Polonese  ->  Polonese
             pontifex  ->  pontifexes|pontifices
          portmanteau  ->  portmanteaus|portmanteaux
           Portuguese  ->  Portuguese
               possum  ->  possums
               potato  ->  potatoes
                  pox  ->  pox
               pragma  ->  pragmas|pragmata
              premium  ->  premiums
          prima donna  ->  prima donnas|prime donne
                  pro  ->  pros
          proceedings  ->  proceedings
         prolegomenon  ->  prolegomena
                proof  ->  proofs
     proof of concept  ->  proofs of concept
          prosecutrix  ->  prosecutrixes|prosecutrices
           prospectus  ->  prospectuses|prospectus
            protozoan  ->  protozoans
            protozoon  ->  protozoa
                 puma  ->  pumas
     TODO:siverb             put  ->  put
              quantum  ->  quantums|quanta
 TODO:singular_noun quartermaster general  ->  quartermasters general
               quarto  ->  quartos
                 quiz  ->  quizzes
              quizzes  ->  quiz                           # VERB FORM
               quorum  ->  quorums
               rabies  ->  rabies
               radius  ->  radiuses|radii
                radix  ->  radices
               ragman  ->  ragmen
                rebus  ->  rebuses
   TODO:siverb            rehoes  ->  rehoe
             reindeer  ->  reindeer
             repo      ->  repos
   TODO:siverb           reshoes  ->  reshoe
                rhino  ->  rhinos
           rhinoceros  ->  rhinoceroses|rhinoceros
   TODO:siverb              roes  ->  roe
                  Rom  ->  Roma
            Romagnese  ->  Romagnese
                Roman  ->  Romans
             Romanese  ->  Romanese
               Romany  ->  Romanies
                romeo  ->  romeos
                 roof  ->  roofs
              rostrum  ->  rostrums|rostra
               ruckus  ->  ruckuses
               salmon  ->  salmon
            Sangirese  ->  Sangirese
   TODO: siverb              sank  ->  sank
           Sarawakese  ->  Sarawakese
              sarcoma  ->  sarcomas|sarcomata
            sassafras  ->  sassafrases
                  saw  ->  saw                            # VERB FORM (1st/2nd pers.)
                  saw  ->  saws                           # NOUN FORM
                 saws  ->  saw                            # VERB FORM (3rd pers.)
                scarf  ->  scarves
               schema  ->  schemas|schemata
             scissors  ->  scissors
     pair of scissors  ->  pairs of scissors
     pair of slippers  ->  pairs of slippers
             Scotsman  ->  Scotsmen
             sea-bass  ->  sea-bass
               seaman  ->  seamen
                 self  ->  selves
               Selman  ->  Selmans
           Senegalese  ->  Senegalese
               seraph  ->  seraphs|seraphim
               series  ->  series
    TODO:siverb        shall eat  ->  shall eat
               shaman  ->  shamans
              Shavese  ->  Shavese
            Shawanese  ->  Shawanese
    TODO:singular_noun multivalue              she  ->  they
                sheaf  ->  sheaves
               shears  ->  shears
                sheep  ->  sheep
                shelf  ->  shelves
   TODO:siverb             shoes  ->  shoe
   TODO:siverb       should have  ->  should have
              Siamese  ->  Siamese
              siemens  ->  siemens
              Sienese  ->  Sienese
            Sikkimese  ->  Sikkimese
                silex  ->  silices
              simplex  ->  simplexes|simplices
           Singhalese  ->  Singhalese
            Sinhalese  ->  Sinhalese
                sinus  ->  sinuses|sinus
                 size  ->  sizes
                sizes  ->  size                           #VERB FORM
                slice  ->  slices
             smallpox  ->  smallpox
                Smith  ->  Smiths
   TODO:siverb         snowshoes  ->  snowshoe
           Sogdianese  ->  Sogdianese
            soliloquy  ->  soliloquies
                 solo  ->  solos|soli
                 soma  ->  somas|somata
   TODO:singular_noun tough    son of a bitch  ->  sons of bitches
              Sonaman  ->  Sonamans
              soprano  ->  sopranos|soprani
   TODO:siverb            sought  ->  sought
   TODO:siverb       spattlehoes  ->  spattlehoe
              species  ->  species
             spectrum  ->  spectrums|spectra
             speculum  ->  speculums|specula
   TODO:siverb             spent  ->  spent
         spermatozoon  ->  spermatozoa
               sphinx  ->  sphinxes|sphinges
         spokesperson  ->  spokespeople|spokespersons
              stadium  ->  stadiums|stadia
               stamen  ->  stamens|stamina
               status  ->  statuses|status
               stereo  ->  stereos
               stigma  ->  stigmas|stigmata
             stimulus  ->  stimuli
                stoma  ->  stomas|stomata
              stomach  ->  stomachs
               storey  ->  storeys
                story  ->  stories
              stratum  ->  strata
               strife  ->  strifes
                stylo  ->  stylos
               stylus  ->  styluses|styli
             succubus  ->  succubuses|succubi
             Sudanese  ->  Sudanese
               suffix  ->  suffixes
            Sundanese  ->  Sundanese
             superior  ->  superiors
               supply  ->  supplies
  TODO:singular_noun    Surgeon-General  ->  Surgeons-General
              surplus  ->  surpluses
            Swahilese  ->  Swahilese
                swine  ->  swines|swine
      TODO:singular_noun multiple return        syringe  ->  syringes
               syrinx  ->  syrinxes|syringes
              tableau  ->  tableaus|tableaux
                 taco  ->  tacos
              Tacoman  ->  Tacomans
              talouse  ->  talouses
               tattoo  ->  tattoos
               taxman  ->  taxmen
                tempo  ->  tempos|tempi
           Tenggerese  ->  Tenggerese
            testatrix  ->  testatrixes|testatrices
               testes  ->  testes
      TODO:singular_noun multiple return         testis  ->  testes
      TODO:siadj           that  ->  those
      TODO:siadj          their  ->  their
           # POSSESSIVE FORM (GENDER-INCLUSIVE)
      TODO:singular_noun multiple return       themself  ->  themselves
           # ugly but gaining currency
      TODO:singular_noun multiple return           they  ->  they
           # for indeterminate gender
                thief  ->  thiefs|thieves
      TODO:siadj           this  ->  these
              thought  ->  thoughts                       # NOUN FORM
              thought  ->  thought                        # VERB FORM
      TODO:siverb         throes  ->  throe
      TODO:siverb   ticktacktoes  ->  ticktacktoe
                Times  ->  Timeses
             Timorese  ->  Timorese
      TODO:siverb        tiptoes  ->  tiptoe
             Tirolese  ->  Tirolese
             titmouse  ->  titmice
      TODO:singular_noun multivalue         to her  ->  to them
      TODO:singular_noun multivalue     to herself  ->  to themselves
      TODO:singular_noun multivalue         to him  ->  to them
      TODO:singular_noun multivalue     to himself  ->  to themselves
                to it  ->  to them
                to it  ->  to them                        # ACCUSATIVE
            to itself  ->  to themselves
                to me  ->  to us
            to myself  ->  to ourselves
      TODO:singular_noun multivalue        to them  ->  to them
           # for indeterminate gender
      TODO:singular_noun multivalue    to themself  ->  to themselves
            # ugly but gaining currency
               to you  ->  to you
          to yourself  ->  to yourselves
            Tocharese  ->  Tocharese
      TODO:siverb           toes  ->  toe
               tomato  ->  tomatoes
            Tonkinese  ->  Tonkinese
          tonsillitis  ->  tonsillitises|tonsillitides
                tooth  ->  teeth
             Torinese  ->  Torinese
                torus  ->  toruses|tori
            trapezium  ->  trapeziums|trapezia
               trauma  ->  traumas|traumata
              travois  ->  travois
              tranche  ->  tranches
              trellis  ->  trellises
     TODO:siverb           tries  ->  try
               trilby  ->  trilbys
             trousers  ->  trousers
            trousseau  ->  trousseaus|trousseaux
                trout  ->  trout
      TODO:siverb            try  ->  tries
                 tuna  ->  tuna
                 turf  ->  turfs|turves
             Tyrolese  ->  Tyrolese
            ultimatum  ->  ultimatums|ultimata
            umbilicus  ->  umbilicuses|umbilici
                umbra  ->  umbras|umbrae
      TODO:siverb     undershoes  ->  undershoe
      TODO:siverb        unshoes  ->  unshoe
               uterus  ->  uteruses|uteri
               vacuum  ->  vacuums|vacua
               vellum  ->  vellums
                velum  ->  velums|vela
           Vermontese  ->  Vermontese
             Veronese  ->  Veronese
             vertebra  ->  vertebrae
               vertex  ->  vertexes|vertices
             Viennese  ->  Viennese
           Vietnamese  ->  Vietnamese
             virtuoso  ->  virtuosos|virtuosi
                virus  ->  viruses
				 vita  ->  vitae
                vixen  ->  vixens
               vortex  ->  vortexes|vortices
               walrus  ->  walruses
   TODO:siverb               was  ->  were
   TODO:siverb    was faced with  ->  were faced with
   TODO:siverb        was hoping  ->  were hoping
           Wenchowese  ->  Wenchowese
   TODO:siverb              were  ->  were
   TODO:siverb        were found  ->  were found
                wharf  ->  wharves
              whiting  ->  whiting
           Whitmanese  ->  Whitmanese
                 whiz  ->  whizzes
   TODO:singular_noun multivalue             whizz  ->  whizzes
               widget  ->  widgets
                 wife  ->  wives
           wildebeest  ->  wildebeests|wildebeest
                 will  ->  will                           # VERB FORM
                 will  ->  wills                          # NOUN FORM
             will eat  ->  will eat                       # VERB FORM
                wills  ->  will                           # VERB FORM
                 wish  ->  wishes
   TODO:singular_noun multivalue          with him  ->  with them
              with it  ->  with them                      # ACCUSATIVE
   TODO:siverb              woes  ->  woe
                 wolf  ->  wolves
                woman  ->  women
   woman of substance  ->  women of substance
    TODO:siadj          woman's  ->  women's                        # POSSESSIVE FORM
                won't  ->  won't                          # VERB FORM
            woodlouse  ->  woodlice
              Yakiman  ->  Yakimans
             Yengeese  ->  Yengeese
               yeoman  ->  yeomen
             yeowoman  ->  yeowomen
                  yes  ->  yeses
            Yokohaman  ->  Yokohamans
                  you  ->  you
   TODO:siadj              your  ->  your                           # POSSESSIVE FORM
             yourself  ->  yourselves
                Yuman  ->  Yumans
            Yunnanese  ->  Yunnanese
                 zero  ->  zeros
                 zoon  ->  zoa


================================================
FILE: tests/test_an.py
================================================
import inflect


def test_an():
    p = inflect.engine()

    assert p.an("cat") == "a cat"
    assert p.an("ant") == "an ant"
    assert p.an("a") == "an a"
    assert p.an("b") == "a b"
    assert p.an("honest cat") == "an honest cat"
    assert p.an("dishonest cat") == "a dishonest cat"
    assert p.an("Honolulu sunset") == "a Honolulu sunset"
    assert p.an("mpeg") == "an mpeg"
    assert p.an("onetime holiday") == "a onetime holiday"
    assert p.an("Ugandan person") == "a Ugandan person"
    assert p.an("Ukrainian person") == "a Ukrainian person"
    assert p.an("Unabomber") == "a Unabomber"
    assert p.an("unanimous decision") == "a unanimous decision"
    assert p.an("US farmer") == "a US farmer"
    assert p.an("wild PIKACHU appeared") == "a wild PIKACHU appeared"


def test_an_abbreviation():
    p = inflect.engine()

    assert p.an("YAML code block") == "a YAML code block"
    assert p.an("Core ML function") == "a Core ML function"
    assert p.an("JSON code block") == "a JSON code block"


================================================
FILE: tests/test_classical_all.py
================================================
import inflect


class Test:
    def test_classical(self):
        p = inflect.engine()

        # DEFAULT...

        assert p.plural_noun("error", 0) == "errors", "classical 'zero' not active"
        assert p.plural_noun("wildebeest") == "wildebeests", (
            "classical 'herd' not active"
        )
        assert p.plural_noun("Sally") == "Sallys", "classical 'names' active"
        assert p.plural_noun("brother") == "brothers", "classical others not active"
        assert p.plural_noun("person") == "people", "classical 'persons' not active"
        assert p.plural_noun("formula") == "formulas", "classical 'ancient' not active"

        # CLASSICAL PLURALS ACTIVATED...

        p.classical(all=True)
        assert p.plural_noun("error", 0) == "error", "classical 'zero' active"
        assert p.plural_noun("wildebeest") == "wildebeest", "classical 'herd' active"
        assert p.plural_noun("Sally") == "Sallys", "classical 'names' active"
        assert p.plural_noun("brother") == "brethren", "classical others active"
        assert p.plural_noun("person") == "persons", "classical 'persons' active"
        assert p.plural_noun("formula") == "formulae", "classical 'ancient' active"

        # CLASSICAL PLURALS DEACTIVATED...

        p.classical(all=False)
        assert p.plural_noun("error", 0) == "errors", "classical 'zero' not active"
        assert p.plural_noun("wildebeest") == "wildebeests", (
            "classical 'herd' not active"
        )
        assert p.plural_noun("Sally") == "Sallies", "classical 'names' not active"
        assert p.plural_noun("brother") == "brothers", "classical others not active"
        assert p.plural_noun("person") == "people", "classical 'persons' not active"
        assert p.plural_noun("formula") == "formulas", "classical 'ancient' not active"

        # CLASSICAL PLURALS REREREACTIVATED...

        p.classical()
        assert p.plural_noun("error", 0) == "error", "classical 'zero' active"
        assert p.plural_noun("wildebeest") == "wildebeest", "classical 'herd' active"
        assert p.plural_noun("Sally") == "Sallys", "classical 'names' active"
        assert p.plural_noun("brother") == "brethren", "classical others active"
        assert p.plural_noun("person") == "persons", "classical 'persons' active"
        assert p.plural_noun("formula") == "formulae", "classical 'ancient' active"


================================================
FILE: tests/test_classical_ancient.py
================================================
import inflect


def test_ancient_1():
    p = inflect.engine()

    # DEFAULT...

    assert p.plural_noun("formula") == "formulas"

    # "person" PLURALS ACTIVATED...

    p.classical(ancient=True)
    assert p.plural_noun("formula") == "formulae"

    # OTHER CLASSICALS NOT ACTIVATED...

    assert p.plural_noun("wildebeest") == "wildebeests"
    assert p.plural_noun("error", 0) == "errors"
    assert p.plural_noun("Sally") == "Sallys"
    assert p.plural_noun("brother") == "brothers"
    assert p.plural_noun("person") == "people"


================================================
FILE: tests/test_classical_herd.py
================================================
import inflect


def test_ancient_1():
    p = inflect.engine()

    # DEFAULT...

    assert p.plural_noun("wildebeest") == "wildebeests"

    # "person" PLURALS ACTIVATED...

    p.classical(herd=True)
    assert p.plural_noun("wildebeest") == "wildebeest"

    # OTHER CLASSICALS NOT ACTIVATED...

    assert p.plural_noun("formula") == "formulas"
    assert p.plural_noun("error", 0) == "errors"
    assert p.plural_noun("Sally") == "Sallys"
    assert p.plural_noun("brother") == "brothers"
    assert p.plural_noun("person") == "people"


================================================
FILE: tests/test_classical_names.py
================================================
import inflect


def test_ancient_1():
    p = inflect.engine()

    # DEFAULT...

    assert p.plural_noun("Sally") == "Sallys"
    assert p.plural_noun("Jones", 0) == "Joneses"

    # "person" PLURALS ACTIVATED...

    p.classical(names=True)
    assert p.plural_noun("Sally") == "Sallys"
    assert p.plural_noun("Jones", 0) == "Joneses"

    # OTHER CLASSICALS NOT ACTIVATED...

    assert p.plural_noun("wildebeest") == "wildebeests"
    assert p.plural_noun("formula") == "formulas"
    assert p.plural_noun("error", 0) == "errors"
    assert p.plural_noun("brother") == "brothers"
    assert p.plural_noun("person") == "people"


================================================
FILE: tests/test_classical_person.py
================================================
import inflect


def test_ancient_1():
    p = inflect.engine()

    # DEFAULT...

    assert p.plural_noun("person") == "people"

    # "person" PLURALS ACTIVATED...

    p.classical(persons=True)
    assert p.plural_noun("person") == "persons"

    # OTHER CLASSICALS NOT ACTIVATED...

    assert p.plural_noun("wildebeest") == "wildebeests"
    assert p.plural_noun("formula") == "formulas"
    assert p.plural_noun("error", 0) == "errors"
    assert p.plural_noun("brother") == "brothers"
    assert p.plural_noun("Sally") == "Sallys"
    assert p.plural_noun("Jones", 0) == "Joneses"


================================================
FILE: tests/test_classical_zero.py
================================================
import inflect


def test_ancient_1():
    p = inflect.engine()

    # DEFAULT...

    assert p.plural_noun("error", 0) == "errors"

    # "person" PLURALS ACTIVATED...

    p.classical(zero=True)
    assert p.plural_noun("error", 0) == "error"

    # OTHER CLASSICALS NOT ACTIVATED...

    assert p.plural_noun("wildebeest") == "wildebeests"
    assert p.plural_noun("formula") == "formulas"
    assert p.plural_noun("person") == "people"
    assert p.plural_noun("brother") == "brothers"
    assert p.plural_noun("Sally") == "Sallys"


================================================
FILE: tests/test_compounds.py
================================================
import inflect

p = inflect.engine()


def test_compound_1():
    assert p.singular_noun("hello-out-there") == "hello-out-there"


def test_compound_2():
    assert p.singular_noun("hello out there") == "hello out there"


def test_compound_3():
    assert p.singular_noun("continue-to-operate") == "continue-to-operate"


def test_compound_4():
    assert p.singular_noun("case of diapers") == "case of diapers"


def test_unit_handling_degree():
    test_cases = {
        "degree celsius": "degrees celsius",
        # 'degree Celsius': 'degrees Celsius',
        "degree fahrenheit": "degrees fahrenheit",
        "degree rankine": "degrees rankine",
        "degree fahrenheit second": "degree fahrenheit seconds",
    }
    for singular, plural in test_cases.items():
        assert p.plural(singular) == plural


def test_unit_handling_fractional():
    test_cases = {
        "pound per square inch": "pounds per square inch",
        "metre per second": "metres per second",
        "kilometre per hour": "kilometres per hour",
        "cubic metre per second": "cubic metres per second",
        "dollar a year": "dollars a year",
        # Correct pluralization of denominator
        "foot per square second": "feet per square second",
        "mother-in-law per lifetime": "mothers-in-law per lifetime",
        "pound-force per square inch": "pounds-force per square inch",
    }
    for singular, plural in test_cases.items():
        assert p.plural(singular) == plural


def test_unit_handling_combined():
    test_cases = {
        # Heat transfer coefficient unit
        "watt per square meter degree celsius": "watts per square meter degree celsius",
        "degree celsius per hour": "degrees celsius per hour",
        "degree fahrenheit hour square foot per btuit inch": (
            "degree fahrenheit hour square feet per btuit inch"
        ),
        # 'degree Celsius per hour': 'degrees Celsius per hour',
        # 'degree Fahrenheit hour square foot per BtuIT inch':
        #   'degree Fahrenheit hour square feet per BtuIT inch'
    }
    for singular, plural in test_cases.items():
        assert p.plural(singular) == plural


def test_unit_open_compound_nouns():
    test_cases = {
        "high school": "high schools",
        "master genie": "master genies",
        "MASTER genie": "MASTER genies",
        "Blood brother": "Blood brothers",
        "prima donna": "prima donnas",
        "prima DONNA": "prima DONNAS",
    }
    for singular, plural in test_cases.items():
        assert p.plural(singular) == plural


def test_unit_open_compound_nouns_classical():
    p.classical(all=True)
    test_cases = {
        "master genie": "master genii",
        "MASTER genie": "MASTER genii",
        "Blood brother": "Blood brethren",
        "prima donna": "prime donne",
        "prima DONNA": "prime DONNE",
    }
    for singular, plural in test_cases.items():
        assert p.plural(singular) == plural
    p.classical(all=False)


================================================
FILE: tests/test_inflections.py
================================================
import os

import pytest

import inflect


def is_eq(p, a, b):
    return (
        p.compare(a, b)
        or p.plnounequal(a, b)
        or p.plverbequal(a, b)
        or p.pladjequal(a, b)
    )


def test_many():  # noqa: C901
    p = inflect.engine()

    data = get_data()

    for line in data:
        if "TODO:" in line:
            continue
        try:
            singular, rest = line.split("->", 1)
        except ValueError:
            continue
        singular = singular.strip()
        rest = rest.strip()
        try:
            plural, comment = rest.split("#", 1)
        except ValueError:
            plural = rest.strip()
            comment = ""
        try:
            mod_plural, class_plural = plural.split("|", 1)
            mod_plural = mod_plural.strip()
            class_plural = class_plural.strip()
        except ValueError:
            mod_plural = class_plural = plural.strip()
        if "verb" in comment.lower():
            is_nv = "_V"
        elif "noun" in comment.lower():
            is_nv = "_N"
        else:
            is_nv = ""

        p.classical(all=0, names=0)
        mod_PL_V = p.plural_verb(singular)
        mod_PL_N = p.plural_noun(singular)
        mod_PL = p.plural(singular)
        if is_nv == "_V":
            mod_PL_val = mod_PL_V
        elif is_nv == "_N":
            mod_PL_val = mod_PL_N
        else:
            mod_PL_val = mod_PL

        p.classical(all=1)
        class_PL_V = p.plural_verb(singular)
        class_PL_N = p.plural_noun(singular)
        class_PL = p.plural(singular)
        if is_nv == "_V":
            class_PL_val = class_PL_V
        elif is_nv == "_N":
            class_PL_val = class_PL_N
        else:
            class_PL_val = class_PL

        check_all(
            p, is_nv, singular, mod_PL_val, class_PL_val, mod_plural, class_plural
        )


def check_all(p, is_nv, singular, mod_PL_val, class_PL_val, mod_plural, class_plural):
    assert mod_plural == mod_PL_val
    assert class_plural == class_PL_val
    assert is_eq(p, singular, mod_plural) in ("s:p", "p:s", "eq")
    assert is_eq(p, mod_plural, singular) in ("p:s", "s:p", "eq")
    assert is_eq(p, singular, class_plural) in ("s:p", "p:s", "eq")
    assert is_eq(p, class_plural, singular) in ("p:s", "s:p", "eq")
    assert singular != ""
    expected = mod_PL_val if class_PL_val else f"{mod_PL_val}|{class_PL_val}"
    assert mod_PL_val == expected

    if is_nv != "_V":
        assert p.singular_noun(mod_plural, 1) == singular

        assert p.singular_noun(class_plural, 1) == singular


def test_def():
    p = inflect.engine()

    p.defnoun("kin", "kine")
    p.defnoun("(.*)x", "$1xen")

    p.defverb("foobar", "feebar", "foobar", "feebar", "foobars", "feebar")

    p.defadj("red", "red|gules")

    assert p.no("kin", 0) == "no kine"
    assert p.no("kin", 1) == "1 kin"
    assert p.no("kin", 2) == "2 kine"

    assert p.no("regex", 0) == "no regexen"

    assert p.plural("foobar", 2) == "feebar"
    assert p.plural("foobars", 2) == "feebar"

    assert p.plural("red", 0) == "red"
    assert p.plural("red", 1) == "red"
    assert p.plural("red", 2) == "red"
    p.classical(all=True)
    assert p.plural("red", 0) == "red"
    assert p.plural("red", 1) == "red"
    assert p.plural("red", 2) == "gules"


def test_ordinal():
    p = inflect.engine()
    assert p.ordinal(0) == "0th"
    assert p.ordinal(1) == p.ordinal("1") == "1st"
    assert p.ordinal(2) == "2nd"
    assert p.ordinal(3) == "3rd"
    assert p.ordinal(4) == "4th"
    assert p.ordinal(5) == "5th"
    assert p.ordinal(6) == "6th"
    assert p.ordinal(7) == "7th"
    assert p.ordinal(8) == "8th"
    assert p.ordinal(9) == "9th"
    assert p.ordinal(10) == "10th"
    assert p.ordinal(11) == "11th"
    assert p.ordinal(12) == "12th"
    assert p.ordinal(13) == "13th"
    assert p.ordinal(14) == "14th"
    assert p.ordinal(15) == "15th"
    assert p.ordinal(16) == "16th"
    assert p.ordinal(17) == "17th"
    assert p.ordinal(18) == "18th"
    assert p.ordinal(19) == "19th"
    assert p.ordinal(20) == "20th"
    assert p.ordinal(21) == "21st"
    assert p.ordinal(22) == "22nd"
    assert p.ordinal(23) == "23rd"
    assert p.ordinal(24) == "24th"
    assert p.ordinal(100) == "100th"
    assert p.ordinal(101) == "101st"
    assert p.ordinal(102) == "102nd"
    assert p.ordinal(103) == "103rd"
    assert p.ordinal(104) == "104th"

    assert p.ordinal(1.1) == p.ordinal("1.1") == "1.1st"
    assert p.ordinal(1.2) == "1.2nd"
    assert p.ordinal(5.502) == "5.502nd"

    assert p.ordinal("zero") == "zeroth"
    assert p.ordinal("one") == "first"
    assert p.ordinal("two") == "second"
    assert p.ordinal("three") == "third"
    assert p.ordinal("four") == "fourth"
    assert p.ordinal("five") == "fifth"
    assert p.ordinal("six") == "sixth"
    assert p.ordinal("seven") == "seventh"
    assert p.ordinal("eight") == "eighth"
    assert p.ordinal("nine") == "ninth"
    assert p.ordinal("ten") == "tenth"
    assert p.ordinal("eleven") == "eleventh"
    assert p.ordinal("twelve") == "twelfth"
    assert p.ordinal("thirteen") == "thirteenth"
    assert p.ordinal("fourteen") == "fourteenth"
    assert p.ordinal("fifteen") == "fifteenth"
    assert p.ordinal("sixteen") == "sixteenth"
    assert p.ordinal("seventeen") == "seventeenth"
    assert p.ordinal("eighteen") == "eighteenth"
    assert p.ordinal("nineteen") == "nineteenth"
    assert p.ordinal("twenty") == "twentieth"
    assert p.ordinal("twenty-one") == "twenty-first"
    assert p.ordinal("twenty-two") == "
Download .txt
gitextract_jpl9oo1o/

├── .coveragerc
├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── NEWS.rst
├── README.rst
├── SECURITY.md
├── docs/
│   ├── conf.py
│   ├── history.rst
│   └── index.rst
├── inflect/
│   ├── __init__.py
│   ├── compat/
│   │   ├── __init__.py
│   │   └── py38.py
│   └── py.typed
├── mypy.ini
├── pyproject.toml
├── pytest.ini
├── ruff.toml
├── tea.yaml
├── tests/
│   ├── inflections.txt
│   ├── test_an.py
│   ├── test_classical_all.py
│   ├── test_classical_ancient.py
│   ├── test_classical_herd.py
│   ├── test_classical_names.py
│   ├── test_classical_person.py
│   ├── test_classical_zero.py
│   ├── test_compounds.py
│   ├── test_inflections.py
│   ├── test_join.py
│   ├── test_numwords.py
│   ├── test_pl_si.py
│   ├── test_pwd.py
│   └── test_unicode.py
├── towncrier.toml
└── tox.ini
Download .txt
SYMBOL INDEX (170 symbols across 15 files)

FILE: inflect/__init__.py
  class UnknownClassicalModeError (line 86) | class UnknownClassicalModeError(Exception):
  class BadNumValueError (line 90) | class BadNumValueError(Exception):
  class BadChunkingOptionError (line 94) | class BadChunkingOptionError(Exception):
  class NumOutOfRangeError (line 98) | class NumOutOfRangeError(Exception):
  class BadUserDefinedPatternError (line 102) | class BadUserDefinedPatternError(Exception):
  class BadRcFileError (line 106) | class BadRcFileError(Exception):
  class BadGenderError (line 110) | class BadGenderError(Exception):
  function enclose (line 114) | def enclose(s: str) -> str:
  function joinstem (line 118) | def joinstem(cutpoint: Optional[int] = 0, words: Optional[Iterable[str]]...
  function bysize (line 139) | def bysize(words: Iterable[str]) -> Dict[int, set]:
  function make_pl_si_lists (line 156) | def make_pl_si_lists(
  function get_si_pron (line 1734) | def get_si_pron(thecase, word, gender) -> str:
  class Words (line 2039) | class Words(str):
    method __init__ (line 2045) | def __init__(self, orig) -> None:
  class _WordMeta (line 2063) | class _WordMeta(type):  # Too dynamic to be supported by mypy...
    method __instancecheck__ (line 2064) | def __instancecheck__(self, instance: Any) -> bool:
  class Word (line 2067) | class Word(metaclass=_WordMeta):  # type: ignore[no-redef]
  class engine (line 2071) | class engine:
    method __init__ (line 2072) | def __init__(self) -> None:
    method _number_args (line 2085) | def _number_args(self):
    method _number_args (line 2089) | def _number_args(self, val):
    method defnoun (line 2093) | def defnoun(self, singular: Optional[Word], plural: Optional[Word]) ->...
    method defverb (line 2105) | def defverb(
    method defadj (line 2130) | def defadj(self, singular: Optional[Word], plural: Optional[Word]) -> ...
    method defa (line 2141) | def defa(self, pattern: Optional[Word]) -> int:
    method defan (line 2151) | def defan(self, pattern: Optional[Word]) -> int:
    method checkpat (line 2160) | def checkpat(self, pattern: Optional[Word]) -> None:
    method checkpatplural (line 2171) | def checkpatplural(self, pattern: Optional[Word]) -> None:
    method ud_match (line 2178) | def ud_match(self, word: Word, wordlist: Sequence[Optional[Word]]) -> ...
    method classical (line 2190) | def classical(self, **kwargs) -> None:
    method num (line 2224) | def num(
    method gender (line 2246) | def gender(self, gender: str) -> None:
    method _get_value_from_ast (line 2263) | def _get_value_from_ast(self, obj):
    method _string_to_substitute (line 2279) | def _string_to_substitute(
    method inflect (line 2312) | def inflect(self, text: Word) -> str:
    method postprocess (line 2351) | def postprocess(self, orig: str, inflected) -> str:
    method partition_word (line 2381) | def partition_word(self, text: str) -> Tuple[str, str, str]:
    method plural (line 2389) | def plural(self, text: Word, count: Optional[Union[str, int, Any]] = N...
    method plural_noun (line 2413) | def plural_noun(
    method plural_verb (line 2434) | def plural_verb(
    method plural_adj (line 2458) | def plural_adj(
    method compare (line 2479) | def compare(self, word1: Word, word2: Word) -> Union[str, bool]:
    method compare_nouns (line 2508) | def compare_nouns(self, word1: Word, word2: Word) -> Union[str, bool]:
    method compare_verbs (line 2524) | def compare_verbs(self, word1: Word, word2: Word) -> Union[str, bool]:
    method compare_adjs (line 2540) | def compare_adjs(self, word1: Word, word2: Word) -> Union[str, bool]:
    method singular_noun (line 2556) | def singular_noun(
    method _plequal (line 2597) | def _plequal(self, word1: str, word2: str, pl) -> Union[str, bool]:
    method _pl_reg_plurals (line 2619) | def _pl_reg_plurals(self, pair: str, stems: str, end1: str, end2: str)...
    method _pl_check_plurals_N (line 2623) | def _pl_check_plurals_N(self, word1: str, word2: str) -> bool:
    method _pl_check_plurals_adj (line 2656) | def _pl_check_plurals_adj(self, word1: str, word2: str) -> bool:
    method get_count (line 2669) | def get_count(self, count: Optional[Union[str, int]] = None) -> Union[...
    method _plnoun (line 2690) | def _plnoun(  # noqa: C901
    method _handle_prepositional_phrase (line 2980) | def _handle_prepositional_phrase(cls, phrase, transform, sep):
    method _handle_long_compounds (line 3005) | def _handle_long_compounds(self, word: Words, count: int) -> Union[str...
    method _find_pivot (line 3030) | def _find_pivot(words, candidates):
    method _pl_special_verb (line 3039) | def _pl_special_verb(  # noqa: C901
    method _pl_general_verb (line 3121) | def _pl_general_verb(
    method _pl_special_adjective (line 3145) | def _pl_special_adjective(
    method _sinoun (line 3182) | def _sinoun(  # noqa: C901
    method a (line 3510) | def a(self, text: Word, count: Optional[Union[int, str, Any]] = 1) -> ...
    method _indef_article (line 3566) | def _indef_article(self, word: str, count: Union[int, str, Any]) -> str:
    method no (line 3591) | def no(self, text: Word, count: Optional[Union[int, str]] = None) -> str:
    method present_participle (line 3629) | def present_participle(self, word: Word) -> str:
    method ordinal (line 3648) | def ordinal(self, num: Union[Number, Word]) -> str:
    method millfn (line 3681) | def millfn(self, ind: int = 0) -> str:
    method unitfn (line 3686) | def unitfn(self, units: int, mindex: int = 0) -> str:
    method tenfn (line 3689) | def tenfn(self, tens, units, mindex=0) -> str:
    method hundfn (line 3701) | def hundfn(self, hundreds: int, tens: int, units: int, mindex: int) ->...
    method group1sub (line 3713) | def group1sub(self, mo: Match) -> str:
    method group1bsub (line 3722) | def group1bsub(self, mo: Match) -> str:
    method group2sub (line 3729) | def group2sub(self, mo: Match) -> str:
    method group3sub (line 3738) | def group3sub(self, mo: Match) -> str:
    method hundsub (line 3756) | def hundsub(self, mo: Match) -> str:
    method tensub (line 3763) | def tensub(self, mo: Match) -> str:
    method unitsub (line 3766) | def unitsub(self, mo: Match) -> str:
    method enword (line 3769) | def enword(self, num: str, group: int) -> str:
    method _sub_ord (line 3799) | def _sub_ord(val):
    method _chunk_num (line 3804) | def _chunk_num(cls, num, decimal, group):
    method _remove_last_blank (line 3813) | def _remove_last_blank(chunks):
    method _get_sign (line 3824) | def _get_sign(num):
    method number_to_words (line 3828) | def number_to_words(  # noqa: C901
    method _render (line 3943) | def _render(chunks, decimal, comma):
    method join (line 3955) | def join(

FILE: tests/test_an.py
  function test_an (line 4) | def test_an():
  function test_an_abbreviation (line 24) | def test_an_abbreviation():

FILE: tests/test_classical_all.py
  class Test (line 4) | class Test:
    method test_classical (line 5) | def test_classical(self):

FILE: tests/test_classical_ancient.py
  function test_ancient_1 (line 4) | def test_ancient_1():

FILE: tests/test_classical_herd.py
  function test_ancient_1 (line 4) | def test_ancient_1():

FILE: tests/test_classical_names.py
  function test_ancient_1 (line 4) | def test_ancient_1():

FILE: tests/test_classical_person.py
  function test_ancient_1 (line 4) | def test_ancient_1():

FILE: tests/test_classical_zero.py
  function test_ancient_1 (line 4) | def test_ancient_1():

FILE: tests/test_compounds.py
  function test_compound_1 (line 6) | def test_compound_1():
  function test_compound_2 (line 10) | def test_compound_2():
  function test_compound_3 (line 14) | def test_compound_3():
  function test_compound_4 (line 18) | def test_compound_4():
  function test_unit_handling_degree (line 22) | def test_unit_handling_degree():
  function test_unit_handling_fractional (line 34) | def test_unit_handling_fractional():
  function test_unit_handling_combined (line 50) | def test_unit_handling_combined():
  function test_unit_open_compound_nouns (line 66) | def test_unit_open_compound_nouns():
  function test_unit_open_compound_nouns_classical (line 79) | def test_unit_open_compound_nouns_classical():

FILE: tests/test_inflections.py
  function is_eq (line 8) | def is_eq(p, a, b):
  function test_many (line 17) | def test_many():  # noqa: C901
  function check_all (line 76) | def check_all(p, is_nv, singular, mod_PL_val, class_PL_val, mod_plural, ...
  function test_def (line 93) | def test_def():
  function test_ordinal (line 121) | def test_ordinal():
  function test_decimal_ordinals (line 190) | def test_decimal_ordinals():
  function test_prespart (line 203) | def test_prespart():
  function test_inflect_on_tuples (line 213) | def test_inflect_on_tuples():
  function test_inflect_on_builtin_constants (line 225) | def test_inflect_on_builtin_constants():
  function test_inflect_keyword_args (line 240) | def test_inflect_keyword_args():
  function test_NameError_in_strings (line 258) | def test_NameError_in_strings():
  function get_data (line 265) | def get_data():

FILE: tests/test_join.py
  function test_join (line 4) | def test_join():

FILE: tests/test_numwords.py
  function test_loop (line 4) | def test_loop():
  function test_lines (line 19) | def test_lines():
  function test_array (line 47) | def test_array():
  function go (line 378) | def go(p, i):
  function test_issue_131 (line 395) | def test_issue_131():

FILE: tests/test_pl_si.py
  function classical (line 7) | def classical(request):
  function test_pl_si (line 12) | def test_pl_si(classical, word):

FILE: tests/test_pwd.py
  class Test (line 16) | class Test:
    method test_enclose (line 17) | def test_enclose(self):
    method test_joinstem (line 21) | def test_joinstem(self):
    method test_classical (line 28) | def test_classical(self):
    method test_num (line 77) | def test_num(self):
    method test_inflect (line 107) | def test_inflect(self):
    method test_user_input_fns (line 137) | def test_user_input_fns(self):
    method test_user_input_defverb (line 192) | def test_user_input_defverb(self):
    method test_user_input_defverb_compare (line 199) | def test_user_input_defverb_compare(self):
    method test_postprocess (line 205) | def test_postprocess(self):
    method test_partition_word (line 221) | def test_partition_word(self):
    method test_pl (line 238) | def test_pl(self):
    method test_sinoun (line 302) | def test_sinoun(self):
    method test_gender (line 334) | def test_gender(self):
    method test_compare_simple (line 444) | def test_compare_simple(self, sing, plur, res):
    method test_compare_nouns (line 463) | def test_compare_nouns(self, sing, plur, res):
    method test_compare_verbs (line 474) | def test_compare_verbs(self, sing, plur, res):
    method test_compare_adjectives (line 494) | def test_compare_adjectives(self, sing, plur, res):
    method test_compare_your_our (line 498) | def test_compare_your_our(self):
    method test__pl_reg_plurals (line 505) | def test__pl_reg_plurals(self):
    method test__pl_check_plurals_N (line 514) | def test__pl_check_plurals_N(self):
    method test__pl_check_plurals_adj (line 522) | def test__pl_check_plurals_adj(self):
    method test_count (line 533) | def test_count(self):
    method test__plnoun (line 562) | def test__plnoun(self):
    method test_plnoun_retains_case (line 688) | def test_plnoun_retains_case(self, sing, plur):
    method test_classical_pl (line 691) | def test_classical_pl(self):
    method test__pl_special_verb (line 697) | def test__pl_special_verb(self):
    method test__pl_general_verb (line 730) | def test__pl_general_verb(self):
    method test__pl_special_adjective (line 764) | def test__pl_special_adjective(self, adj, plur):
    method test_a (line 814) | def test_a(self, sing, plur):
    method test_a_alt (line 818) | def test_a_alt(self):
    method test_a_and_an_same_method (line 826) | def test_a_and_an_same_method(self):
    method test_no (line 831) | def test_no(self):
    method test_prespart (line 865) | def test_prespart(self, sing, plur):
    method test_ordinal (line 893) | def test_ordinal(self, num, ord):
    method test_millfn (line 897) | def test_millfn(self):
    method test_unitfn (line 908) | def test_unitfn(self):
    method test_tenfn (line 917) | def test_tenfn(self):
    method test_hundfn (line 928) | def test_hundfn(self):
    method test_enword (line 938) | def test_enword(self):
    method test_enword_number_args_override (line 975) | def test_enword_number_args_override(self):
    method test_numwords (line 980) | def test_numwords(self):
    method test_numwords_group_chunking_error (line 1053) | def test_numwords_group_chunking_error(self):
    method test_numwords_group (line 1105) | def test_numwords_group(self, input, kwargs, expect):
    method test_wordlist (line 1109) | def test_wordlist(self):
    method test_doc_examples (line 1165) | def test_doc_examples(self):
    method test_unknown_method (line 1228) | def test_unknown_method(self):

FILE: tests/test_unicode.py
  class TestUnicode (line 4) | class TestUnicode:
    method test_unicode_plural (line 7) | def test_unicode_plural(self):
Condensed preview — 39 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (282K chars).
[
  {
    "path": ".coveragerc",
    "chars": 357,
    "preview": "[run]\nomit =\n\t# leading `*/` for pytest-dev/pytest-cov#456\n\t*/.tox/*\ndisable_warnings =\n\tcouldnt-parse\n\n[report]\nshow_mi"
  },
  {
    "path": ".editorconfig",
    "chars": 246,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = tab\nindent_size = 4\ninsert_final_newline = true\nend_of_line = lf\n\n[*.py]"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 23,
    "preview": "tidelift: pypi/inflect\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 3334,
    "preview": "name: tests\n\non:\n  merge_group:\n  push:\n    branches-ignore:\n    # temporary GH branches relating to merge queues (jarac"
  },
  {
    "path": ".gitignore",
    "chars": 68,
    "preview": "*.pyc\nMANIFEST\ndist/*\ncover/*\nhtmlcov/*\n.tox/*\n*.egg-info\n.coverage\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 157,
    "preview": "repos:\n- repo: https://github.com/astral-sh/ruff-pre-commit\n  rev: v0.12.0\n  hooks:\n  - id: ruff-check\n    args: [--fix,"
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 373,
    "preview": "version: 2\npython:\n  install:\n  - path: .\n    extra_requirements:\n      - doc\n\nsphinx:\n  configuration: docs/conf.py\n\n# "
  },
  {
    "path": "NEWS.rst",
    "chars": 7726,
    "preview": "v7.5.0\n======\n\nFeatures\n--------\n\n- Updated `ast` classes for Python 3.14 compatibility. (#225)\n\n\nv7.4.0\n======\n\nFeature"
  },
  {
    "path": "README.rst",
    "chars": 23030,
    "preview": ".. image:: https://img.shields.io/pypi/v/inflect.svg\n   :target: https://pypi.org/project/inflect\n\n.. image:: https://im"
  },
  {
    "path": "SECURITY.md",
    "chars": 180,
    "preview": "# Security Contact\n\nTo report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/"
  },
  {
    "path": "docs/conf.py",
    "chars": 1546,
    "preview": "from __future__ import annotations\n\nextensions = [\n    'sphinx.ext.autodoc',\n    'jaraco.packaging.sphinx',\n]\n\nmaster_do"
  },
  {
    "path": "docs/history.rst",
    "chars": 78,
    "preview": ":tocdepth: 2\n\n.. _changes:\n\nHistory\n*******\n\n.. include:: ../NEWS (links).rst\n"
  },
  {
    "path": "docs/index.rst",
    "chars": 360,
    "preview": "Welcome to |project| documentation!\n===================================\n\n.. sidebar-links::\n   :home:\n   :pypi:\n\n.. toct"
  },
  {
    "path": "inflect/__init__.py",
    "chars": 103339,
    "preview": "\"\"\"\ninflect: english language inflection\n - correctly generate plurals, ordinals, indefinite articles\n - convert numbers"
  },
  {
    "path": "inflect/compat/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "inflect/compat/py38.py",
    "chars": 160,
    "preview": "import sys\n\n\nif sys.version_info > (3, 9):\n    from typing import Annotated\nelse:  # pragma: no cover\n    from typing_ex"
  },
  {
    "path": "inflect/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "mypy.ini",
    "chars": 375,
    "preview": "[mypy]\n# Is the project well-typed?\nstrict = False\n\n# Early opt-in even when strict = False\nwarn_unused_ignores = True\nw"
  },
  {
    "path": "pyproject.toml",
    "chars": 1846,
    "preview": "[build-system]\nrequires = [\n\t\"setuptools>=77\",\n\t\"setuptools_scm[toml]>=3.4.1\",\n\t# jaraco/skeleton#174\n\t\"coherent.license"
  },
  {
    "path": "pytest.ini",
    "chars": 584,
    "preview": "[pytest]\nnorecursedirs=dist build .tox .eggs\naddopts=\n\t--doctest-modules\n\t--import-mode importlib\nconsider_namespace_pac"
  },
  {
    "path": "ruff.toml",
    "chars": 1203,
    "preview": "[lint]\nextend-select = [\n\t# upstream\n\n\t\"C901\", # complex-structure\n\t\"I\", # isort\n\t\"PERF401\", # manual-list-comprehension"
  },
  {
    "path": "tea.yaml",
    "chars": 126,
    "preview": "# https://tea.xyz/what-is-this-file\n---\nversion: 1.0.0\ncodeOwners:\n  - '0x32392EaEA1FDE87733bEEc3b184C9006501c4A82'\nquor"
  },
  {
    "path": "tests/inflections.txt",
    "chars": 36101,
    "preview": "                    a  ->  as                             # NOUN FORM\n      TODO:sing              a  ->  some          "
  },
  {
    "path": "tests/test_an.py",
    "chars": 1018,
    "preview": "import inflect\n\n\ndef test_an():\n    p = inflect.engine()\n\n    assert p.an(\"cat\") == \"a cat\"\n    assert p.an(\"ant\") == \"a"
  },
  {
    "path": "tests/test_classical_all.py",
    "chars": 2386,
    "preview": "import inflect\n\n\nclass Test:\n    def test_classical(self):\n        p = inflect.engine()\n\n        # DEFAULT...\n\n        a"
  },
  {
    "path": "tests/test_classical_ancient.py",
    "chars": 541,
    "preview": "import inflect\n\n\ndef test_ancient_1():\n    p = inflect.engine()\n\n    # DEFAULT...\n\n    assert p.plural_noun(\"formula\") ="
  },
  {
    "path": "tests/test_classical_herd.py",
    "chars": 543,
    "preview": "import inflect\n\n\ndef test_ancient_1():\n    p = inflect.engine()\n\n    # DEFAULT...\n\n    assert p.plural_noun(\"wildebeest\""
  },
  {
    "path": "tests/test_classical_names.py",
    "chars": 635,
    "preview": "import inflect\n\n\ndef test_ancient_1():\n    p = inflect.engine()\n\n    # DEFAULT...\n\n    assert p.plural_noun(\"Sally\") == "
  },
  {
    "path": "tests/test_classical_person.py",
    "chars": 589,
    "preview": "import inflect\n\n\ndef test_ancient_1():\n    p = inflect.engine()\n\n    # DEFAULT...\n\n    assert p.plural_noun(\"person\") =="
  },
  {
    "path": "tests/test_classical_zero.py",
    "chars": 536,
    "preview": "import inflect\n\n\ndef test_ancient_1():\n    p = inflect.engine()\n\n    # DEFAULT...\n\n    assert p.plural_noun(\"error\", 0) "
  },
  {
    "path": "tests/test_compounds.py",
    "chars": 2979,
    "preview": "import inflect\n\np = inflect.engine()\n\n\ndef test_compound_1():\n    assert p.singular_noun(\"hello-out-there\") == \"hello-ou"
  },
  {
    "path": "tests/test_inflections.py",
    "chars": 8740,
    "preview": "import os\n\nimport pytest\n\nimport inflect\n\n\ndef is_eq(p, a, b):\n    return (\n        p.compare(a, b)\n        or p.plnoune"
  },
  {
    "path": "tests/test_join.py",
    "chars": 1678,
    "preview": "import inflect\n\n\ndef test_join():\n    p = inflect.engine()\n\n    # Three words...\n    words = \"apple banana carrot\".split"
  },
  {
    "path": "tests/test_numwords.py",
    "chars": 15632,
    "preview": "import inflect\n\n\ndef test_loop():\n    p = inflect.engine()\n\n    for thresh in range(21):\n        for n in range(21):\n   "
  },
  {
    "path": "tests/test_pl_si.py",
    "chars": 364,
    "preview": "import pytest\n\nimport inflect\n\n\n@pytest.fixture(params=[False, True], ids=['classical off', 'classical on'])\ndef classic"
  },
  {
    "path": "tests/test_pwd.py",
    "chars": 42637,
    "preview": "import pytest\nfrom typeguard import TypeCheckError\n\nimport inflect\nfrom inflect import (\n    BadChunkingOptionError,\n   "
  },
  {
    "path": "tests/test_unicode.py",
    "chars": 396,
    "preview": "import inflect\n\n\nclass TestUnicode:\n    \"\"\"Unicode compatibility test cases\"\"\"\n\n    def test_unicode_plural(self):\n     "
  },
  {
    "path": "towncrier.toml",
    "chars": 95,
    "preview": "[tool.towncrier]\ntitle_format = \"{version}\"\ndirectory = \"newsfragments\"  # jaraco/skeleton#184\n"
  },
  {
    "path": "tox.ini",
    "chars": 1324,
    "preview": "[testenv]\ndescription = perform primary checks (tests, style, types, coverage)\ndeps =\nsetenv =\n\tPYTHONWARNDEFAULTENCODIN"
  }
]

About this extraction

This page contains the full source code of the jazzband/inflect GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 39 files (255.2 KB), approximately 72.9k tokens, and a symbol index with 170 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!