Skip to content

Commit bf1141c

Browse files
derek73claude
andcommitted
feat: default spec is Derek's chosen format — quoted nickname, parenthesized maiden
'{title} {given} "{nickname}" {middle} {family} ({maiden}) {suffix}': nickname in the classic quoted position after the given name, maiden parenthesized after the family. The collapse's existing empty-'""' and empty-'()' rules cover both decorations, so the née trailing-strip special case is deleted. Trade-off documented: the quoted nickname round-trips exactly; parenthesized maiden re-parses as a nickname (custom 'née {maiden}' spec available for lossless round-trip). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e7a5c4d commit bf1141c

5 files changed

Lines changed: 20 additions & 26 deletions

File tree

docs/release_log.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Release Log
2828
- Change suffix-delimiter rendering: with a custom ``Policy``/``Constants`` suffix delimiter configured (e.g. ``suffix_delimiter="/"``, ``"John Smith, RN/CRNA"``), 1.x split the token and rendered ``suffix="RN, CRNA"``; 2.0 keeps the no-space delimiter-core token whole (``suffix="RN/CRNA"``) -- role assignment is unchanged, only rendering differs (anti-#100, migration plan deviation 5; ``tests/v2/cases.py`` row ``suffix_delimiter_no_space_core``). Only fires with a non-default policy, so it does not appear in the (default-policy) differential corpus
2929
- Change ``comparison_key()``/``matches()`` (both APIs) to fold components with ``str.casefold()`` where 1.4 used ``str.lower()``: Unicode case-pair forms now compare equal -- ``"STRASSE"`` matches ``"Straße"``, and a Greek final-sigma variant matches its regular-sigma form. Strictly more permissive (anything 1.4 matched still matches); parse output is unaffected -- vocabulary normalization itself uses ``lower()``, v1-parity (``tests/v2/test_types.py`` row ``test_matches_casefolds_unicode_case_pairs``)
3030
- Change delimiter-overlap precedence in the 2.0 API: a pair listed in ``Policy.maiden_delimiters`` is dropped from the effective ``nickname_delimiters`` set (maiden wins), so ``Policy(maiden_delimiters={("(", ")")})`` alone routes parenthesized content to ``maiden`` -- no need to rebuild the nickname set. The default nickname set is now the public ``DEFAULT_NICKNAME_DELIMITERS`` constant. The 1.x facade keeps v1's nickname-wins precedence on overlap via a shim-side pre-subtraction, so no ``HumanName`` behavior changes (``tests/v2/cases.py`` row ``maiden_delimiters_win_when_shared``; ``tests/v2/test_config_shim.py`` row ``test_snapshot_overlap_keeps_v1_nickname_precedence``)
31-
- Change the 2.0 API's default ``render()``/``str()`` spec to show every non-empty field: ``"{title} {given} {middle} {family} {suffix} ({nickname}) née {maiden}"``. v1's default already showed ``({nickname})``; 2.0 had dropped it (so ``str(parse("Von Johnson (smith)"))`` silently lost the nickname) and adds ``née {maiden}``. Both decorations round-trip -- parentheses re-extract as the nickname and ``née`` is a maiden marker. The ``HumanName`` facade keeps v1's own ``string_format`` default, unchanged (``tests/v2/test_render.py`` row ``test_default_str_includes_nickname_and_maiden``)
31+
- Change the 2.0 API's default ``render()``/``str()`` spec to show every non-empty field: ``'{title} {given} "{nickname}" {middle} {family} ({maiden}) {suffix}'``. v1's default already showed the nickname; 2.0 had dropped it (so ``str(parse("Von Johnson (smith)"))`` silently lost it) and never showed the maiden name. The quoted nickname round-trips exactly; the parenthesized maiden re-parses as a nickname (presentation over lossless round-trip -- use ``née {maiden}`` in a custom spec if you need the round-trip). The ``HumanName`` facade keeps v1's own ``string_format`` default, unchanged (``tests/v2/test_render.py`` row ``test_default_str_includes_nickname_and_maiden``)
3232
- Recognize typographic nickname delimiters by default in BOTH APIs (closes #273): smart quotes (``“Jack”``), German/Polish low-high quotes (``„Hansi“``), Swedish right-right quotes (``”Ann”``), guillemets in both directions (``«Petit»``, ``»Hansi«``, inner spacing tolerated), CJK corner brackets (``「タロ」``/``『ハナ』``), and fullwidth parentheses. In 1.x these leaked into ``middle`` as literal text. Curly *single* quotes stay excluded (U+2019 is the apostrophe in "O’Connor" -- pinned by ``curly_apostrophe_stays_literal``). The v1 keyed idioms work on the new named sentinels (``smart_double_quotes``, ``guillemets``, ...). A delimiter character consumed by another pair's extraction no longer emits a spurious ``unbalanced-delimiter`` ambiguity (``tests/v2/cases.py`` rows ``*_nickname``; ``tests/v2/pipeline/test_extract.py``). Not present in the differential corpus
3333

3434
Everything else in the 486-name differential corpus (built from the

docs/usage.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,13 @@ Nicknames and maiden names
7070
'Jones'
7171

7272
Both fields appear in the default ``str()`` rendering — the nickname
73-
in parentheses, the maiden name after ``née``:
73+
quoted after the given name, the maiden name parenthesized after the
74+
family name:
7475

7576
.. doctest::
7677

7778
>>> str(parse("Jane (Janie) Smith née Jones"))
78-
'Jane Smith (Janie) née Jones'
79+
'Jane "Janie" Smith (Jones)'
7980

8081
Comparing names
8182
----------------

nameparser/_render.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,6 @@ def _collapse(rendered: str) -> str:
5151
.replace(' ""', ""))
5252
rendered = _SPACE_BEFORE_COMMA.sub(",", rendered)
5353
rendered = _SPACES.sub(" ", rendered.strip())
54-
# the default spec's maiden decoration: an empty {maiden} leaves a
55-
# trailing bare 'née' (lowercase, exactly the template's spelling
56-
# -- a capitalized name 'Née' as content is untouched); on an
57-
# all-empty parse the decoration IS the whole string
58-
rendered = rendered.removesuffix(" née")
59-
if rendered == "née":
60-
rendered = ""
6154
if rendered and _COMMA_CHAR.fullmatch(rendered[-1]):
6255
rendered = rendered[:-1]
6356
return rendered.strip(", ")

nameparser/_types.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -542,16 +542,16 @@ def matches(self, other: str | ParsedName, *,
542542

543543
def render(
544544
self,
545-
spec: str = ("{title} {given} {middle} {family} {suffix} "
546-
"({nickname}) née {maiden}"),
545+
spec: str = ('{title} {given} "{nickname}" {middle} {family} '
546+
"({maiden}) {suffix}"),
547547
) -> str:
548548
"""Fill the str.format spec from the seven role fields and the
549549
derived views; empty fields collapse (#254), including the
550-
default spec's decorations -- '()' around an absent nickname
551-
and a trailing orphaned 'née'. The default shows every
552-
non-empty field and round-trips: parentheses re-extract as the
553-
nickname and 'née' is a maiden marker. Unknown keys raise
554-
KeyError naming the valid fields."""
550+
default spec's decorations (empty '""' and '()' wrappers).
551+
The default shows every non-empty field: the nickname quoted
552+
after the given name, the maiden name parenthesized after the
553+
family name. Unknown keys raise KeyError naming the valid
554+
fields."""
555555
import nameparser._render as _render
556556
return _render.render(self, spec)
557557

tests/v2/test_render.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,18 @@ def test_render_empty_parse_is_empty_string() -> None:
7171

7272
def test_default_str_includes_nickname_and_maiden() -> None:
7373
# Derek's terminal find (2026-07-19): str(parse('Von Johnson
74-
# (smith)')) dropped the nickname. v1's default string_format
75-
# always showed '({nickname})'; the 2.0 default restores it and
76-
# adds 'née {maiden}' -- and both decorations round-trip: parens
77-
# re-extract as nickname, née is a maiden marker.
74+
# (smith)')) dropped the nickname; the default spec now shows all
75+
# seven fields -- nickname quoted after the given name, maiden
76+
# parenthesized after the family name (Derek's chosen format).
77+
# The nickname decoration round-trips exactly; the maiden parens
78+
# re-extract as a NICKNAME on reparse (documented trade-off:
79+
# presentation over lossless round-trip).
7880
from nameparser import parse
7981

80-
assert str(parse("Von Johnson (smith)")) == "Von Johnson (smith)"
81-
assert str(parse("Jane Smith née Jones")) == "Jane Smith née Jones"
82+
assert str(parse("Von Johnson (smith)")) == 'Von "smith" Johnson'
83+
assert str(parse("Jane Smith née Jones")) == "Jane Smith (Jones)"
8284
both = parse("Jane (Janie) Smith née Jones")
83-
assert str(both) == "Jane Smith (Janie) née Jones"
84-
reparsed = parse(str(both))
85-
assert reparsed.nickname == "Janie" and reparsed.maiden == "Jones"
85+
assert str(both) == 'Jane "Janie" Smith (Jones)'
8686
# no orphaned decoration when the fields are empty
8787
assert str(parse("Dr. Juan Q. Xavier de la Vega III")) == (
8888
"Dr. Juan Q. Xavier de la Vega III")

0 commit comments

Comments
 (0)