Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pydantic/_internal/_typing_extra.py: 57%

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

174 statements  

1"""Logic for interacting with type annotations, mostly extensions, shims and hacks to wrap Python's typing module.""" 

2 

3from __future__ import annotations 

4 

5import re 

6import sys 

7import types 

8import typing 

9from collections.abc import Callable, MutableMapping 

10from functools import partial 

11from inspect import Signature, signature 

12from types import NoneType 

13from typing import TYPE_CHECKING, Any, ForwardRef, cast 

14 

15import typing_extensions 

16from typing_extensions import deprecated, get_args, get_origin # noqa: UP035 (for `get_args` and `get_origin`) 

17from typing_inspection import typing_objects 

18from typing_inspection.introspection import is_union_origin 

19 

20from pydantic.version import version_short 

21 

22from ._namespace_utils import GlobalsNamespace, MappingNamespace, NsResolver, get_module_ns_of 

23 

24if sys.version_info >= (3, 14): 

25 import annotationlib 

26 

27if TYPE_CHECKING: 

28 from pydantic import BaseModel 

29 from pydantic.fields import FieldInfo 

30 

31# As per https://typing-extensions.readthedocs.io/en/latest/#runtime-use-of-types, 

32# always check for both `typing` and `typing_extensions` variants of a typing construct. 

33# (this is implemented differently than the suggested approach in the `typing_extensions` 

34# docs for performance). 

35 

36 

37_t_annotated = typing.Annotated 

38_te_annotated = typing_extensions.Annotated 

39 

40 

41def is_annotated(tp: Any, /) -> bool: 

42 """Return whether the provided argument is a `Annotated` special form. 

43 

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

45 is_annotated(Annotated[int, ...]) 

46 #> True 

47 ``` 

48 """ 

49 origin = get_origin(tp) 

50 return origin is _t_annotated or origin is _te_annotated 

51 

52 

53def annotated_type(tp: Any, /) -> Any | None: 

54 """Return the type of the `Annotated` special form, or `None`.""" 

55 return tp.__origin__ if typing_objects.is_annotated(get_origin(tp)) else None 

56 

57 

58def unpack_type(tp: Any, /) -> Any | None: 

59 """Return the type wrapped by the `Unpack` special form, or `None`.""" 

60 return get_args(tp)[0] if typing_objects.is_unpack(get_origin(tp)) else None 

61 

62 

63_classvar_re = re.compile(r'((\w+\.)?Annotated\[)?(\w+\.)?ClassVar\[') 

64 

65 

66def is_classvar_annotation(tp: Any, /) -> bool: 

67 """Return whether the provided argument represents a class variable annotation. 

68 

69 Although not explicitly stated by the typing specification, `ClassVar` can be used 

70 inside `Annotated` and as such, this function checks for this specific scenario. 

71 

72 Because this function is used to detect class variables before evaluating forward references 

73 (or because evaluation failed), we also implement a naive regex match implementation. This is 

74 required because class variables are inspected before fields are collected, so we try to be 

75 as accurate as possible. 

76 """ 

77 if typing_objects.is_classvar(tp): 

78 return True 

79 

80 origin = get_origin(tp) 

81 

82 if typing_objects.is_classvar(origin): 

83 return True 

84 

85 if typing_objects.is_annotated(origin): 

86 annotated_type = tp.__origin__ 

87 if typing_objects.is_classvar(annotated_type) or typing_objects.is_classvar(get_origin(annotated_type)): 

88 return True 

89 

90 str_ann: str | None = None 

91 if isinstance(tp, typing.ForwardRef): 

92 str_ann = tp.__forward_arg__ 

93 if isinstance(tp, str): 

94 str_ann = tp 

95 

96 if str_ann is not None and _classvar_re.match(str_ann): 

97 # stdlib dataclasses do something similar, although a bit more advanced 

98 # (see `dataclass._is_type`). 

99 return True 

100 

101 return False 

102 

103 

104def is_namedtuple(tp: Any, /) -> bool: 

105 """Return whether the provided argument is a named tuple class. 

106 

107 The class can be created using `typing.NamedTuple` or `collections.namedtuple`. 

108 Parametrized generic classes are *not* assumed to be named tuples. 

109 """ 

110 from ._utils import lenient_issubclass # circ. import 

111 

112 return lenient_issubclass(tp, tuple) and hasattr(tp, '_fields') 

113 

114 

115# TODO In 2.12, delete this export. It is currently defined only to not break 

116# pydantic-settings which relies on it: 

117origin_is_union = is_union_origin 

118 

119 

120def is_generic_alias(tp: Any, /) -> bool: 

121 return isinstance(tp, (types.GenericAlias, typing._GenericAlias)) # pyright: ignore[reportAttributeAccessIssue] 

122 

123 

124# TODO: Ideally, we should avoid relying on the private `typing` constructs: 

125WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias, types.UnionType) # pyright: ignore[reportAttributeAccessIssue] 

126 

127# Similarly, we shouldn't rely on this `_Final` class, which is even more private than `_GenericAlias`: 

128typing_base: Any = typing._Final # pyright: ignore[reportAttributeAccessIssue] 

129 

130 

131### Annotation evaluations functions: 

132 

133 

134def parent_frame_namespace(*, parent_depth: int = 2, force: bool = False) -> MutableMapping[str, Any] | None: 

135 """Fetch the local namespace of the parent frame where this function is called. 

136 

137 Using this function is mostly useful to resolve forward annotations pointing to members defined in a local namespace, 

138 such as assignments inside a function. Using the standard library tools, it is currently not possible to resolve 

139 such annotations: 

140 

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

142 from typing import get_type_hints 

143 

144 def func() -> None: 

145 Alias = int 

146 

147 class C: 

148 a: 'Alias' 

149 

150 # Raises a `NameError: 'Alias' is not defined` 

151 get_type_hints(C) 

152 ``` 

153 

154 Pydantic uses this function when a Pydantic model is being defined to fetch the parent frame locals. However, 

155 this only allows us to fetch the parent frame namespace and not other parents (e.g. a model defined in a function, 

156 itself defined in another function). Inspecting the next outer frames (using `f_back`) is not reliable enough 

157 (see https://discuss.python.org/t/20659). 

158 

159 Because this function is mostly used to better resolve forward annotations, nothing is returned if the parent frame's 

160 code object is defined at the module level. In this case, the locals of the frame will be the same as the module 

161 globals where the class is defined (see `_namespace_utils.get_module_ns_of`). However, if you still want to fetch 

162 the module globals (e.g. when rebuilding a model, where the frame where the rebuild call is performed might contain 

163 members that you want to use for forward annotations evaluation), you can use the `force` parameter. 

164 

165 Args: 

166 parent_depth: The depth at which to get the frame. Defaults to 2, meaning the parent frame where this function 

167 is called will be used. 

168 force: Whether to always return the frame locals, even if the frame's code object is defined at the module level. 

169 

170 Returns: 

171 The locals of the namespace, or `None` if it was skipped as per the described logic. 

172 """ 

173 frame = sys._getframe(parent_depth) 

174 

175 if frame.f_code.co_name.startswith('<generic parameters of'): 

176 # As `parent_frame_namespace` is mostly called in `ModelMetaclass.__new__`, 

177 # the parent frame can be the annotation scope if the PEP 695 generic syntax is used. 

178 # (see https://docs.python.org/3/reference/executionmodel.html#annotation-scopes, 

179 # https://docs.python.org/3/reference/compound_stmts.html#generic-classes). 

180 # In this case, the code name is set to `<generic parameters of MyClass>`, 

181 # and we need to skip this frame as it is irrelevant. 

182 frame = cast(types.FrameType, frame.f_back) # guaranteed to not be `None` 

183 

184 # note, we don't copy frame.f_locals here (or during the last return call), because we don't expect the namespace to be 

185 # modified down the line if this becomes a problem, we could implement some sort of frozen mapping structure to enforce this. 

186 if force: 

187 return frame.f_locals 

188 

189 # If either of the following conditions are true, the class is defined at the top module level. 

190 # To better understand why we need both of these checks, see 

191 # https://github.com/pydantic/pydantic/pull/10113#discussion_r1714981531. 

192 if frame.f_back is None or frame.f_code.co_name == '<module>': 

193 return None 

194 

195 return frame.f_locals 

196 

197 

198def _type_convert(arg: Any) -> Any: 

199 """Convert `None` to `NoneType` and strings to `ForwardRef` instances. 

200 

201 This is a vendored version of the private `typing._type_convert()` function. When 

202 evaluating a type, `ForwardRef._evaluate()` ends up being called, and is 

203 responsible for making this conversion. However, we still have to apply 

204 it for the first argument passed to our type evaluation functions, similarly 

205 to the `typing.get_type_hints()` function. 

206 """ 

207 if arg is None: 

208 return NoneType 

209 if isinstance(arg, str): 

210 if sys.version_info[:3] == (3, 10, 0): 

211 return ForwardRef(arg, is_argument=False) 

212 else: 

213 # Like `typing.get_type_hints()`, assume the arg can be in any context, 

214 # hence the proper `is_argument` and `is_class` args: 

215 return ForwardRef(arg, is_argument=False, is_class=True) 

216 return arg 

217 

218 

219def safe_get_annotations(obj: Any) -> dict[str, Any]: 

220 """Get the annotations for the provided object, accounting for potential deferred forward references. 

221 

222 Starting with Python 3.14, accessing the `__annotations__` attribute might raise a `NameError` if 

223 a referenced symbol isn't defined yet. In this case, we return the annotation in the *forward ref* 

224 format. 

225 """ 

226 if sys.version_info >= (3, 14): 

227 return annotationlib.get_annotations(obj, format=annotationlib.Format.FORWARDREF) 

228 else: 

229 if isinstance(obj, type): 

230 return obj.__dict__.get('__annotations__', {}) 

231 else: 

232 return getattr(obj, '__annotations__', {}) 

233 

234 

235def get_model_type_hints( 

236 model_class: type[BaseModel], 

237 *, 

238 ns_resolver: NsResolver | None = None, 

239) -> dict[str, tuple[Any, bool]]: 

240 """Collect annotations from a Pydantic model class, including those from parent classes. 

241 

242 Args: 

243 model_class: The Pydantic model class to inspect. 

244 ns_resolver: A namespace resolver instance to use. Defaults to an empty instance. 

245 

246 Returns: 

247 A dictionary mapping annotation names to a two-tuple: the first element is the evaluated 

248 type or the original annotation if a `NameError` occurred, the second element is a boolean 

249 indicating if whether the evaluation succeeded. 

250 """ 

251 hints: dict[str, Any] | dict[str, tuple[Any, bool]] = {} 

252 ns_resolver = ns_resolver or NsResolver() 

253 

254 for base in reversed(model_class.__mro__[:-1]): 

255 # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals 

256 # from the ns_resolver, but we want to be able to know which specific field failed 

257 # to evaluate: 

258 ann = safe_get_annotations(base) 

259 

260 if not ann: 

261 continue 

262 

263 with ns_resolver.push(base): 

264 base_model_fields: dict[str, FieldInfo] | None = base.__dict__.get('__pydantic_fields__') 

265 

266 for name, value in ann.items(): 

267 if name.startswith('_'): 

268 globalns, localns = ns_resolver.types_namespace 

269 

270 # For private attributes, we only need the annotation to detect the `ClassVar` special form. 

271 # For this reason, we still try to evaluate it, but we also catch any possible exception (on 

272 # top of the `NameError`s caught in `try_eval_type`) that could happen so that users are free 

273 # to use any kind of forward annotation for private fields (e.g. circular imports, new typing 

274 # syntax, etc). 

275 try: 

276 hints[name] = try_eval_type(value, globalns, localns) 

277 except Exception: 

278 hints[name] = (value, False) 

279 else: 

280 if base_model_fields is not None and name in base_model_fields: 

281 # Avoid unnecessarily evaluating annotations from parent models, as we'll end up 

282 # copying the `FieldInfo` instance from it anyway if we need to. 

283 # We use the `annotation` attribute here, but in reality could put anything here, 

284 # As we are guaranteed to not make use of it: 

285 hints[name] = (base_model_fields[name].annotation, True) 

286 else: 

287 globalns, localns = ns_resolver.types_namespace 

288 

289 hints[name] = try_eval_type(value, globalns, localns) 

290 

291 return hints 

292 

293 

294def get_cls_type_hints( 

295 obj: type[Any], 

296 *, 

297 ns_resolver: NsResolver | None = None, 

298) -> dict[str, Any]: 

299 """Collect annotations from a class, including those from parent classes. 

300 

301 Args: 

302 obj: The class to inspect. 

303 ns_resolver: A namespace resolver instance to use. Defaults to an empty instance. 

304 """ 

305 hints: dict[str, Any] = {} 

306 ns_resolver = ns_resolver or NsResolver() 

307 

308 for base in reversed(obj.__mro__): 

309 # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals 

310 # from the ns_resolver, but we want to be able to know which specific field failed 

311 # to evaluate: 

312 ann = safe_get_annotations(base) 

313 

314 if not ann: 

315 continue 

316 

317 with ns_resolver.push(base): 

318 globalns, localns = ns_resolver.types_namespace 

319 for name, value in ann.items(): 

320 hints[name] = eval_type(value, globalns, localns) 

321 return hints 

322 

323 

324def try_eval_type( 

325 value: Any, 

326 globalns: GlobalsNamespace | None = None, 

327 localns: MappingNamespace | None = None, 

328) -> tuple[Any, bool]: 

329 """Try evaluating the annotation using the provided namespaces. 

330 

331 Args: 

332 value: The value to evaluate. If `None`, it will be replaced by `type[None]`. If an instance 

333 of `str`, it will be converted to a `ForwardRef`. 

334 localns: The global namespace to use during annotation evaluation. 

335 globalns: The local namespace to use during annotation evaluation. 

336 

337 Returns: 

338 A two-tuple containing the possibly evaluated type and a boolean indicating 

339 whether the evaluation succeeded or not. 

340 """ 

341 try: 

342 return eval_type(value, globalns, localns), True 

343 except NameError: 

344 return _type_convert(value), False 

345 

346 

347def eval_type( 

348 value: Any, 

349 globalns: GlobalsNamespace | None = None, 

350 localns: MappingNamespace | None = None, 

351 type_params: tuple[Any, ...] | None = None, 

352) -> Any: 

353 """Evaluate the annotation using the provided namespaces. 

354 

355 This function relies on our vendored `typing._eval_type()` function, and provide better error messages 

356 if possible. 

357 

358 Args: 

359 value: The value to evaluate (will be converted to `NoneType` if `None`, and to a `ForwardRef` instance if a string). 

360 localns: The global namespace to use during annotation evaluation. 

361 globalns: The local namespace to use during annotation evaluation. 

362 type_params: The type params belonging to the object being evaluated. 

363 """ 

364 value = _type_convert(value) 

365 try: 

366 return _eval_type(value, globalns, localns, type_params) 

367 except ImportError as e: 

368 # Starting in Python 3.15, resolving a lazy import during evaluation can fail, typically because of a 

369 # circular import between modules (an `ImportError` can also happen without lazy imports but this is 

370 # way less likely). Report it as a `NameError` so that callers treat the annotation as not resolvable 

371 # yet, exactly an undefined forward reference. 

372 if sys.version_info >= (3, 12): 

373 name = e.name_from if e.name_from is not None else e.name 

374 else: 

375 name = e.name 

376 raise NameError(f'Unable to resolve the import of {name!r}: {e}', name=name) from e 

377 except TypeError as e: 

378 if 'Unable to evaluate type annotation' in str(e): 

379 raise 

380 

381 # In most cases, value is a `ForwardRef`, otherwise the `TypeError` would have been raised when the annotation 

382 # was defined (at least on Python < 3.14). However, in rare cases, the `TypeError` can also appear when evaluating nested 

383 # parts of the annotation (e.g. `list["1 + 'a'"]`). 

384 if isinstance(value, ForwardRef): 

385 message = f'Unable to evaluate type annotation {value.__forward_arg__!r}.' 

386 else: 

387 message = f'Unable to evaluate type annotation {value!r}.' 

388 if sys.version_info >= (3, 11): 

389 e.add_note(message) 

390 raise 

391 else: 

392 raise TypeError(message) from e 

393 except RecursionError as e: 

394 message = ( 

395 "If you made use of an implicit recursive type alias (e.g. `MyType = list['MyType']), " 

396 'consider using PEP 695 type aliases instead. For more details, refer to the documentation: ' 

397 f'https://pydantic.dev/docs/validation/{version_short()}/concepts/types/#named-recursive-types' 

398 ) 

399 if sys.version_info >= (3, 11): 

400 e.add_note(message) 

401 raise 

402 else: 

403 raise RecursionError(f'{e.args[0]}\n{message}') 

404 

405 

406@deprecated( 

407 '`eval_type_lenient()` is deprecated, use `try_eval_type()` instead.', 

408 category=None, 

409) 

410def eval_type_lenient( 

411 value: Any, 

412 globalns: GlobalsNamespace | None = None, 

413 localns: MappingNamespace | None = None, 

414) -> Any: # pragma: no cover 

415 ev, _ = try_eval_type(value, globalns, localns) 

416 return ev 

417 

418 

419def _eval_type( 

420 value: Any, 

421 globalns: GlobalsNamespace | None = None, 

422 localns: MappingNamespace | None = None, 

423 type_params: tuple[Any, ...] | None = None, 

424) -> Any: 

425 """Evaluate all forward references for a given type. 

426 

427 This is a vendored version of the private `typing._eval_type()` function, adapted to work for all supported Python versions. 

428 """ 

429 if sys.version_info >= (3, 14): 

430 # Starting in 3.14, `_eval_type()` does *not* apply `_type_convert()` 

431 # anymore. This means the `None` -> `NoneType` conversion does not apply: 

432 evaluated = typing._eval_type( # type: ignore 

433 value, 

434 globalns, 

435 localns, 

436 type_params=type_params, 

437 # This is relevant when evaluating types from `TypedDict` classes, where string annotations 

438 # are automatically converted to `ForwardRef` instances with a module set. In this case, 

439 # Our `globalns` is irrelevant and we need to indicate `typing._eval_type()` that it should 

440 # infer it from the `ForwardRef.__forward_module__` attribute instead (`typing.get_type_hints()` 

441 # does the same). Note that this would probably be unnecessary if we properly iterated over the 

442 # `__orig_bases__` for TypedDicts in `get_cls_type_hints()`: 

443 prefer_fwd_module=True, 

444 ) 

445 if evaluated is None: 

446 evaluated = NoneType 

447 return evaluated 

448 elif sys.version_info >= (3, 13): 

449 return typing._eval_type( # type: ignore 

450 value, globalns, localns, type_params=type_params 

451 ) 

452 else: 

453 return typing._eval_type( # type: ignore 

454 value, globalns, localns 

455 ) 

456 

457 

458def signature_no_eval(f: Callable[..., Any]) -> Signature: 

459 """Get the signature of a callable without evaluating any annotations.""" 

460 if sys.version_info >= (3, 14): 

461 from annotationlib import Format 

462 

463 return signature(f, annotation_format=Format.FORWARDREF) 

464 else: 

465 return signature(f) 

466 

467 

468def get_function_type_hints( 

469 function: Callable[..., Any], 

470 *, 

471 include_keys: set[str] | None = None, 

472 globalns: GlobalsNamespace | None = None, 

473 localns: MappingNamespace | None = None, 

474) -> dict[str, Any]: 

475 """Return type hints for a function. 

476 

477 This is similar to the `typing.get_type_hints` function, with a few differences: 

478 - Support `functools.partial` by using the underlying `func` attribute. 

479 - Do not wrap type annotation of a parameter with `Optional` if it has a default value of `None` 

480 (related bug: https://github.com/python/cpython/issues/90353, only fixed in 3.11+). 

481 """ 

482 if isinstance(function, partial): 

483 annotations = safe_get_annotations(function.func) 

484 else: 

485 annotations = safe_get_annotations(function) 

486 

487 if globalns is None: 

488 globalns = get_module_ns_of(function) 

489 type_params: tuple[Any, ...] | None = None 

490 if localns is None: 

491 # If localns was specified, it is assumed to already contain type params. This is because 

492 # Pydantic has more advanced logic to do so (see `_namespace_utils.ns_for_function`). 

493 type_params = getattr(function, '__type_params__', ()) 

494 

495 type_hints = {} 

496 for name, value in annotations.items(): 

497 if include_keys is not None and name not in include_keys: 

498 continue 

499 

500 type_hints[name] = eval_type(value, globalns, localns, type_params) 

501 

502 return type_hints