Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/astroid/brain/brain_typing.py: 49%

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

169 statements  

1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html 

2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE 

3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt 

4 

5"""Astroid hooks for typing.py support.""" 

6 

7from __future__ import annotations 

8 

9import textwrap 

10import typing 

11from collections.abc import Iterator 

12from functools import partial 

13from typing import Final 

14 

15from astroid import context, nodes 

16from astroid.brain.helpers import register_module_extender 

17from astroid.builder import AstroidBuilder, _extract_single_node, extract_node 

18from astroid.const import PY312_PLUS, PY313_PLUS, PY314_PLUS, PY315_PLUS 

19from astroid.exceptions import ( 

20 AstroidSyntaxError, 

21 AttributeInferenceError, 

22 InferenceError, 

23 UseInferenceDefault, 

24) 

25from astroid.inference_tip import inference_tip 

26from astroid.manager import AstroidManager 

27 

28TYPING_TYPEVARS = {"TypeVar", "NewType"} 

29TYPING_TYPEVARS_QUALIFIED: Final = { 

30 "typing.TypeVar", 

31 "typing.NewType", 

32 "typing_extensions.TypeVar", 

33} 

34TYPING_TYPEDDICT_QUALIFIED: Final = {"typing.TypedDict", "typing_extensions.TypedDict"} 

35TYPING_TYPE_TEMPLATE = """ 

36class Meta(type): 

37 def __getitem__(self, item): 

38 return self 

39 

40 @property 

41 def __args__(self): 

42 return () 

43 

44class {0}(metaclass=Meta): 

45 pass 

46""" 

47TYPING_MEMBERS = set(getattr(typing, "__all__", [])) 

48 

49TYPING_ALIAS = frozenset( 

50 ( 

51 "typing.Hashable", 

52 "typing.Awaitable", 

53 "typing.Coroutine", 

54 "typing.AsyncIterable", 

55 "typing.AsyncIterator", 

56 "typing.Iterable", 

57 "typing.Iterator", 

58 "typing.Reversible", 

59 "typing.Sized", 

60 "typing.Container", 

61 "typing.Collection", 

62 "typing.Callable", 

63 "typing.AbstractSet", 

64 "typing.MutableSet", 

65 "typing.Mapping", 

66 "typing.MutableMapping", 

67 "typing.Sequence", 

68 "typing.MutableSequence", 

69 "typing.ByteString", # scheduled for removal in 3.17 

70 "typing.Tuple", 

71 "typing.List", 

72 "typing.Deque", 

73 "typing.Set", 

74 "typing.FrozenSet", 

75 "typing.MappingView", 

76 "typing.KeysView", 

77 "typing.ItemsView", 

78 "typing.ValuesView", 

79 "typing.ContextManager", 

80 "typing.AsyncContextManager", 

81 "typing.Dict", 

82 "typing.DefaultDict", 

83 "typing.OrderedDict", 

84 "typing.Counter", 

85 "typing.ChainMap", 

86 "typing.Generator", 

87 "typing.AsyncGenerator", 

88 "typing.Type", 

89 "typing.Pattern", 

90 "typing.Match", 

91 ) 

92) 

93 

94CLASS_GETITEM_TEMPLATE = """ 

95@classmethod 

96def __class_getitem__(cls, item): 

97 return cls 

98""" 

99 

100 

101def looks_like_typing_typevar_or_newtype(node) -> bool: 

102 func = node.func 

103 if isinstance(func, nodes.Attribute): 

104 return func.attrname in TYPING_TYPEVARS 

105 if isinstance(func, nodes.Name): 

106 return func.name in TYPING_TYPEVARS 

107 return False 

108 

109 

110def infer_typing_typevar_or_newtype( 

111 node: nodes.Call, context_itton: context.InferenceContext | None = None 

112) -> Iterator[nodes.ClassDef]: 

113 """Infer a typing.TypeVar(...) or typing.NewType(...) call.""" 

114 try: 

115 func = next(node.func.infer(context=context_itton)) 

116 except (InferenceError, StopIteration) as exc: 

117 raise UseInferenceDefault from exc 

118 

119 if func.qname() not in TYPING_TYPEVARS_QUALIFIED: 

120 raise UseInferenceDefault 

121 if not node.args: 

122 raise UseInferenceDefault 

123 # Cannot infer from a dynamic class name (f-string) 

124 if isinstance(node.args[0], nodes.JoinedStr): 

125 raise UseInferenceDefault 

126 

127 typename = node.args[0].as_string().strip("'") 

128 try: 

129 # ``typing`` accepts any string as a name, so don't splice it into the 

130 # template: a crafted value such as ``"T(Base): #"`` would break out of 

131 # the identifier position and inject bases and a body into the class. 

132 # Build the class with a fixed name and assign the real one afterwards. 

133 node = extract_node(TYPING_TYPE_TEMPLATE.format("_TypeVar")) 

134 except AstroidSyntaxError as exc: 

135 raise InferenceError from exc 

136 node.name = typename 

137 return node.infer(context=context_itton) 

138 

139 

140def _looks_like_typing_subscript(node) -> bool: 

141 """Try to figure out if a Subscript node *might* be a typing-related subscript.""" 

142 if isinstance(node, nodes.Name): 

143 return node.name in TYPING_MEMBERS 

144 if isinstance(node, nodes.Attribute): 

145 return node.attrname in TYPING_MEMBERS 

146 if isinstance(node, nodes.Subscript): 

147 return _looks_like_typing_subscript(node.value) 

148 return False 

149 

150 

151def infer_typing_attr( 

152 node: nodes.Subscript, ctx: context.InferenceContext | None = None 

153) -> Iterator[nodes.ClassDef]: 

154 """Infer a typing.X[...] subscript.""" 

155 try: 

156 value = next(node.value.infer()) # type: ignore[union-attr] # value shouldn't be None for Subscript. 

157 except (InferenceError, StopIteration) as exc: 

158 raise UseInferenceDefault from exc 

159 

160 if not value.qname().startswith("typing.") or value.qname() in TYPING_ALIAS: 

161 # If typing subscript belongs to an alias handle it separately. 

162 raise UseInferenceDefault 

163 

164 if ( 

165 PY313_PLUS 

166 and isinstance(value, nodes.FunctionDef) 

167 and value.qname() == "typing.Annotated" 

168 ): 

169 # typing.Annotated is a FunctionDef on 3.13+ 

170 node._explicit_inference = lambda node, context: iter([value]) 

171 return iter([value]) 

172 

173 if isinstance(value, nodes.ClassDef) and value.qname() in { 

174 "typing.Generic", 

175 "typing.Annotated", 

176 "typing_extensions.Annotated", 

177 }: 

178 # typing.Generic and typing.Annotated (PY39) are subscriptable 

179 # through __class_getitem__. Since astroid can't easily 

180 # infer the native methods, replace them for an easy inference tip 

181 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) 

182 value.locals["__class_getitem__"] = [func_to_add] 

183 if ( 

184 isinstance(node.parent, nodes.ClassDef) 

185 and node in node.parent.bases 

186 and getattr(node.parent, "__cache", None) 

187 ): 

188 # node.parent.slots is evaluated and cached before the inference tip 

189 # is first applied. Remove the last result to allow a recalculation of slots 

190 cache = node.parent.__cache # type: ignore[attr-defined] # Unrecognized getattr 

191 if cache.get(node.parent.slots) is not None: 

192 del cache[node.parent.slots] 

193 # Avoid re-instantiating this class every time it's seen 

194 node._explicit_inference = lambda node, context: iter([value]) 

195 return iter([value]) 

196 

197 node = extract_node(TYPING_TYPE_TEMPLATE.format(value.qname().split(".")[-1])) 

198 return node.infer(context=ctx) 

199 

200 

201def _looks_like_generic_class_pep695(node: nodes.ClassDef) -> bool: 

202 """Check if class is using type parameter. Python 3.12+.""" 

203 return len(node.type_params) > 0 

204 

205 

206def infer_typing_generic_class_pep695( 

207 node: nodes.ClassDef, ctx: context.InferenceContext | None = None 

208) -> Iterator[nodes.ClassDef]: 

209 """Add __class_getitem__ for generic classes. Python 3.12+.""" 

210 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) 

211 node.locals["__class_getitem__"] = [func_to_add] 

212 return iter([node]) 

213 

214 

215def _looks_like_typedDict( # pylint: disable=invalid-name 

216 node: nodes.FunctionDef | nodes.ClassDef, 

217) -> bool: 

218 """Check if node is TypedDict FunctionDef.""" 

219 return node.qname() in TYPING_TYPEDDICT_QUALIFIED 

220 

221 

222def infer_typedDict( # pylint: disable=invalid-name 

223 node: nodes.FunctionDef, ctx: context.InferenceContext | None = None 

224) -> Iterator[nodes.ClassDef]: 

225 """Replace TypedDict FunctionDef with ClassDef.""" 

226 class_def = nodes.ClassDef( 

227 name="TypedDict", 

228 lineno=node.lineno, 

229 col_offset=node.col_offset, 

230 parent=node.parent, 

231 end_lineno=node.end_lineno, 

232 end_col_offset=node.end_col_offset, 

233 ) 

234 class_def.postinit(bases=[extract_node("dict")], body=[], decorators=None) 

235 func_to_add = _extract_single_node("dict") 

236 class_def.locals["__call__"] = [func_to_add] 

237 # TypedDict subclasses have ``__required_keys__`` and ``__optional_keys__`` 

238 # class attributes at runtime (e.g. ``MyDict.__required_keys__``), even 

239 # though the annotation-only body never declares them. 

240 for attr in ("__required_keys__", "__optional_keys__"): 

241 func_to_add = _extract_single_node("dict") 

242 class_def.locals[attr] = [func_to_add] 

243 return iter([class_def]) 

244 

245 

246def _looks_like_typing_alias(node: nodes.Call) -> bool: 

247 """ 

248 Returns True if the node corresponds to a call to _alias function. 

249 

250 For example : 

251 

252 MutableSet = _alias(collections.abc.MutableSet, T) 

253 

254 :param node: call node 

255 """ 

256 return ( 

257 isinstance(node.func, nodes.Name) 

258 # TODO: remove _DeprecatedGenericAlias when Py3.14 min 

259 and node.func.name in {"_alias", "_DeprecatedGenericAlias"} 

260 and len(node.args) == 2 

261 and ( 

262 # _alias function works also for builtins object such as list and dict 

263 isinstance(node.args[0], (nodes.Attribute, nodes.Name)) 

264 ) 

265 ) 

266 

267 

268def _forbid_class_getitem_access(node: nodes.ClassDef) -> None: 

269 """Disable the access to __class_getitem__ method for the node in parameters.""" 

270 

271 def full_raiser(origin_func, attr, *args, **kwargs): 

272 """ 

273 Raises an AttributeInferenceError in case of access to __class_getitem__ method. 

274 Otherwise, just call origin_func. 

275 """ 

276 if attr == "__class_getitem__": 

277 raise AttributeInferenceError("__class_getitem__ access is not allowed") 

278 return origin_func(attr, *args, **kwargs) 

279 

280 try: 

281 node.getattr("__class_getitem__") 

282 # If we are here, then we are sure to modify an object that does have 

283 # __class_getitem__ method (which origin is the protocol defined in 

284 # collections module) whereas the typing module considers it should not. 

285 # We do not want __class_getitem__ to be found in the classdef 

286 partial_raiser = partial(full_raiser, node.getattr) 

287 node.getattr = partial_raiser 

288 except AttributeInferenceError: 

289 pass 

290 

291 

292def infer_typing_alias( 

293 node: nodes.Call, ctx: context.InferenceContext | None = None 

294) -> Iterator[nodes.ClassDef]: 

295 """ 

296 Infers the call to _alias function 

297 Insert ClassDef, with same name as aliased class, 

298 in mro to simulate _GenericAlias. 

299 

300 :param node: call node 

301 :param context: inference context 

302 

303 # TODO: evaluate if still necessary when Py3.12 is minimum 

304 """ 

305 if not ( 

306 isinstance(node.parent, nodes.Assign) 

307 and len(node.parent.targets) == 1 

308 and isinstance(node.parent.targets[0], nodes.AssignName) 

309 ): 

310 raise UseInferenceDefault 

311 try: 

312 res = next(node.args[0].infer(context=ctx)) 

313 except StopIteration as e: 

314 raise InferenceError(node=node.args[0], context=ctx) from e 

315 

316 assign_name = node.parent.targets[0] 

317 

318 class_def = nodes.ClassDef( 

319 name=assign_name.name, 

320 lineno=assign_name.lineno, 

321 col_offset=assign_name.col_offset, 

322 parent=node.parent, 

323 end_lineno=assign_name.end_lineno, 

324 end_col_offset=assign_name.end_col_offset, 

325 ) 

326 if isinstance(res, nodes.ClassDef): 

327 # Only add `res` as base if it's a `ClassDef` 

328 # This isn't the case for `typing.Pattern` and `typing.Match` 

329 class_def.postinit(bases=[res], body=[], decorators=None) 

330 

331 maybe_type_var = node.args[1] 

332 if isinstance(maybe_type_var, nodes.Const) and maybe_type_var.value > 0: 

333 # If typing alias is subscriptable, add `__class_getitem__` to ClassDef 

334 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) 

335 class_def.locals["__class_getitem__"] = [func_to_add] 

336 else: 

337 # If not, make sure that `__class_getitem__` access is forbidden. 

338 # This is an issue in cases where the aliased class implements it, 

339 # but the typing alias isn't subscriptable. E.g., `typing.ByteString` for PY39+ 

340 _forbid_class_getitem_access(class_def) 

341 

342 # Avoid re-instantiating this class every time it's seen 

343 node._explicit_inference = lambda node, context: iter([class_def]) 

344 return iter([class_def]) 

345 

346 

347def _looks_like_special_alias(node: nodes.Call) -> bool: 

348 """Return True if call is for Tuple or Callable alias. 

349 

350 In PY37 and PY38 the call is to '_VariadicGenericAlias' with 'tuple' as 

351 first argument. In PY39+ it is replaced by a call to '_TupleType'. 

352 

353 PY37: Tuple = _VariadicGenericAlias(tuple, (), inst=False, special=True) 

354 PY39: Tuple = _TupleType(tuple, -1, inst=False, name='Tuple') 

355 

356 PY37: Callable = _VariadicGenericAlias(collections.abc.Callable, (), special=True) 

357 PY39: Callable = _CallableType(collections.abc.Callable, 2) 

358 """ 

359 return ( 

360 isinstance(node.func, nodes.Name) 

361 and node.args 

362 and ( 

363 ( 

364 node.func.name == "_TupleType" 

365 and isinstance(node.args[0], nodes.Name) 

366 and node.args[0].name == "tuple" 

367 ) 

368 or ( 

369 node.func.name == "_CallableType" 

370 and isinstance(node.args[0], nodes.Attribute) 

371 and node.args[0].as_string() == "collections.abc.Callable" 

372 ) 

373 ) 

374 ) 

375 

376 

377def infer_special_alias( 

378 node: nodes.Call, ctx: context.InferenceContext | None = None 

379) -> Iterator[nodes.ClassDef]: 

380 """Infer call to tuple alias as new subscriptable class typing.Tuple.""" 

381 if not ( 

382 isinstance(node.parent, nodes.Assign) 

383 and len(node.parent.targets) == 1 

384 and isinstance(node.parent.targets[0], nodes.AssignName) 

385 ): 

386 raise UseInferenceDefault 

387 try: 

388 res = next(node.args[0].infer(context=ctx)) 

389 except StopIteration as e: 

390 raise InferenceError(node=node.args[0], context=ctx) from e 

391 

392 assign_name = node.parent.targets[0] 

393 class_def = nodes.ClassDef( 

394 name=assign_name.name, 

395 parent=node.parent, 

396 lineno=assign_name.lineno, 

397 col_offset=assign_name.col_offset, 

398 end_lineno=assign_name.end_lineno, 

399 end_col_offset=assign_name.end_col_offset, 

400 ) 

401 class_def.postinit(bases=[res], body=[], decorators=None) 

402 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) 

403 class_def.locals["__class_getitem__"] = [func_to_add] 

404 # Avoid re-instantiating this class every time it's seen 

405 node._explicit_inference = lambda node, context: iter([class_def]) 

406 return iter([class_def]) 

407 

408 

409def _looks_like_typing_cast(node: nodes.Call) -> bool: 

410 return (isinstance(node.func, nodes.Name) and node.func.name == "cast") or ( 

411 isinstance(node.func, nodes.Attribute) and node.func.attrname == "cast" 

412 ) 

413 

414 

415def infer_typing_cast( 

416 node: nodes.Call, ctx: context.InferenceContext | None = None 

417) -> Iterator[nodes.NodeNG]: 

418 """Infer call to cast() returning same type as casted-from var.""" 

419 if not isinstance(node.func, (nodes.Name, nodes.Attribute)): 

420 raise UseInferenceDefault 

421 

422 try: 

423 func = next(node.func.infer(context=ctx)) 

424 except (InferenceError, StopIteration) as exc: 

425 raise UseInferenceDefault from exc 

426 if not ( 

427 isinstance(func, nodes.FunctionDef) 

428 and func.qname() == "typing.cast" 

429 and len(node.args) == 2 

430 ): 

431 raise UseInferenceDefault 

432 

433 return node.args[1].infer(context=ctx) 

434 

435 

436def _typing_transform(): 

437 code = textwrap.dedent(""" 

438 class Generic: 

439 @classmethod 

440 def __class_getitem__(cls, item): return cls 

441 class ParamSpec: 

442 @property 

443 def args(self): 

444 return ParamSpecArgs(self) 

445 @property 

446 def kwargs(self): 

447 return ParamSpecKwargs(self) 

448 class ParamSpecArgs: ... 

449 class ParamSpecKwargs: ... 

450 class TypeAlias: ... 

451 class Type: 

452 @classmethod 

453 def __class_getitem__(cls, item): return cls 

454 class TypeVar: 

455 @classmethod 

456 def __class_getitem__(cls, item): return cls 

457 class TypeVarTuple: ... 

458 class ContextManager: 

459 @classmethod 

460 def __class_getitem__(cls, item): return cls 

461 class AsyncContextManager: 

462 @classmethod 

463 def __class_getitem__(cls, item): return cls 

464 class Pattern: 

465 @classmethod 

466 def __class_getitem__(cls, item): return cls 

467 class Match: 

468 @classmethod 

469 def __class_getitem__(cls, item): return cls 

470 """) 

471 if PY314_PLUS: 

472 code += textwrap.dedent(""" 

473 from annotationlib import ForwardRef 

474 class Union: 

475 @classmethod 

476 def __class_getitem__(cls, item): return cls 

477 """) 

478 if PY315_PLUS: 

479 # typing.ByteString was removed from the typing module in Python 3.15 

480 # (it was deprecated since 3.12 and present at module level until 3.14). 

481 # Inject a stub so code using `typing.ByteString` can still be inferred. 

482 code += textwrap.dedent(""" 

483 class ByteString: ... 

484 """) 

485 return AstroidBuilder(AstroidManager()).string_build(code) 

486 

487 

488def register(manager: AstroidManager) -> None: 

489 manager.register_transform( 

490 nodes.Call, 

491 inference_tip(infer_typing_typevar_or_newtype), 

492 looks_like_typing_typevar_or_newtype, 

493 ) 

494 manager.register_transform( 

495 nodes.Subscript, inference_tip(infer_typing_attr), _looks_like_typing_subscript 

496 ) 

497 manager.register_transform( 

498 nodes.Call, inference_tip(infer_typing_cast), _looks_like_typing_cast 

499 ) 

500 

501 manager.register_transform( 

502 nodes.FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict 

503 ) 

504 

505 manager.register_transform( 

506 nodes.Call, inference_tip(infer_typing_alias), _looks_like_typing_alias 

507 ) 

508 manager.register_transform( 

509 nodes.Call, inference_tip(infer_special_alias), _looks_like_special_alias 

510 ) 

511 

512 if PY312_PLUS: 

513 register_module_extender(manager, "typing", _typing_transform) 

514 manager.register_transform( 

515 nodes.ClassDef, 

516 inference_tip(infer_typing_generic_class_pep695), 

517 _looks_like_generic_class_pep695, 

518 )