|
7 | 7 | from __future__ import annotations |
8 | 8 |
|
9 | 9 | import dataclasses |
10 | | -import re |
11 | 10 | import warnings |
12 | 11 | from collections.abc import Iterator |
13 | 12 | from typing import Any |
|
28 | 27 | # still-executing __init__. |
29 | 28 | import nameparser.config # noqa: F401 |
30 | 29 |
|
| 30 | +import nameparser._render as _render |
31 | 31 | from nameparser._config_shim import CONSTANTS, Constants, _cached_parser |
32 | 32 | from nameparser._lexicon import _normalize |
33 | 33 | from nameparser._parser import Parser |
34 | 34 | from nameparser._types import FOLDED_TAG, ParsedName, Role |
35 | 35 |
|
36 | 36 | _V2_FIELD = {"first": "given", "last": "family"} # v1 name -> v2 name |
37 | | -_MEMBERS = ("title", "first", "middle", "last", "suffix", "nickname", |
38 | | - "maiden") |
| 37 | +_V1_SPELLING = {v2: v1 for v1, v2 in _V2_FIELD.items()} |
| 38 | +# derived from Role: declaration order IS the canonical field order |
| 39 | +# (never restated), rendered in the v1 spellings |
| 40 | +_MEMBERS = tuple(_V1_SPELLING.get(r.value, r.value) for r in Role) |
| 41 | + |
39 | 42 |
|
40 | | -_SPACES = re.compile(r"\s+") |
41 | | -_SPACE_BEFORE_COMMA = re.compile(r"\s+,") |
42 | | -# the three comma characters, same set as the pipeline's COMMA_CHARS |
43 | | -_TRAILING_COMMA = re.compile(r"[,،,]$") |
44 | 43 |
|
45 | 44 | #: v1 parsing hooks the facade never calls (spec §2 exception 2 / #280). |
46 | 45 | _V1_HOOKS = ( |
@@ -230,8 +229,11 @@ def _resolve(self) -> Parser: |
230 | 229 | extra_suffix_delimiters=frozenset( |
231 | 230 | {self.suffix_delimiter})) |
232 | 231 | self._lexicon, self._policy = lexicon, policy |
| 232 | + self._parser = _cached_parser(lexicon, policy) |
233 | 233 | self._snapshot_gen = gen |
234 | | - return _cached_parser(self._lexicon, self._policy) |
| 234 | + # the fast path is a plain attribute return: hashing the two |
| 235 | + # value objects for the lru lookup is the whole fast-path cost |
| 236 | + return self._parser |
235 | 237 |
|
236 | 238 | def parse_full_name(self) -> None: |
237 | 239 | """Re-parse the stored ``full_name`` (v1's documented re-parse |
@@ -572,19 +574,21 @@ def comparison_key(self) -> tuple[str, ...]: |
572 | 574 | # -- dunders ------------------------------------------------------------ |
573 | 575 |
|
574 | 576 | def collapse_whitespace(self, string: str) -> str: |
575 | | - # v1 parser.py:976 verbatim (regexes.spaces / regexes.commas) |
576 | | - string = _SPACES.sub(" ", string.strip()) |
577 | | - if string and _TRAILING_COMMA.search(string): |
| 577 | + # v1 parser.py:976 verbatim, over _render's regexes (the #254 |
| 578 | + # collapse owns them; this public method keeps v1's narrower |
| 579 | + # two-step contract for initials() and direct callers) |
| 580 | + string = _render._SPACES.sub(" ", string.strip()) |
| 581 | + if string and _render._COMMA_CHAR.fullmatch(string[-1]): |
578 | 582 | string = string[:-1] |
579 | 583 | return string |
580 | 584 |
|
581 | 585 | def __str__(self) -> str: |
582 | 586 | if self.string_format is not None: |
583 | | - _s = self.string_format.format( |
| 587 | + rendered = self.string_format.format( |
584 | 588 | **{k: v or "" for k, v in self.as_dict().items()}) |
585 | | - _s = _s.replace(" ()", "").replace(" ''", "").replace(' ""', "") |
586 | | - _s = _SPACE_BEFORE_COMMA.sub(",", _s) |
587 | | - return self.collapse_whitespace(_s).strip(", ") |
| 589 | + # the full #254 collapse is _render._collapse -- one owner |
| 590 | + # for the cleanup chain the v1 __str__ spelled inline |
| 591 | + return _render._collapse(rendered) |
588 | 592 | return " ".join(self) |
589 | 593 |
|
590 | 594 | def __repr__(self) -> str: |
|
0 commit comments