1"""The 1.x vocabulary names, served from their 2.2 homes.
2
3The 2.0 API named its concepts for what they are -- particles, bound
4given names, given-name titles, suffix words -- while the data modules
5kept the 1.x names a little longer. #293 moved all four to match. A 1.x
6name resolves to its 2.2 constant, warns at the line that read it, and
7names the path to migrate to; the whole layer goes away in 3.0 with the
8rest of the v1 facade.
9
10Two of the four moved module and all: prefixes -> particles and
11bound_first_names -> bound_given_names, whose old modules are now
12data-free shims that are nothing but a docstring and an alias table.
13The other two renamed a constant in place, so titles.py and suffixes.py
14carry their alias table at the bottom of the file, beside their data.
15
16Same PEP 562 hook as nameparser/locales/__init__.py, but deliberately
17without that module's write-back: a retired name stays served by
18``__getattr__`` for the life of the process, so every read reaches the
19warning. Suppressing the repeats is the warnings module's own job, and
20it does it per LOCATION -- ``__warningregistry__`` lives in the READING
21module's globals and is keyed on (text, category, lineno). That is the
22granularity the advice is written at: one line that reads a retired
23name is told once however often it runs, and a second line, in that
24file or another, is told for itself. Caching the resolved value into
25the module globals instead would silence every reader after the first,
26and the first is whoever imported earliest -- routinely a dependency,
27whose author is not the person who has to edit anything.
28
29PEP 562 defines the hook for attribute ACCESS and nothing else, which
30is why every alias-bearing module also carries an ``__all__`` naming
31its retired names: ``from x import *`` reads ``__all__``, or failing
32that the module ``__dict__``, and consults ``__getattr__`` in neither
33case. Without the list a star import binds no retired name and issues
34no diagnostic. See the note at the ``__all__`` in prefixes.py.
35"""
36from __future__ import annotations
37
38import importlib
39import sys
40import warnings
41from collections.abc import Callable, Mapping
42from typing import Any
43
44_MESSAGE = (
45 "{module}.{old} is deprecated since 2.2 and will be removed in 3.0; "
46 "use {new_module}.{new} instead."
47)
48
49
50def alias_getattr(
51 module: str,
52 aliases: Mapping[str, tuple[str, str]],
53) -> tuple[Callable[[str], Any], Callable[[], list[str]]]:
54 """Build the ``__getattr__``/``__dir__`` pair for a module carrying
55 deprecated vocabulary names.
56
57 ``aliases`` maps each old attribute name to the ``(module, name)``
58 it now lives at. Assign the result at module level, in the ``else``
59 of a ``TYPE_CHECKING`` guard that declares the same names::
60
61 if TYPE_CHECKING:
62 OLD_NAME: frozenset[str] # 1.x alias, removed in 3.0 (#293)
63 else:
64 __getattr__, __dir__ = alias_getattr(__name__, {...})
65
66 (a placeholder rather than a real retired name, for the reason the
67 ``stacklevel`` comment below gives)
68
69 The guard is load-bearing. mypy honors an assigned module
70 ``__getattr__`` (PEP 484's convention for one) and thereafter
71 answers EVERY missing attribute of that module from its return
72 type, so a bare assignment turns off missing-attribute checking for
73 the whole module. On titles.py and suffixes.py, which keep their
74 live constants and are still imported from, that cost real
75 checking: ``from nameparser.config.titles import TITLE`` type-
76 checked clean. Keeping the assignment out of the type checker's
77 view restores it, and the declarations in the other branch type
78 each retired name as the ``frozenset[str]`` it is rather than
79 ``Any``. The package ships ``py.typed``, so both reach callers.
80 Runtime is untouched -- ``TYPE_CHECKING`` is False, so only the
81 ``else`` ever runs -- and the two branches delete together in 3.0.
82
83 Which leaves the ``Any`` return below typing nothing outside this
84 module: mypy reads no module ``__getattr__`` for the alias-bearing
85 modules any more, and does not analyze the ``else`` branch it is
86 assigned in. It stays ``Any`` as what ``getattr`` itself returns.
87 """
88
89 def __getattr__(name: str) -> Any: # noqa: ANN401
90 target = aliases.get(name)
91 if target is None:
92 raise AttributeError(f"module {module!r} has no attribute {name!r}")
93 new_module, new_name = target
94 # resolved BEFORE warning, so a mistyped alias target fails as
95 # a ModuleNotFoundError or an AttributeError from here rather
96 # than first advising the reader to move to a path that does
97 # not exist.
98 value = getattr(importlib.import_module(new_module), new_name)
99 warnings.warn(
100 _MESSAGE.format(
101 module=module, old=name, new_module=new_module, new=new_name),
102 DeprecationWarning,
103 # 2: the frame that touched the name -- for the
104 # `from nameparser.config.prefixes import ...` form, the
105 # importing module, which is the place that has to be
106 # edited. Which name it imports does not matter here, and
107 # spelling one out would put a retired name in a file that
108 # serves no single vocabulary (tests/v2/test_config_aliases
109 # ::test_no_internal_code_reads_a_retired_vocabulary_name)
110 stacklevel=2,
111 )
112 return value
113
114 def __dir__() -> list[str]:
115 # UNION, not just the aliases: a module __dir__ REPLACES the
116 # default listing rather than adding to it, so dropping the
117 # module's own globals here would take the live constants out
118 # of tab completion and every getattr-free member scan --
119 # autodoc's included. Pinned by test_config_aliases
120 # ::test_dir_lists_the_live_names_as_well_as_the_retired_ones.
121 #
122 # .get, not [...]: a module dropped from sys.modules -- a test
123 # that reloads the package, a plugin teardown -- would otherwise
124 # make dir() raise KeyError, which is not among the things dir()
125 # may do to a caller. The aliases are held in the closure and
126 # are still nameable, so they are what is left to list (#356).
127 live = sys.modules.get(module)
128 names = set(vars(live)) if live is not None else set()
129 return sorted(names | set(aliases))
130
131 # The table itself, reachable without tripping a warning. __all__ is
132 # hand-written per module (it must stay in SOURCE order for autodoc,
133 # which this function cannot know), so the two lists are maintained
134 # separately and a row added to only one of them is the failure
135 # fc46a9b closed for the other direction: a table row missing from
136 # __all__ is silently dropped by `from x import *` with no warning
137 # and no AttributeError. test_config_aliases
138 # ::test_every_alias_table_row_reaches_star_import cross-checks them.
139 __getattr__.deprecated_aliases = dict(aliases) # type: ignore[attr-defined]
140
141 return __getattr__, __dir__