Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nameparser/config/_deprecated.py: 70%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

23 statements  

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__