Skip to content

Commit adbb772

Browse files
authored
Merge pull request #233 from derek73/fix/issue-227-regexes-dict
Modernize config constant data structures
2 parents 2358c77 + df720b1 commit adbb772

10 files changed

Lines changed: 66 additions & 60 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ The library has two layers: `nameparser/config/` (data) and `nameparser/parser.p
7373

7474
### Configuration layer (`nameparser/config/`)
7575

76-
Each module defines a plain Python set of known name pieces:
76+
Most modules define a plain Python set of known name pieces; `capitalization.py` and `regexes.py` define dicts:
7777

7878
- `titles.py``TITLES` (prenominals) and `FIRST_NAME_TITLES` (e.g. "Sir", which treat the following name as first, not last)
7979
- `suffixes.py``SUFFIX_ACRONYMS` (with periods, e.g. "M.D.") and `SUFFIX_NOT_ACRONYMS` (e.g. "Jr.")
8080
- `prefixes.py``PREFIXES` (lastname particles, e.g. "de", "van")
8181
- `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
8282
- `conjunctions.py``CONJUNCTIONS` (e.g. "and", "of") used to chain multi-word titles
8383
- `capitalization.py``CAPITALIZATION_EXCEPTIONS` mapping (e.g. `{'phd': 'Ph.D.'}`)
84-
- `regexes.py` — compiled regular expressions wrapped in a `TupleManager`
84+
- `regexes.py`dict of compiled regular expressions (wrapped in `RegexTupleManager` by `Constants`)
8585

8686
`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.
8787

docs/customize.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@ Editable attributes of nameparser.config.CONSTANTS
5353
* :py:data:`~nameparser.config.CAPITALIZATION_EXCEPTIONS` - Dictionary of pieces that do not capitalize the first letter, e.g. "Ph.D".
5454
* :py:data:`~nameparser.config.REGEXES` - Regular expressions used to find words, initials, nicknames, etc.
5555

56-
Each set of constants comes with :py:func:`~nameparser.config.SetManager.add` and :py:func:`~nameparser.config.SetManager.remove` methods for tuning
56+
Each set-valued constant comes with :py:func:`~nameparser.config.SetManager.add` and :py:func:`~nameparser.config.SetManager.remove` methods for tuning
5757
the constants for your project. These methods automatically lower case and
58-
remove punctuation to normalize them for comparison.
58+
remove punctuation to normalize them for comparison. The two dict-valued
59+
constants (``CAPITALIZATION_EXCEPTIONS`` and ``REGEXES``) are edited with
60+
normal dict operations.
5961

6062
Adding Custom Nickname Delimiters
6163
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

docs/release_log.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Release Log
4343
- Add German/Austrian nobility and ecclesiastical titles to ``TITLES`` (closes #101)
4444
- 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)
4545
- Change ``Constants.__repr__`` to report collection sizes and non-default scalar config, replacing the uninformative ``<Constants() instance>`` (#221)
46+
- 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)
47+
- 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)
4648
* 1.2.1 - June 19, 2026
4749
- 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``
4850
- Add ``python -m nameparser "Name String"`` command-line helper that prints a parsed name

nameparser/config/__init__.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@ def _is_dunder(attr: str) -> bool:
154154

155155
class TupleManager(dict[str, T]):
156156
'''
157-
A dictionary with dot.notation access. Subclass of ``dict``. Makes the tuple constants
158-
more friendly.
157+
A dictionary with dot.notation access. Subclass of ``dict``. Wraps the
158+
mapping config constants (``capitalization_exceptions``, ``regexes``, and
159+
the nickname/maiden delimiter buckets). The name is historical: before
160+
1.3.0 these constants were tuples of pairs.
159161
'''
160162

161163
def __getattr__(self, attr: str) -> T | None:
@@ -273,12 +275,12 @@ class Constants:
273275
The subset of prefixes that are never a first name, so a *leading* one
274276
marks the whole name as a surname. Must stay disjoint from
275277
``bound_first_names``.
276-
:type capitalization_exceptions: tuple or dict
277-
:param capitalization_exceptions:
278+
:type capitalization_exceptions: dict or iterable of (key, value) tuples
279+
:param capitalization_exceptions:
278280
:py:attr:`~capitalization.CAPITALIZATION_EXCEPTIONS` wrapped with :py:class:`TupleManager`.
279-
:type regexes: tuple or dict
280-
:param regexes:
281-
:py:attr:`regexes` wrapped with :py:class:`TupleManager`.
281+
:type regexes: dict or iterable of (name, compiled pattern) tuples
282+
:param regexes:
283+
:py:attr:`~regexes.REGEXES` wrapped with :py:class:`RegexTupleManager`.
282284
283285
:py:attr:`nickname_delimiters` and :py:attr:`maiden_delimiters` are not
284286
constructor arguments -- they're always set in ``__init__`` (see the
@@ -476,8 +478,8 @@ def __init__(self,
476478
conjunctions: Iterable[str] = CONJUNCTIONS,
477479
bound_first_names: Iterable[str] = BOUND_FIRST_NAMES,
478480
non_first_name_prefixes: Iterable[str] = NON_FIRST_NAME_PREFIXES,
479-
capitalization_exceptions: TupleManager[str] | Iterable[tuple[str, str]] = CAPITALIZATION_EXCEPTIONS,
480-
regexes: RegexTupleManager | TupleManager[re.Pattern[str]] | Iterable[tuple[str, re.Pattern[str]]] = REGEXES,
481+
capitalization_exceptions: Mapping[str, str] | Iterable[tuple[str, str]] = CAPITALIZATION_EXCEPTIONS,
482+
regexes: Mapping[str, re.Pattern[str]] | Iterable[tuple[str, re.Pattern[str]]] = REGEXES,
481483
patronymic_name_order: bool = False,
482484
middle_name_as_last: bool = False,
483485
) -> None:
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
CAPITALIZATION_EXCEPTIONS = (
2-
('ii', 'II'),
3-
('iii', 'III'),
4-
('iv', 'IV'),
5-
('md', 'M.D.'),
6-
('phd', 'Ph.D.'),
7-
)
1+
CAPITALIZATION_EXCEPTIONS = {
2+
'ii': 'II',
3+
'iii': 'III',
4+
'iv': 'IV',
5+
'md': 'M.D.',
6+
'phd': 'Ph.D.',
7+
}
88
"""
99
Any pieces that are not capitalized by capitalizing the first letter.
1010
"""

nameparser/config/conjunctions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
CONJUNCTIONS = set([
1+
CONJUNCTIONS = {
22
'&',
33
'and',
44
'et',
@@ -7,7 +7,7 @@
77
'the',
88
'und',
99
'y',
10-
])
10+
}
1111
"""
1212
Pieces that should join to their neighboring pieces, e.g. "and", "y" and "&".
1313
"of" and "the" are also include to facilitate joining multiple titles,

nameparser/config/prefixes.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#: means that name is not auto-fixed, whereas a wrong member misparses a real
1010
#: person. Must stay a subset of :py:data:`PREFIXES` and disjoint from
1111
#: :py:data:`~nameparser.config.bound_first_names.BOUND_FIRST_NAMES`.
12-
NON_FIRST_NAME_PREFIXES = set([
12+
NON_FIRST_NAME_PREFIXES = {
1313
"'t",
1414
'af',
1515
'auf',
@@ -32,7 +32,7 @@
3232
'vd',
3333
'vom',
3434
'zu',
35-
])
35+
}
3636

3737
#: Name pieces that appear before a last name. Prefixes join to the piece
3838
#: that follows them to make one new piece. They can be chained together, e.g
@@ -48,7 +48,7 @@
4848
#: is guaranteed to also be a prefix (and still join forward), with no drift --
4949
#: mirroring ``TITLES = FIRST_NAME_TITLES | {...}`` in
5050
#: :py:mod:`nameparser.config.titles`.
51-
PREFIXES = NON_FIRST_NAME_PREFIXES | set([
51+
PREFIXES = NON_FIRST_NAME_PREFIXES | {
5252
'aan',
5353
'aen',
5454
'abu',
@@ -86,7 +86,7 @@
8686
'vander',
8787
'vel',
8888
'von',
89-
])
89+
}
9090

9191
# Guard the two invariants the docstring above promises, so a future edit that
9292
# breaks them fails at import time instead of silently drifting until a test

nameparser/config/regexes.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,39 @@
88

99
EMPTY_REGEX = re.compile('')
1010

11-
REGEXES = set([
12-
("spaces", re.compile(r"\s+")),
13-
("word", re.compile(r"(\w|\.)+")),
14-
("mac", re.compile(r'^(ma?c)(\w{2,})', re.I)),
15-
("initial", re.compile(r'^(\w\.|[A-Z])?$')),
16-
("quoted_word", re.compile(r'(?<!\w)\'([^\s]*?)\'(?!\w)')),
17-
("double_quotes", re.compile(r'\"(.*?)\"')),
18-
("parenthesis", re.compile(r'\((.*?)\)')),
19-
("roman_numeral", re.compile(r'^(X|IX|IV|V?I{0,3})$', re.I)),
20-
("no_vowels",re.compile(r'^[^aeyiuo]+$', re.I)),
21-
("period_not_at_end",re.compile(r'.*\..+$', re.I)),
22-
("emoji",re_emoji),
23-
("phd", re.compile(r'\s(ph\.?\s+d\.?)', re.I)),
24-
("space_before_comma", re.compile(r'\s+,')),
25-
("east_slavic_patronymic", re.compile(
11+
REGEXES = {
12+
"spaces": re.compile(r"\s+"),
13+
"word": re.compile(r"(\w|\.)+"),
14+
"mac": re.compile(r'^(ma?c)(\w{2,})', re.I),
15+
"initial": re.compile(r'^(\w\.|[A-Z])?$'),
16+
"quoted_word": re.compile(r'(?<!\w)\'([^\s]*?)\'(?!\w)'),
17+
"double_quotes": re.compile(r'\"(.*?)\"'),
18+
"parenthesis": re.compile(r'\((.*?)\)'),
19+
"roman_numeral": re.compile(r'^(X|IX|IV|V?I{0,3})$', re.I),
20+
"no_vowels": re.compile(r'^[^aeyiuo]+$', re.I),
21+
"period_not_at_end": re.compile(r'.*\..+$', re.I),
22+
"emoji": re_emoji,
23+
"phd": re.compile(r'\s(ph\.?\s+d\.?)', re.I),
24+
"space_before_comma": re.compile(r'\s+,'),
25+
"east_slavic_patronymic": re.compile(
2626
r'(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich)$',
2727
re.I,
28-
)),
29-
("east_slavic_patronymic_cyrillic", re.compile(
28+
),
29+
"east_slavic_patronymic_cyrillic": re.compile(
3030
r'(ович|овна|евич|евна|ична|ильич|кузьмич|лукич|фомич|фокич)$',
3131
re.I,
32-
)),
33-
("turkic_patronymic_marker", re.compile(
32+
),
33+
"turkic_patronymic_marker": re.compile(
3434
r"^(oglu|oğlu|ogly|ogli|o['’ʻ]g['’ʻ]li"
3535
r"|qizi|qızı|kizi|kyzy|gyzy|uly|uulu)$",
3636
re.I,
37-
)),
38-
("turkic_patronymic_marker_cyrillic", re.compile(
37+
),
38+
"turkic_patronymic_marker_cyrillic": re.compile(
3939
r'^(оглу|оглы|оғлу|ўғли|угли|кызы|гызы|қызы|қизи|улы|ұлы|уулу)$',
4040
re.I,
41-
)),
42-
("period_abbreviation", re.compile(r'^[^\W\d_]{2,}\.$')),
43-
])
41+
),
42+
"period_abbreviation": re.compile(r'^[^\W\d_]{2,}\.$'),
43+
}
4444
"""
4545
All regular expressions used by the parser are precompiled and stored in the config.
4646
"""

nameparser/config/suffixes.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
SUFFIX_NOT_ACRONYMS = set([
1+
SUFFIX_NOT_ACRONYMS = {
22
'dr',
33
'esq',
44
'esquire',
@@ -21,14 +21,14 @@
2121
# literally instead of going through nickname/suffix disambiguation).
2222
'ret',
2323
'vet',
24-
])
24+
}
2525
"""
2626
2727
Post-nominal pieces that are not acronyms. The parser does not remove periods
2828
when matching against these pieces.
2929
3030
"""
31-
SUFFIX_ACRONYMS_AMBIGUOUS = set([
31+
SUFFIX_ACRONYMS_AMBIGUOUS = {
3232
# Suffix acronyms that also commonly work as given-name nicknames on
3333
# their own (e.g. "Ed", "JD"). Read only by HumanName.parse_nicknames()
3434
# when deciding whether parenthesized/quoted content is a nickname or a
@@ -42,15 +42,15 @@
4242
# certifications/degrees (e.g. 'mba', 'cpa', 'phd') don't need an entry.
4343
'ed',
4444
'jd',
45-
])
45+
}
4646
"""
4747
4848
Acronym suffixes from SUFFIX_ACRONYMS that also plausibly collide with a
4949
common given-name nickname. Not a partition of SUFFIX_ACRONYMS -- a small,
5050
standalone exception list consulted only by parse_nicknames().
5151
5252
"""
53-
SUFFIX_ACRONYMS = set([
53+
SUFFIX_ACRONYMS = {
5454
'8-vsb',
5555
'aas',
5656
'aba',
@@ -683,7 +683,7 @@
683683
'vcp',
684684
'vd',
685685
'vrd',
686-
])
686+
}
687687
"""
688688
689689
Post-nominal acronyms. Titles, degrees and other things people stick after their name

nameparser/config/titles.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FIRST_NAME_TITLES = set([
1+
FIRST_NAME_TITLES = {
22
'aunt',
33
'auntie',
44
'brother',
@@ -21,7 +21,7 @@
2121
'shaikh',
2222
'cheikh',
2323
'shekh',
24-
])
24+
}
2525
"""
2626
When these titles appear with a single other name, that name is a first name, e.g.
2727
"Sir John", "Sister Mary", "Queen Elizabeth".
@@ -31,7 +31,7 @@
3131
#: Many of these from wikipedia: https://en.wikipedia.org/wiki/Title.
3232
#: The parser recognizes chains of these including conjunctions allowing
3333
#: recognition titles like "Deputy Secretary of State".
34-
TITLES = FIRST_NAME_TITLES | set([
34+
TITLES = FIRST_NAME_TITLES | {
3535
"attaché",
3636
"chargé d'affaires",
3737
"king's",
@@ -683,4 +683,4 @@
683683
'woodman',
684684
'writer',
685685
'zoologist',
686-
])
686+
}

0 commit comments

Comments
 (0)