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
19 changes: 16 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,22 @@ uv run sphinx-build -b html docs dist/docs
# - Format matches existing entries — see 1.3.0 block for a current example

# Release checklist (PyPI publish is triggered automatically by GitHub Actions on release creation)
# 0. Review docs/ for anything stale — especially usage.rst (examples, API surface)
# and any .rst files that reference config constants or HumanName kwargs
# Also review AGENTS.md for stale commands, architecture notes, or gotchas
# 0. Sweep for stale prose. Grepping for the changed SYMBOL finds almost none of
# it: prose describes behavior in words, not identifiers. Walk these sites
# explicitly — every one of them has gone stale at least once.
# - docs/*.rst: usage.rst examples and API surface, plus any file listing
# config constants or HumanName kwargs
# - docstrings that autodoc renders (nameparser/_policy.py, _types.py, ...).
# These ARE the API reference in modules.html, so a docs/*.rst-only sweep
# structurally misses half the rendered page — that is how the guide and
# the reference ended up teaching different Policy spellings in 2.1
# - user-facing MESSAGES: warnings and raise text often hand the reader code
# to paste, and it has to be code that still works (and type-checks)
# - pipeline stage header blocks: each _pipeline/*.py docstring declares
# "Reads:". That is checkable, so check it rather than reading it:
# compare it against grep -oE '\b(policy|lexicon)\.[a-z_]+' on the module
# - tests/v2/cases.py notes, which explain why a row lands where it does
# - AGENTS.md itself, for stale commands, architecture notes, or gotchas
# And check for open Dependabot PRs on uv.lock (namedivider-python) and merge them
# first — pyproject floats >=0.4 so fresh installs get the newest namedivider, but
# CI's ja-extra job installs from uv.lock and only tests what the lock pins
Expand Down
2 changes: 1 addition & 1 deletion nameparser/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def __post_init__(self) -> None:
f"written in {'it' if one else 'them'} will never "
f"divide. Supply covering surnames, pass a "
f"segmenter, or deactivate with "
f"Policy(segment_scripts=()).{ja_hint}",
f"Policy(segment_scripts=frozenset()).{ja_hint}",
UserWarning, stacklevel=3)

def __repr__(self) -> str:
Expand Down
7 changes: 6 additions & 1 deletion nameparser/_pipeline/_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
A Role.MAIDEN region is the WHOLE inner span, marker word included --
nothing here strips one. classify tags a marker inside it like any
other token, and group drops it from a multi-token clause (#329).
Reads: Policy.nickname_delimiters, Policy.maiden_delimiters.
Reads: Policy.nickname_delimiters, Policy.maiden_delimiters, and
Lexicon.suffix_words / suffix_acronyms / suffix_acronyms_ambiguous
through _suffix_shaped, which lets a clause's CONTENT overrule the
delimiter's verdict: 'Andrew Perkins (MBA)' is not a nickname, so
only the two delimiter spans are masked and the content rejoins the
token stream.

Matching rules (the #273 mechanism): one left-to-right scan over the
original text, no nesting. At each position the LEFTMOST boundary-valid
Expand Down
3 changes: 2 additions & 1 deletion nameparser/_pipeline/_post_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Consumes: tokens (roles assigned).
Produces: tokens with roles adjusted by the post rules.
Reads: Policy.patronymic_rules; Lexicon.given_name_titles.
Reads: Policy.patronymic_rules, Policy.middle_as_family;
Lexicon.given_name_titles.

Rules (each a small pure function over the role-bearing tokens):
1. v1 handle_firstnames: when the parse is exactly a title plus ONE
Expand Down
8 changes: 4 additions & 4 deletions nameparser/_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ class Policy:
"""The behavior switches a parser runs with: name order,
patronymic rules, delimiter routing, input scrubbing. Immutable
and hashable; every field has a safe default, so construct with
only what you change -- ``Policy(maiden_delimiters={("(", ")")})``
only what you change -- ``Policy(maiden_delimiters=frozenset({("(", ")")}))``
-- and pass the result to ``Parser(policy=...)``."""

#: How positional (no-comma) input maps onto given/middle/family.
Expand All @@ -584,7 +584,7 @@ class Policy:
#: stored as sorted pairs). The default reads wholly-Han/Hangul
#: names, and kana-licensed Japanese names, family-first -- see
#: :data:`~nameparser.DEFAULT_SCRIPT_ORDERS`. Opt out with
#: ``script_orders={}``. Latin-script and mixed-script input is
#: ``script_orders=()``. Latin-script and mixed-script input is
#: never affected. Like name_order, ignored where a comma already
#: decides the family name.
script_orders: tuple[tuple[Script, tuple[Role, Role, Role]], ...] = (
Expand All @@ -602,7 +602,7 @@ class Policy:
#: locales.ZH for Chinese and locales.JA -- which activates
#: Script.HIRAGANA alongside it, the kana license's carrier key --
#: for Japanese.
#: Opt out with ``segment_scripts=()``; note a PolicyPatch unions
#: Opt out with ``segment_scripts=frozenset()``; note a PolicyPatch unions
#: rather than replaces, so a pack can only add scripts, never
#: disable one.
segment_scripts: frozenset[Script] = frozenset({Script.HANGUL})
Expand All @@ -622,7 +622,7 @@ class Policy:
#: (open, close) pairs whose enclosed content becomes the maiden
#: field instead; a pair listed here is dropped from the effective
#: nickname set (maiden wins, see __post_init__), so
#: maiden_delimiters={("(", ")")} is the whole recipe (#274).
#: maiden_delimiters=frozenset({("(", ")")}) is the whole recipe (#274).
#: A maiden_markers word opening the enclosed content is dropped
#: from the value, but only where that content holds more than one
#: token: a lone "(Nee)" is a maiden NAME, not a marker (#329).
Expand Down
14 changes: 14 additions & 0 deletions tests/v2/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,20 @@ def test_segmenterless_activation_without_vocabulary_warns() -> None:
parser_for(locales.JA)


def test_the_activation_warning_offers_a_spelling_that_type_checks() -> None:
# The message's actionable half is the deactivation it offers, and
# a user pastes it verbatim. nameparser ships py.typed, so that
# paste has to survive a type checker: Policy(segment_scripts=())
# is an arg-type error, since the field is annotated with what it
# STORES (a frozenset) rather than everything the constructor
# accepts. Nothing pinned this half of the message before.
with pytest.warns(UserWarning) as caught:
parser_for(locales.JA)
message = str(caught[0].message)
assert "Policy(segment_scripts=frozenset())" in message
assert "Policy(segment_scripts=())" not in message


def test_a_segmenter_silences_the_activation_warning() -> None:
# any segmenter counts: the gap is "nothing can divide these",
# not "you did not use namedivider"
Expand Down