diff --git a/AGENTS.md b/AGENTS.md index 6405df3..85a70f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ The library has two layers: `nameparser/config/` (data) and `nameparser/parser.p ### Configuration layer (`nameparser/config/`) -Each module defines a plain Python set of known name pieces: +Most modules define a plain Python set of known name pieces; `capitalization.py` and `regexes.py` define dicts: - `titles.py` — `TITLES` (prenominals) and `FIRST_NAME_TITLES` (e.g. "Sir", which treat the following name as first, not last) - `suffixes.py` — `SUFFIX_ACRONYMS` (with periods, e.g. "M.D.") and `SUFFIX_NOT_ACRONYMS` (e.g. "Jr.") @@ -81,7 +81,7 @@ Each module defines a plain Python set of known name pieces: - `bound_first_names.py` — `BOUND_FIRST_NAMES` (bound given-name prefixes, e.g. "abdul", "abu"); `_join_bound_first_name` joins the first non-title piece to its following piece before the main assignment loop - `conjunctions.py` — `CONJUNCTIONS` (e.g. "and", "of") used to chain multi-word titles - `capitalization.py` — `CAPITALIZATION_EXCEPTIONS` mapping (e.g. `{'phd': 'Ph.D.'}`) -- `regexes.py` — compiled regular expressions wrapped in a `TupleManager` +- `regexes.py` — dict of compiled regular expressions (wrapped in `RegexTupleManager` by `Constants`) `config/__init__.py` wraps everything into `SetManager` and `TupleManager` instances inside a `Constants` class. A module-level singleton `CONSTANTS` is shared across all `HumanName` instances by default. diff --git a/docs/customize.rst b/docs/customize.rst index 6eba656..dab9ac3 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -53,9 +53,11 @@ Editable attributes of nameparser.config.CONSTANTS * :py:data:`~nameparser.config.CAPITALIZATION_EXCEPTIONS` - Dictionary of pieces that do not capitalize the first letter, e.g. "Ph.D". * :py:data:`~nameparser.config.REGEXES` - Regular expressions used to find words, initials, nicknames, etc. -Each set of constants comes with :py:func:`~nameparser.config.SetManager.add` and :py:func:`~nameparser.config.SetManager.remove` methods for tuning +Each set-valued constant comes with :py:func:`~nameparser.config.SetManager.add` and :py:func:`~nameparser.config.SetManager.remove` methods for tuning the constants for your project. These methods automatically lower case and -remove punctuation to normalize them for comparison. +remove punctuation to normalize them for comparison. The two dict-valued +constants (``CAPITALIZATION_EXCEPTIONS`` and ``REGEXES``) are edited with +normal dict operations. Adding Custom Nickname Delimiters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/release_log.rst b/docs/release_log.rst index 363169a..070c9d6 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -43,6 +43,8 @@ Release Log - Add German/Austrian nobility and ecclesiastical titles to ``TITLES`` (closes #101) - Add German/Dutch last-name prefixes and title/degree suffixes; fix ``join_on_conjunctions()`` to register multi-word prefix chains (e.g. ``"von und zu"``) as prefixes, mirroring existing title handling (closes #18) - Change ``Constants.__repr__`` to report collection sizes and non-default scalar config, replacing the uninformative ```` (#221) + - Change ``REGEXES`` from a ``set`` of ``(name, pattern)`` tuples to a ``dict``, so a duplicate name is a visible overwrite in the source instead of a nondeterministic winner at import time; code iterating ``REGEXES`` directly now gets keys instead of pairs — use ``.items()`` (#227) + - Change ``CAPITALIZATION_EXCEPTIONS`` from a tuple of ``(key, value)`` tuples to a ``dict``; code iterating it directly now gets keys instead of pairs — use ``.items()`` (#233) * 1.2.1 - June 19, 2026 - Fix ``initials()`` interpolating the literal ``None`` for empty name parts when ``empty_attribute_default = None`` (e.g. ``"J. None D."``); empty parts now render as an empty string and a fully-empty result returns ``empty_attribute_default`` - Add ``python -m nameparser "Name String"`` command-line helper that prints a parsed name diff --git a/nameparser/config/__init__.py b/nameparser/config/__init__.py index ed703a3..54732d4 100644 --- a/nameparser/config/__init__.py +++ b/nameparser/config/__init__.py @@ -154,8 +154,10 @@ def _is_dunder(attr: str) -> bool: class TupleManager(dict[str, T]): ''' - A dictionary with dot.notation access. Subclass of ``dict``. Makes the tuple constants - more friendly. + A dictionary with dot.notation access. Subclass of ``dict``. Wraps the + mapping config constants (``capitalization_exceptions``, ``regexes``, and + the nickname/maiden delimiter buckets). The name is historical: before + 1.3.0 these constants were tuples of pairs. ''' def __getattr__(self, attr: str) -> T | None: @@ -273,12 +275,12 @@ class Constants: The subset of prefixes that are never a first name, so a *leading* one marks the whole name as a surname. Must stay disjoint from ``bound_first_names``. - :type capitalization_exceptions: tuple or dict - :param capitalization_exceptions: + :type capitalization_exceptions: dict or iterable of (key, value) tuples + :param capitalization_exceptions: :py:attr:`~capitalization.CAPITALIZATION_EXCEPTIONS` wrapped with :py:class:`TupleManager`. - :type regexes: tuple or dict - :param regexes: - :py:attr:`regexes` wrapped with :py:class:`TupleManager`. + :type regexes: dict or iterable of (name, compiled pattern) tuples + :param regexes: + :py:attr:`~regexes.REGEXES` wrapped with :py:class:`RegexTupleManager`. :py:attr:`nickname_delimiters` and :py:attr:`maiden_delimiters` are not constructor arguments -- they're always set in ``__init__`` (see the @@ -476,8 +478,8 @@ def __init__(self, conjunctions: Iterable[str] = CONJUNCTIONS, bound_first_names: Iterable[str] = BOUND_FIRST_NAMES, non_first_name_prefixes: Iterable[str] = NON_FIRST_NAME_PREFIXES, - capitalization_exceptions: TupleManager[str] | Iterable[tuple[str, str]] = CAPITALIZATION_EXCEPTIONS, - regexes: RegexTupleManager | TupleManager[re.Pattern[str]] | Iterable[tuple[str, re.Pattern[str]]] = REGEXES, + capitalization_exceptions: Mapping[str, str] | Iterable[tuple[str, str]] = CAPITALIZATION_EXCEPTIONS, + regexes: Mapping[str, re.Pattern[str]] | Iterable[tuple[str, re.Pattern[str]]] = REGEXES, patronymic_name_order: bool = False, middle_name_as_last: bool = False, ) -> None: diff --git a/nameparser/config/capitalization.py b/nameparser/config/capitalization.py index 0172691..fcd5376 100644 --- a/nameparser/config/capitalization.py +++ b/nameparser/config/capitalization.py @@ -1,10 +1,10 @@ -CAPITALIZATION_EXCEPTIONS = ( - ('ii', 'II'), - ('iii', 'III'), - ('iv', 'IV'), - ('md', 'M.D.'), - ('phd', 'Ph.D.'), -) +CAPITALIZATION_EXCEPTIONS = { + 'ii': 'II', + 'iii': 'III', + 'iv': 'IV', + 'md': 'M.D.', + 'phd': 'Ph.D.', +} """ Any pieces that are not capitalized by capitalizing the first letter. """ diff --git a/nameparser/config/conjunctions.py b/nameparser/config/conjunctions.py index 695c9ee..003f8c8 100644 --- a/nameparser/config/conjunctions.py +++ b/nameparser/config/conjunctions.py @@ -1,4 +1,4 @@ -CONJUNCTIONS = set([ +CONJUNCTIONS = { '&', 'and', 'et', @@ -7,7 +7,7 @@ 'the', 'und', 'y', -]) +} """ Pieces that should join to their neighboring pieces, e.g. "and", "y" and "&". "of" and "the" are also include to facilitate joining multiple titles, diff --git a/nameparser/config/prefixes.py b/nameparser/config/prefixes.py index 192aaba..43ee541 100644 --- a/nameparser/config/prefixes.py +++ b/nameparser/config/prefixes.py @@ -9,7 +9,7 @@ #: means that name is not auto-fixed, whereas a wrong member misparses a real #: person. Must stay a subset of :py:data:`PREFIXES` and disjoint from #: :py:data:`~nameparser.config.bound_first_names.BOUND_FIRST_NAMES`. -NON_FIRST_NAME_PREFIXES = set([ +NON_FIRST_NAME_PREFIXES = { "'t", 'af', 'auf', @@ -32,7 +32,7 @@ 'vd', 'vom', 'zu', -]) +} #: Name pieces that appear before a last name. Prefixes join to the piece #: that follows them to make one new piece. They can be chained together, e.g @@ -48,7 +48,7 @@ #: is guaranteed to also be a prefix (and still join forward), with no drift -- #: mirroring ``TITLES = FIRST_NAME_TITLES | {...}`` in #: :py:mod:`nameparser.config.titles`. -PREFIXES = NON_FIRST_NAME_PREFIXES | set([ +PREFIXES = NON_FIRST_NAME_PREFIXES | { 'aan', 'aen', 'abu', @@ -86,7 +86,7 @@ 'vander', 'vel', 'von', -]) +} # Guard the two invariants the docstring above promises, so a future edit that # breaks them fails at import time instead of silently drifting until a test diff --git a/nameparser/config/regexes.py b/nameparser/config/regexes.py index dbf8b2d..9c496a0 100644 --- a/nameparser/config/regexes.py +++ b/nameparser/config/regexes.py @@ -8,39 +8,39 @@ EMPTY_REGEX = re.compile('') -REGEXES = set([ - ("spaces", re.compile(r"\s+")), - ("word", re.compile(r"(\w|\.)+")), - ("mac", re.compile(r'^(ma?c)(\w{2,})', re.I)), - ("initial", re.compile(r'^(\w\.|[A-Z])?$')), - ("quoted_word", re.compile(r'(?