brain_namedtuple_enum.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  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. """Astroid hooks for the Python standard library."""
  5. from __future__ import annotations
  6. import functools
  7. import keyword
  8. from collections.abc import Iterator
  9. from textwrap import dedent
  10. from typing import Final
  11. from astroid import arguments, bases, nodes, util
  12. from astroid.builder import AstroidBuilder, _extract_single_node, extract_node
  13. from astroid.context import InferenceContext
  14. from astroid.exceptions import (
  15. AstroidTypeError,
  16. AstroidValueError,
  17. InferenceError,
  18. UseInferenceDefault,
  19. )
  20. from astroid.inference_tip import inference_tip
  21. from astroid.manager import AstroidManager
  22. from astroid.nodes.scoped_nodes.scoped_nodes import SYNTHETIC_ROOT
  23. ENUM_QNAME: Final[str] = "enum.Enum"
  24. TYPING_NAMEDTUPLE_QUALIFIED: Final = {
  25. "typing.NamedTuple",
  26. "typing_extensions.NamedTuple",
  27. }
  28. TYPING_NAMEDTUPLE_BASENAMES: Final = {
  29. "NamedTuple",
  30. "typing.NamedTuple",
  31. "typing_extensions.NamedTuple",
  32. }
  33. def _infer_first(node, context):
  34. if isinstance(node, util.UninferableBase):
  35. raise UseInferenceDefault
  36. try:
  37. value = next(node.infer(context=context))
  38. except StopIteration as exc:
  39. raise InferenceError from exc
  40. if isinstance(value, util.UninferableBase):
  41. raise UseInferenceDefault()
  42. return value
  43. def _find_func_form_arguments(node, context):
  44. def _extract_namedtuple_arg_or_keyword( # pylint: disable=inconsistent-return-statements
  45. position, key_name=None
  46. ):
  47. if len(args) > position:
  48. return _infer_first(args[position], context)
  49. if key_name and key_name in found_keywords:
  50. return _infer_first(found_keywords[key_name], context)
  51. args = node.args
  52. keywords = node.keywords
  53. found_keywords = (
  54. {keyword.arg: keyword.value for keyword in keywords} if keywords else {}
  55. )
  56. name = _extract_namedtuple_arg_or_keyword(position=0, key_name="typename")
  57. names = _extract_namedtuple_arg_or_keyword(position=1, key_name="field_names")
  58. if name and names:
  59. return name.value, names
  60. raise UseInferenceDefault()
  61. def infer_func_form(
  62. node: nodes.Call,
  63. base_type: nodes.NodeNG,
  64. *,
  65. parent: nodes.NodeNG,
  66. context: InferenceContext | None = None,
  67. enum: bool = False,
  68. ) -> tuple[nodes.ClassDef, str, list[str]]:
  69. """Specific inference function for namedtuple or Python 3 enum."""
  70. # node is a Call node, class name as first argument and generated class
  71. # attributes as second argument
  72. # namedtuple or enums list of attributes can be a list of strings or a
  73. # whitespace-separate string
  74. try:
  75. name, names = _find_func_form_arguments(node, context)
  76. try:
  77. attributes: list[str] = names.value.replace(",", " ").split()
  78. except AttributeError as exc:
  79. # Handle attributes of NamedTuples
  80. if not enum:
  81. attributes = []
  82. fields = _get_namedtuple_fields(node)
  83. if fields:
  84. fields_node = extract_node(fields)
  85. attributes = [
  86. _infer_first(const, context).value for const in fields_node.elts
  87. ]
  88. # Handle attributes of Enums
  89. else:
  90. # Enums supports either iterator of (name, value) pairs
  91. # or mappings.
  92. if hasattr(names, "items") and isinstance(names.items, list):
  93. attributes = [
  94. _infer_first(const[0], context).value
  95. for const in names.items
  96. if isinstance(const[0], nodes.Const)
  97. ]
  98. elif hasattr(names, "elts"):
  99. # Enums can support either ["a", "b", "c"]
  100. # or [("a", 1), ("b", 2), ...], but they can't
  101. # be mixed.
  102. if all(isinstance(const, nodes.Tuple) for const in names.elts):
  103. attributes = [
  104. _infer_first(const.elts[0], context).value
  105. for const in names.elts
  106. if isinstance(const, nodes.Tuple)
  107. ]
  108. else:
  109. attributes = [
  110. _infer_first(const, context).value for const in names.elts
  111. ]
  112. else:
  113. raise AttributeError from exc
  114. if not attributes:
  115. raise AttributeError from exc
  116. except (AttributeError, InferenceError) as exc:
  117. raise UseInferenceDefault from exc
  118. if not enum:
  119. # namedtuple maps sys.intern(str()) over over field_names
  120. attributes = [str(attr) for attr in attributes]
  121. # XXX this should succeed *unless* __str__/__repr__ is incorrect or throws
  122. # in which case we should not have inferred these values and raised earlier
  123. attributes = [attr for attr in attributes if " " not in attr]
  124. # If we can't infer the name of the class, don't crash, up to this point
  125. # we know it is a namedtuple anyway.
  126. name = name or "Uninferable"
  127. # we want to return a Class node instance with proper attributes set
  128. class_node = nodes.ClassDef(
  129. name,
  130. lineno=node.lineno,
  131. col_offset=node.col_offset,
  132. end_lineno=node.end_lineno,
  133. end_col_offset=node.end_col_offset,
  134. parent=parent,
  135. )
  136. class_node.postinit(
  137. bases=[base_type],
  138. body=[],
  139. decorators=None,
  140. )
  141. # XXX add __init__(*attributes) method
  142. for attr in attributes:
  143. fake_node = nodes.EmptyNode()
  144. fake_node.parent = class_node
  145. fake_node.attrname = attr
  146. class_node.instance_attrs[attr] = [fake_node]
  147. return class_node, name, attributes
  148. def _has_namedtuple_base(node):
  149. """Predicate for class inference tip.
  150. :type node: ClassDef
  151. :rtype: bool
  152. """
  153. return set(node.basenames) & TYPING_NAMEDTUPLE_BASENAMES
  154. def _looks_like(node, name) -> bool:
  155. func = node.func
  156. if isinstance(func, nodes.Attribute):
  157. return func.attrname == name
  158. if isinstance(func, nodes.Name):
  159. return func.name == name
  160. return False
  161. _looks_like_namedtuple = functools.partial(_looks_like, name="namedtuple")
  162. _looks_like_enum = functools.partial(_looks_like, name="Enum")
  163. _looks_like_typing_namedtuple = functools.partial(_looks_like, name="NamedTuple")
  164. def infer_named_tuple(
  165. node: nodes.Call, context: InferenceContext | None = None
  166. ) -> Iterator[nodes.ClassDef]:
  167. """Specific inference function for namedtuple Call node."""
  168. tuple_base: nodes.Name = _extract_single_node("tuple")
  169. class_node, name, attributes = infer_func_form(
  170. node, tuple_base, parent=SYNTHETIC_ROOT, context=context
  171. )
  172. call_site = arguments.CallSite.from_call(node, context=context)
  173. func = util.safe_infer(
  174. _extract_single_node("import collections; collections.namedtuple")
  175. )
  176. assert isinstance(func, nodes.NodeNG)
  177. try:
  178. rename_arg_bool_value = next(
  179. call_site.infer_argument(func, "rename", context or InferenceContext())
  180. ).bool_value()
  181. rename = rename_arg_bool_value is True
  182. except (InferenceError, StopIteration):
  183. rename = False
  184. try:
  185. attributes = _check_namedtuple_attributes(name, attributes, rename)
  186. except AstroidTypeError as exc:
  187. raise UseInferenceDefault("TypeError: " + str(exc)) from exc
  188. except AstroidValueError as exc:
  189. raise UseInferenceDefault("ValueError: " + str(exc)) from exc
  190. replace_args = ", ".join(f"{arg}=None" for arg in attributes)
  191. field_def = (
  192. " {name} = property(lambda self: self[{index:d}], "
  193. "doc='Alias for field number {index:d}')"
  194. )
  195. field_defs = "\n".join(
  196. field_def.format(name=name, index=index)
  197. for index, name in enumerate(attributes)
  198. )
  199. fake = AstroidBuilder(AstroidManager()).string_build(
  200. f"""
  201. class {name}(tuple):
  202. __slots__ = ()
  203. _fields = {attributes!r}
  204. def _asdict(self):
  205. return self.__dict__
  206. @classmethod
  207. def _make(cls, iterable, new=tuple.__new__, len=len):
  208. return new(cls, iterable)
  209. def _replace(self, {replace_args}):
  210. return self
  211. def __getnewargs__(self):
  212. return tuple(self)
  213. {field_defs}
  214. """
  215. )
  216. class_node.locals["_asdict"] = fake.body[0].locals["_asdict"]
  217. class_node.locals["_make"] = fake.body[0].locals["_make"]
  218. class_node.locals["_replace"] = fake.body[0].locals["_replace"]
  219. class_node.locals["_fields"] = fake.body[0].locals["_fields"]
  220. for attr in attributes:
  221. class_node.locals[attr] = fake.body[0].locals[attr]
  222. # we use UseInferenceDefault, we can't be a generator so return an iterator
  223. return iter([class_node])
  224. def _get_renamed_namedtuple_attributes(field_names):
  225. names = list(field_names)
  226. seen = set()
  227. for i, name in enumerate(field_names):
  228. # pylint: disable = too-many-boolean-expressions
  229. if (
  230. not all(c.isalnum() or c == "_" for c in name)
  231. or keyword.iskeyword(name)
  232. or not name
  233. or name[0].isdigit()
  234. or name.startswith("_")
  235. or name in seen
  236. ):
  237. names[i] = "_%d" % i
  238. seen.add(name)
  239. return tuple(names)
  240. def _check_namedtuple_attributes(typename, attributes, rename=False):
  241. attributes = tuple(attributes)
  242. if rename:
  243. attributes = _get_renamed_namedtuple_attributes(attributes)
  244. # The following snippet is derived from the CPython Lib/collections/__init__.py sources
  245. # <snippet>
  246. for name in (typename, *attributes):
  247. if not isinstance(name, str):
  248. raise AstroidTypeError(
  249. f"Type names and field names must be strings, not {type(name)!r}"
  250. )
  251. if not name.isidentifier():
  252. raise AstroidValueError(
  253. "Type names and field names must be valid" + f"identifiers: {name!r}"
  254. )
  255. if keyword.iskeyword(name):
  256. raise AstroidValueError(
  257. f"Type names and field names cannot be a keyword: {name!r}"
  258. )
  259. seen = set()
  260. for name in attributes:
  261. if name.startswith("_") and not rename:
  262. raise AstroidValueError(
  263. f"Field names cannot start with an underscore: {name!r}"
  264. )
  265. if name in seen:
  266. raise AstroidValueError(f"Encountered duplicate field name: {name!r}")
  267. seen.add(name)
  268. # </snippet>
  269. return attributes
  270. def infer_enum(
  271. node: nodes.Call, context: InferenceContext | None = None
  272. ) -> Iterator[bases.Instance]:
  273. """Specific inference function for enum Call node."""
  274. # Raise `UseInferenceDefault` if `node` is a call to a a user-defined Enum.
  275. try:
  276. inferred = node.func.infer(context)
  277. except (InferenceError, StopIteration) as exc:
  278. raise UseInferenceDefault from exc
  279. if not any(
  280. isinstance(item, nodes.ClassDef) and item.qname() == ENUM_QNAME
  281. for item in inferred
  282. ):
  283. raise UseInferenceDefault
  284. enum_meta = _extract_single_node(
  285. """
  286. class EnumMeta(object):
  287. 'docstring'
  288. def __call__(self, node):
  289. class EnumAttribute(object):
  290. name = ''
  291. value = 0
  292. return EnumAttribute()
  293. def __iter__(self):
  294. class EnumAttribute(object):
  295. name = ''
  296. value = 0
  297. return [EnumAttribute()]
  298. def __reversed__(self):
  299. class EnumAttribute(object):
  300. name = ''
  301. value = 0
  302. return (EnumAttribute, )
  303. def __next__(self):
  304. return next(iter(self))
  305. def __getitem__(self, attr):
  306. class Value(object):
  307. @property
  308. def name(self):
  309. return ''
  310. @property
  311. def value(self):
  312. return attr
  313. return Value()
  314. __members__ = ['']
  315. """
  316. )
  317. # FIXME arguably, the base here shouldn't be the EnumMeta class definition
  318. # itself, but a reference (Name) to it. Otherwise, the invariant that all
  319. # children of a node have that node as their parent is broken.
  320. class_node = infer_func_form(
  321. node,
  322. enum_meta,
  323. parent=SYNTHETIC_ROOT,
  324. context=context,
  325. enum=True,
  326. )[0]
  327. return iter([class_node.instantiate_class()])
  328. INT_FLAG_ADDITION_METHODS = """
  329. def __or__(self, other):
  330. return {name}(self.value | other.value)
  331. def __and__(self, other):
  332. return {name}(self.value & other.value)
  333. def __xor__(self, other):
  334. return {name}(self.value ^ other.value)
  335. def __add__(self, other):
  336. return {name}(self.value + other.value)
  337. def __div__(self, other):
  338. return {name}(self.value / other.value)
  339. def __invert__(self):
  340. return {name}(~self.value)
  341. def __mul__(self, other):
  342. return {name}(self.value * other.value)
  343. """
  344. def infer_enum_class(node: nodes.ClassDef) -> nodes.ClassDef:
  345. """Specific inference for enums."""
  346. for basename in (b for cls in node.mro() for b in cls.basenames):
  347. if node.root().name == "enum":
  348. # Skip if the class is directly from enum module.
  349. break
  350. dunder_members = {}
  351. target_names = set()
  352. for local, values in node.locals.items():
  353. if (
  354. any(not isinstance(value, nodes.AssignName) for value in values)
  355. or local == "_ignore_"
  356. ):
  357. continue
  358. stmt = values[0].statement()
  359. if isinstance(stmt, nodes.Assign):
  360. if isinstance(stmt.targets[0], nodes.Tuple):
  361. targets = stmt.targets[0].itered()
  362. else:
  363. targets = stmt.targets
  364. elif isinstance(stmt, nodes.AnnAssign):
  365. targets = [stmt.target]
  366. else:
  367. continue
  368. inferred_return_value = None
  369. if stmt.value is not None:
  370. if isinstance(stmt.value, nodes.Const):
  371. if isinstance(stmt.value.value, str):
  372. inferred_return_value = repr(stmt.value.value)
  373. else:
  374. inferred_return_value = stmt.value.value
  375. else:
  376. inferred_return_value = stmt.value.as_string()
  377. new_targets = []
  378. for target in targets:
  379. if isinstance(target, nodes.Starred):
  380. continue
  381. target_names.add(target.name)
  382. # Replace all the assignments with our mocked class.
  383. classdef = dedent(
  384. """
  385. class {name}({types}):
  386. @property
  387. def value(self):
  388. return {return_value}
  389. @property
  390. def _value_(self):
  391. return {return_value}
  392. @property
  393. def name(self):
  394. return "{name}"
  395. @property
  396. def _name_(self):
  397. return "{name}"
  398. """.format(
  399. name=target.name,
  400. types=", ".join(node.basenames),
  401. return_value=inferred_return_value,
  402. )
  403. )
  404. if "IntFlag" in basename:
  405. # Alright, we need to add some additional methods.
  406. # Unfortunately we still can't infer the resulting objects as
  407. # Enum members, but once we'll be able to do that, the following
  408. # should result in some nice symbolic execution
  409. classdef += INT_FLAG_ADDITION_METHODS.format(name=target.name)
  410. fake = AstroidBuilder(
  411. AstroidManager(), apply_transforms=False
  412. ).string_build(classdef)[target.name]
  413. fake.parent = target.parent
  414. for method in node.mymethods():
  415. fake.locals[method.name] = [method]
  416. new_targets.append(fake.instantiate_class())
  417. if stmt.value is None:
  418. continue
  419. dunder_members[local] = fake
  420. node.locals[local] = new_targets
  421. # The undocumented `_value2member_map_` member:
  422. node.locals["_value2member_map_"] = [
  423. nodes.Dict(
  424. parent=node,
  425. lineno=node.lineno,
  426. col_offset=node.col_offset,
  427. end_lineno=node.end_lineno,
  428. end_col_offset=node.end_col_offset,
  429. )
  430. ]
  431. members = nodes.Dict(
  432. parent=node,
  433. lineno=node.lineno,
  434. col_offset=node.col_offset,
  435. end_lineno=node.end_lineno,
  436. end_col_offset=node.end_col_offset,
  437. )
  438. members.postinit(
  439. [
  440. (
  441. nodes.Const(k, parent=members),
  442. nodes.Name(
  443. v.name,
  444. parent=members,
  445. lineno=v.lineno,
  446. col_offset=v.col_offset,
  447. end_lineno=v.end_lineno,
  448. end_col_offset=v.end_col_offset,
  449. ),
  450. )
  451. for k, v in dunder_members.items()
  452. ]
  453. )
  454. node.locals["__members__"] = [members]
  455. # The enum.Enum class itself defines two @DynamicClassAttribute data-descriptors
  456. # "name" and "value" (which we override in the mocked class for each enum member
  457. # above). When dealing with inference of an arbitrary instance of the enum
  458. # class, e.g. in a method defined in the class body like:
  459. # class SomeEnum(enum.Enum):
  460. # def method(self):
  461. # self.name # <- here
  462. # In the absence of an enum member called "name" or "value", these attributes
  463. # should resolve to the descriptor on that particular instance, i.e. enum member.
  464. # For "value", we have no idea what that should be, but for "name", we at least
  465. # know that it should be a string, so infer that as a guess.
  466. if "name" not in target_names:
  467. code = dedent(
  468. '''
  469. @property
  470. def name(self):
  471. """The name of the Enum member.
  472. This is a reconstruction by astroid: enums are too dynamic to understand, but we at least
  473. know 'name' should be a string, so this is astroid's best guess.
  474. """
  475. return ''
  476. '''
  477. )
  478. name_dynamicclassattr = AstroidBuilder(AstroidManager()).string_build(code)[
  479. "name"
  480. ]
  481. node.locals["name"] = [name_dynamicclassattr]
  482. break
  483. return node
  484. def infer_typing_namedtuple_class(class_node, context: InferenceContext | None = None):
  485. """Infer a subclass of typing.NamedTuple."""
  486. # Check if it has the corresponding bases
  487. annassigns_fields = [
  488. annassign.target.name
  489. for annassign in class_node.body
  490. if isinstance(annassign, nodes.AnnAssign)
  491. ]
  492. code = dedent(
  493. """
  494. from collections import namedtuple
  495. namedtuple({typename!r}, {fields!r})
  496. """
  497. ).format(typename=class_node.name, fields=",".join(annassigns_fields))
  498. node = extract_node(code)
  499. try:
  500. generated_class_node = next(infer_named_tuple(node, context))
  501. except StopIteration as e:
  502. raise InferenceError(node=node, context=context) from e
  503. for method in class_node.mymethods():
  504. generated_class_node.locals[method.name] = [method]
  505. for body_node in class_node.body:
  506. if isinstance(body_node, nodes.Assign):
  507. for target in body_node.targets:
  508. attr = target.name
  509. generated_class_node.locals[attr] = class_node.locals[attr]
  510. elif isinstance(body_node, nodes.ClassDef):
  511. generated_class_node.locals[body_node.name] = [body_node]
  512. return iter((generated_class_node,))
  513. def infer_typing_namedtuple_function(node, context: InferenceContext | None = None):
  514. """
  515. Starting with python3.9, NamedTuple is a function of the typing module.
  516. The class NamedTuple is build dynamically through a call to `type` during
  517. initialization of the `_NamedTuple` variable.
  518. """
  519. klass = extract_node(
  520. """
  521. from typing import _NamedTuple
  522. _NamedTuple
  523. """
  524. )
  525. return klass.infer(context)
  526. def infer_typing_namedtuple(
  527. node: nodes.Call, context: InferenceContext | None = None
  528. ) -> Iterator[nodes.ClassDef]:
  529. """Infer a typing.NamedTuple(...) call."""
  530. # This is essentially a namedtuple with different arguments
  531. # so we extract the args and infer a named tuple.
  532. try:
  533. func = next(node.func.infer())
  534. except (InferenceError, StopIteration) as exc:
  535. raise UseInferenceDefault from exc
  536. if func.qname() not in TYPING_NAMEDTUPLE_QUALIFIED:
  537. raise UseInferenceDefault
  538. if len(node.args) != 2:
  539. raise UseInferenceDefault
  540. if not isinstance(node.args[1], (nodes.List, nodes.Tuple)):
  541. raise UseInferenceDefault
  542. return infer_named_tuple(node, context)
  543. def _get_namedtuple_fields(node: nodes.Call) -> str:
  544. """Get and return fields of a NamedTuple in code-as-a-string.
  545. Because the fields are represented in their code form we can
  546. extract a node from them later on.
  547. """
  548. names = []
  549. container = None
  550. try:
  551. container = next(node.args[1].infer())
  552. except (InferenceError, StopIteration) as exc:
  553. raise UseInferenceDefault from exc
  554. # We pass on IndexError as we'll try to infer 'field_names' from the keywords
  555. except IndexError:
  556. pass
  557. if not container:
  558. for keyword_node in node.keywords:
  559. if keyword_node.arg == "field_names":
  560. try:
  561. container = next(keyword_node.value.infer())
  562. except (InferenceError, StopIteration) as exc:
  563. raise UseInferenceDefault from exc
  564. break
  565. if not isinstance(container, nodes.BaseContainer):
  566. raise UseInferenceDefault
  567. for elt in container.elts:
  568. if isinstance(elt, nodes.Const):
  569. names.append(elt.as_string())
  570. continue
  571. if not isinstance(elt, (nodes.List, nodes.Tuple)):
  572. raise UseInferenceDefault
  573. if len(elt.elts) != 2:
  574. raise UseInferenceDefault
  575. names.append(elt.elts[0].as_string())
  576. if names:
  577. field_names = f"({','.join(names)},)"
  578. else:
  579. field_names = ""
  580. return field_names
  581. def _is_enum_subclass(cls: nodes.ClassDef) -> bool:
  582. """Return whether cls is a subclass of an Enum."""
  583. return cls.is_subtype_of("enum.Enum")
  584. def register(manager: AstroidManager) -> None:
  585. manager.register_transform(
  586. nodes.Call, inference_tip(infer_named_tuple), _looks_like_namedtuple
  587. )
  588. manager.register_transform(nodes.Call, inference_tip(infer_enum), _looks_like_enum)
  589. manager.register_transform(
  590. nodes.ClassDef, infer_enum_class, predicate=_is_enum_subclass
  591. )
  592. manager.register_transform(
  593. nodes.ClassDef,
  594. inference_tip(infer_typing_namedtuple_class),
  595. _has_namedtuple_base,
  596. )
  597. manager.register_transform(
  598. nodes.FunctionDef,
  599. inference_tip(infer_typing_namedtuple_function),
  600. lambda node: node.name == "NamedTuple"
  601. and getattr(node.root(), "name", None) == "typing",
  602. )
  603. manager.register_transform(
  604. nodes.Call,
  605. inference_tip(infer_typing_namedtuple),
  606. _looks_like_typing_namedtuple,
  607. )