Skip to content

Commit 410f2aa

Browse files
derek73claude
andcommitted
Finding the sites names the vocabulary modules; no call-per-constant claim (#1012 review)
2.8.1 warns where code reads a constant of a removed vocabulary module, never at a bare import, so migrate.rst's "Finding the sites" says to upgrade to 2.8.1, lists the bare import among what passes and still fails, and adds the eight modules to the search list. Three sites still claimed one call per constant where TITLE_PHRASES has none and several are comprehensions; the release bullet no longer pairs TITLES with a call returning more words; AGENTS.md's layering example names a module that exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bae8c75 commit 410f2aa

5 files changed

Lines changed: 29 additions & 17 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ The compiled sets, read by `Lexicon` (mostly as the `_sets` field of the same na
236236
The 2.0 rewrite landed as underscore-private modules beside the v1 code, which 2.0 and then 3.0 deleted. These conventions apply to all of it. The full design record (rationale, settled-decision logs, dated amendments) lives in untracked `docs/superpowers/specs/`; this section is the enforceable subset. **A commit that establishes or amends one of these conventions must update this section in the same commit** — grep-driven staleness sweeps miss paraphrased prose (see Workflow above), so write-time maintenance is the mechanism, audits are the backstop.
237237

238238
- **Module layout**: every module is underscore-private (`_types.py`, `_lexicon.py`, `_policy.py`, `_locale.py`, `_render.py`, `_reading_index.py`, `_pipeline/`, `_parser.py`). The public import surface is exactly `nameparser`, `nameparser.locales` and the public data modules under `nameparser.config` (`surnames`, `regexes`; the vocabulary's compiled sets are private since 3.0, #1007); `nameparser/__init__.py` holds re-exports and `__all__` only — no logic.
239-
- **Layering is enforced by `tests/v2/test_layering.py`** (exact-module matching; `if TYPE_CHECKING:` imports don't count): `_types` imports nothing internal at module level — its rendering delegates import `_render` at call time; `_lexicon` and `_policy` sit above `_types` independently (`_lexicon` may import `nameparser.config.*` DATA modules, e.g. `config/maiden_markers.py`, and `_reading_index` at call time only, to compile a configured lexicon's readings, since `tools/vocab/build.py` loads it by path, 3.0's 4i-b); `_reading_index` imports nothing internal at module level and the index, `config/_index.py`, at call time, since `tools/vocab/build.py` loads it by path; `_locale` sits on `_lexicon`+`_policy` (plus `_types` for the shared pickle mixin, and `config/_rules.py` at call time, in `rules_patch`); `_render` imports `_types`, `_lexicon` (for `Lexicon.default()` and `_normalize`) and `_policy` (for the scripts that write no initials); `_pipeline/*` imports `_types`+`_lexicon`+`_policy` plus in-package `_pipeline` helpers, `_assemble` also `_reading_index`, for the kinds `Reading.languages` matches (`CATEGORY`; the files themselves it reads through `Lexicon._files`, 4i-d; `_vocab` imported it in 3.0's 4b and 4c for a listed suffix word's kind and a patronymic particle's, which since 4i-b the lexicon compiles into `_sets`); `_parser` sits on everything except `_render`, `_reading_index` at call time only, to validate a choice's tags and move the case masks it moves (the choice itself compiles in `_lexicon._built`); `__main__` imports only the public package; locale pack modules (`locales/*.py`) import `_locale`/`_lexicon`/`_policy`/`_types` only (`_types` joined the list in #272 — `locales/ja.py`'s segmenter factory constructs a `Segmentation`), and the `locales/__init__` additionally lazy-imports its packs (PEP 562). Extend the test's `ALLOWED` table when adding a module.
239+
- **Layering is enforced by `tests/v2/test_layering.py`** (exact-module matching; `if TYPE_CHECKING:` imports don't count): `_types` imports nothing internal at module level — its rendering delegates import `_render` at call time; `_lexicon` and `_policy` sit above `_types` independently (`_lexicon` may import `nameparser.config.*` DATA modules, e.g. `config/surnames.py` and the private `config/_compiled.py`, and `_reading_index` at call time only, to compile a configured lexicon's readings, since `tools/vocab/build.py` loads it by path, 3.0's 4i-b); `_reading_index` imports nothing internal at module level and the index, `config/_index.py`, at call time, since `tools/vocab/build.py` loads it by path; `_locale` sits on `_lexicon`+`_policy` (plus `_types` for the shared pickle mixin, and `config/_rules.py` at call time, in `rules_patch`); `_render` imports `_types`, `_lexicon` (for `Lexicon.default()` and `_normalize`) and `_policy` (for the scripts that write no initials); `_pipeline/*` imports `_types`+`_lexicon`+`_policy` plus in-package `_pipeline` helpers, `_assemble` also `_reading_index`, for the kinds `Reading.languages` matches (`CATEGORY`; the files themselves it reads through `Lexicon._files`, 4i-d; `_vocab` imported it in 3.0's 4b and 4c for a listed suffix word's kind and a patronymic particle's, which since 4i-b the lexicon compiles into `_sets`); `_parser` sits on everything except `_render`, `_reading_index` at call time only, to validate a choice's tags and move the case masks it moves (the choice itself compiles in `_lexicon._built`); `__main__` imports only the public package; locale pack modules (`locales/*.py`) import `_locale`/`_lexicon`/`_policy`/`_types` only (`_types` joined the list in #272 — `locales/ja.py`'s segmenter factory constructs a `Segmentation`), and the `locales/__init__` additionally lazy-imports its packs (PEP 562). Extend the test's `ALLOWED` table when adding a module.
240240
- **Canonical field order** — the seven roles in `Role` enum declaration order, defined once and derived everywhere (properties, `as_dict`, reprs, `comparison_key`). Never restate the order literally. `Role` is a `StrEnum`: members compare as their field-name strings, and `tokens_for()` coerces strings via `_coerce_enum`.
241241
- **Method organization**, fixed section order in every class: fields + `__post_init__` validation → alternative constructors → dunders (construction/equality → protocol → operators) → properties → public methods by concern (access → editing → comparison → rendering delegates) → private helpers last, except a helper serving exactly one section may sit at that section's head.
242242
- **Validation is eager and fail-loud**: every `raise` states the offending value, the expected form, and the fix. Exception taxonomy: wrong type — including wrong element type inside a collection, bare `str` where an iterable of strings is expected, or a `Mapping` where a plain iterable is expected — raises `TypeError`; well-typed but unacceptable values raise `ValueError`; failed enum lookups stay `ValueError` for any input (stdlib `EnumType` precedent). **When the message hands the reader code to paste, that code has to survive a type checker, and then RUN** — nameparser ships `py.typed`, and a placeholder can type-check and still raise: the 2.4 release sweep's first fix to the `capitalization_exceptions` hint offered `('key', 'Value')`, which passes mypy and fails #459's mask check, so `test_add_capitalization_exceptions_raises_pointing_at_replace` now executes the offered code. #337's segmenterless warning offered `Policy(segment_scripts=())`, an `arg-type` error, because these fields are annotated with what they STORE rather than everything the constructor accepts. Prefer the `frozenset()` / `()` spellings in messages and docstrings, and pin the offered spelling in a test — the warning tests matched on `ja_segmenter` and never checked the actionable half of the message. **A warning emitted in `Parser.__post_init__` needs `parser_for` to re-emit it from its own frame** (the `catch_warnings(record=True)` block at its return): `__post_init__`'s `stacklevel` is sized for direct `Parser(...)` construction, and through `parser_for`'s extra frame the default one-line rendering attributes the warning to the library's own `return Parser(...)` — the exact call the message tells the user to change becomes invisible. No single stacklevel serves both entry points; a new construction warning gets the re-emission for free, but a new CONSTRUCTION SITE for `Parser` inside this package needs its own re-emission or its callers get library-attributed warnings (#337 review).

‎docs/customize.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ kind, as the examples under :ref:`title-detection-off` do, and
8383
:meth:`~nameparser.Lexicon.readings_of` answers for one word. The
8484
modules that exposed them as constants in 2.x (``nameparser.config.titles``,
8585
``particles``, ``suffixes`` and the rest) are gone in 3.0, and
86-
:doc:`migrate` gives the ``words`` call for each constant.
86+
:doc:`migrate` gives the call or comprehension for each constant.
8787
``nameparser.config.surnames``, the Korean surname list, stays.
8888
``REGEXES`` in ``nameparser.config.regexes`` is a plain dict the parser
8989
does not read; editing it at runtime is not a supported override.

‎docs/migrate.rst‎

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ exceptions `Finding the sites`_ names:
6767
``suffixes``, ``particles``, ``conjunctions``,
6868
``bound_given_names``, ``given_names``, ``maiden_markers`` and
6969
``capitalization``, which 2.8.1 warns of
70-
- ``Lexicon.default().words(kind=...)``, one call per constant in
71-
`Vocabulary constants → Lexicon.words`_;
70+
- ``Lexicon.default().words(kind=...)``, the call or comprehension
71+
for each constant in `Vocabulary constants → Lexicon.words`_;
7272
``nameparser.config.surnames`` stays
7373

7474
The 2.8 deprecation warnings go with the code they warned about. In 3.0
@@ -79,7 +79,7 @@ a ``Constants`` cannot load, because there is no class to load it into.
7979
Finding the sites
8080
~~~~~~~~~~~~~~~~~
8181

82-
Upgrade to 2.8 first. A ``DeprecationWarning`` is hidden by default
82+
Upgrade to 2.8.1 first. A ``DeprecationWarning`` is hidden by default
8383
outside ``__main__``, so run your tests with warnings turned into
8484
errors, and each use 3.0 removes stops the run at the line that makes
8585
it:
@@ -89,7 +89,10 @@ it:
8989
python -W error::DeprecationWarning -m pytest
9090
9191
2.8 warns where code *uses* a removed name, not where it imports one,
92-
so some code passes that check and still fails on 3.0:
92+
and 2.8.1 warns where code reads a constant of a removed vocabulary
93+
module (``from nameparser.config.titles import TITLES`` included), not
94+
where it imports the module, so some code passes that check and still
95+
fails on 3.0:
9396

9497
* *reading* the shared ``CONSTANTS`` (``"dr" in CONSTANTS.titles``, or
9598
``copy.copy(CONSTANTS)``), which 2.8 left quiet so that importing
@@ -101,10 +104,17 @@ so some code passes that check and still fails on 3.0:
101104
importing ``nameparser.parser`` or the two 1.x word-list modules,
102105
``nameparser.config.prefixes`` and
103106
``nameparser.config.bound_first_names``
107+
* importing a vocabulary module without reading a constant from it —
108+
``import nameparser.config.titles`` or ``from nameparser.config import
109+
particles``
104110

105111
So search your code for ``CONSTANTS``, ``HumanName``, ``Constants``,
106112
``nameparser.parser``, ``nameparser.util``, ``config.prefixes`` and
107-
``config.bound_first_names`` as well.
113+
``config.bound_first_names`` as well, and for the vocabulary modules:
114+
``config.titles``, ``config.suffixes``, ``config.particles``,
115+
``config.conjunctions``, ``config.bound_given_names``,
116+
``config.given_names``, ``config.maiden_markers`` and
117+
``config.capitalization``.
108118

109119
Coming from 1.x
110120
~~~~~~~~~~~~~~~
@@ -475,10 +485,12 @@ Through 2.x each default word list was a module constant, frozen since
475485
2.2: ``from nameparser.config.titles import TITLES``. 3.0 removes those
476486
modules, which 2.8.1 warns of, and a lexicon lists its words instead,
477487
in the reading kinds it is configured by. Each 2.x constant is the call
478-
beside it, with ``d = Lexicon.default()``, a comprehension where the
479-
compiler derives the set from a clash between readings (a title word
480-
with a ``given`` reading and no particle reading reads as a title only
481-
inside a title phrase):
488+
beside it, with ``d = Lexicon.default()``: a ``words`` call, or a
489+
comprehension over ``words`` and ``readings_of`` where the compiler
490+
derives the set from more than one reading (a title word with a
491+
``given`` reading and no particle reading reads as a title only inside
492+
a title phrase), and for ``TITLE_PHRASES`` a note, since no call lists
493+
it:
482494

483495
.. list-table::
484496
:header-rows: 1
@@ -503,8 +515,8 @@ inside a title phrase):
503515
* - ``TITLE_PHRASES``
504516
- No call: each two-word phrase is compiled from a title reading's
505517
``attaches`` property (``readings_of("chief")`` shows
506-
``attaches=('military', 'office')``), and the phrases are not
507-
listed
518+
``properties=(('attaches', ('military', 'office')),)``), and the
519+
phrases are not listed
508520
* - ``PARTICLES``
509521
- ``d.words(kind="particle")``
510522
* - ``NON_GIVEN_NAME_PARTICLES``

‎docs/release_log.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Release Log
3030

3131
- **Configure a Lexicon by reading; its set fields are internal.** ``Lexicon.default().remove("dean", kind="given")`` reads ``Dean Smith`` title ``Dean``, the spelling of 2.8.0's ``Lexicon.default().add(titles={"dean"})``, which now raises ``TypeError`` naming that call, as do ``Lexicon(titles=...)`` and ``dataclasses.replace`` with a set field, and reading ``lexicon.titles`` raises ``AttributeError``; ``add(*words, kind=..., **properties)``, ``remove(*words, kind=None)`` and ``readings_of(word)``, returning ``LexiconReading``, take the vocabulary files' kinds, and :doc:`migrate` has a row per 2.8.0 field. A 2.8.0 pickle of a ``Lexicon``, ``Parser`` or ``Locale`` loads where readings compile its sets back, keeping the vocabulary it was pickled with rather than taking 3.0's, and is refused otherwise; a 2.x call or pickle that left a word in a marker set over a set it took the word out of reads as though the marker went with it (after ``remove(titles={"air"})``, ``John Smith, AIR`` gives suffix ``AIR``, given ``AIR`` in 2.8.0). :issue:`754`
3232
- **Deprecate loading a Lexicon, Parser or locale pack pickled by 2.x, which 3.1 refuses.** ``pickle.loads`` of a ``Parser`` pickled by 2.8.0 emits a ``DeprecationWarning``, once for each ``Lexicon`` it holds, and 3.1 will refuse it; re-pickle the loaded object under 3.0 and that pickle loads without one. The errors naming a word-by-word fix for a 2.x ``Lexicon`` spelling keep it in 3.0 and point at :doc:`migrate` in 3.1. :issue:`754`, :issue:`1003`
33-
- **Remove the vocabulary modules in nameparser.config, which 2.8.1 warns of.** ``from nameparser.config.titles import TITLES`` raises ``ModuleNotFoundError``, as it does for ``suffixes``, ``particles``, ``conjunctions``, ``bound_given_names``, ``given_names``, ``maiden_markers`` and ``capitalization``, where 2.8.0 imported each; ``Lexicon.default().words(kind="title")`` lists the words, :doc:`migrate` gives the call for every constant, and ``nameparser.config.surnames`` stays. :issue:`1007`
33+
- **Remove the vocabulary modules in nameparser.config, which 2.8.1 warns of.** ``from nameparser.config.titles import TITLES`` raises ``ModuleNotFoundError``, as it does for ``suffixes``, ``particles``, ``conjunctions``, ``bound_given_names``, ``given_names``, ``maiden_markers`` and ``capitalization``, where 2.8.0 imported each; ``Lexicon.default().words(kind="title")`` lists the title words in 3.0, :doc:`migrate` gives the call or comprehension for each constant, and ``nameparser.config.surnames`` stays. :issue:`1007`
3434
- **Remove HumanName, Constants and the rest of the 1.x compatibility layer.** ``from nameparser import HumanName`` raises ``ImportError``, where 1.4.0 through 2.8.0 imported it (2.8.0 warning where it was constructed); ``parse("Dr. Juan de la Vega")`` reads the same name, with given ``Juan`` and family ``de la Vega``. ``nameparser.parser``, ``nameparser.util``, ``nameparser.config``'s ``Constants``, ``CONSTANTS`` and managers, and the five word-list names and two module paths 2.2 renamed go too, and a pickled ``HumanName`` or ``Constants`` no longer loads. See :doc:`migrate`. :issue:`357`
3535

3636
**Behavior Changes**

‎tests/v2/test_contracts.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ def test_every_vocabulary_constant_is_frozen() -> None:
143143
the constants are module-level, reachable by callers (``surnames``
144144
publicly, ``_compiled`` by its private name) as well as read by the
145145
cached ``Lexicon.default()``, so a mutable one lets a caller's copy
146-
and the default disagree. A locale pack has one consumer and one moment -- the
147-
``Lexicon(...)`` in its own module body, whose fields are frozen
148-
copies -- so ``locales/zh.py``'s ``_SURNAMES`` could not desync
146+
and the default disagree. A locale pack has one consumer and one
147+
moment -- the ``Lexicon(...)`` in its own module body, whose fields
148+
are frozen copies -- so ``locales/zh.py``'s ``_SURNAMES`` could not desync
149149
anything even as a plain ``set``. It is a ``frozenset`` anyway.
150150
"""
151151
import importlib

0 commit comments

Comments
 (0)