Skip to content

Rename the vocabulary data modules to the 2.0 terminology (prefixes.py → particles.py), with a 2.x deprecation bridge #293

Description

@derek73

The 2.0 API named its concepts for what they are — particles, bound_given_names, given_name_titles, suffix_words — but the nameparser/config data modules that feed them still carry the 1.x names. A v2 user who follows a Lexicon field docstring's "full default list" link lands in a module named for the old model (prefixes.py), and the naming mismatch has to be re-explained in every docstring that crosses the boundary.

The data modules are semi-public: the docs point at them by path (``:data:`~nameparser.config.prefixes.PREFIXES```), so direct imports exist in the wild and a silent rename would hard-break them. Every 2.0 removal got a 1.x warning first; this rename deserves the same bridge.

Rename table (one themed sweep — the point is that the data layer matches the Lexicon fields)

Current Renamed Feeds
prefixes.py / PREFIXES particles.py / PARTICLES Lexicon.particles
NON_FIRST_NAME_PREFIXES NON_GIVEN_NAME_PARTICLES complement of Lexicon.particles_ambiguous — renamed, not inverted; see below
bound_first_names.py / BOUND_FIRST_NAMES bound_given_names.py / BOUND_GIVEN_NAMES Lexicon.bound_given_names
FIRST_NAME_TITLES (stays in titles.py) GIVEN_NAME_TITLES Lexicon.given_name_titles
SUFFIX_NOT_ACRONYMS (stays in suffixes.py) SUFFIX_WORDS Lexicon.suffix_words

Why the data layer keeps the never-given set instead of storing PARTICLES_AMBIGUOUS directly

The stored set and the Lexicon field are a source and a derivation, not one concept under two names — the v2 API flipped the presentation deliberately, and each layer's encoding serves its audience:

  • Safe-by-default for new vocabulary. Under the current encoding, a particle newly added to PARTICLES is automatically may-be-given (leading occurrence reads as a given name, with the particle-or-given flag) unless it is deliberately added to the never-given list. Storing the ambiguous set instead would require two-place additions to get the safe behavior, and forgetting the second place silently produces the unsafe one — the particle swallows leading given names unflagged. Wrongly claiming "never a given name" misparses real people (Von Miller, Vander), so the risky claim must stay the explicit opt-in.
  • The v1 shim needs the never-given set through 2.x anyway. Constants.non_first_name_prefixes is mutable v1 surface; today the complement translation runs one direction (particles_ambiguous = prefixes − non_first_name_prefixes at snapshot time). Inverting the stored set would require both directions of the derivation to exist simultaneously.
  • The import-time invariants in prefixes.py (e.g. disjointness from the bound given names) are naturally stated about the never-given set, and a rename-plus-inversion diff can't be reviewed as a behavior-preserving rename.

The one comment explaining the complement lives at the derivation site in _default_lexicon() (the # FLIPPED from v1 note).

Also in scope: freeze the vocabulary constants (setfrozenset)

The rename is the cheap moment to make the data-module set constants immutable, and it closes a real latent hazard, not just a hygiene itch: Lexicon.default() is functools.cached — it reads these sets once — while the v1 shim's Constants instances copy from them at each construction. Runtime mutation of a module constant (TITLES.add("dean") as a global-default hack) therefore silently desyncs the two APIs: fresh Constants see the entry, the cached default Lexicon never does. Frozen constants make that unrepresentable — the mutation raises AttributeError at the mutation site instead of splitting the defaults.

Why here: the new names have zero existing users and can be born frozen; the old names already warn through the PEP 562 bridge, so a mutation attempt fails loudly in code that is already being told to migrate; and everything internal treats the sets as immutable today (_default_lexicon() wraps each in frozenset(...) at the call site — that wrapping can then drop; the shim's SetManagers copy at construction; module-level | compositions and the import-time subset/disjoint asserts work identically on frozensets).

Scope: all vocabulary set constants across the data modules (SUFFIX_ACRONYMS, SUFFIX_WORDS, CONJUNCTIONS, MAIDEN_MARKERS, TITLES, GIVEN_NAME_TITLES, the renamed particle/bound sets, …) — freezing only the renamed five would leave an inconsistent trap. CAPITALIZATION_EXCEPTIONS (a dict) stays out: MappingProxyType at module level has pickling wrinkles, it isn't part of the rename table, and it can be its own decision later.

Lands as its own commit inside this bundle, after the renames — the rename commits stay provable as pure renames ("same sets, new names"), and the freeze commit is a separate, trivially-reviewable diff (mutation raises; composition and asserts still hold). One semantic change per commit, same as the PARTICLES_AMBIGUOUS scoping call above.

Mechanism

  • The new module/constant names become canonical; all internal imports (_lexicon._default_lexicon(), _config_shim._snapshot(), the import-time subset asserts) move to them.
  • The old module names remain as thin re-export shims using module-level __getattr__ (PEP 562, the same pattern as nameparser/locales/__init__.py), emitting a DeprecationWarning that names the new path. Old constant names within surviving modules (FIRST_NAME_TITLES, SUFFIX_NOT_ACRONYMS) get the same treatment via __getattr__ on their module.
  • The v1 Constants attribute names (prefixes, first_name_titles, …) are facade surface and do not change — the shim simply imports from the renamed modules.
  • Docs sweep: every :data: cross-reference moves to the new paths; customize.rst/migrate.rst note the rename and the 3.0 endpoint.

Sequencing

  • 2.1: canonical rename + deprecated aliases (this issue).
  • 3.0: aliases removed with the rest of the facade layer (add to the 3.0 milestone checklist on merge).

Import of an old name must warn exactly once per process per name, and the suite's filterwarnings = ["error"] means any internal import still using an old path fails CI immediately — the bridge can't rot silently.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions