Repository navigation
Parser has no initials, so a name parsed under a custom lexicon cannot have that view answered under it #464
Description
Parser carries three crossings that let a name parsed under a custom
lexicon be rendered or compared under that same lexicon:
Parser.capitalized(name, force=...)→ParsedName.capitalized(lexicon=...)Parser.matches(a, b)→ parsesstrarguments with THIS parserParser.revise(name, **fields)→ classifies each value with THIS parser
There is no Parser.initials. ParsedName.initials() takes
(spec, delimiter, separator) and nothing else
(nameparser/_types.py:856), so there is no supported way to tell that
view which vocabulary the name was parsed under.
Why it matters now
initials() reads token tags, and tags are the parse's own record, so
for a name the parser produced the missing parameter costs nothing —
the tags already carry the custom lexicon's decisions. It costs
something as soon as a field is spliced in after the parse:
>>> from nameparser import Parser, Lexicon
>>> p = Parser()
>>> p.parse("john de la vega").initials()
'j. v.'
>>> p.parse("john smith").replace(family="de la vega").initials()
'j. d. l. v.'The spliced tokens carry no reading, so every word of the field
contributes an initial. The facade agrees with the parse, not with
the core (HumanName("john de la vega").initials() is 'j. v.', and
so is 1.4.0's), because HumanName.initials_list() computes from the
field STRINGS with its own vocabulary. So the divergence is v2-core
only, and it has shipped since 2.0.0.
Why the obvious fix was written and dropped
PR #463 added a fallback: where a token carries no reading, ask a
vocabulary the way the parser would have. capitalized() can do this
honestly — it is handed a lexicon. initials() is not, so the
fallback had to reach for Lexicon.default() and guess. Under a
caller's own vocabulary the guess is wrong in the worst direction —
it does not merely fail to fix the defect, it erases a field that was
answering correctly:
>>> from nameparser import Parser, Lexicon
>>> p = Parser(lexicon=Lexicon.default().add(particles={"y"}))
>>> p.parse("Juan de y").initials()
'J. d. y.'
>>> p.parse("Juan Perez").replace(family="de y").initials()
'J. d. y.' # today
'J.' # with the Lexicon.default() fallbackUnder the caller's lexicon de y is an all-particle family, so all of
its words are name words and initial (rules.md#R2/#R3). Under the
default vocabulary y is a conjunction and de a particle, so both
are skipped and the whole field's initials vanish. The fallback was
reverted for that reason; the underlying defect is unfixed and
recorded as accepted in docs/design/rules.md#R3 and
docs/design/decisions.md.
What this issue asks for
A crossing that lets initials() be answered under the parse's own
vocabulary, so a future fallback is answerable rather than a guess.
The obvious shape, matching the three crossings that already exist:
class Parser:
def initials(self, name: ParsedName, spec: str = ...,
delimiter: str = ".", separator: str = " ") -> str:
...Open questions worth deciding on the issue rather than in review:
-
Does the crossing alone close the defect, or is a second change
needed?Parser.initialswould give the view a vocabulary; whether
the view should then fall back for spliced text is the decision PR
Render views honor the parser's decisions instead of re-deciding (#458) #463 got wrong once, and it should be taken explicitly. Note that
ParsedName.initials()— the no-parser call — would still have no
vocabulary and would still answer from tags alone, so the two
surfaces would disagree by design.capitalized()already has that
shape (lexicon=Nonemeans the default), which is a precedent for
either answer. -
Or should
ParsedName.initials()grow alexicon=parameter
instead, matchingParsedName.capitalized(lexicon=None, *, force)
exactly, withParser.initialsas the thin delegate? That is the
smaller surface change and the one that makes the two methods
symmetric. It is also the one that makesParsedName.initials()
able to guessLexicon.default()on an omitted argument — which is
the same guess this issue says is wrong, only now the caller has a
way to avoid it. -
Is
Parser.revise()the answer we want to keep recommending?
It already crosses this correctly —p.revise(name, family="de y")
classifies the value with the parser's own lexicon and initials
J. d. y.— so the crossing exists for anyone who edits through
the parser. This issue is aboutreplace(), which deliberately does
NOT classify, and about the many callers who reach for it first.
Measured on
fix/458-461-render-honors-the-parse @ e39d8a3, 2026-08-29, and on the
released 1.4.0 wheel for the facade comparison.
Activity
docs: the seven findings a design-docs review of #463 raised