Skip to content

Commit 4097e7b

Browse files
derek73claude
andcommitted
Fix constants argument silently discarding Constants subclasses
__init__ validated the constants argument with an exact-type check (type(self.C) is not type(CONSTANTS)), so an instance of a Constants subclass failed the check and was silently replaced with fresh defaults, discarding the caller's configuration without any error. Validate explicitly instead: None means a private per-instance Constants (unchanged), any Constants instance — including subclasses — is used as given, and anything else raises TypeError rather than being silently swapped for defaults. The signature annotation now admits None, which was always accepted and documented. Also fix stale prose in customize.rst: "pass something falsey" no longer describes the behavior (only None gets per-instance config now), and the keyword argument is `constants`, not `constant`. Closes #226 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d7ad5b6 commit 4097e7b

4 files changed

Lines changed: 29 additions & 6 deletions

File tree

docs/customize.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,10 +456,11 @@ e.g. parsing names on multiple threads.
456456

457457

458458
If you'd prefer new instances to have their own config values, one shortcut is to pass
459-
``None`` as the second argument (or ``constant`` keyword argument) when
459+
``None`` as the second argument (or ``constants`` keyword argument) when
460460
instantiating ``HumanName``. Each instance always has a ``C`` attribute, but if
461-
you didn't pass something falsey to the ``constants`` argument then it's a
462-
reference to the module-level config values with the behavior described above.
461+
you didn't pass ``None`` (or your own :py:class:`~nameparser.config.Constants`
462+
instance) to the ``constants`` argument then it's a reference to the
463+
module-level config values with the behavior described above.
463464

464465
.. doctest:: module config
465466
:options: +ELLIPSIS, +NORMALIZE_WHITESPACE

docs/release_log.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Release Log
22
===========
33
* 1.3.0 - Unreleased
4+
- Fix the ``constants`` constructor argument silently discarding ``Constants`` *subclass* instances: the exact-type check replaced them with fresh defaults, throwing away the caller's configuration. Subclass instances are now used as given; anything that is neither ``None`` nor a ``Constants`` instance now raises ``TypeError`` instead of being silently swapped for defaults (closes #226)
45
- Fix ``IndexError`` in ``initials()``/``initials_list()`` when a ``*_list`` attribute was assigned directly with an element containing unnormalized whitespace (e.g. ``name.middle_list = ['Q R']``), bypassing the parser's whitespace normalization (closes #232)
56
- Fix ``HumanName`` acting as its own iterator with a stored cursor: breaking out of a loop, iterating in nested loops, or calling ``len(name)`` mid-loop corrupted subsequent iteration; ``iter(name)`` now returns a fresh independent iterator each time. ``next(name)`` on the instance itself (undocumented) now raises ``TypeError`` — call ``next(iter(name))`` instead (closes #225)
67
- Fix parsing writing back into the ``Constants`` it reads (usually the shared module-level ``CONSTANTS``): pieces derived while parsing a name — period-joined titles/suffixes like ``"Lt.Gov."`` and conjunction-joined pieces like ``"Mr. and Mrs."`` or ``"von und zu"`` — are now tracked per parse instead of being permanently ``add()``-ed to the config, so parse results no longer depend on which names were parsed earlier in the process and parsing no longer mutates shared state across threads

nameparser/parser.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ class HumanName:
9595
def __init__(
9696
self,
9797
full_name: str | bytes = "",
98-
constants: Constants = CONSTANTS,
98+
constants: Constants | None = CONSTANTS,
9999
encoding: str = DEFAULT_ENCODING,
100100
string_format: str | None = None,
101101
initials_format: str | None = None,
@@ -110,9 +110,14 @@ def __init__(
110110
nickname: str | list[str] | None = None,
111111
maiden: str | list[str] | None = None,
112112
) -> None:
113+
if constants is None:
114+
constants = Constants()
115+
elif not isinstance(constants, Constants):
116+
raise TypeError(
117+
"constants must be a Constants instance or None, "
118+
f"got {type(constants).__name__}"
119+
)
113120
self.C = constants
114-
if type(self.C) is not type(CONSTANTS):
115-
self.C = Constants()
116121

117122
# Lookup entries derived while parsing this instance (period-joined
118123
# titles/suffixes like "Lt.Gov.", conjunction-joined pieces like

tests/test_constants.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ def test_add_title(self) -> None:
2525
self.m(hn.first, "Awanui-a-Rangi", hn)
2626
self.m(hn.last, "Black", hn)
2727

28+
def test_constants_subclass_instance_is_used(self) -> None:
29+
class CustomConstants(Constants):
30+
pass
31+
32+
c = CustomConstants()
33+
c.titles.add('chancellor')
34+
hn = HumanName("Chancellor Jane Smith", constants=c)
35+
self.assertIs(hn.C, c)
36+
self.m(hn.title, "Chancellor", hn)
37+
self.m(hn.first, "Jane", hn)
38+
self.m(hn.last, "Smith", hn)
39+
40+
def test_constants_invalid_type_raises_typeerror(self) -> None:
41+
with pytest.raises(TypeError):
42+
HumanName("John Doe", constants="not a Constants") # type: ignore[arg-type]
43+
2844
def test_remove_title(self) -> None:
2945
hn = HumanName("Hon Solo", constants=None)
3046
start_len = len(hn.C.titles)

0 commit comments

Comments
 (0)