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

176 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 elif name == 'model_config': 

280 # `model_config` is never a field, and `collect_model_fields()` ignores its annotation, 

281 # so there is no need to evaluate it: 

282 hints[name] = (value, False) 

283 else: 

284 if base_model_fields is not None and name in base_model_fields: 

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

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

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

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

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

290 else: 

291 globalns, localns = ns_resolver.types_namespace 

292 

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

294 

295 return hints 

296 

297 

298def get_cls_type_hints( 

299 obj: type[Any], 

300 *, 

301 ns_resolver: NsResolver | None = None, 

302) -> dict[str, Any]: 

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

304 

305 Args: 

306 obj: The class to inspect. 

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

308 """ 

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

310 ns_resolver = ns_resolver or NsResolver() 

311 

312 for base in reversed(obj.__mro__): 

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

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

315 # to evaluate: 

316 ann = safe_get_annotations(base) 

317 

318 if not ann: 

319 continue 

320 

321 with ns_resolver.push(base): 

322 globalns, localns = ns_resolver.types_namespace 

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

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

325 return hints 

326 

327 

328def try_eval_type( 

329 value: Any, 

330 globalns: GlobalsNamespace | None = None, 

331 localns: MappingNamespace | None = None, 

332) -> tuple[Any, bool]: 

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

334 

335 Args: 

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

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

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

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

340 

341 Returns: 

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

343 whether the evaluation succeeded or not. 

344 """ 

345 try: 

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

347 except NameError: 

348 return _type_convert(value), False 

349 

350 

351def eval_type( 

352 value: Any, 

353 globalns: GlobalsNamespace | None = None, 

354 localns: MappingNamespace | None = None, 

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

356) -> Any: 

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

358 

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

360 if possible. 

361 

362 Args: 

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

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

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

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

367 """ 

368 value = _type_convert(value) 

369 try: 

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

371 except ImportError as e: 

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

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

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

375 # yet, exactly an undefined forward reference. 

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

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

378 else: 

379 name = e.name 

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

381 except TypeError as e: 

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

383 raise 

384 

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

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

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

388 if isinstance(value, ForwardRef): 

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

390 else: 

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

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

393 e.add_note(message) 

394 raise 

395 else: 

396 raise TypeError(message) from e 

397 except RecursionError as e: 

398 message = ( 

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

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

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

402 ) 

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

404 e.add_note(message) 

405 raise 

406 else: 

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

408 

409 

410@deprecated( 

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

412 category=None, 

413) 

414def eval_type_lenient( 

415 value: Any, 

416 globalns: GlobalsNamespace | None = None, 

417 localns: MappingNamespace | None = None, 

418) -> Any: # pragma: no cover 

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

420 return ev 

421 

422 

423def _eval_type( 

424 value: Any, 

425 globalns: GlobalsNamespace | None = None, 

426 localns: MappingNamespace | None = None, 

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

428) -> Any: 

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

430 

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

432 """ 

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

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

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

436 evaluated = typing._eval_type( # type: ignore 

437 value, 

438 globalns, 

439 localns, 

440 type_params=type_params, 

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

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

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

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

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

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

447 prefer_fwd_module=True, 

448 ) 

449 if evaluated is None: 

450 evaluated = NoneType 

451 return evaluated 

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

453 return typing._eval_type( # type: ignore 

454 value, globalns, localns, type_params=type_params 

455 ) 

456 else: 

457 return typing._eval_type( # type: ignore 

458 value, globalns, localns 

459 ) 

460 

461 

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

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

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

465 from annotationlib import Format 

466 

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

468 else: 

469 return signature(f) 

470 

471 

472def get_function_type_hints( 

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

474 *, 

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

476 globalns: GlobalsNamespace | None = None, 

477 localns: MappingNamespace | None = None, 

478) -> dict[str, Any]: 

479 """Return type hints for a function. 

480 

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

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

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

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

485 """ 

486 if isinstance(function, partial): 

487 annotations = safe_get_annotations(function.func) 

488 else: 

489 annotations = safe_get_annotations(function) 

490 

491 if globalns is None: 

492 globalns = get_module_ns_of(function) 

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

494 if localns is None: 

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

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

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

498 

499 type_hints = {} 

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

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

502 continue 

503 

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

505 

506 return type_hints