Coverage for src/typing_inspection/typing_objects.py: 94%

126 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-15 11:48 +0000

1"""Low-level introspection utilities for [`typing`][] members. 

2 

3The provided functions in this module check against both the [`typing`][] and [`typing_extensions`][] 

4variants, if they exists and are different. 

5""" 

6# ruff: noqa: UP006 

7 

8import collections.abc 

9import contextlib 

10import re 

11import sys 

12import typing 

13import warnings 

14from textwrap import dedent 

15from types import FunctionType, GenericAlias 

16from typing import Any, Final 

17 

18import typing_extensions 

19from typing_extensions import LiteralString, TypeAliasType, TypeIs, deprecated 

20 

21__all__ = ( 

22 'DEPRECATED_ALIASES', 

23 'NoneType', 

24 'is_annotated', 

25 'is_any', 

26 'is_classvar', 

27 'is_concatenate', 

28 'is_deprecated', 

29 'is_final', 

30 'is_forwardref', 

31 'is_generic', 

32 'is_literal', 

33 'is_literalstring', 

34 'is_namedtuple', 

35 'is_never', 

36 'is_newtype', 

37 'is_nodefault', 

38 'is_noreturn', 

39 'is_notrequired', 

40 'is_paramspec', 

41 'is_paramspecargs', 

42 'is_paramspeckwargs', 

43 'is_readonly', 

44 'is_required', 

45 'is_self', 

46 'is_typealias', 

47 'is_typealiastype', 

48 'is_typeguard', 

49 'is_typeis', 

50 'is_typevar', 

51 'is_typevartuple', 

52 'is_union', 

53 'is_unpack', 

54) 

55 

56_IS_PY310 = sys.version_info[:2] == (3, 10) 

57 

58 

59def _compile_identity_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType: 

60 """Create a function checking that the function argument is the (unparameterized) typing :paramref:`member`. 

61 

62 The function will make sure to check against both the `typing` and `typing_extensions` 

63 variants as depending on the Python version, the `typing_extensions` variant might be different. 

64 For instance, on Python 3.9: 

65 

66 ```pycon 

67 >>> from typing import Literal as t_Literal 

68 >>> from typing_extensions import Literal as te_Literal, get_origin 

69 

70 >>> t_Literal is te_Literal 

71 False 

72 >>> get_origin(t_Literal[1]) 

73 typing.Literal 

74 >>> get_origin(te_Literal[1]) 

75 typing_extensions.Literal 

76 ``` 

77 """ 

78 in_typing = hasattr(typing, member) 

79 in_typing_extensions = hasattr(typing_extensions, member) 

80 

81 if in_typing and in_typing_extensions: 

82 if getattr(typing, member) is getattr(typing_extensions, member): 

83 check_code = f'obj is typing.{member}' 

84 else: 

85 check_code = f'obj is typing.{member} or obj is typing_extensions.{member}' 

86 elif in_typing and not in_typing_extensions: 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true

87 check_code = f'obj is typing.{member}' 

88 elif not in_typing and in_typing_extensions: 88 ↛ 91line 88 didn't jump to line 91 because the condition on line 88 was always true

89 check_code = f'obj is typing_extensions.{member}' 

90 else: 

91 check_code = 'False' 

92 

93 func_code = dedent(f""" 

94 def {function_name}(obj: Any, /) -> bool: 

95 return {check_code} 

96 """) 

97 

98 locals_: dict[str, Any] = {} 

99 globals_: dict[str, Any] = {'Any': Any, 'typing': typing, 'typing_extensions': typing_extensions} 

100 exec(func_code, globals_, locals_) 

101 return locals_[function_name] 

102 

103 

104def _compile_isinstance_check_function(member: LiteralString, function_name: LiteralString) -> FunctionType: 

105 """Create a function checking that the function is an instance of the typing `member`. 

106 

107 The function will make sure to check against both the `typing` and `typing_extensions` 

108 variants as depending on the Python version, the `typing_extensions` variant might be different. 

109 """ 

110 in_typing = hasattr(typing, member) 

111 in_typing_extensions = hasattr(typing_extensions, member) 

112 

113 if in_typing and in_typing_extensions: 

114 if getattr(typing, member) is getattr(typing_extensions, member): 

115 check_code = f'isinstance(obj, typing.{member})' 

116 else: 

117 check_code = f'isinstance(obj, (typing.{member}, typing_extensions.{member}))' 

118 elif in_typing and not in_typing_extensions: 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true

119 check_code = f'isinstance(obj, typing.{member})' 

120 elif not in_typing and in_typing_extensions: 120 ↛ 123line 120 didn't jump to line 123 because the condition on line 120 was always true

121 check_code = f'isinstance(obj, typing_extensions.{member})' 

122 else: 

123 check_code = 'False' 

124 

125 func_code = dedent(f""" 

126 def {function_name}(obj: Any, /) -> 'TypeIs[{member}]': 

127 return {check_code} 

128 """) 

129 

130 locals_: dict[str, Any] = {} 

131 globals_: dict[str, Any] = {'Any': Any, 'typing': typing, 'typing_extensions': typing_extensions} 

132 exec(func_code, globals_, locals_) 

133 return locals_[function_name] 

134 

135 

136if sys.version_info >= (3, 10): 

137 from types import NoneType 

138else: 

139 NoneType = type(None) 

140 

141# Keep this ordered, as per `typing.__all__`: 

142 

143is_annotated = _compile_identity_check_function('Annotated', 'is_annotated') 

144is_annotated.__doc__ = """ 

145Return whether the argument is the [`Annotated`][typing.Annotated] [special form][]. 

146 

147```pycon 

148>>> is_annotated(Annotated) 

149True 

150>>> is_annotated(Annotated[int, ...]) 

151False 

152``` 

153""" 

154 

155is_any = _compile_identity_check_function('Any', 'is_any') 

156is_any.__doc__ = """ 

157Return whether the argument is the [`Any`][typing.Any] [special form][]. 

158 

159```pycon 

160>>> is_any(Any) 

161True 

162``` 

163""" 

164 

165is_classvar = _compile_identity_check_function('ClassVar', 'is_classvar') 

166is_classvar.__doc__ = """ 

167Return whether the argument is the [`ClassVar`][typing.ClassVar] [type qualifier][]. 

168 

169```pycon 

170>>> is_classvar(ClassVar) 

171True 

172>>> is_classvar(ClassVar[int]) 

173>>> False 

174``` 

175""" 

176 

177is_concatenate = _compile_identity_check_function('Concatenate', 'is_concatenate') 

178is_concatenate.__doc__ = """ 

179Return whether the argument is the [`Concatenate`][typing.Concatenate] [special form][]. 

180 

181```pycon 

182>>> is_concatenate(Concatenate) 

183True 

184>>> is_concatenate(Concatenate[int, P]) 

185False 

186``` 

187""" 

188 

189is_final = _compile_identity_check_function('Final', 'is_final') 

190is_final.__doc__ = """ 

191Return whether the argument is the [`Final`][typing.Final] [type qualifier][]. 

192 

193```pycon 

194>>> is_final(Final) 

195True 

196>>> is_final(Final[int]) 

197False 

198``` 

199""" 

200 

201 

202# Unlikely to have a different version in `typing-extensions`, but keep it consistent. 

203# Also note that starting in 3.14, this is an alias to `annotationlib.ForwardRef`, but 

204# accessing it from `typing` doesn't seem to be deprecated. 

205is_forwardref = _compile_isinstance_check_function('ForwardRef', 'is_forwardref') 

206is_forwardref.__doc__ = """ 

207Return whether the argument is an instance of [`ForwardRef`][typing.ForwardRef]. 

208 

209```pycon 

210>>> is_forwardref(ForwardRef('T')) 

211True 

212``` 

213""" 

214 

215 

216is_generic = _compile_identity_check_function('Generic', 'is_generic') 

217is_generic.__doc__ = """ 

218Return whether the argument is the [`Generic`][typing.Generic] [special form][]. 

219 

220```pycon 

221>>> is_generic(Generic) 

222True 

223>>> is_generic(Generic[T]) 

224False 

225``` 

226""" 

227 

228is_literal = _compile_identity_check_function('Literal', 'is_literal') 

229is_literal.__doc__ = """ 

230Return whether the argument is the [`Literal`][typing.Literal] [special form][]. 

231 

232```pycon 

233>>> is_literal(Literal) 

234True 

235>>> is_literal(Literal["a"]) 

236False 

237``` 

238""" 

239 

240 

241# `get_origin(Optional[int]) is Union`, so `is_optional()` isn't implemented. 

242 

243is_paramspec = _compile_isinstance_check_function('ParamSpec', 'is_paramspec') 

244is_paramspec.__doc__ = """ 

245Return whether the argument is an instance of [`ParamSpec`][typing.ParamSpec]. 

246 

247```pycon 

248>>> P = ParamSpec('P') 

249>>> is_paramspec(P) 

250True 

251``` 

252""" 

253 

254# Protocol? 

255 

256is_typevar = _compile_isinstance_check_function('TypeVar', 'is_typevar') 

257is_typevar.__doc__ = """ 

258Return whether the argument is an instance of [`TypeVar`][typing.TypeVar]. 

259 

260```pycon 

261>>> T = TypeVar('T') 

262>>> is_typevar(T) 

263True 

264``` 

265""" 

266 

267is_typevartuple = _compile_isinstance_check_function('TypeVarTuple', 'is_typevartuple') 

268is_typevartuple.__doc__ = """ 

269Return whether the argument is an instance of [`TypeVarTuple`][typing.TypeVarTuple]. 

270 

271```pycon 

272>>> Ts = TypeVarTuple('Ts') 

273>>> is_typevartuple(Ts) 

274True 

275``` 

276""" 

277 

278is_union = _compile_identity_check_function('Union', 'is_union') 

279is_union.__doc__ = """ 

280Return whether the argument is the [`Union`][typing.Union] [special form][]. 

281 

282This function can also be used to check for the [`Optional`][typing.Optional] [special form][], 

283as at runtime, `Optional[int]` is equivalent to `Union[int, None]`. 

284 

285```pycon 

286>>> is_union(Union) 

287True 

288>>> is_union(Union[int, str]) 

289False 

290``` 

291 

292!!! warning 

293 This does not check for unions using the [new syntax][types-union] (e.g. `int | str`). 

294""" 

295 

296 

297def is_namedtuple(obj: Any, /) -> bool: 

298 """Return whether the argument is a named tuple type. 

299 

300 This includes [`NamedTuple`][typing.NamedTuple] subclasses and classes created from the 

301 [`collections.namedtuple`][] factory function. 

302 

303 ```pycon 

304 >>> class User(NamedTuple): 

305 ... name: str 

306 ... 

307 >>> is_namedtuple(User) 

308 True 

309 >>> City = collections.namedtuple('City', []) 

310 >>> is_namedtuple(City) 

311 True 

312 >>> is_namedtuple(NamedTuple) 

313 False 

314 ``` 

315 """ 

316 return isinstance(obj, type) and issubclass(obj, tuple) and hasattr(obj, '_fields') # pyright: ignore[reportUnknownArgumentType] 

317 

318 

319# TypedDict? 

320 

321# BinaryIO? IO? TextIO? 

322 

323is_literalstring = _compile_identity_check_function('LiteralString', 'is_literalstring') 

324is_literalstring.__doc__ = """ 

325Return whether the argument is the [`LiteralString`][typing.LiteralString] [special form][]. 

326 

327```pycon 

328>>> is_literalstring(LiteralString) 

329True 

330``` 

331""" 

332 

333is_never = _compile_identity_check_function('Never', 'is_never') 

334is_never.__doc__ = """ 

335Return whether the argument is the [`Never`][typing.Never] [special form][]. 

336 

337```pycon 

338>>> is_never(Never) 

339True 

340``` 

341""" 

342 

343if sys.version_info >= (3, 10): 

344 is_newtype = _compile_isinstance_check_function('NewType', 'is_newtype') 

345else: # On Python 3.10, `NewType` is a function. 

346 

347 def is_newtype(obj: Any, /) -> bool: 

348 return hasattr(obj, '__supertype__') 

349 

350 

351is_newtype.__doc__ = """ 

352Return whether the argument is a [`NewType`][typing.NewType]. 

353 

354```pycon 

355>>> UserId = NewType("UserId", int) 

356>>> is_newtype(UserId) 

357True 

358``` 

359""" 

360 

361is_nodefault = _compile_identity_check_function('NoDefault', 'is_nodefault') 

362is_nodefault.__doc__ = """ 

363Return whether the argument is the [`NoDefault`][typing.NoDefault] sentinel object. 

364 

365```pycon 

366>>> is_nodefault(NoDefault) 

367True 

368``` 

369""" 

370 

371is_noreturn = _compile_identity_check_function('NoReturn', 'is_noreturn') 

372is_noreturn.__doc__ = """ 

373Return whether the argument is the [`NoReturn`][typing.NoReturn] [special form][]. 

374 

375```pycon 

376>>> is_noreturn(NoReturn) 

377True 

378>>> is_noreturn(Never) 

379False 

380``` 

381""" 

382 

383is_notrequired = _compile_identity_check_function('NotRequired', 'is_notrequired') 

384is_notrequired.__doc__ = """ 

385Return whether the argument is the [`NotRequired`][typing.NotRequired] [special form][]. 

386 

387```pycon 

388>>> is_notrequired(NotRequired) 

389True 

390``` 

391""" 

392 

393is_paramspecargs = _compile_isinstance_check_function('ParamSpecArgs', 'is_paramspecargs') 

394is_paramspecargs.__doc__ = """ 

395Return whether the argument is an instance of [`ParamSpecArgs`][typing.ParamSpecArgs]. 

396 

397```pycon 

398>>> P = ParamSpec('P') 

399>>> is_paramspecargs(P.args) 

400True 

401``` 

402""" 

403 

404is_paramspeckwargs = _compile_isinstance_check_function('ParamSpecKwargs', 'is_paramspeckwargs') 

405is_paramspeckwargs.__doc__ = """ 

406Return whether the argument is an instance of [`ParamSpecKwargs`][typing.ParamSpecKwargs]. 

407 

408```pycon 

409>>> P = ParamSpec('P') 

410>>> is_paramspeckwargs(P.kwargs) 

411True 

412``` 

413""" 

414 

415is_readonly = _compile_identity_check_function('ReadOnly', 'is_readonly') 

416is_readonly.__doc__ = """ 

417Return whether the argument is the [`ReadOnly`][typing.ReadOnly] [special form][]. 

418 

419```pycon 

420>>> is_readonly(ReadOnly) 

421True 

422``` 

423""" 

424 

425is_required = _compile_identity_check_function('Required', 'is_required') 

426is_required.__doc__ = """ 

427Return whether the argument is the [`Required`][typing.Required] [special form][]. 

428 

429```pycon 

430>>> is_required(Required) 

431True 

432``` 

433""" 

434 

435is_self = _compile_identity_check_function('Self', 'is_self') 

436is_self.__doc__ = """ 

437Return whether the argument is the [`Self`][typing.Self] [special form][]. 

438 

439```pycon 

440>>> is_self(Self) 

441True 

442``` 

443""" 

444 

445# TYPE_CHECKING? 

446 

447is_typealias = _compile_identity_check_function('TypeAlias', 'is_typealias') 

448is_typealias.__doc__ = """ 

449Return whether the argument is the [`TypeAlias`][typing.TypeAlias] [special form][]. 

450 

451```pycon 

452>>> is_typealias(TypeAlias) 

453True 

454``` 

455""" 

456 

457is_typeguard = _compile_identity_check_function('TypeGuard', 'is_typeguard') 

458is_typeguard.__doc__ = """ 

459Return whether the argument is the [`TypeGuard`][typing.TypeGuard] [special form][]. 

460 

461```pycon 

462>>> is_typeguard(TypeGuard) 

463True 

464``` 

465""" 

466 

467is_typeis = _compile_identity_check_function('TypeIs', 'is_typeis') 

468is_typeis.__doc__ = """ 

469Return whether the argument is the [`TypeIs`][typing.TypeIs] [special form][]. 

470 

471```pycon 

472>>> is_typeis(TypeIs) 

473True 

474``` 

475""" 

476 

477_is_typealiastype_inner = _compile_isinstance_check_function('TypeAliasType', '_is_typealiastype_inner') 

478 

479 

480if _IS_PY310: 

481 # Parameterized PEP 695 type aliases are instances of `types.GenericAlias` in typing_extensions>=4.13.0. 

482 # On Python 3.10, with `Alias[int]` being such an instance of `GenericAlias`, 

483 # `isinstance(Alias[int], TypeAliasType)` returns `True`. 

484 # See https://github.com/python/cpython/issues/89828. 

485 def is_typealiastype(obj: Any, /) -> 'TypeIs[TypeAliasType]': 

486 return type(obj) is not GenericAlias and _is_typealiastype_inner(obj) 

487else: 

488 is_typealiastype = _compile_isinstance_check_function('TypeAliasType', 'is_typealiastype') 

489 

490is_typealiastype.__doc__ = """ 

491Return whether the argument is a [`TypeAliasType`][typing.TypeAliasType] instance. 

492 

493```pycon 

494>>> type MyInt = int 

495>>> is_typealiastype(MyInt) 

496True 

497>>> MyStr = TypeAliasType("MyStr", str) 

498>>> is_typealiastype(MyStr): 

499True 

500>>> type MyList[T] = list[T] 

501>>> is_typealiastype(MyList[int]) 

502False 

503``` 

504""" 

505 

506is_unpack = _compile_identity_check_function('Unpack', 'is_unpack') 

507is_unpack.__doc__ = """ 

508Return whether the argument is the [`Unpack`][typing.Unpack] [special form][]. 

509 

510```pycon 

511>>> is_unpack(Unpack) 

512True 

513>>> is_unpack(Unpack[Ts]) 

514False 

515``` 

516""" 

517 

518 

519if sys.version_info >= (3, 13): 

520 

521 def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]': 

522 return isinstance(obj, (warnings.deprecated, typing_extensions.deprecated)) 

523 

524else: 

525 

526 def is_deprecated(obj: Any, /) -> 'TypeIs[deprecated]': 

527 return isinstance(obj, typing_extensions.deprecated) 

528 

529 

530is_deprecated.__doc__ = """ 

531Return whether the argument is a [`deprecated`][warnings.deprecated] instance. 

532 

533This also includes the [`typing_extensions` backport][typing_extensions.deprecated]. 

534 

535```pycon 

536>>> is_deprecated(warnings.deprecated('message')) 

537True 

538>>> is_deprecated(typing_extensions.deprecated('message')) 

539True 

540``` 

541""" 

542 

543 

544# Aliases defined in the `typing` module using `typing._SpecialGenericAlias` (itself aliased as `alias()`): 

545DEPRECATED_ALIASES: Final[dict[Any, type[Any]]] = { 

546 typing.Hashable: collections.abc.Hashable, 

547 typing.Awaitable: collections.abc.Awaitable, 

548 typing.Coroutine: collections.abc.Coroutine, 

549 typing.AsyncIterable: collections.abc.AsyncIterable, 

550 typing.AsyncIterator: collections.abc.AsyncIterator, 

551 typing.Iterable: collections.abc.Iterable, 

552 typing.Iterator: collections.abc.Iterator, 

553 typing.Reversible: collections.abc.Reversible, 

554 typing.Sized: collections.abc.Sized, 

555 typing.Container: collections.abc.Container, 

556 typing.Collection: collections.abc.Collection, 

557 # type ignore reason: https://github.com/python/typeshed/issues/6257: 

558 typing.Callable: collections.abc.Callable, # pyright: ignore[reportAssignmentType, reportUnknownMemberType] 

559 typing.AbstractSet: collections.abc.Set, 

560 typing.MutableSet: collections.abc.MutableSet, 

561 typing.Mapping: collections.abc.Mapping, 

562 typing.MutableMapping: collections.abc.MutableMapping, 

563 typing.Sequence: collections.abc.Sequence, 

564 typing.MutableSequence: collections.abc.MutableSequence, 

565 typing.Tuple: tuple, 

566 typing.List: list, 

567 typing.Deque: collections.deque, 

568 typing.Set: set, 

569 typing.FrozenSet: frozenset, 

570 typing.MappingView: collections.abc.MappingView, 

571 typing.KeysView: collections.abc.KeysView, 

572 typing.ItemsView: collections.abc.ItemsView, 

573 typing.ValuesView: collections.abc.ValuesView, 

574 typing.Dict: dict, 

575 typing.DefaultDict: collections.defaultdict, 

576 typing.OrderedDict: collections.OrderedDict, 

577 typing.Counter: collections.Counter, 

578 typing.ChainMap: collections.ChainMap, 

579 typing.Generator: collections.abc.Generator, 

580 typing.AsyncGenerator: collections.abc.AsyncGenerator, 

581 typing.Type: type, 

582 # Defined in `typing.__getattr__`: 

583 typing.Pattern: re.Pattern, 

584 typing.Match: re.Match, 

585 typing.ContextManager: contextlib.AbstractContextManager, 

586 typing.AsyncContextManager: contextlib.AbstractAsyncContextManager, 

587 # Skipped: `ByteString` (deprecated, removed in 3.14) 

588} 

589"""A mapping between the deprecated typing aliases to their replacement, as per [PEP 585](https://peps.python.org/pep-0585/).""" 

590 

591 

592# Add the `typing_extensions` aliases: 

593for alias, target in list(DEPRECATED_ALIASES.items()): 

594 # Use `alias.__name__` when we drop support for Python 3.9 

595 if (te_alias := getattr(typing_extensions, alias._name, None)) is not None: 595 ↛ 593line 595 didn't jump to line 593 because the condition on line 595 was always true

596 DEPRECATED_ALIASES[te_alias] = target