Skip to content

Commit f609d40

Browse files
derek73claude
andcommitted
docs: user-facing docstrings across the public API surface
The task pages delegate detail to the reference ('see modules for the full field list'), but Token/Ambiguity/Lexicon/Policy/Locale had no class docstrings (autodoc rendered bare constructor signatures), Role inherited Enum's 'Create a collection of name/value pairs', and no dataclass field carried a #: doc. Every public class now opens with what it is / how you get one / how it relates to its neighbors, every field has a one-line #: doc that renders in the reference, and parse()/Parser.parse()'s first sentences speak to users instead of citing spec sections. Maintainer notes stay, demoted below the user-facing text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0ab19fd commit f609d40

5 files changed

Lines changed: 143 additions & 23 deletions

File tree

nameparser/_lexicon.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,16 +128,47 @@ def _normpairs(
128128

129129
@dataclass(frozen=True, slots=True)
130130
class Lexicon:
131+
"""The vocabulary a parser matches against: which words are
132+
titles, particles, suffixes, and so on. Immutable and hashable.
133+
Start from :meth:`default` (the shipped vocabulary) or
134+
:meth:`empty`, derive variants with :meth:`add` / :meth:`remove` /
135+
``|`` (union), and pass the result to ``Parser(lexicon=...)``.
136+
Entries are normalized at construction -- lowercased, edge periods
137+
stripped -- so matching is case-insensitive."""
138+
139+
#: Pre-nominal titles ("dr", "sir", "capt").
131140
titles: frozenset[str] = frozenset()
141+
#: Titles whose single following name reads as a GIVEN name
142+
#: ("sheikh", "sister") rather than a family name.
132143
given_name_titles: frozenset[str] = frozenset()
144+
#: Post-nominal acronym suffixes, matched with or without periods
145+
#: ("phd" matches "PhD" and "Ph.D.").
133146
suffix_acronyms: frozenset[str] = frozenset()
147+
#: Post-nominal word suffixes ("jr", "esquire", "iii").
134148
suffix_words: frozenset[str] = frozenset()
149+
#: Subset of suffix_acronyms counted as suffixes only when written
150+
#: WITH periods -- their bare forms are common surnames ("ma",
151+
#: "do": "Jack Ma" keeps his family name).
135152
suffix_acronyms_ambiguous: frozenset[str] = frozenset()
153+
#: Family-name particles that chain onto the following piece
154+
#: ("van", "de", "bin").
136155
particles: frozenset[str] = frozenset()
156+
#: Subset of particles that can also BE a given name: a leading
157+
#: one reads as given and records a particle-or-given ambiguity
158+
#: ("Van Johnson").
137159
particles_ambiguous: frozenset[str] = frozenset()
160+
#: Words that join surrounding pieces into one ("y", "and", "и").
138161
conjunctions: frozenset[str] = frozenset()
162+
#: Given-name prefixes that bind to the following word to form one
163+
#: given name ("abdul" -> "Abdul Salam"); never standalone names.
139164
bound_given_names: frozenset[str] = frozenset()
165+
#: Marker words introducing a birth surname, routed to the maiden
166+
#: field ("née", "geb.", "roz.").
140167
maiden_markers: frozenset[str] = frozenset()
168+
#: Lowercase word -> exact-cased replacement used by capitalized()
169+
#: ("phd" -> "Ph.D."). Pair-valued: change it with
170+
#: dataclasses.replace(), not add()/remove(); read it as a mapping
171+
#: via capitalization_exceptions_map.
141172
# Canonical storage: sorted tuple of (key, value) pairs. The
142173
# constructor tolerates any Mapping (or pair iterable) at runtime and
143174
# canonicalizes here; this closes the caller-aliasing hole and keeps

nameparser/_locale.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,20 @@
2020

2121
@dataclass(frozen=True, slots=True)
2222
class Locale:
23+
"""A named, shareable bundle of vocabulary and behavior for a
24+
naming tradition: a lexicon fragment plus a policy patch, applied
25+
together by :func:`nameparser.parser_for`. The packs shipped with
26+
nameparser live in :mod:`nameparser.locales`; building your own
27+
needs no registration -- construct one and pass it to
28+
``parser_for``."""
29+
30+
#: Identifier, lowercase ``[a-z0-9_]+`` (e.g. "ru", "tr_az").
2331
code: str
32+
#: Vocabulary ADDED to the base parser's lexicon (unioned; a pack
33+
#: never removes base vocabulary).
2434
lexicon: Lexicon
35+
#: Behavior changes folded onto the base policy (set-valued fields
36+
#: union; scalars override, later pack wins).
2537
policy: PolicyPatch = PolicyPatch()
2638

2739
# in the class body so @dataclass(slots=True) keeps them

nameparser/_parser.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,18 @@
2424

2525
@dataclass(frozen=True, slots=True)
2626
class Parser:
27-
"""Immutable, thread-safe, picklable by construction (spec §4): all
28-
validity checking happens at construction; a Parser that constructs
29-
successfully cannot fail at parse time on any str content. The None
30-
field defaults resolve in __post_init__; after construction both
31-
fields are always non-None (the annotations state the steady-state
32-
truth, hence the assignment ignores on the defaults)."""
27+
"""A configured name parser: a :class:`Lexicon` (vocabulary) plus
28+
a :class:`Policy` (behavior), both defaulted when omitted. Build
29+
one when you need non-default configuration, build it once, and
30+
call :meth:`parse` many times -- it is immutable, thread-safe, and
31+
picklable by construction: all validity checking happens at
32+
construction, so a Parser that constructs successfully cannot fail
33+
at parse time on any str content.
34+
35+
(The None field defaults resolve in __post_init__; after
36+
construction both fields are always non-None -- the annotations
37+
state the steady-state truth, hence the assignment ignores on the
38+
defaults.)"""
3339

3440
lexicon: Lexicon = None # type: ignore[assignment] # None -> default()
3541
policy: Policy = None # type: ignore[assignment] # None -> Policy()
@@ -55,9 +61,10 @@ def __repr__(self) -> str:
5561
return f"Parser({self.lexicon!r}, {self.policy!r})"
5662

5763
def parse(self, text: str) -> ParsedName:
58-
"""Total over str (spec §5a): content never raises; non-str
59-
raises TypeError eagerly, with a decode hint for bytes (#245:
60-
bytes support ended with 1.x)."""
64+
"""Parse one name string into a :class:`ParsedName`. Never
65+
raises on string content (unparseable input yields empty
66+
fields plus ambiguities); non-str raises TypeError eagerly,
67+
with a decode hint for bytes (bytes support ended with 1.x)."""
6168
if isinstance(text, bytes):
6269
raise TypeError(
6370
"parse() takes str, not bytes -- decode first, e.g. "
@@ -75,7 +82,10 @@ def _default_parser() -> Parser:
7582

7683

7784
def parse(text: str) -> ParsedName:
78-
"""Module-level convenience over a lazily created default Parser."""
85+
"""Parse a name with the default configuration and return a
86+
:class:`ParsedName`. Equivalent to ``Parser().parse(text)``; build
87+
your own :class:`Parser` (or use :func:`parser_for`) for custom
88+
vocabulary or behavior. Never raises on string content."""
7989
return _default_parser().parse(text)
8090

8191

nameparser/_policy.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,38 @@ def _reject_bare_string_order(value: object) -> None:
4848

4949
@dataclass(frozen=True, slots=True)
5050
class Policy:
51+
"""The behavior switches a parser runs with: name order,
52+
patronymic rules, delimiter routing, input scrubbing. Immutable
53+
and hashable; every field has a safe default, so construct with
54+
only what you change -- ``Policy(maiden_delimiters={("(", ")")})``
55+
-- and pass the result to ``Parser(policy=...)``."""
56+
57+
#: How positional (no-comma) input maps onto given/middle/family;
58+
#: one of the exported GIVEN_FIRST (default), FAMILY_FIRST,
59+
#: FAMILY_FIRST_GIVEN_LAST orders.
5160
name_order: tuple[Role, Role, Role] = GIVEN_FIRST
61+
#: Opt-in detectors that reorder patronymic-shaped names
62+
#: (EAST_SLAVIC, TURKIC); usually set via a locale pack.
5263
patronymic_rules: frozenset[PatronymicRule] = frozenset()
64+
#: Folds middle into family instead of splitting them (v1's
65+
#: middle_name_as_last).
5366
middle_as_family: bool = False # v1's middle_name_as_last
54-
# v1 default delimiter set (#273)
67+
#: (open, close) pairs whose enclosed content becomes the nickname
68+
#: field. Defaults to DEFAULT_NICKNAME_DELIMITERS (#273).
5569
nickname_delimiters: frozenset[tuple[str, str]] = DEFAULT_NICKNAME_DELIMITERS
56-
# empty by default (v1 parity); a pair listed here routes to maiden
57-
# AND is dropped from the effective nickname set (maiden wins, see
58-
# __post_init__), so maiden_delimiters={("(", ")")} is the whole
59-
# parenthesized-maiden recipe (#274)
70+
#: (open, close) pairs whose enclosed content becomes the maiden
71+
#: field instead; a pair listed here is dropped from the effective
72+
#: nickname set (maiden wins, see __post_init__), so
73+
#: maiden_delimiters={("(", ")")} is the whole recipe (#274).
6074
maiden_delimiters: frozenset[tuple[str, str]] = frozenset()
75+
#: Separators beyond a comma that split suffix groups (e.g. " - ").
6176
extra_suffix_delimiters: frozenset[str] = frozenset()
77+
#: Allows a post-comma segment to read as a suffix via the lenient
78+
#: initial-shaped test; False requires the strict acronym form.
6279
lenient_comma_suffixes: bool = True
80+
#: Removes emoji from the input before parsing.
6381
strip_emoji: bool = True
82+
#: Removes bidirectional control characters before parsing.
6483
strip_bidi: bool = True # =False replaces v1's opt-out CONSTANTS.regexes.bidi = False
6584

6685
# in the class body so @dataclass(slots=True) keeps them

nameparser/_types.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525

2626

2727
class Role(Enum):
28+
"""The seven fields of a parsed name, one per :class:`Token`.
29+
Declaration order is the canonical field order everywhere
30+
(``as_dict()``, ``comparison_key()``, rendering). Pass a member to
31+
:meth:`ParsedName.tokens_for`; a member's ``.value`` is the field's
32+
string name (``Role.GIVEN.value == "given"``)."""
33+
2834
# Declaration order IS the canonical field order (conventions §3):
2935
# every listing of the seven fields anywhere derives from this.
3036
TITLE = "title"
@@ -37,9 +43,15 @@ class Role(Enum):
3743

3844

3945
class Span(NamedTuple):
40-
"""Provenance range into ParsedName.original. end is exclusive."""
46+
"""Where a :class:`Token` came from: a character range into
47+
:attr:`ParsedName.original` such that ``original[start:end]`` is
48+
the token's source text (``end`` exclusive). A plain two-int
49+
NamedTuple; ``None`` in :attr:`Token.span` marks a synthetic token
50+
with no source position."""
4151

52+
#: First character index (0-based).
4253
start: int
54+
#: One past the last character index.
4355
end: int
4456

4557
def __add__(self, other: object) -> NoReturn: # type: ignore[override]
@@ -120,9 +132,23 @@ def _guarded_setstate(self: object, state: dict[str, object]) -> None:
120132

121133
@dataclass(frozen=True, slots=True)
122134
class Token:
135+
"""One classified word of a parsed name: its text, where it came
136+
from, which field it belongs to, and how it was classified. Read
137+
tokens off :attr:`ParsedName.tokens` or
138+
:meth:`ParsedName.tokens_for`; you only construct one directly
139+
when hand-building a :class:`ParsedName`."""
140+
141+
#: The word exactly as written in the input (never empty).
123142
text: str
124-
span: Span | None # None = synthetic (from replace())
143+
#: Position in ParsedName.original; None marks a synthetic token
144+
#: (e.g. introduced by replace()) with no source position.
145+
span: Span | None
146+
#: The field this token belongs to.
125147
role: Role
148+
#: Classification labels. Exactly the four in STABLE_TAGS
149+
#: ("particle", "conjunction", "initial", "joined") are API;
150+
#: namespaced tags like "vocab:..." are unstable debugging
151+
#: provenance -- never match against them.
126152
tags: frozenset[str] = frozenset()
127153

128154
# in the class body so @dataclass(slots=True) keeps them
@@ -187,7 +213,10 @@ def __repr__(self) -> str:
187213

188214

189215
class AmbiguityKind(StrEnum):
190-
"""Stable identifiers (API); members ARE their string values."""
216+
"""The stable vocabulary of :class:`Ambiguity` kinds. A StrEnum:
217+
members ARE their string values, so ``kind == "particle-or-given"``
218+
compares directly. New kinds may be added in minor releases;
219+
existing values never change meaning."""
191220

192221
ORDER = "order"
193222
SUFFIX_OR_NICKNAME = "suffix-or-nickname"
@@ -198,8 +227,17 @@ class AmbiguityKind(StrEnum):
198227

199228
@dataclass(frozen=True, slots=True)
200229
class Ambiguity:
230+
"""A call the parser made that could legitimately have gone the
231+
other way, surfaced on :attr:`ParsedName.ambiguities` instead of
232+
silently guessed away. The parse still commits to one reading --
233+
an Ambiguity is a flag for review, not an error."""
234+
235+
#: Which known ambiguity shape this is (stable API values).
201236
kind: AmbiguityKind
237+
#: Human-readable specifics of this occurrence (wording unstable).
202238
detail: str
239+
#: The tokens involved -- always a subset of the owning
240+
#: ParsedName's tokens; may be empty (e.g. unbalanced-delimiter).
203241
tokens: tuple[Token, ...]
204242

205243
# in the class body so @dataclass(slots=True) keeps them
@@ -232,15 +270,25 @@ def __repr__(self) -> str:
232270

233271
@dataclass(frozen=True, slots=True)
234272
class ParsedName:
235-
"""Immutable result of a parse. Constructor-enforced invariants:
236-
spans ascending, non-overlapping, in bounds of `original`; every
237-
Ambiguity's tokens are a subset of `tokens`. Provenance semantics
238-
(text == original[span] for parser-produced names) are documented,
239-
not enforced -- transforms like replace() legitimately break them.
273+
"""The immutable result of parsing one name string. Read the seven
274+
fields as strings (``.given``, ``.family``, ...); inspect structure
275+
through :attr:`tokens` / :meth:`tokens_for`; correct a parse with
276+
:meth:`replace` (returns a new value); produce output with
277+
:meth:`render`, :meth:`initials`, :meth:`capitalized`, or ``str()``.
278+
279+
Constructor-enforced invariants: spans ascending, non-overlapping,
280+
in bounds of `original`; every Ambiguity's tokens are a subset of
281+
`tokens`. Provenance semantics (text == original[span] for
282+
parser-produced names) are documented, not enforced -- transforms
283+
like replace() legitimately break them.
240284
"""
241285

286+
#: The input string exactly as passed to parse().
242287
original: str
288+
#: Every classified token, in document order.
243289
tokens: tuple[Token, ...]
290+
#: Judgment calls that could have gone the other way; empty for
291+
#: most names (see Ambiguity).
244292
ambiguities: tuple[Ambiguity, ...] = ()
245293

246294
# in the class body so @dataclass(slots=True) keeps them

0 commit comments

Comments
 (0)