nameparser 2.1.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {nameparser-2.1.0/nameparser.egg-info → nameparser-2.2.0}/PKG-INFO +3 -1
  2. {nameparser-2.1.0 → nameparser-2.2.0}/README.rst +2 -0
  3. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/__main__.py +1 -1
  4. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_config_shim.py +41 -41
  5. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_facade.py +59 -20
  6. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_lexicon.py +101 -47
  7. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_parser.py +29 -21
  8. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_pipeline/_assemble.py +4 -0
  9. nameparser-2.2.0/nameparser/_pipeline/_assign.py +429 -0
  10. nameparser-2.2.0/nameparser/_pipeline/_classify.py +228 -0
  11. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_pipeline/_extract.py +81 -27
  12. nameparser-2.2.0/nameparser/_pipeline/_group.py +989 -0
  13. nameparser-2.2.0/nameparser/_pipeline/_pieces.py +313 -0
  14. nameparser-2.2.0/nameparser/_pipeline/_post_rules.py +629 -0
  15. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_pipeline/_script_segment.py +26 -115
  16. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_pipeline/_segment.py +22 -25
  17. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_pipeline/_state.py +46 -8
  18. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_pipeline/_tokenize.py +16 -27
  19. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_pipeline/_vocab.py +154 -5
  20. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_policy.py +15 -3
  21. nameparser-2.2.0/nameparser/_render.py +316 -0
  22. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_types.py +138 -24
  23. nameparser-2.2.0/nameparser/_version.py +19 -0
  24. nameparser-2.2.0/nameparser/config/__init__.py +41 -0
  25. nameparser-2.2.0/nameparser/config/_deprecated.py +141 -0
  26. nameparser-2.2.0/nameparser/config/_invariants.py +53 -0
  27. nameparser-2.2.0/nameparser/config/bound_first_names.py +23 -0
  28. nameparser-2.2.0/nameparser/config/bound_given_names.py +60 -0
  29. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/config/conjunctions.py +2 -2
  30. nameparser-2.2.0/nameparser/config/maiden_markers.py +111 -0
  31. nameparser-2.2.0/nameparser/config/particles.py +275 -0
  32. nameparser-2.2.0/nameparser/config/prefixes.py +38 -0
  33. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/config/suffixes.py +133 -41
  34. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/config/surnames.py +3 -4
  35. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/config/titles.py +58 -19
  36. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/locales/__init__.py +4 -3
  37. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/locales/ja.py +6 -3
  38. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/locales/ru.py +5 -3
  39. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/locales/tr_az.py +3 -2
  40. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/locales/zh.py +2 -1
  41. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/parser.py +2 -1
  42. {nameparser-2.1.0 → nameparser-2.2.0/nameparser.egg-info}/PKG-INFO +3 -1
  43. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser.egg-info/SOURCES.txt +15 -2
  44. nameparser-2.1.0/tests/test_bound_first_names.py → nameparser-2.2.0/tests/test_bound_given_names.py +58 -1
  45. nameparser-2.2.0/tests/test_capitalization.py +364 -0
  46. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_conjunctions.py +1 -1
  47. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_first_name.py +24 -4
  48. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_initials.py +27 -6
  49. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_nicknames.py +0 -1
  50. nameparser-2.1.0/tests/test_prefixes.py → nameparser-2.2.0/tests/test_particles.py +29 -25
  51. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_python_api.py +1 -1
  52. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_titles.py +3 -2
  53. nameparser-2.2.0/tests/v2/_differential_fixtures.py +109 -0
  54. nameparser-2.2.0/tests/v2/cases.py +3601 -0
  55. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_assign.py +190 -5
  56. nameparser-2.2.0/tests/v2/pipeline/test_group.py +1130 -0
  57. nameparser-2.2.0/tests/v2/pipeline/test_pieces.py +103 -0
  58. nameparser-2.2.0/tests/v2/pipeline/test_post_rules.py +816 -0
  59. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_script_segment.py +1 -1
  60. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_state.py +8 -2
  61. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_vocab.py +83 -2
  62. nameparser-2.2.0/tests/v2/rules_doc.py +200 -0
  63. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_benchmark.py +154 -14
  64. nameparser-2.2.0/tests/v2/test_cases.py +72 -0
  65. nameparser-2.2.0/tests/v2/test_config_aliases.py +404 -0
  66. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_config_shim.py +9 -6
  67. nameparser-2.2.0/tests/v2/test_contracts.py +241 -0
  68. nameparser-2.2.0/tests/v2/test_differential.py +1361 -0
  69. nameparser-2.2.0/tests/v2/test_doc_citations.py +176 -0
  70. nameparser-2.2.0/tests/v2/test_doc_spellings.py +30 -0
  71. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_facade.py +45 -2
  72. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_facade_cases.py +14 -1
  73. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_layering.py +63 -5
  74. nameparser-2.2.0/tests/v2/test_ledger_guards.py +2452 -0
  75. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_lexicon.py +36 -4
  76. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_locales.py +23 -10
  77. nameparser-2.2.0/tests/v2/test_parser.py +1499 -0
  78. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_properties.py +19 -6
  79. nameparser-2.2.0/tests/v2/test_regex_sync.py +211 -0
  80. nameparser-2.2.0/tests/v2/test_render.py +641 -0
  81. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_reprs.py +15 -1
  82. nameparser-2.2.0/tests/v2/test_rules_doc.py +145 -0
  83. nameparser-2.2.0/tests/v2/test_rules_doc_grammar.py +95 -0
  84. nameparser-2.1.0/nameparser/_pipeline/_assign.py +0 -354
  85. nameparser-2.1.0/nameparser/_pipeline/_classify.py +0 -97
  86. nameparser-2.1.0/nameparser/_pipeline/_group.py +0 -408
  87. nameparser-2.1.0/nameparser/_pipeline/_post_rules.py +0 -130
  88. nameparser-2.1.0/nameparser/_render.py +0 -177
  89. nameparser-2.1.0/nameparser/_version.py +0 -9
  90. nameparser-2.1.0/nameparser/config/__init__.py +0 -20
  91. nameparser-2.1.0/nameparser/config/_invariants.py +0 -27
  92. nameparser-2.1.0/nameparser/config/bound_first_names.py +0 -28
  93. nameparser-2.1.0/nameparser/config/maiden_markers.py +0 -70
  94. nameparser-2.1.0/nameparser/config/prefixes.py +0 -130
  95. nameparser-2.1.0/tests/test_capitalization.py +0 -144
  96. nameparser-2.1.0/tests/v2/cases.py +0 -1807
  97. nameparser-2.1.0/tests/v2/pipeline/test_group.py +0 -252
  98. nameparser-2.1.0/tests/v2/pipeline/test_post_rules.py +0 -122
  99. nameparser-2.1.0/tests/v2/test_cases.py +0 -26
  100. nameparser-2.1.0/tests/v2/test_contracts.py +0 -104
  101. nameparser-2.1.0/tests/v2/test_differential.py +0 -696
  102. nameparser-2.1.0/tests/v2/test_parser.py +0 -813
  103. nameparser-2.1.0/tests/v2/test_regex_sync.py +0 -366
  104. nameparser-2.1.0/tests/v2/test_render.py +0 -290
  105. {nameparser-2.1.0 → nameparser-2.2.0}/AUTHORS +0 -0
  106. {nameparser-2.1.0 → nameparser-2.2.0}/LICENSE +0 -0
  107. {nameparser-2.1.0 → nameparser-2.2.0}/MANIFEST.in +0 -0
  108. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/__init__.py +0 -0
  109. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_locale.py +0 -0
  110. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/_pipeline/__init__.py +0 -0
  111. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/config/capitalization.py +0 -0
  112. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/config/regexes.py +0 -0
  113. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/py.typed +0 -0
  114. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser/util.py +0 -0
  115. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser.egg-info/dependency_links.txt +0 -0
  116. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser.egg-info/requires.txt +0 -0
  117. {nameparser-2.1.0 → nameparser-2.2.0}/nameparser.egg-info/top_level.txt +0 -0
  118. {nameparser-2.1.0 → nameparser-2.2.0}/pyproject.toml +0 -0
  119. {nameparser-2.1.0 → nameparser-2.2.0}/setup.cfg +0 -0
  120. {nameparser-2.1.0 → nameparser-2.2.0}/tests/__init__.py +0 -0
  121. {nameparser-2.1.0 → nameparser-2.2.0}/tests/base.py +0 -0
  122. {nameparser-2.1.0 → nameparser-2.2.0}/tests/conftest.py +0 -0
  123. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_brute_force.py +0 -0
  124. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_comma_variants.py +0 -0
  125. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_constants.py +0 -0
  126. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_east_slavic_patronymic_order.py +0 -0
  127. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_middle_name_as_last.py +0 -0
  128. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_output_format.py +0 -0
  129. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_suffixes.py +0 -0
  130. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_turkic_patronymic_order.py +0 -0
  131. {nameparser-2.1.0 → nameparser-2.2.0}/tests/test_variations.py +0 -0
  132. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/__init__.py +0 -0
  133. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/conftest.py +0 -0
  134. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/__init__.py +0 -0
  135. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_assemble.py +0 -0
  136. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_classify.py +0 -0
  137. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_extract.py +0 -0
  138. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_segment.py +0 -0
  139. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/pipeline/test_tokenize.py +0 -0
  140. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_cli.py +0 -0
  141. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_locale.py +0 -0
  142. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_policy.py +0 -0
  143. {nameparser-2.1.0 → nameparser-2.2.0}/tests/v2/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nameparser
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: A simple Python module for parsing human names into their individual components.
5
5
  Author-email: Derek Gulbranson <derek73@gmail.com>
6
6
  License: LGPL
@@ -75,6 +75,8 @@ Quick Start Example
75
75
  ]>
76
76
  >>> name.family_base, name.family_particles
77
77
  ('Vega', 'de la')
78
+ >>> name.given_names, name.surnames
79
+ ('Juan Q. Xavier', 'Q. Xavier de la Vega')
78
80
  >>> name.render("{family}, {given}")
79
81
  'de la Vega, Juan'
80
82
 
@@ -47,6 +47,8 @@ Quick Start Example
47
47
  ]>
48
48
  >>> name.family_base, name.family_particles
49
49
  ('Vega', 'de la')
50
+ >>> name.given_names, name.surnames
51
+ ('Juan Q. Xavier', 'Q. Xavier de la Vega')
50
52
  >>> name.render("{family}, {given}")
51
53
  'de la Vega, Juan'
52
54
 
@@ -1,4 +1,4 @@
1
- """Command-line debug helper over the 2.0 API (migration spec §6).
1
+ """Command-line debug helper over the 2.0 API.
2
2
 
3
3
  python -m nameparser "Dr. Juan Q. Xavier de la Vega III"
4
4
  python -m nameparser --json "Doe, John"
@@ -1,5 +1,6 @@
1
- """v1 ``Constants`` compatibility shim over Lexicon/Policy (migration
2
- spec §3). ``nameparser.config`` re-exports these names from the swap
1
+ """v1 ``Constants`` compatibility shim over Lexicon/Policy
2
+ (mechanisms.md#CONFIG-SHIM-SNAPSHOT). ``nameparser.config``
3
+ re-exports these names from the swap
3
4
  commit onward; the whole module is deleted in 3.0 with the facade.
4
5
 
5
6
  Layering: facade layer -- may import anything public; here that's
@@ -99,7 +100,7 @@ def _normalize_iterable_of_strings(
99
100
  class SetManager:
100
101
  """v1 ``SetManager`` surface over a plain set of ``lc()``-normalized
101
102
  strings. Mutations call ``_on_change`` (the owning Constants'
102
- generation bump, wired by a later task). ``__call__`` and the
103
+ generation bump, wired by the facade). ``__call__`` and the
103
104
  missing-member-tolerant ``remove()`` are gone per the #243 schedule
104
105
  (warned 1.3.0, removed 2.0): ``remove()`` of a missing member raises
105
106
  ``KeyError``, matching ``set.remove``.
@@ -316,7 +317,7 @@ class TupleManager(dict[str, object]):
316
317
  ``AttributeError`` naming the key (#256, warned 1.4, enforced 2.0 --
317
318
  the v1 ``DeprecationWarning`` is gone, this shim only speaks 2.0).
318
319
  Mutations call ``_on_change`` (the owning Constants' generation
319
- bump, wired by a later task).
320
+ bump, wired by the facade).
320
321
  """
321
322
 
322
323
  _on_change: Callable[[], None] | None
@@ -431,7 +432,7 @@ class TupleManager(dict[str, object]):
431
432
 
432
433
 
433
434
  #: The named delimiter buckets, translated to the ``Policy``
434
- #: (open, close) pairs they stand for (spec §3). The first three are
435
+ #: (open, close) pairs they stand for. The first three are
435
436
  #: v1's; the rest are the #273 typographic conventions, named so the
436
437
  #: v1 keyed idioms (pop/move/del) work on them like the originals.
437
438
  #: Keep in sync with DEFAULT_NICKNAME_DELIMITERS in _policy.py (pinned
@@ -472,8 +473,8 @@ class RegexTupleManager(TupleManager): # pickle-compat: do NOT delete
472
473
 
473
474
  class _DelimiterManager(TupleManager):
474
475
  """v1 ``nickname_delimiters``/``maiden_delimiters`` bucket. In 2.0
475
- only the named sentinels in ``_DELIMITER_SENTINELS`` exist (spec
476
- §3; the v1 trio plus the #273 typographic pairs) -- assigning any
476
+ only the named sentinels in ``_DELIMITER_SENTINELS`` exist (the v1
477
+ trio plus the #273 typographic pairs) -- assigning any
477
478
  other key raises so a caller reaches for a custom-delimiter Policy
478
479
  kwarg instead of a dict entry that silently does nothing. ``pop()``/
479
480
  ``__setitem__``/``__delitem__`` stay open (inherited) for the
@@ -517,7 +518,7 @@ class _RegexesProxy:
517
518
  ``CONSTANTS.regexes.word`` stays informational -- but 2.0 configures
518
519
  parsing behavior through named ``Policy`` flags, not by mutating a
519
520
  regex, so any attribute *or* item assignment raises ``TypeError``
520
- (spec §3's uniform read-only rule).
521
+ (the shim's uniform read-only rule).
521
522
  """
522
523
 
523
524
  @staticmethod
@@ -664,36 +665,38 @@ _SHARED_MUTATION_MESSAGE = (
664
665
  )
665
666
 
666
667
 
667
- def _default_vocab() -> dict[str, set[str]]:
668
+ def _default_vocab() -> dict[str, frozenset[str]]:
668
669
  # v1 data modules stay the single vocabulary source through 2.x
669
670
  # (same rule as Lexicon.default()).
670
- from nameparser.config.bound_first_names import BOUND_FIRST_NAMES
671
+ from nameparser.config.bound_given_names import BOUND_GIVEN_NAMES
671
672
  from nameparser.config.conjunctions import CONJUNCTIONS
672
- from nameparser.config.prefixes import (
673
- NON_FIRST_NAME_PREFIXES, PREFIXES,
673
+ from nameparser.config.particles import (
674
+ NON_GIVEN_NAME_PARTICLES, PARTICLES,
674
675
  )
675
676
  from nameparser.config.suffixes import (
676
- SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_NOT_ACRONYMS,
677
+ SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_WORDS,
677
678
  )
678
- from nameparser.config.titles import FIRST_NAME_TITLES, TITLES
679
+ from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
679
680
  return {
680
- "prefixes": PREFIXES,
681
+ "prefixes": PARTICLES,
681
682
  "suffix_acronyms": SUFFIX_ACRONYMS,
682
- "suffix_not_acronyms": SUFFIX_NOT_ACRONYMS,
683
+ "suffix_not_acronyms": SUFFIX_WORDS,
683
684
  "suffix_acronyms_ambiguous": SUFFIX_ACRONYMS_AMBIGUOUS,
684
685
  "titles": TITLES,
685
- "first_name_titles": FIRST_NAME_TITLES,
686
+ "first_name_titles": GIVEN_NAME_TITLES,
686
687
  "conjunctions": CONJUNCTIONS,
687
- "bound_first_names": BOUND_FIRST_NAMES,
688
- "non_first_name_prefixes": NON_FIRST_NAME_PREFIXES,
688
+ "bound_first_names": BOUND_GIVEN_NAMES,
689
+ "non_first_name_prefixes": NON_GIVEN_NAME_PARTICLES,
689
690
  }
690
691
 
691
692
 
692
693
  class _RenderDefaults(NamedTuple):
693
694
  """v1 scalar rendering knobs that have no home on ``Policy``
694
- (spec §3): ``__str__``/initials formatting and capitalization stay
695
+ (mechanisms.md#CONFIG-SHIM-SNAPSHOT): ``__str__``/initials
696
+ formatting and capitalization stay
695
697
  per-Constants defaults, layered onto a shared ``Parser`` by the
696
- facade (a later task) rather than folded into the cache key."""
698
+ facade (nameparser/_facade.py) rather than folded into the cache
699
+ key."""
697
700
 
698
701
  string_format: str | None
699
702
  initials_format: str
@@ -707,7 +710,8 @@ class _RenderDefaults(NamedTuple):
707
710
  @functools.lru_cache(maxsize=64)
708
711
  def _cached_parser(lexicon: Lexicon, policy: Policy) -> Parser:
709
712
  # keyed on hashable value objects: shared across every facade whose
710
- # Constants resolve to the same snapshot (spec §3)
713
+ # Constants resolve to the same snapshot
714
+ # (mechanisms.md#CONFIG-SHIM-SNAPSHOT)
711
715
  return Parser(lexicon=lexicon, policy=policy)
712
716
 
713
717
 
@@ -716,8 +720,8 @@ class Constants:
716
720
  a frozen ``(Lexicon, Policy, _RenderDefaults)`` snapshot via
717
721
  ``_snapshot()``. ``_generation`` increments on every mutation;
718
722
  facades compare it against a cached value to decide whether their
719
- snapshot is stale (dirty-tracking, spec §3 -- the facade itself is
720
- a later task).
723
+ snapshot is stale (dirty-tracking -- the facade side lives in
724
+ nameparser/_facade.py).
721
725
 
722
726
  The module-level ``CONSTANTS`` singleton (below) has ``_shared``
723
727
  flipped to ``True``: any mutation reached through it emits
@@ -980,7 +984,7 @@ class Constants:
980
984
 
981
985
  def _build_snapshot(self) -> tuple[Lexicon, Policy, _RenderDefaults]:
982
986
  """Resolve this v1-shaped, mutable Constants into the frozen
983
- 2.0 value objects it corresponds to (spec §3). A pure read: no
987
+ 2.0 value objects it corresponds to. A pure read: no
984
988
  generation bump, no deprecation warning even on the shared
985
989
  singleton -- only direct attribute mutation is on the 3.0
986
990
  removal path.
@@ -1038,9 +1042,9 @@ class Constants:
1038
1042
  particles=particles,
1039
1043
  # complement translation: v1 marks the never-given subset;
1040
1044
  # v2 marks the may-be-given subset. The trailing union keeps
1041
- # a config v1 accepted: prefixes.py asserts its own data has
1042
- # no word in both non_first_name_prefixes and
1043
- # bound_first_names, but nothing stops a caller adding one at
1045
+ # a config v1 accepted: particles.py asserts its own data has
1046
+ # no word in both NON_GIVEN_NAME_PARTICLES and
1047
+ # BOUND_GIVEN_NAMES, but nothing stops a caller adding one at
1044
1048
  # runtime, and v1 then lets the bound rule win (leading "dos
1045
1049
  # Santos Silva" parses first="dos Santos"). Treating such a
1046
1050
  # word as may-be-given reproduces that rather than raising.
@@ -1062,23 +1066,19 @@ class Constants:
1062
1066
  bound_given_names=bound,
1063
1067
  # v1 Constants has no manager for these (#274 is 2.0
1064
1068
  # behavior); the data module is the only source
1065
- maiden_markers=frozenset(MAIDEN_MARKERS),
1069
+ maiden_markers=MAIDEN_MARKERS,
1066
1070
  # likewise no v1 manager: the unspaced-name segmentation
1067
1071
  # vocabulary is 2.0 behavior (#271), so it rides in the
1068
1072
  # snapshot only -- v1's Constants surface stays frozen.
1069
- # Unwrapped where maiden_markers above is wrapped: this
1070
- # module is born frozen (#293), so no wrap
1071
1073
  surnames=KOREAN_SURNAMES,
1072
1074
  # likewise no v1 manager: the glued-honorific tail set is
1073
1075
  # 2.1 behavior (#308), so it rides in the snapshot only.
1074
- # Wrapped, unlike surnames above: suffixes.py is still a
1075
- # mutable v1 module, not born-frozen like surnames.py
1076
- # (#293). Intersect with the word set: Lexicon enforces
1077
- # tails <= suffix_words, and v1 semantics are that deleting
1078
- # a suffix word turns the behavior off -- a lingering tail
1079
- # simply stops mattering, the same rule ambiguous_acronyms
1080
- # gets against suffix_acronyms above.
1081
- honorific_tails=frozenset(GLUED_HONORIFICS) & suffix_words,
1076
+ # Intersect with the word set: Lexicon enforces tails <=
1077
+ # suffix_words, and v1 semantics are that deleting a suffix
1078
+ # word turns the behavior off -- a lingering tail simply
1079
+ # stops mattering, the same rule ambiguous_acronyms gets
1080
+ # against suffix_acronyms above.
1081
+ honorific_tails=GLUED_HONORIFICS & suffix_words,
1082
1082
  # TupleManager is dict[str, object] (v1 parity: values were
1083
1083
  # never statically str-typed); every real entry is a str,
1084
1084
  # same assumption _DelimiterManager's sentinel lookup makes
@@ -1099,8 +1099,8 @@ class Constants:
1099
1099
  _SENTINEL_PAIRS[k] for k in self.maiden_delimiters
1100
1100
  if k not in self.nickname_delimiters),
1101
1101
  # suffix_delimiter is a _RenderDefaults-only field here; the
1102
- # facade layers it onto extra_suffix_delimiters per instance
1103
- # (a later task) -- _snapshot() itself stays pure translation
1102
+ # facade layers it onto extra_suffix_delimiters per
1103
+ # instance -- _snapshot() itself stays pure translation
1104
1104
  )
1105
1105
  defaults = _RenderDefaults(
1106
1106
  self.string_format, self.initials_format, self.initials_delimiter,
@@ -1,4 +1,5 @@
1
- """The 2.0 ``HumanName`` facade (migration spec §2): a mutable wrapper
1
+ """The 2.0 ``HumanName`` facade (mechanisms.md#FACADE-CONTRACT): a
2
+ mutable wrapper
2
3
  over a frozen ParsedName, delegating parsing to the core Parser resolved
3
4
  from the bound Constants shim. Keeps every v1 spelling. Deleted in 3.0.
4
5
 
@@ -31,7 +32,8 @@ import nameparser._render as _render
31
32
  from nameparser._config_shim import CONSTANTS, Constants, _cached_parser
32
33
  from nameparser._lexicon import _normalize
33
34
  from nameparser._parser import Parser
34
- from nameparser._types import FOLDED_TAG, ParsedName, Role, Token
35
+ from nameparser._types import (FOLDED_TAG, UNCLASSIFIED_TAG, ParsedName,
36
+ Role, Token)
35
37
 
36
38
  _V2_FIELD = {"first": "given", "last": "family"} # v1 name -> v2 name
37
39
  _V1_SPELLING = {v2: v1 for v1, v2 in _V2_FIELD.items()}
@@ -41,7 +43,8 @@ _MEMBERS = tuple(_V1_SPELLING.get(r.value, r.value) for r in Role)
41
43
 
42
44
 
43
45
 
44
- #: v1 parsing hooks the facade never calls (spec §2 exception 2 / #280).
46
+ #: v1 parsing hooks the facade never calls
47
+ #: (mechanisms.md#FACADE-CONTRACT / #280).
45
48
  _V1_HOOKS = (
46
49
  "pre_process", "post_process", "parse_full_name", "parse_pieces",
47
50
  "parse_nicknames", "join_on_conjunctions", "squash_emoji",
@@ -152,7 +155,8 @@ class HumanName:
152
155
  DeprecationWarning, stacklevel=3)
153
156
 
154
157
  # -- render defaults -----------------------------------------------------
155
- # One-line validating setters (spec §2): assigning a non-str (or, for
158
+ # One-line validating setters (mechanisms.md#FACADE-CONTRACT):
159
+ # assigning a non-str (or, for
156
160
  # the two fields that allow it, non-str-non-None) raises TypeError at
157
161
  # assignment time instead of failing later inside .format().
158
162
 
@@ -218,7 +222,8 @@ class HumanName:
218
222
  # -- config / parsing ---------------------------------------------------
219
223
 
220
224
  def _resolve(self) -> Parser:
221
- """Dirty-tracked parser resolution (spec §3): rebuild the
225
+ """Dirty-tracked parser resolution
226
+ (mechanisms.md#CONFIG-SHIM-SNAPSHOT): rebuild the
222
227
  snapshot only when the bound Constants' generation moved."""
223
228
  gen = self._C._generation
224
229
  if self._snapshot_gen != gen:
@@ -472,10 +477,14 @@ class HumanName:
472
477
  return _normalize(text) in self._lexicon.conjunctions
473
478
 
474
479
  def _split_last(self) -> tuple[list[str], list[str]]:
475
- # v1 parser.py _split_last, verbatim: vocabulary lookup at ACCESS
476
- # time (so assigned last names split too), with the all-particle
477
- # guard (a family name is assumed not to consist entirely of
478
- # particles, e.g. surname "Do" which also appears in PREFIXES)
480
+ # rules.md#R2: "a name part whose every word is particle
481
+ # vocabulary is a part where none of them is doing a
482
+ # particle's work" -- the all-particle guard
483
+ # below is this rule, and predates its statement: v1 assumed a
484
+ # family name does not consist entirely of particles, e.g. the
485
+ # surname "Do" which also appears in PARTICLES. v1
486
+ # parser.py _split_last otherwise verbatim, vocabulary lookup
487
+ # at ACCESS time so assigned last names split too.
479
488
  words = " ".join(self.last_list).split()
480
489
  i = 0
481
490
  while i < len(words) and self._is_particle(words[i]):
@@ -517,20 +526,40 @@ class HumanName:
517
526
  if len(initials) > 0:
518
527
  return self.initials_separator.join(initials)
519
528
  # Return '' (never empty_attribute_default, which may be None)
520
- # when a part has no initialable words, e.g. a middle name
521
- # consisting only of prefixes ("de la"). Callers drop these
522
- # parts entirely.
529
+ # when a part has no initialable words. group_initials below
530
+ # decides what that means: one such element among others is
531
+ # dropped; a group that yields nothing AND is wholly particles
532
+ # initials its words; and a group that yields nothing for any
533
+ # other reason -- a conjunction, or particles mixed with one --
534
+ # is still dropped ("Vega, Santa de y" drops its middle).
523
535
  return ""
524
536
 
525
537
  def _initials_lists(self) -> tuple[list[str], list[str], list[str]]:
526
538
  """Initials for the first, middle and last name groups. Parts
527
- that yield no initials (e.g. a prefix-only middle name like
528
- "de la") are dropped rather than kept as empty strings.
539
+ that yield no initials are dropped rather than kept as empty
540
+ strings -- except a part that is wholly PARTICLES, whose words
541
+ initial as ordinary name words since #404, so the prefix-only
542
+ middle name "de la" is no longer an example of the dropping.
529
543
  """
530
544
  def group_initials(names: list[str],
531
545
  firstname: bool = False) -> list[str]:
532
- return [i for i in (self._process_initial(n, firstname)
533
- for n in names if n) if i]
546
+ got = [i for i in (self._process_initial(n, firstname)
547
+ for n in names if n) if i]
548
+ words = [w for n in names if n for w in n.split()]
549
+ if got or not words or not all(self._is_particle(w)
550
+ for w in words):
551
+ return got
552
+ # rules.md#R3: "except the particles of a part whose every
553
+ # word is one, which are not acting as particles there"
554
+ # -- nothing survived
555
+ # the filter, so the whole group is particles. The
556
+ # facade's twin of the core's
557
+ # UNJOINED_TAG. NOT pinned against it: both case runners
558
+ # compare the seven role fields only, and Case carries no
559
+ # initials column, so the one covering test is
560
+ # tests/test_initials.py::test_initials_middle_name_all_prefixes. _split_last already applies the same guard to
561
+ # the base, which is why last_base was never empty here.
562
+ return [w[0] for w in words]
534
563
  return (group_initials(self.first_list, True),
535
564
  group_initials(self.middle_list),
536
565
  group_initials(self.last_list))
@@ -685,7 +714,8 @@ class HumanName:
685
714
  self._suffix_delimiter = state.get("suffix_delimiter",
686
715
  defaults.suffix_delimiter)
687
716
  self._full_name = state.get("_full_name", "")
688
- # Components come back exactly as pickled (spec §2): synthetic
717
+ # Components come back exactly as pickled
718
+ # (mechanisms.md#FACADE-CONTRACT): synthetic
689
719
  # tokens, never a re-parse. Build them per *_list ENTRY rather
690
720
  # than from one joined string -- an entry may hold several words
691
721
  # ("Ph. D.", "Q.C. M.P."), and re-splitting the joined string on
@@ -717,8 +747,17 @@ class HumanName:
717
747
  f"nameparser"
718
748
  )
719
749
  for position, word in enumerate(entry.split()):
720
- tokens.append(Token(
721
- word, None, role,
722
- frozenset({"joined"}) if position else frozenset()))
750
+ # UNCLASSIFIED_TAG for the same reason replace()
751
+ # stamps it: a pickle carries the *_list STRINGS
752
+ # and no tags, so nothing here was read by a parse
753
+ # and case repair must ask the vocabulary rather
754
+ # than read an absent conjunction tag. Without it a
755
+ # restored "juan ortega y gasset" repairs to
756
+ # "Ortega Y Gasset", which is neither v1's answer
757
+ # nor the same name's unpickled one.
758
+ tags = {UNCLASSIFIED_TAG}
759
+ if position:
760
+ tags.add("joined")
761
+ tokens.append(Token(word, None, role, frozenset(tags)))
723
762
  self._parsed = ParsedName(
724
763
  original=str(state.get("original", "")), tokens=tuple(tokens))
@@ -55,7 +55,7 @@ _VOCAB_FIELDS = (
55
55
  #: nothing needs the acronym half: the shipped tails are CJK
56
56
  #: honorifics, which are words.
57
57
  #: The same relation is asserted a second time in config/suffixes.py,
58
- #: over the raw GLUED_HONORIFICS/SUFFIX_NOT_ACRONYMS constants at
58
+ #: over the raw GLUED_HONORIFICS/SUFFIX_WORDS constants at
59
59
  #: import. The two are not redundant in the way they look: that one
60
60
  #: is an `assert`, stripped under `python -O`, while the check here
61
61
  #: raises unconditionally -- so under -O this is what still holds the
@@ -75,6 +75,25 @@ _SUBSET_FIELDS = (
75
75
  )
76
76
 
77
77
 
78
+ #: The fields whose entries may be PHRASES: stored space-joined and
79
+ #: folded per word (_title_key), and exempt from the dead-entry warning
80
+ #: below. Every other field is matched one word at a time, where a
81
+ #: multi-word entry can never match.
82
+ #:
83
+ #: The two are not the same mechanism, and the difference is why the
84
+ #: exemption is a list rather than a rule. A given_name_titles run is
85
+ #: identified per word FIRST -- 'lt' and 'col' are each title
86
+ #: vocabulary -- and only then joined and looked up. A maiden marker
87
+ #: phrase has no such per-word foothold: 'z' and 'domu' are not markers
88
+ #: individually, and adding them separately (which this warning used to
89
+ #: advise) reads 'Maria Kowalska z domu Nowak' as maiden 'domu Nowak'
90
+ #: and strips 'Anna z Nowak' of its family name. So markers are matched
91
+ #: by a genuine multi-token lookahead instead --
92
+ #: _pipeline._vocab.maiden_marker_run, longest first -- and only the
93
+ #: STORAGE rule is shared with titles.
94
+ _PHRASE_FIELDS = ("given_name_titles", "maiden_markers")
95
+
96
+
78
97
  def _normalize(word: str) -> str:
79
98
  """Lowercase, strip whitespace and EDGE periods -- v1's lc()
80
99
  semantics. Interior periods survive on purpose: 'J.R.' must not
@@ -110,8 +129,9 @@ def _title_key(words: Iterable[str]) -> str:
110
129
  A multi-word title is matched as one key ('lt col'), so the fold has
111
130
  to run per word and rejoin -- _normalize on the whole phrase would
112
131
  leave interior periods. Defined once because it is built at match
113
- time (post_rules) and at translation time (the v1 facade's
114
- first_name_titles), and a divergence between the two fails silently:
132
+ time (post_rules for H1, group for the P5 licence -- which must
133
+ agree, see #369) and at translation time (the v1 facade's
134
+ first_name_titles), and a divergence between them fails silently:
115
135
  the entry simply stops matching.
116
136
 
117
137
  Words that fold away are DROPPED, not joined as empty. Keeping the
@@ -191,13 +211,14 @@ def _normset(
191
211
  raise TypeError(
192
212
  f"Lexicon.{field_name} entries must be strings, got {w!r}"
193
213
  )
194
- # given_name_titles is the one field whose entries are matched as
195
- # a multi-word run, so it folds per word: stored as the same key
196
- # post_rules builds, or 'lt. col' would be kept verbatim and
197
- # never match anything (a silent no-op on the config surface).
198
- # Every other field holds single words, where the two folds
199
- # agree.
200
- n = _title_key(w.split()) if field_name == "given_name_titles" \
214
+ # A phrase field's entries are matched as a multi-word run, so
215
+ # they fold per word: stored as the same key the match site
216
+ # builds, or 'lt. col' (and 'z. domu') would be kept verbatim
217
+ # and never match anything -- a silent no-op on the config
218
+ # surface. Every other field holds single words, where the two
219
+ # folds agree. See _PHRASE_FIELDS for how the two differ once
220
+ # stored.
221
+ n = _title_key(w.split()) if field_name in _PHRASE_FIELDS \
201
222
  else _normalize(w)
202
223
  # "." or "" is a data bug (stray split artifact, empty CSV
203
224
  # cell); dropping it silently would also let a data-module typo
@@ -207,7 +228,7 @@ def _normset(
207
228
  f"Lexicon.{field_name} entry {w!r} normalizes to empty "
208
229
  f"(lowercase + strip periods/whitespace leaves nothing)"
209
230
  )
210
- # Every field but given_name_titles is matched one word at a
231
+ # Every field outside _PHRASE_FIELDS is matched one word at a
211
232
  # time, so a multi-word entry can never match -- the library
212
233
  # itself shipped eight such dead entries for years (repaired
213
234
  # 2026-07-26). Warn, never raise: an inert entry produces
@@ -217,8 +238,10 @@ def _normset(
217
238
  # new instance's __post_init__; remove() stores nothing, so
218
239
  # warning there would name entries the caller is trying to get
219
240
  # RID of, with "split it" advice that makes no sense for a
220
- # no-op.
221
- if (warn and field_name != "given_name_titles"
241
+ # no-op. Both invariants are indifferent to WHICH fields are
242
+ # exempt: the exemption only decides whether a warning exists to
243
+ # be emitted once or suppressed.
244
+ if (warn and field_name not in _PHRASE_FIELDS
222
245
  # interior whitespace test; split() covers all Unicode
223
246
  # whitespace
224
247
  and n != "".join(n.split())):
@@ -303,8 +326,12 @@ class Lexicon:
303
326
  Entries are normalized at construction -- lowercased, edge periods
304
327
  stripped -- so matching is case-insensitive. Vocabulary entries are
305
328
  single words -- a multi-word entry warns at construction and can
306
- never match (``given_name_titles``, matched as a space-joined run,
307
- is the one exception). Field docs below show examples, not full
329
+ never match. Two fields are exempt, and they differ in HOW they
330
+ match: ``given_name_titles`` is looked up as the space-joined run
331
+ of words the parse has ALREADY read as titles, while
332
+ ``maiden_markers`` is matched by lookahead, longest first, over
333
+ words that need not be markers on their own (``"z domu"``).
334
+ Field docs below show examples, not full
308
335
  contents; inspect any field's shipped vocabulary directly, e.g.
309
336
  ``Lexicon.default().conjunctions``."""
310
337
 
@@ -313,7 +340,7 @@ class Lexicon:
313
340
  titles: frozenset[str] = frozenset()
314
341
  #: Titles whose single following name reads as a GIVEN name
315
342
  #: ("sheikh", "sister", ...) rather than a family name. Full
316
- #: default list: :data:`~nameparser.config.titles.FIRST_NAME_TITLES`.
343
+ #: default list: :data:`~nameparser.config.titles.GIVEN_NAME_TITLES`.
317
344
  given_name_titles: frozenset[str] = frozenset()
318
345
  #: Post-nominal acronym suffixes, matched with or without periods
319
346
  #: ("phd" matches "PhD" and "Ph.D."). Full default list:
@@ -321,7 +348,7 @@ class Lexicon:
321
348
  suffix_acronyms: frozenset[str] = frozenset()
322
349
  #: Post-nominal word suffixes ("jr", "esquire", "iii", ...). Full
323
350
  #: default list:
324
- #: :data:`~nameparser.config.suffixes.SUFFIX_NOT_ACRONYMS`.
351
+ #: :data:`~nameparser.config.suffixes.SUFFIX_WORDS`.
325
352
  suffix_words: frozenset[str] = frozenset()
326
353
  #: Subset of suffix_acronyms counted as suffixes only when written
327
354
  #: WITH periods -- their bare forms are common surnames ("ma",
@@ -330,14 +357,32 @@ class Lexicon:
330
357
  suffix_acronyms_ambiguous: frozenset[str] = frozenset()
331
358
  #: Family-name particles that chain onto the following piece
332
359
  #: ("van", "de", "bin", ...). Full default list:
333
- #: :data:`~nameparser.config.prefixes.PREFIXES`.
360
+ #: :data:`~nameparser.config.particles.PARTICLES`.
334
361
  particles: frozenset[str] = frozenset()
335
- #: Subset of particles that can also BE a given name: a leading
336
- #: one reads as given and records a particle-or-given ambiguity
337
- #: ("Van Johnson", but also "Van Buren"). No constant of its own
338
- #: -- the default derives
362
+ #: Subset of particles that can also BE a given name ("Van
363
+ #: Johnson", but also "Van Buren"). Membership decides nothing
364
+ #: about chaining: the prefix chain skips the name's first piece
365
+ #: and never consults this set, so it leaves a leading particle a
366
+ #: piece of its own whether listed or not -- "de Mesnil" groups
367
+ #: into two pieces exactly as "van Gogh" does, and since #367 the
368
+ #: NAME in "Dr. de Mesnil" and "Dr. Van Johnson" groups into those
369
+ #: same two pieces behind the title piece, a title not being
370
+ #: part of the name it precedes. What membership
371
+ #: decides is what becomes of that piece afterwards. Under ANY
372
+ #: ``name_order`` a member records a particle-or-given ambiguity
373
+ #: and a non-member records none, and a non-member is additionally
374
+ #: folded back into the family name once roles exist WHERE IT OPENS
375
+ #: THE NAME, so the whole name is the surname ("de Mesnil" -- a bare "de", with nothing to
376
+ #: fold into, is left alone). That fold is order-independent too
377
+ #: (#359) -- not because the word could not be a given name, which
378
+ #: it can where position forces it (``parse("de")`` reports given
379
+ #: "de"), but because a particle OPENING the name has the rest of
380
+ #: the name to join forward to, and that evidence is positional
381
+ #: rather than vocabulary, so no declared order contradicts it. Which field a MEMBER's piece
382
+ #: lands in is ``name_order``'s question, not this set's.
383
+ #: No constant of its own -- the default derives
339
384
  #: as particles minus
340
- #: :data:`~nameparser.config.prefixes.NON_FIRST_NAME_PREFIXES`
385
+ #: :data:`~nameparser.config.particles.NON_GIVEN_NAME_PARTICLES`
341
386
  #: (which marks the opposite, never-given subset).
342
387
  particles_ambiguous: frozenset[str] = frozenset()
343
388
  #: Words or characters that join surrounding pieces into one
@@ -347,10 +392,15 @@ class Lexicon:
347
392
  #: Given-name prefixes that bind to the following word to form one
348
393
  #: given name ("abdul" -> "Abdul Salam"); never standalone names.
349
394
  #: Full default list:
350
- #: :data:`~nameparser.config.bound_first_names.BOUND_FIRST_NAMES`.
395
+ #: :data:`~nameparser.config.bound_given_names.BOUND_GIVEN_NAMES`.
351
396
  bound_given_names: frozenset[str] = frozenset()
352
397
  #: Marker words introducing a birth surname, routed to the maiden
353
- #: field ("née", "geb.", "roz.", ...). Full default list:
398
+ #: field ("née", "geb.", "rozená", ...). An entry may be a PHRASE
399
+ #: ("z domu"): entries are matched by lookahead, longest first, so
400
+ #: a phrase wins where it matches and a word entry that starts one
401
+ #: still matches on its own everywhere else. A phrase matches only
402
+ #: where its words stand together -- a bracketed clause or a comma
403
+ #: between them ends the run. Full default list:
354
404
  #: :data:`~nameparser.config.maiden_markers.MAIDEN_MARKERS`.
355
405
  maiden_markers: frozenset[str] = frozenset()
356
406
  #: Family names for the unspaced-name segmentation stage (#271),
@@ -419,8 +469,8 @@ class Lexicon:
419
469
  # the expensive one -- three working configurations broken
420
470
  # across two attempts. Do not add a third.
421
471
  #
422
- # The v2 form of prefixes.py's NON_FIRST_NAME_PREFIXES-disjoint-
423
- # from-BOUND_FIRST_NAMES assertion. That module guards its own
472
+ # The v2 form of particles.py's NON_GIVEN_NAME_PARTICLES-disjoint-
473
+ # from-BOUND_GIVEN_NAMES assertion. That module guards its own
424
474
  # data at import; this guards vocabulary a caller supplies.
425
475
  contradictory = (
426
476
  self.bound_given_names & self.particles) - self.particles_ambiguous
@@ -614,39 +664,43 @@ class Lexicon:
614
664
  @functools.cache
615
665
  def _default_lexicon() -> Lexicon:
616
666
  # v1 data modules are the single source of vocabulary through 2.x.
617
- from nameparser.config.bound_first_names import BOUND_FIRST_NAMES
667
+ from nameparser.config.bound_given_names import BOUND_GIVEN_NAMES
618
668
  from nameparser.config.capitalization import CAPITALIZATION_EXCEPTIONS
619
669
  from nameparser.config.conjunctions import CONJUNCTIONS
620
670
  from nameparser.config.maiden_markers import MAIDEN_MARKERS
621
- from nameparser.config.prefixes import NON_FIRST_NAME_PREFIXES, PREFIXES
671
+ from nameparser.config.particles import NON_GIVEN_NAME_PARTICLES, PARTICLES
622
672
  from nameparser.config.suffixes import (
623
673
  GLUED_HONORIFICS, SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS,
624
- SUFFIX_NOT_ACRONYMS,
674
+ SUFFIX_WORDS,
625
675
  )
626
676
  from nameparser.config.surnames import KOREAN_SURNAMES
627
- from nameparser.config.titles import FIRST_NAME_TITLES, TITLES
628
-
629
- # v1 data modules export plain `set[str]`; wrap each at this call site
630
- # so the strictly-typed frozenset[str] fields never see a bare set.
677
+ from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
678
+
679
+ # every vocabulary constant is a frozenset since #293, so each one
680
+ # feeds its strictly-typed frozenset[str] field as it stands -- and
681
+ # this cache reading them ONCE is the reason they are frozen. A
682
+ # mutated module set always reached a freshly built Constants, and
683
+ # reached this Lexicon only when the edit landed before the first
684
+ # call; after it, the cache was already built and the same edit was
685
+ # invisible here. Which of the two a program got was not something
686
+ # the code doing the mutating could see.
631
687
  # keep in sync with _config_shim.Constants._snapshot() (pinned by the
632
688
  # default-Constants equality test in tests/v2/test_config_shim.py)
633
689
  return Lexicon(
634
- titles=frozenset(TITLES),
635
- given_name_titles=frozenset(FIRST_NAME_TITLES),
636
- suffix_acronyms=frozenset(SUFFIX_ACRONYMS),
637
- suffix_words=frozenset(SUFFIX_NOT_ACRONYMS),
638
- suffix_acronyms_ambiguous=frozenset(SUFFIX_ACRONYMS_AMBIGUOUS),
639
- particles=frozenset(PREFIXES),
690
+ titles=TITLES,
691
+ given_name_titles=GIVEN_NAME_TITLES,
692
+ suffix_acronyms=SUFFIX_ACRONYMS,
693
+ suffix_words=SUFFIX_WORDS,
694
+ suffix_acronyms_ambiguous=SUFFIX_ACRONYMS_AMBIGUOUS,
695
+ particles=PARTICLES,
640
696
  # FLIPPED from v1: v1 marks the never-given subset; v2 marks the
641
697
  # may-be-given subset (migration: complement translation).
642
- particles_ambiguous=frozenset(PREFIXES - NON_FIRST_NAME_PREFIXES),
643
- conjunctions=frozenset(CONJUNCTIONS),
644
- bound_given_names=frozenset(BOUND_FIRST_NAMES),
645
- maiden_markers=frozenset(MAIDEN_MARKERS),
646
- # surnames.py is born frozen (#293) -- no call-site wrap needed,
647
- # unlike the v1 modules above (their wraps drop when #293 lands)
698
+ particles_ambiguous=PARTICLES - NON_GIVEN_NAME_PARTICLES,
699
+ conjunctions=CONJUNCTIONS,
700
+ bound_given_names=BOUND_GIVEN_NAMES,
701
+ maiden_markers=MAIDEN_MARKERS,
648
702
  surnames=KOREAN_SURNAMES,
649
- honorific_tails=frozenset(GLUED_HONORIFICS),
703
+ honorific_tails=GLUED_HONORIFICS,
650
704
  # pass canonical pair-tuples so this strictly-typed call site never
651
705
  # feeds a Mapping to the tuple-annotated field; __post_init__
652
706
  # still tolerates a Mapping at runtime for interactive use