Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ 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.")
- `prefixes.py` — `PREFIXES` (lastname particles, e.g. "de", "van")
- `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.

Expand Down
6 changes: 4 additions & 2 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 2 additions & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 ``<Constants() instance>`` (#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
Expand Down
20 changes: 11 additions & 9 deletions nameparser/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
14 changes: 7 additions & 7 deletions nameparser/config/capitalization.py
Original file line number Diff line number Diff line change
@@ -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.
"""
4 changes: 2 additions & 2 deletions nameparser/config/conjunctions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CONJUNCTIONS = set([
CONJUNCTIONS = {
'&',
'and',
'et',
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions nameparser/config/prefixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand Down
48 changes: 24 additions & 24 deletions nameparser/config/regexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'(?<!\w)\'([^\s]*?)\'(?!\w)')),
("double_quotes", re.compile(r'\"(.*?)\"')),
("parenthesis", re.compile(r'\((.*?)\)')),
("roman_numeral", re.compile(r'^(X|IX|IV|V?I{0,3})$', re.I)),
("no_vowels",re.compile(r'^[^aeyiuo]+$', re.I)),
("period_not_at_end",re.compile(r'.*\..+$', re.I)),
("emoji",re_emoji),
("phd", re.compile(r'\s(ph\.?\s+d\.?)', re.I)),
("space_before_comma", re.compile(r'\s+,')),
("east_slavic_patronymic", re.compile(
REGEXES = {
"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'(?<!\w)\'([^\s]*?)\'(?!\w)'),
"double_quotes": re.compile(r'\"(.*?)\"'),
"parenthesis": re.compile(r'\((.*?)\)'),
"roman_numeral": re.compile(r'^(X|IX|IV|V?I{0,3})$', re.I),
"no_vowels": re.compile(r'^[^aeyiuo]+$', re.I),
"period_not_at_end": re.compile(r'.*\..+$', re.I),
"emoji": re_emoji,
"phd": re.compile(r'\s(ph\.?\s+d\.?)', re.I),
"space_before_comma": re.compile(r'\s+,'),
"east_slavic_patronymic": re.compile(
r'(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich)$',
re.I,
)),
("east_slavic_patronymic_cyrillic", re.compile(
),
"east_slavic_patronymic_cyrillic": re.compile(
r'(ович|овна|евич|евна|ична|ильич|кузьмич|лукич|фомич|фокич)$',
re.I,
)),
("turkic_patronymic_marker", re.compile(
),
"turkic_patronymic_marker": re.compile(
r"^(oglu|oğlu|ogly|ogli|o['’ʻ]g['’ʻ]li"
r"|qizi|qızı|kizi|kyzy|gyzy|uly|uulu)$",
re.I,
)),
("turkic_patronymic_marker_cyrillic", re.compile(
),
"turkic_patronymic_marker_cyrillic": re.compile(
r'^(оглу|оглы|оғлу|ўғли|угли|кызы|гызы|қызы|қизи|улы|ұлы|уулу)$',
re.I,
)),
("period_abbreviation", re.compile(r'^[^\W\d_]{2,}\.$')),
])
),
"period_abbreviation": re.compile(r'^[^\W\d_]{2,}\.$'),
}
"""
All regular expressions used by the parser are precompiled and stored in the config.
"""
12 changes: 6 additions & 6 deletions nameparser/config/suffixes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SUFFIX_NOT_ACRONYMS = set([
SUFFIX_NOT_ACRONYMS = {
'dr',
'esq',
'esquire',
Expand All @@ -21,14 +21,14 @@
# literally instead of going through nickname/suffix disambiguation).
'ret',
'vet',
])
}
"""

Post-nominal pieces that are not acronyms. The parser does not remove periods
when matching against these pieces.

"""
SUFFIX_ACRONYMS_AMBIGUOUS = set([
SUFFIX_ACRONYMS_AMBIGUOUS = {
# Suffix acronyms that also commonly work as given-name nicknames on
# their own (e.g. "Ed", "JD"). Read only by HumanName.parse_nicknames()
# when deciding whether parenthesized/quoted content is a nickname or a
Expand All @@ -42,15 +42,15 @@
# certifications/degrees (e.g. 'mba', 'cpa', 'phd') don't need an entry.
'ed',
'jd',
])
}
"""

Acronym suffixes from SUFFIX_ACRONYMS that also plausibly collide with a
common given-name nickname. Not a partition of SUFFIX_ACRONYMS -- a small,
standalone exception list consulted only by parse_nicknames().

"""
SUFFIX_ACRONYMS = set([
SUFFIX_ACRONYMS = {
'8-vsb',
'aas',
'aba',
Expand Down Expand Up @@ -683,7 +683,7 @@
'vcp',
'vd',
'vrd',
])
}
"""

Post-nominal acronyms. Titles, degrees and other things people stick after their name
Expand Down
8 changes: 4 additions & 4 deletions nameparser/config/titles.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FIRST_NAME_TITLES = set([
FIRST_NAME_TITLES = {
'aunt',
'auntie',
'brother',
Expand All @@ -21,7 +21,7 @@
'shaikh',
'cheikh',
'shekh',
])
}
"""
When these titles appear with a single other name, that name is a first name, e.g.
"Sir John", "Sister Mary", "Queen Elizabeth".
Expand All @@ -31,7 +31,7 @@
#: Many of these from wikipedia: https://en.wikipedia.org/wiki/Title.
#: The parser recognizes chains of these including conjunctions allowing
#: recognition titles like "Deputy Secretary of State".
TITLES = FIRST_NAME_TITLES | set([
TITLES = FIRST_NAME_TITLES | {
"attaché",
"chargé d'affaires",
"king's",
Expand Down Expand Up @@ -683,4 +683,4 @@
'woodman',
'writer',
'zoologist',
])
}
Loading