Skip to content

Commit e57ac42

Browse files
Infer instances of builtins created by __new__() (#1590)
1 parent c213532 commit e57ac42

File tree

3 files changed

+72
-12
lines changed

3 files changed

+72
-12
lines changed

ChangeLog

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Release date: TBA
4747

4848
Refs PyCQA/pylint#5113
4949

50+
* Instances of builtins created with ``__new__(cls, value)`` are now inferred.
51+
5052
* Infer the return value of the ``.copy()`` method on ``dict``, ``list``, ``set``,
5153
and ``frozenset``.
5254

astroid/bases.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
"""This module contains base classes and functions for the nodes and some
66
inference utils.
77
"""
8+
from __future__ import annotations
89

910
import collections
11+
import collections.abc
12+
from typing import TYPE_CHECKING
1013

1114
from astroid import decorators
1215
from astroid.const import PY310_PLUS
@@ -28,6 +31,9 @@
2831
helpers = lazy_import("helpers")
2932
manager = lazy_import("manager")
3033

34+
if TYPE_CHECKING:
35+
from astroid import nodes
36+
3137

3238
# TODO: check if needs special treatment
3339
BOOL_SPECIAL_METHOD = "__bool__"
@@ -372,25 +378,50 @@ def infer_call_result(self, caller, context):
372378
on ``object.__new__`` will be of type ``object``,
373379
which is incorrect for the argument in general.
374380
If no context is given the ``object.__new__`` call argument will
375-
correctly inferred except when inside a call that requires
381+
be correctly inferred except when inside a call that requires
376382
the additional context (such as a classmethod) of the boundnode
377383
to determine which class the method was called from
378384
"""
379385

380-
# If we're unbound method __new__ of builtin object, the result is an
386+
# If we're unbound method __new__ of a builtin, the result is an
381387
# instance of the class given as first argument.
382-
if (
383-
self._proxied.name == "__new__"
384-
and self._proxied.parent.frame(future=True).qname() == "builtins.object"
385-
):
386-
if caller.args:
387-
node_context = context.extra_context.get(caller.args[0])
388-
infer = caller.args[0].infer(context=node_context)
389-
else:
390-
infer = []
391-
return (Instance(x) if x is not Uninferable else x for x in infer)
388+
if self._proxied.name == "__new__":
389+
qname = self._proxied.parent.frame(future=True).qname()
390+
# Avoid checking builtins.type: _infer_type_new_call() does more validation
391+
if qname.startswith("builtins.") and qname != "builtins.type":
392+
return self._infer_builtin_new(caller, context)
392393
return self._proxied.infer_call_result(caller, context)
393394

395+
def _infer_builtin_new(
396+
self,
397+
caller: nodes.Call,
398+
context: InferenceContext,
399+
) -> collections.abc.Generator[
400+
nodes.Const | Instance | type[Uninferable], None, None
401+
]:
402+
# pylint: disable-next=import-outside-toplevel; circular import
403+
from astroid import nodes
404+
405+
if not caller.args:
406+
return
407+
# Attempt to create a constant
408+
if len(caller.args) > 1:
409+
value = None
410+
if isinstance(caller.args[1], nodes.Const):
411+
value = caller.args[1].value
412+
else:
413+
inferred_arg = next(caller.args[1].infer(), None)
414+
if isinstance(inferred_arg, nodes.Const):
415+
value = inferred_arg.value
416+
if value is not None:
417+
yield nodes.const_factory(value)
418+
return
419+
420+
node_context = context.extra_context.get(caller.args[0])
421+
infer = caller.args[0].infer(context=node_context)
422+
423+
yield from (Instance(x) if x is not Uninferable else x for x in infer)
424+
394425
def bool_value(self, context=None):
395426
return True
396427

tests/unittest_inference.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3768,6 +3768,33 @@ class MetaBook(type):
37683768
]
37693769
self.assertEqual(titles, ["Catch 22", "Ubik", "Grimus"])
37703770

3771+
@staticmethod
3772+
def test_builtin_new() -> None:
3773+
ast_node = extract_node("int.__new__(int, 42)")
3774+
inferred = next(ast_node.infer())
3775+
assert isinstance(inferred, nodes.Const)
3776+
assert inferred.value == 42
3777+
3778+
ast_node2 = extract_node("int.__new__(int)")
3779+
inferred2 = next(ast_node2.infer())
3780+
assert isinstance(inferred2, Instance)
3781+
assert not isinstance(inferred2, nodes.Const)
3782+
assert inferred2._proxied is inferred._proxied
3783+
3784+
ast_node3 = extract_node(
3785+
"""
3786+
x = 43
3787+
int.__new__(int, x) #@
3788+
"""
3789+
)
3790+
inferred3 = next(ast_node3.infer())
3791+
assert isinstance(inferred3, nodes.Const)
3792+
assert inferred3.value == 43
3793+
3794+
ast_node4 = extract_node("int.__new__()")
3795+
with pytest.raises(InferenceError):
3796+
next(ast_node4.infer())
3797+
37713798
@pytest.mark.xfail(reason="Does not support function metaclasses")
37723799
def test_function_metaclasses(self):
37733800
# These are not supported right now, although

0 commit comments

Comments
 (0)