Coverage for pydantic/_internal/_namespace_utils.py: 100.00%

81 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 15:02 +0000

1from __future__ import annotations 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

2 

3import sys 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

4from collections.abc import Generator, Iterator, Mapping 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

5from contextlib import contextmanager 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

6from functools import cached_property 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

7from typing import Any, Callable, NamedTuple, TypeVar 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

8 

9from typing_extensions import ParamSpec, TypeAlias, TypeAliasType, TypeVarTuple 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

10 

11GlobalsNamespace: TypeAlias = 'dict[str, Any]' 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

12"""A global namespace. 1stCDabcdmnuBvwEFefghopxPyzGHijklqrA

13 

14In most cases, this is a reference to the `__dict__` attribute of a module. 

15This namespace type is expected as the `globals` argument during annotations evaluation. 

16""" 

17 

18MappingNamespace: TypeAlias = Mapping[str, Any] 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

19"""Any kind of namespace. 1stCDabcdmnuBvwEFefghopxPyzGHijklqrA

20 

21In most cases, this is a local namespace (e.g. the `__dict__` attribute of a class, 

22the [`f_locals`][frame.f_locals] attribute of a frame object, when dealing with types 

23defined inside functions). 

24This namespace type is expected as the `locals` argument during annotations evaluation. 

25""" 

26 

27_TypeVarLike: TypeAlias = 'TypeVar | ParamSpec | TypeVarTuple' 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

28 

29 

30class NamespacesTuple(NamedTuple): 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

31 """A tuple of globals and locals to be used during annotations evaluation. 

32 

33 This datastructure is defined as a named tuple so that it can easily be unpacked: 

34 

35 ```python {lint="skip" test="skip"} 

36 def eval_type(typ: type[Any], ns: NamespacesTuple) -> None: 

37 return eval(typ, *ns) 

38 ``` 

39 """ 

40 

41 globals: GlobalsNamespace 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

42 """The namespace to be used as the `globals` argument during annotations evaluation.""" 1stCDabcdmnuBvwEFefghopxPyzGHijklqrA

43 

44 locals: MappingNamespace 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

45 """The namespace to be used as the `locals` argument during annotations evaluation.""" 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

46 

47 

48def get_module_ns_of(obj: Any) -> dict[str, Any]: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

49 """Get the namespace of the module where the object is defined. 

50 

51 Caution: this function does not return a copy of the module namespace, so the result 

52 should not be mutated. The burden of enforcing this is on the caller. 

53 """ 

54 module_name = getattr(obj, '__module__', None) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

55 if module_name: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

56 try: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

57 return sys.modules[module_name].__dict__ 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

58 except KeyError: 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

59 # happens occasionally, see https://github.com/pydantic/pydantic/issues/2363 

60 return {} 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

61 return {} 1abcdmnefghopijklqr

62 

63 

64# Note that this class is almost identical to `collections.ChainMap`, but need to enforce 

65# immutable mappings here: 

66class LazyLocalNamespace(Mapping[str, Any]): 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

67 """A lazily evaluated mapping, to be used as the `locals` argument during annotations evaluation. 

68 

69 While the [`eval`][eval] function expects a mapping as the `locals` argument, it only 

70 performs `__getitem__` calls. The [`Mapping`][collections.abc.Mapping] abstract base class 

71 is fully implemented only for type checking purposes. 

72 

73 Args: 

74 *namespaces: The namespaces to consider, in ascending order of priority. 

75 

76 Example: 

77 ```python {lint="skip" test="skip"} 

78 ns = LazyLocalNamespace({'a': 1, 'b': 2}, {'a': 3}) 

79 ns['a'] 

80 #> 3 

81 ns['b'] 

82 #> 2 

83 ``` 

84 """ 

85 

86 def __init__(self, *namespaces: MappingNamespace) -> None: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

87 self._namespaces = namespaces 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

88 

89 @cached_property 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

90 def data(self) -> dict[str, Any]: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

91 return {k: v for ns in self._namespaces for k, v in ns.items()} 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

92 

93 def __len__(self) -> int: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

94 return len(self.data) 1stabcdKBvwefghyzijkl

95 

96 def __getitem__(self, key: str) -> Any: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

97 return self.data[key] 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

98 

99 def __contains__(self, key: object) -> bool: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

100 return key in self.data 1abcdmnuefghopxijklqrA

101 

102 def __iter__(self) -> Iterator[str]: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

103 return iter(self.data) 1IJstabcdmnuKBLMvwefghopxNOyzijklqrA

104 

105 

106def ns_for_function(obj: Callable[..., Any], parent_namespace: MappingNamespace | None = None) -> NamespacesTuple: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

107 """Return the global and local namespaces to be used when evaluating annotations for the provided function. 

108 

109 The global namespace will be the `__dict__` attribute of the module the function was defined in. 

110 The local namespace will contain the `__type_params__` introduced by PEP 695. 

111 

112 Args: 

113 obj: The object to use when building namespaces. 

114 parent_namespace: Optional namespace to be added with the lowest priority in the local namespace. 

115 If the passed function is a method, the `parent_namespace` will be the namespace of the class 

116 the method is defined in. Thus, we also fetch type `__type_params__` from there (i.e. the 

117 class-scoped type variables). 

118 """ 

119 locals_list: list[MappingNamespace] = [] 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

120 if parent_namespace is not None: 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

121 locals_list.append(parent_namespace) 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

122 

123 # Get the `__type_params__` attribute introduced by PEP 695. 

124 # Note that the `typing._eval_type` function expects type params to be 

125 # passed as a separate argument. However, internally, `_eval_type` calls 

126 # `ForwardRef._evaluate` which will merge type params with the localns, 

127 # essentially mimicking what we do here. 

128 type_params: tuple[_TypeVarLike, ...] = getattr(obj, '__type_params__', ()) 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

129 if parent_namespace is not None: 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

130 # We also fetch type params from the parent namespace. If present, it probably 

131 # means the function was defined in a class. This is to support the following: 

132 # https://github.com/python/cpython/issues/124089. 

133 type_params += parent_namespace.get('__type_params__', ()) 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

134 

135 locals_list.append({t.__name__: t for t in type_params}) 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

136 

137 # What about short-circuiting to `obj.__globals__`? 

138 globalns = get_module_ns_of(obj) 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

139 

140 return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list)) 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

141 

142 

143class NsResolver: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

144 """A class responsible for the namespaces resolving logic for annotations evaluation. 

145 

146 This class handles the namespace logic when evaluating annotations mainly for class objects. 

147 

148 It holds a stack of classes that are being inspected during the core schema building, 

149 and the `types_namespace` property exposes the globals and locals to be used for 

150 type annotation evaluation. Additionally -- if no class is present in the stack -- a 

151 fallback globals and locals can be provided using the `namespaces_tuple` argument 

152 (this is useful when generating a schema for a simple annotation, e.g. when using 

153 `TypeAdapter`). 

154 

155 The namespace creation logic is unfortunately flawed in some cases, for backwards 

156 compatibility reasons and to better support valid edge cases. See the description 

157 for the `parent_namespace` argument and the example for more details. 

158 

159 Args: 

160 namespaces_tuple: The default globals and locals to use if no class is present 

161 on the stack. This can be useful when using the `GenerateSchema` class 

162 with `TypeAdapter`, where the "type" being analyzed is a simple annotation. 

163 parent_namespace: An optional parent namespace that will be added to the locals 

164 with the lowest priority. For a given class defined in a function, the locals 

165 of this function are usually used as the parent namespace: 

166 

167 ```python {lint="skip" test="skip"} 

168 from pydantic import BaseModel 

169 

170 def func() -> None: 

171 SomeType = int 

172 

173 class Model(BaseModel): 

174 f: 'SomeType' 

175 

176 # when collecting fields, an namespace resolver instance will be created 

177 # this way: 

178 # ns_resolver = NsResolver(parent_namespace={'SomeType': SomeType}) 

179 ``` 

180 

181 For backwards compatibility reasons and to support valid edge cases, this parent 

182 namespace will be used for *every* type being pushed to the stack. In the future, 

183 we might want to be smarter by only doing so when the type being pushed is defined 

184 in the same module as the parent namespace. 

185 

186 Example: 

187 ```python {lint="skip" test="skip"} 

188 ns_resolver = NsResolver( 

189 parent_namespace={'fallback': 1}, 

190 ) 

191 

192 class Sub: 

193 m: 'Model' 

194 

195 class Model: 

196 some_local = 1 

197 sub: Sub 

198 

199 ns_resolver = NsResolver() 

200 

201 # This is roughly what happens when we build a core schema for `Model`: 

202 with ns_resolver.push(Model): 

203 ns_resolver.types_namespace 

204 #> NamespacesTuple({'Sub': Sub}, {'Model': Model, 'some_local': 1}) 

205 # First thing to notice here, the model being pushed is added to the locals. 

206 # Because `NsResolver` is being used during the model definition, it is not 

207 # yet added to the globals. This is useful when resolving self-referencing annotations. 

208 

209 with ns_resolver.push(Sub): 

210 ns_resolver.types_namespace 

211 #> NamespacesTuple({'Sub': Sub}, {'Sub': Sub, 'Model': Model}) 

212 # Second thing to notice: `Sub` is present in both the globals and locals. 

213 # This is not an issue, just that as described above, the model being pushed 

214 # is added to the locals, but it happens to be present in the globals as well 

215 # because it is already defined. 

216 # Third thing to notice: `Model` is also added in locals. This is a backwards 

217 # compatibility workaround that allows for `Sub` to be able to resolve `'Model'` 

218 # correctly (as otherwise models would have to be rebuilt even though this 

219 # doesn't look necessary). 

220 ``` 

221 """ 

222 

223 def __init__( 1IJstCDabcdmnuLMvwEFefghopxPNOyzGHijklqrA

224 self, 

225 namespaces_tuple: NamespacesTuple | None = None, 

226 parent_namespace: MappingNamespace | None = None, 

227 ) -> None: 

228 self._base_ns_tuple = namespaces_tuple or NamespacesTuple({}, {}) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

229 self._parent_ns = parent_namespace 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

230 self._types_stack: list[type[Any] | TypeAliasType] = [] 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

231 

232 @cached_property 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

233 def types_namespace(self) -> NamespacesTuple: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

234 """The current global and local namespaces to be used for annotations evaluation.""" 

235 if not self._types_stack: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

236 # TODO: should we merge the parent namespace here? 

237 # This is relevant for TypeAdapter, where there are no types on the stack, and we might 

238 # need access to the parent_ns. Right now, we sidestep this in `type_adapter.py` by passing 

239 # locals to both parent_ns and the base_ns_tuple, but this is a bit hacky. 

240 # we might consider something like: 

241 # if self._parent_ns is not None: 

242 # # Hacky workarounds, see class docstring: 

243 # # An optional parent namespace that will be added to the locals with the lowest priority 

244 # locals_list: list[MappingNamespace] = [self._parent_ns, self._base_ns_tuple.locals] 

245 # return NamespacesTuple(self._base_ns_tuple.globals, LazyLocalNamespace(*locals_list)) 

246 return self._base_ns_tuple 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

247 

248 typ = self._types_stack[-1] 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

249 

250 globalns = get_module_ns_of(typ) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

251 

252 locals_list: list[MappingNamespace] = [] 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

253 # Hacky workarounds, see class docstring: 

254 # An optional parent namespace that will be added to the locals with the lowest priority 

255 if self._parent_ns is not None: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

256 locals_list.append(self._parent_ns) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

257 if len(self._types_stack) > 1: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

258 first_type = self._types_stack[0] 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

259 locals_list.append({first_type.__name__: first_type}) 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

260 

261 # Adding `__type_params__` *before* `vars(typ)`, as the latter takes priority 

262 # (see https://github.com/python/cpython/pull/120272). 

263 # TODO `typ.__type_params__` when we drop support for Python 3.11: 

264 type_params: tuple[_TypeVarLike, ...] = getattr(typ, '__type_params__', ()) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

265 if type_params: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

266 # Adding `__type_params__` is mostly useful for generic classes defined using 

267 # PEP 695 syntax *and* using forward annotations (see the example in 

268 # https://github.com/python/cpython/issues/114053). For TypeAliasType instances, 

269 # it is way less common, but still required if using a string annotation in the alias 

270 # value, e.g. `type A[T] = 'T'` (which is not necessary in most cases). 

271 locals_list.append({t.__name__: t for t in type_params}) 1IJstCDabcdmnuKBLMvwEFefghopxNOyzGHijklqrA

272 

273 # TypeAliasType instances don't have a `__dict__` attribute, so the check 

274 # is necessary: 

275 if hasattr(typ, '__dict__'): 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

276 locals_list.append(vars(typ)) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

277 

278 # The `len(self._types_stack) > 1` check above prevents this from being added twice: 

279 locals_list.append({typ.__name__: typ}) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

280 

281 return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list)) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

282 

283 @contextmanager 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

284 def push(self, typ: type[Any] | TypeAliasType, /) -> Generator[None]: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

285 """Push a type to the stack.""" 

286 self._types_stack.append(typ) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

287 # Reset the cached property: 

288 self.__dict__.pop('types_namespace', None) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

289 try: 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

290 yield 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

291 finally: 

292 self._types_stack.pop() 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA

293 self.__dict__.pop('types_namespace', None) 1IJstCDabcdmnuKBLMvwEFefghopxPNOyzGHijklqrA