Skip to content

Parser has no initials, so a name parsed under a custom lexicon cannot have that view answered under it #464

Description

@derek73

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) → parses str arguments with THIS parser
  • Parser.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() fallback

Under 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:

  1. Does the crossing alone close the defect, or is a second change
    needed?
    Parser.initials would 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=None means the default), which is a precedent for
    either answer.

  2. Or should ParsedName.initials() grow a lexicon= parameter
    instead
    , matching ParsedName.capitalized(lexicon=None, *, force)
    exactly, with Parser.initials as the thin delegate? That is the
    smaller surface change and the one that makes the two methods
    symmetric. It is also the one that makes ParsedName.initials()
    able to guess Lexicon.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.

  3. 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 about replace(), 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.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions