Skip to content

Commit 418a210

Browse files
derek73claude
andcommitted
Tell the caller how to fix a subset violation
The message stated the constraint and left the remedy to be inferred: "given_name_titles must be a subset of titles; not in titles: سيد". Since adding to both fields is the only thing the caller can have meant, the error should say so. given_name_titles marks a subset of titles; not in titles: سيد. Add them to titles as well — add(titles={...}, given_name_titles={...}) Considered auto-adding to the base field instead, and rejected it for 2.0: a typo in a marker field would silently mint new vocabulary (add(given_name_titles={"profesor"}) inventing a title), the same inference would have to extend to the other two marker pairs and to remove()'s cascade, and the direction is one-way -- strict now can relax in 2.1, loose now can never tighten. The message carries most of the ergonomic benefit at none of that cost. Parametrized over all three marker pairs, via constructor lambdas rather than **kwargs: Lexicon's signature is heterogeneous (capitalization_exceptions is pair-valued), so dynamic kwargs fail mypy, and tests/ is type-checked here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d8feb3e commit 418a210

2 files changed

Lines changed: 20 additions & 2 deletions

File tree

nameparser/_lexicon.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,10 @@ def __post_init__(self) -> None:
231231
orphans = getattr(self, marker) - getattr(self, base)
232232
if orphans:
233233
raise ValueError(
234-
f"{marker} must be a subset of {base}; "
235-
f"not in {base}: {', '.join(sorted(orphans))}"
234+
f"{marker} marks a subset of {base}; "
235+
f"not in {base}: {', '.join(sorted(orphans))}. "
236+
f"Add them to {base} as well — "
237+
f"add({base}={{...}}, {marker}={{...}})"
236238
)
237239

238240
# -- constructors ----------------------------------------------------

tests/v2/test_lexicon.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
from collections.abc import Callable
23

34
import pytest
45

@@ -238,6 +239,21 @@ def test_given_name_titles_must_be_subset_of_titles() -> None:
238239
Lexicon(given_name_titles=frozenset({"sheikh"}))
239240

240241

242+
@pytest.mark.parametrize(("make", "base"), [
243+
(lambda w: Lexicon(given_name_titles=w), "titles"),
244+
(lambda w: Lexicon(particles_ambiguous=w), "particles"),
245+
(lambda w: Lexicon(suffix_acronyms_ambiguous=w), "suffix_acronyms"),
246+
], ids=["given_name_titles", "particles_ambiguous", "suffix_acronyms_ambiguous"])
247+
def test_subset_error_names_the_fix(
248+
make: Callable[[frozenset[str]], Lexicon], base: str
249+
) -> None:
250+
# the constraint alone leaves the reader to infer the remedy; every
251+
# marker field's error should name the base field to add to, since
252+
# adding to both is the only thing the caller can have meant
253+
with pytest.raises(ValueError, match=f"Add them to {base} as well"):
254+
make(frozenset({"zzqnovel"}))
255+
256+
241257
def test_remove_orphaning_given_name_titles_raises() -> None:
242258
lex = Lexicon(titles=frozenset({"sheikh"}),
243259
given_name_titles=frozenset({"sheikh"}))

0 commit comments

Comments
 (0)