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

144 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:53 +0000

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 typing 

10from collections.abc import Iterator 

11from functools import partial 

12from typing import Final 

13 

14from astroid import context, extract_node, inference_tip 

15from astroid.builder import _extract_single_node 

16from astroid.const import PY39_PLUS 

17from astroid.exceptions import ( 

18 AttributeInferenceError, 

19 InferenceError, 

20 UseInferenceDefault, 

21) 

22from astroid.manager import AstroidManager 

23from astroid.nodes.node_classes import ( 

24 Assign, 

25 AssignName, 

26 Attribute, 

27 Call, 

28 Const, 

29 JoinedStr, 

30 Name, 

31 NodeNG, 

32 Subscript, 

33 Tuple, 

34) 

35from astroid.nodes.scoped_nodes import ClassDef, FunctionDef 

36 

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

38TYPING_TYPEVARS_QUALIFIED: Final = { 

39 "typing.TypeVar", 

40 "typing.NewType", 

41 "typing_extensions.TypeVar", 

42} 

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

44TYPING_TYPE_TEMPLATE = """ 

45class Meta(type): 

46 def __getitem__(self, item): 

47 return self 

48 

49 @property 

50 def __args__(self): 

51 return () 

52 

53class {0}(metaclass=Meta): 

54 pass 

55""" 

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

57 

58TYPING_ALIAS = frozenset( 

59 ( 

60 "typing.Hashable", 

61 "typing.Awaitable", 

62 "typing.Coroutine", 

63 "typing.AsyncIterable", 

64 "typing.AsyncIterator", 

65 "typing.Iterable", 

66 "typing.Iterator", 

67 "typing.Reversible", 

68 "typing.Sized", 

69 "typing.Container", 

70 "typing.Collection", 

71 "typing.Callable", 

72 "typing.AbstractSet", 

73 "typing.MutableSet", 

74 "typing.Mapping", 

75 "typing.MutableMapping", 

76 "typing.Sequence", 

77 "typing.MutableSequence", 

78 "typing.ByteString", 

79 "typing.Tuple", 

80 "typing.List", 

81 "typing.Deque", 

82 "typing.Set", 

83 "typing.FrozenSet", 

84 "typing.MappingView", 

85 "typing.KeysView", 

86 "typing.ItemsView", 

87 "typing.ValuesView", 

88 "typing.ContextManager", 

89 "typing.AsyncContextManager", 

90 "typing.Dict", 

91 "typing.DefaultDict", 

92 "typing.OrderedDict", 

93 "typing.Counter", 

94 "typing.ChainMap", 

95 "typing.Generator", 

96 "typing.AsyncGenerator", 

97 "typing.Type", 

98 "typing.Pattern", 

99 "typing.Match", 

100 ) 

101) 

102 

103CLASS_GETITEM_TEMPLATE = """ 

104@classmethod 

105def __class_getitem__(cls, item): 

106 return cls 

107""" 

108 

109 

110def looks_like_typing_typevar_or_newtype(node) -> bool: 

111 func = node.func 

112 if isinstance(func, Attribute): 

113 return func.attrname in TYPING_TYPEVARS 

114 if isinstance(func, Name): 

115 return func.name in TYPING_TYPEVARS 

116 return False 

117 

118 

119def infer_typing_typevar_or_newtype(node, context_itton=None): 

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

121 try: 

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

123 except (InferenceError, StopIteration) as exc: 

124 raise UseInferenceDefault from exc 

125 

126 if func.qname() not in TYPING_TYPEVARS_QUALIFIED: 

127 raise UseInferenceDefault 

128 if not node.args: 

129 raise UseInferenceDefault 

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

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

132 raise UseInferenceDefault 

133 

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

135 node = extract_node(TYPING_TYPE_TEMPLATE.format(typename)) 

136 return node.infer(context=context_itton) 

137 

138 

139def _looks_like_typing_subscript(node) -> bool: 

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

141 if isinstance(node, Name): 

142 return node.name in TYPING_MEMBERS 

143 if isinstance(node, Attribute): 

144 return node.attrname in TYPING_MEMBERS 

145 if isinstance(node, Subscript): 

146 return _looks_like_typing_subscript(node.value) 

147 return False 

148 

149 

150def infer_typing_attr( 

151 node: Subscript, ctx: context.InferenceContext | None = None 

152) -> Iterator[ClassDef]: 

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

154 try: 

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

156 except (InferenceError, StopIteration) as exc: 

157 raise UseInferenceDefault from exc 

158 

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

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

161 raise UseInferenceDefault 

162 

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

164 "typing.Generic", 

165 "typing.Annotated", 

166 "typing_extensions.Annotated", 

167 }: 

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

169 # through __class_getitem__. Since astroid can't easily 

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

171 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) 

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

173 if ( 

174 isinstance(node.parent, ClassDef) 

175 and node in node.parent.bases 

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

177 ): 

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

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

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

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

182 del cache[node.parent.slots] 

183 return iter([value]) 

184 

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

186 return node.infer(context=ctx) 

187 

188 

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

190 node: FunctionDef | ClassDef, 

191) -> bool: 

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

193 return node.qname() in TYPING_TYPEDDICT_QUALIFIED 

194 

195 

196def infer_old_typedDict( # pylint: disable=invalid-name 

197 node: ClassDef, ctx: context.InferenceContext | None = None 

198) -> Iterator[ClassDef]: 

199 func_to_add = _extract_single_node("dict") 

200 node.locals["__call__"] = [func_to_add] 

201 return iter([node]) 

202 

203 

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

205 node: FunctionDef, ctx: context.InferenceContext | None = None 

206) -> Iterator[ClassDef]: 

207 """Replace TypedDict FunctionDef with ClassDef.""" 

208 class_def = ClassDef( 

209 name="TypedDict", 

210 lineno=node.lineno, 

211 col_offset=node.col_offset, 

212 parent=node.parent, 

213 end_lineno=node.end_lineno, 

214 end_col_offset=node.end_col_offset, 

215 ) 

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

217 func_to_add = _extract_single_node("dict") 

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

219 return iter([class_def]) 

220 

221 

222def _looks_like_typing_alias(node: Call) -> bool: 

223 """ 

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

225 

226 For example : 

227 

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

229 

230 :param node: call node 

231 """ 

232 return ( 

233 isinstance(node.func, Name) 

234 and node.func.name == "_alias" 

235 and ( 

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

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

238 ) 

239 ) 

240 

241 

242def _forbid_class_getitem_access(node: ClassDef) -> None: 

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

244 

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

246 """ 

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

248 Otherwise, just call origin_func. 

249 """ 

250 if attr == "__class_getitem__": 

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

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

253 

254 try: 

255 node.getattr("__class_getitem__") 

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

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

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

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

260 partial_raiser = partial(full_raiser, node.getattr) 

261 node.getattr = partial_raiser 

262 except AttributeInferenceError: 

263 pass 

264 

265 

266def infer_typing_alias( 

267 node: Call, ctx: context.InferenceContext | None = None 

268) -> Iterator[ClassDef]: 

269 """ 

270 Infers the call to _alias function 

271 Insert ClassDef, with same name as aliased class, 

272 in mro to simulate _GenericAlias. 

273 

274 :param node: call node 

275 :param context: inference context 

276 """ 

277 if ( 

278 not isinstance(node.parent, Assign) 

279 or not len(node.parent.targets) == 1 

280 or not isinstance(node.parent.targets[0], AssignName) 

281 ): 

282 raise UseInferenceDefault 

283 try: 

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

285 except StopIteration as e: 

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

287 

288 assign_name = node.parent.targets[0] 

289 

290 class_def = ClassDef( 

291 name=assign_name.name, 

292 lineno=assign_name.lineno, 

293 col_offset=assign_name.col_offset, 

294 parent=node.parent, 

295 end_lineno=assign_name.end_lineno, 

296 end_col_offset=assign_name.end_col_offset, 

297 ) 

298 if isinstance(res, ClassDef): 

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

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

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

302 

303 maybe_type_var = node.args[1] 

304 if ( 

305 not PY39_PLUS 

306 and not (isinstance(maybe_type_var, Tuple) and not maybe_type_var.elts) 

307 or PY39_PLUS 

308 and isinstance(maybe_type_var, Const) 

309 and maybe_type_var.value > 0 

310 ): 

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

312 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) 

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

314 else: 

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

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

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

318 _forbid_class_getitem_access(class_def) 

319 

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

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

322 return iter([class_def]) 

323 

324 

325def _looks_like_special_alias(node: Call) -> bool: 

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

327 

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

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

330 

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

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

333 

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

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

336 """ 

337 return isinstance(node.func, Name) and ( 

338 not PY39_PLUS 

339 and node.func.name == "_VariadicGenericAlias" 

340 and ( 

341 isinstance(node.args[0], Name) 

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

343 or isinstance(node.args[0], Attribute) 

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

345 ) 

346 or PY39_PLUS 

347 and ( 

348 node.func.name == "_TupleType" 

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

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

351 or node.func.name == "_CallableType" 

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

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

354 ) 

355 ) 

356 

357 

358def infer_special_alias( 

359 node: Call, ctx: context.InferenceContext | None = None 

360) -> Iterator[ClassDef]: 

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

362 if not ( 

363 isinstance(node.parent, Assign) 

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

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

366 ): 

367 raise UseInferenceDefault 

368 try: 

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

370 except StopIteration as e: 

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

372 

373 assign_name = node.parent.targets[0] 

374 class_def = ClassDef( 

375 name=assign_name.name, 

376 parent=node.parent, 

377 lineno=assign_name.lineno, 

378 col_offset=assign_name.col_offset, 

379 end_lineno=assign_name.end_lineno, 

380 end_col_offset=assign_name.end_col_offset, 

381 ) 

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

383 func_to_add = _extract_single_node(CLASS_GETITEM_TEMPLATE) 

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

385 return iter([class_def]) 

386 

387 

388def _looks_like_typing_cast(node: Call) -> bool: 

389 return isinstance(node, Call) and ( 

390 isinstance(node.func, Name) 

391 and node.func.name == "cast" 

392 or isinstance(node.func, Attribute) 

393 and node.func.attrname == "cast" 

394 ) 

395 

396 

397def infer_typing_cast( 

398 node: Call, ctx: context.InferenceContext | None = None 

399) -> Iterator[NodeNG]: 

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

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

402 raise UseInferenceDefault 

403 

404 try: 

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

406 except (InferenceError, StopIteration) as exc: 

407 raise UseInferenceDefault from exc 

408 if ( 

409 not isinstance(func, FunctionDef) 

410 or func.qname() != "typing.cast" 

411 or len(node.args) != 2 

412 ): 

413 raise UseInferenceDefault 

414 

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

416 

417 

418AstroidManager().register_transform( 

419 Call, 

420 inference_tip(infer_typing_typevar_or_newtype), 

421 looks_like_typing_typevar_or_newtype, 

422) 

423AstroidManager().register_transform( 

424 Subscript, inference_tip(infer_typing_attr), _looks_like_typing_subscript 

425) 

426AstroidManager().register_transform( 

427 Call, inference_tip(infer_typing_cast), _looks_like_typing_cast 

428) 

429 

430if PY39_PLUS: 

431 AstroidManager().register_transform( 

432 FunctionDef, inference_tip(infer_typedDict), _looks_like_typedDict 

433 ) 

434else: 

435 AstroidManager().register_transform( 

436 ClassDef, inference_tip(infer_old_typedDict), _looks_like_typedDict 

437 ) 

438 

439AstroidManager().register_transform( 

440 Call, inference_tip(infer_typing_alias), _looks_like_typing_alias 

441) 

442AstroidManager().register_transform( 

443 Call, inference_tip(infer_special_alias), _looks_like_special_alias 

444)