Skip to content

Commit 89796e5

Browse files
derek73claude
andcommitted
Fix bare string to SetManager silently shredding into characters
Every set-backed Constants argument is annotated Iterable[str], which a bare str satisfies — as an iterable of its characters. SetManager's set(elements) then silently replaced the whole default set with single characters (Constants(titles='dr') -> titles == {'d', 'r'}), producing wrong parses with no error anywhere. The type system cannot catch this, so it must be a runtime check. Reject str and bytes in SetManager.__init__ with a TypeError that includes the wrap-it-in-a-list fix, covering all nine set-backed Constants arguments and direct SetManager construction in one place. All internal call sites pass module-level tuples/sets, and pickling restores SetManager by state without re-running __init__, so only the buggy input path is affected. Closes #238 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 192d35e commit 89796e5

3 files changed

Lines changed: 21 additions & 0 deletions

File tree

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 a bare string passed to a set-backed ``Constants`` argument (e.g. ``Constants(titles='dr')``) or to ``SetManager`` being silently split into single characters, replacing the default set and producing wrong parses with no error; it now raises ``TypeError`` with the suggested fix (wrap it in a list) (closes #238)
45
- 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)
56
- 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)
67
- 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)

nameparser/config/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ class SetManager(Set):
6666
_on_change: Callable[[], None] | None
6767

6868
def __init__(self, elements: Iterable[str]) -> None:
69+
# a bare string is an iterable of its characters, so set(elements)
70+
# would silently build a set of single characters (#238)
71+
if isinstance(elements, (str, bytes)):
72+
raise TypeError(
73+
"expected an iterable of strings, got a single "
74+
f"{type(elements).__name__}; wrap it in a list: [{elements!r}]"
75+
)
6976
self.elements = set(elements)
7077
# Optional invalidation hook, wired by an owning Constants so that
7178
# in-place add()/remove() can clear its cached suffixes_prefixes_titles

tests/test_constants.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ def test_constants_class_instead_of_instance_raises_with_hint(self) -> None:
4545
with pytest.raises(TypeError, match=r"did you mean Constants\(\)"):
4646
HumanName("John Doe", constants=Constants) # type: ignore[arg-type]
4747

48+
def test_constants_bare_string_kwarg_raises_typeerror(self) -> None:
49+
# a bare string is an iterable of its characters, so set('dr') would
50+
# silently replace the default titles with {'d', 'r'} (#238); the
51+
# type system can't catch this because str satisfies Iterable[str]
52+
with pytest.raises(TypeError, match=r"wrap it in a list"):
53+
Constants(titles='dr')
54+
55+
def test_set_manager_bare_string_raises_typeerror(self) -> None:
56+
with pytest.raises(TypeError, match=r"wrap it in a list"):
57+
SetManager('dr')
58+
with pytest.raises(TypeError, match=r"wrap it in a list"):
59+
SetManager(b'dr') # type: ignore[arg-type]
60+
4861
def test_remove_title(self) -> None:
4962
hn = HumanName("Hon Solo", constants=None)
5063
start_len = len(hn.C.titles)

0 commit comments

Comments
 (0)