diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000000..bbe6fc2fd0 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1 @@ +Coordinated Disclosure Plan: https://tidelift.com/security diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index f49a98cddb..8b3b2453c9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -333,8 +333,7 @@ jobs: id: generate-python-key run: >- echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('setup.cfg', 'requirements_test_min.txt', - 'requirements_test_brain.txt', 'requirements_test_pre_commit.txt') }}" + hashFiles('setup.cfg', 'requirements_test_min.txt') }}" - name: Restore Python virtual environment id: cache-venv uses: actions/cache@v2.1.4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b8bf9eff0..2eba5c4b7f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,31 +22,31 @@ repos: hooks: - id: black-disable-checker - repo: https://github.com/asottile/pyupgrade - rev: v2.13.0 + rev: v2.18.2 hooks: - id: pyupgrade exclude: tests/testdata args: [--py36-plus] - - repo: https://github.com/ambv/black - rev: 20.8b1 + - repo: https://github.com/psf/black + rev: 21.5b1 hooks: - id: black args: [--safe, --quiet] exclude: tests/testdata|doc/ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: trailing-whitespace exclude: .github/|tests/testdata - id: end-of-file-fixer exclude: tests/testdata - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.2.1 + rev: v2.3.0 hooks: - id: prettier args: [--prose-wrap=always, --print-width=88] - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.0 + - repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [flake8-bugbear] diff --git a/ChangeLog b/ChangeLog index 25728101eb..26f665b4ac 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,54 @@ Release Date: TBA * Astroid now support python 3.10. +* Fix detection of relative imports. + Closes #930 + Closes PyCQA/pylint#4186 + +* Fix inference of instance attributes defined in base classes + + Closes #932 + +* Do not set instance attributes on builtin object() + + Closes #945 + Closes PyCQA/pylint#4232 + Closes PyCQA/pylint#4221 + Closes PyCQA/pylint#3970 + Closes PyCQA/pylint#3595 + +* Fix some spurious cycles detected in ``context.path`` leading to more cases + that can now be inferred + + Closes #926 + +* Add ``kind`` field to ``Const`` nodes, matching the structure of the built-in ast Const. + The kind field is "u" if the literal is a u-prefixed string, and ``None`` otherwise. + + Closes #898 + +* Fix property inference in class contexts for properties defined on the metaclass + + Closes #940 + +* Update enum brain to fix definition of __members__ for subclass-defined Enums + + Closes PyCQA/pylint#3535 + Closes PyCQA/pylint#4358 + +* Update random brain to fix a crash with inference of some sequence elements + + Closes #922 + +* Fix inference of attributes defined in a base class that is an inner class + + Closes #904 + +* Allow inferring a return value of None for non-abstract empty functions and + functions with no return statements (implicitly returning None) + + Closes #485 + What's New in astroid 2.5.6? ============================ Release Date: 2021-04-25 diff --git a/astroid/as_string.py b/astroid/as_string.py index 18aeb75750..884eb527b7 100644 --- a/astroid/as_string.py +++ b/astroid/as_string.py @@ -184,10 +184,8 @@ def visit_classdef(self, node): def visit_compare(self, node): """return an astroid.Compare node as string""" rhs_str = " ".join( - [ - f"{op} {self._precedence_parens(node, expr, is_left=False)}" - for op, expr in node.ops - ] + f"{op} {self._precedence_parens(node, expr, is_left=False)}" + for op, expr in node.ops ) return f"{self._precedence_parens(node, node.left)} {rhs_str}" @@ -580,7 +578,7 @@ def visit_yield(self, node): return f"({expr})" def visit_yieldfrom(self, node): - """ Return an astroid.YieldFrom node as string. """ + """Return an astroid.YieldFrom node as string.""" yi_val = (" " + node.value.accept(self)) if node.value else "" expr = "yield from" + yi_val if node.parent.is_statement: diff --git a/astroid/bases.py b/astroid/bases.py index 20b9935829..97b10366bf 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -207,8 +207,9 @@ def igetattr(self, name, context=None): if not context: context = contextmod.InferenceContext() try: + context.lookupname = name # avoid recursively inferring the same attr on the same class - if context.push((self._proxied, name)): + if context.push(self._proxied): raise exceptions.InferenceError( message="Cannot infer the same attribute again", node=self, @@ -328,6 +329,12 @@ def getitem(self, index, context=None): raise exceptions.InferenceError( "Could not find __getitem__ for {node!r}.", node=self, context=context ) + if len(method.args.arguments) != 2: # (self, index) + raise exceptions.AstroidTypeError( + "__getitem__ for {node!r} does not have correct signature", + node=self, + context=context, + ) return next(method.infer_call_result(self, new_context)) diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index aa43250535..9a9cc98981 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -85,7 +85,7 @@ def _extract_namedtuple_arg_or_keyword( # pylint: disable=inconsistent-return-s def infer_func_form(node, base_type, context=None, enum=False): - """Specific inference function for namedtuple or Python 3 enum. """ + """Specific inference function for namedtuple or Python 3 enum.""" # node is a Call node, class name as first argument and generated class # attributes as second argument @@ -248,7 +248,7 @@ def _get_renamed_namedtuple_attributes(field_names): def infer_enum(node, context=None): - """ Specific inference function for enum Call node. """ + """Specific inference function for enum Call node.""" enum_meta = extract_node( """ class EnumMeta(object): @@ -306,7 +306,7 @@ def __mul__(self, other): def infer_enum_class(node): - """ Specific inference for enums. """ + """Specific inference for enums.""" for basename in node.basenames: # TODO: doesn't handle subclasses yet. This implementation # is a hack to support enums. @@ -315,6 +315,7 @@ def infer_enum_class(node): if node.root().name == "enum": # Skip if the class is directly from enum module. break + dunder_members = {} for local, values in node.locals.items(): if any(not isinstance(value, nodes.AssignName) for value in values): continue @@ -372,7 +373,16 @@ def name(self): for method in node.mymethods(): fake.locals[method.name] = [method] new_targets.append(fake.instantiate_class()) + dunder_members[local] = fake node.locals[local] = new_targets + members = nodes.Dict(parent=node) + members.postinit( + [ + (nodes.Const(k, parent=members), nodes.Name(v.name, parent=members)) + for k, v in dunder_members.items() + ] + ) + node.locals["__members__"] = [members] break return node diff --git a/astroid/brain/brain_random.py b/astroid/brain/brain_random.py index ee5506cbae..6efd1ff134 100644 --- a/astroid/brain/brain_random.py +++ b/astroid/brain/brain_random.py @@ -9,6 +9,8 @@ def _clone_node_with_lineno(node, parent, lineno): + if isinstance(node, astroid.EvaluatedObject): + node = node.original cls = node.__class__ other_fields = node._other_fields _astroid_fields = node._astroid_fields diff --git a/astroid/brain/brain_type.py b/astroid/brain/brain_type.py index ec4cf2a46d..c6fc382b02 100644 --- a/astroid/brain/brain_type.py +++ b/astroid/brain/brain_type.py @@ -17,7 +17,7 @@ """ import sys -from astroid import MANAGER, extract_node, inference_tip, nodes +from astroid import MANAGER, UseInferenceDefault, extract_node, inference_tip, nodes PY39 = sys.version_info >= (3, 9) @@ -47,6 +47,9 @@ def infer_type_sub(node, context=None): :return: the inferred node :rtype: nodes.NodeNG """ + node_scope, _ = node.scope().lookup("type") + if node_scope.qname() != "builtins": + raise UseInferenceDefault() class_src = """ class type: def __class_getitem__(cls, key): diff --git a/astroid/builder.py b/astroid/builder.py index 6a7f79ced0..4a066ee836 100644 --- a/astroid/builder.py +++ b/astroid/builder.py @@ -67,7 +67,7 @@ def _can_assign_attr(node, attrname): else: if slots and attrname not in {slot.value for slot in slots}: return False - return True + return node.qname() != "builtins.object" class AstroidBuilder(raw_building.InspectBuilder): diff --git a/astroid/context.py b/astroid/context.py index 18220ec228..d7bf81bf17 100644 --- a/astroid/context.py +++ b/astroid/context.py @@ -102,7 +102,7 @@ def clone(self): starts with the same context but diverge as each side is inferred so the InferenceContext will need be cloned""" # XXX copy lookupname/callcontext ? - clone = InferenceContext(self.path, inferred=self.inferred) + clone = InferenceContext(self.path.copy(), inferred=self.inferred.copy()) clone.callcontext = self.callcontext clone.boundnode = self.boundnode clone.extra_context = self.extra_context diff --git a/astroid/inference.py b/astroid/inference.py index 20c986478e..dd9a565aec 100644 --- a/astroid/inference.py +++ b/astroid/inference.py @@ -309,6 +309,7 @@ def infer_attribute(self, context=None): elif not context: context = contextmod.InferenceContext() + old_boundnode = context.boundnode try: context.boundnode = owner yield from owner.igetattr(self.attrname, context) @@ -319,7 +320,7 @@ def infer_attribute(self, context=None): ): pass finally: - context.boundnode = None + context.boundnode = old_boundnode return dict(node=self, context=context) diff --git a/astroid/interpreter/_import/spec.py b/astroid/interpreter/_import/spec.py index 7800af8765..0a3db54964 100644 --- a/astroid/interpreter/_import/spec.py +++ b/astroid/interpreter/_import/spec.py @@ -292,15 +292,13 @@ def _precache_zipimporters(path=None): new_paths = _cached_set_diff(req_paths, cached_paths) for entry_path in new_paths: try: - pic[entry_path] = zipimport.zipimporter( # pylint: disable=no-member - entry_path - ) - except zipimport.ZipImportError: # pylint: disable=no-member + pic[entry_path] = zipimport.zipimporter(entry_path) + except zipimport.ZipImportError: continue return { key: value for key, value in pic.items() - if isinstance(value, zipimport.zipimporter) # pylint: disable=no-member + if isinstance(value, zipimport.zipimporter) } diff --git a/astroid/manager.py b/astroid/manager.py index 6903e0ab8e..6525d1badd 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -103,7 +103,7 @@ def ast_from_file(self, filepath, modname=None, fallback=True, source=False): ) def ast_from_string(self, data, modname="", filepath=None): - """ Given some source code as a string, return its corresponding astroid object""" + """Given some source code as a string, return its corresponding astroid object""" # pylint: disable=import-outside-toplevel; circular import from astroid.builder import AstroidBuilder @@ -213,9 +213,7 @@ def zip_import_data(self, filepath): except ValueError: continue try: - importer = zipimport.zipimporter( # pylint: disable=no-member - eggpath + ext - ) + importer = zipimport.zipimporter(eggpath + ext) zmodname = resource.replace(os.path.sep, ".") if importer.is_package(resource): zmodname = zmodname + ".__init__" diff --git a/astroid/mixins.py b/astroid/mixins.py index 7ad8775beb..c7a538d002 100644 --- a/astroid/mixins.py +++ b/astroid/mixins.py @@ -20,7 +20,7 @@ class BlockRangeMixIn: - """override block range """ + """override block range""" @decorators.cachedproperty def blockstart_tolineno(self): diff --git a/astroid/modutils.py b/astroid/modutils.py index 4a4798ada2..a71f2745e7 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -18,6 +18,7 @@ # Copyright (c) 2020 hippo91 # Copyright (c) 2020 Peter Kolbus # Copyright (c) 2021 Pierre Sassoulas +# Copyright (c) 2021 Andreas Finkler # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html # For details: https://github.com/PyCQA/astroid/blob/master/LICENSE @@ -37,6 +38,8 @@ # We disable the import-error so pylint can work without distutils installed. # pylint: disable=no-name-in-module,useless-suppression +import importlib +import importlib.machinery import importlib.util import itertools import os @@ -574,21 +577,11 @@ def is_relative(modname, from_file): from_file = os.path.dirname(from_file) if from_file in sys.path: return False - name = os.path.basename(from_file) - file_path = os.path.dirname(from_file) - parent_spec = importlib.util.find_spec(name, from_file) - while parent_spec is None and len(file_path) > 0: - name = os.path.basename(file_path) + "." + name - file_path = os.path.dirname(file_path) - parent_spec = importlib.util.find_spec(name, from_file) - - if parent_spec is None: - return False - - submodule_spec = importlib.util.find_spec( - name + "." + modname.split(".")[0], parent_spec.submodule_search_locations + return bool( + importlib.machinery.PathFinder.find_spec( + modname.split(".", maxsplit=1)[0], [from_file] + ) ) - return submodule_spec is not None # internal only functions ##################################################### diff --git a/astroid/node_classes.py b/astroid/node_classes.py index 7faf681275..f8d0b23c52 100644 --- a/astroid/node_classes.py +++ b/astroid/node_classes.py @@ -2557,7 +2557,7 @@ class Const(mixins.NoChildrenMixin, NodeNG, bases.Instance): _other_fields = ("value",) - def __init__(self, value, lineno=None, col_offset=None, parent=None): + def __init__(self, value, lineno=None, col_offset=None, parent=None, kind=None): """ :param value: The value that the constant represents. :type value: object @@ -2571,12 +2571,12 @@ def __init__(self, value, lineno=None, col_offset=None, parent=None): :param parent: The parent node in the syntax tree. :type parent: NodeNG or None - """ - self.value = value - """The value that the constant represents. - :type: object + :param kind: The string prefix. "u" for u-prefixed strings and ``None`` otherwise. Python 3.8+ only. + :type kind: str or None """ + self.value = value + self.kind = kind super().__init__(lineno, col_offset, parent) diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index 36995ec1d8..2532b61d1f 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -425,6 +425,7 @@ def visit_const(self, node, parent): getattr(node, "lineno", None), getattr(node, "col_offset", None), parent, + getattr(node, "kind", None), ) def visit_continue(self, node, parent): @@ -550,7 +551,7 @@ def visit_exec(self, node, parent): ) return newnode - # Not used in Python 3.8+. + # Not used in Python 3.9+. def visit_extslice(self, node, parent): """visit an ExtSlice node by returning a fresh instance of it""" newnode = nodes.ExtSlice(parent=parent) @@ -727,7 +728,7 @@ def visit_namedexpr(self, node, parent): ) return newnode - # Not used in Python 3.8+. + # Not used in Python 3.9+. def visit_index(self, node, parent): """visit a Index node by returning a fresh instance of it""" newnode = nodes.Index(parent=parent) @@ -814,6 +815,7 @@ def visit_constant(self, node, parent): getattr(node, "lineno", None), getattr(node, "col_offset", None), parent, + getattr(node, "kind", None), ) # Not used in Python 3.8+. diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py index d7717e64e6..cfc64392a0 100644 --- a/astroid/scoped_nodes.py +++ b/astroid/scoped_nodes.py @@ -1661,11 +1661,12 @@ def is_bound(self): """ return self.type == "classmethod" - def is_abstract(self, pass_is_abstract=True): + def is_abstract(self, pass_is_abstract=True, any_raise_is_abstract=False): """Check if the method is abstract. A method is considered abstract if any of the following is true: * The only statement is 'raise NotImplementedError' + * The only statement is 'raise ' and any_raise_is_abstract is True * The only statement is 'pass' and pass_is_abstract is True * The method is annotated with abc.astractproperty/abc.abstractmethod @@ -1686,6 +1687,8 @@ def is_abstract(self, pass_is_abstract=True): for child_node in self.body: if isinstance(child_node, node_classes.Raise): + if any_raise_is_abstract: + return True if child_node.raises_not_implemented(): return True return pass_is_abstract and isinstance(child_node, node_classes.Pass) @@ -1744,8 +1747,11 @@ def infer_call_result(self, caller=None, context=None): first_return = next(returns, None) if not first_return: - if self.body and isinstance(self.body[-1], node_classes.Assert): - yield node_classes.Const(None) + if self.body: + if self.is_abstract(pass_is_abstract=True, any_raise_is_abstract=True): + yield util.Uninferable + else: + yield node_classes.Const(None) return raise exceptions.InferenceError( @@ -2554,7 +2560,7 @@ def igetattr(self, name, context=None, class_context=True): context = contextmod.copy_context(context) context.lookupname = name - metaclass = self.declared_metaclass(context=context) + metaclass = self.metaclass(context=context) try: attributes = self.getattr(name, context, class_context=class_context) # If we have more than one attribute, make sure that those starting from @@ -2587,9 +2593,12 @@ def igetattr(self, name, context=None, class_context=True): yield from function.infer_call_result( caller=self, context=context ) - # If we have a metaclass, we're accessing this attribute through - # the class itself, which means we can solve the property - elif metaclass: + # If we're in a class context, we need to determine if the property + # was defined in the metaclass (a derived class must be a subclass of + # the metaclass of all its bases), in which case we can resolve the + # property. If not, i.e. the property is defined in some base class + # instead, then we return the property object + elif metaclass and function.parent.scope() is metaclass: # Resolve a property as long as it is not accessed through # the class itself. yield from function.infer_call_result( @@ -2796,7 +2805,7 @@ def has_metaclass_hack(self): return self._metaclass_hack def _islots(self): - """ Return an iterator with the inferred slots. """ + """Return an iterator with the inferred slots.""" if "__slots__" not in self.locals: return None for slots in self.igetattr("__slots__"): diff --git a/requirements_test_brain.txt b/requirements_test_brain.txt index deca747370..bdcf86077c 100644 --- a/requirements_test_brain.txt +++ b/requirements_test_brain.txt @@ -1,5 +1,8 @@ attrs nose -numpy +# Don't test numpy with py310 +# Until a wheel is uploaded to pypi, this would require +# additional dependencies to build it from source +numpy; python_version < "3.10" python-dateutil six diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 0033a0aa24..af9b9cbed9 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,6 +1,7 @@ autoflake==1.4 -black==20.8b1 -pyupgrade==2.13.0 +black==21.5b1 +pyupgrade==2.16.0 black-disable-checker==1.0.1 -pylint==2.7.4 +pylint==2.8.2 isort==5.8.0 +flake8==3.9.2 diff --git a/tests/unittest_brain.py b/tests/unittest_brain.py index 0e8c591198..26fad2e3bf 100644 --- a/tests/unittest_brain.py +++ b/tests/unittest_brain.py @@ -1858,6 +1858,18 @@ def test_inferred_successfully(self): elems = sorted(elem.value for elem in inferred.elts) self.assertEqual(elems, [1, 2]) + def test_no_crash_on_evaluatedobject(self): + node = astroid.extract_node( + """ + from random import sample + class A: pass + sample(list({1: A()}.values()), 1)""" + ) + inferred = next(node.infer()) + assert isinstance(inferred, astroid.List) + assert len(inferred.elts) == 1 + assert isinstance(inferred.elts[0], nodes.Call) + class SubprocessTest(unittest.TestCase): """Test subprocess brain""" diff --git a/tests/unittest_brain_numpy_core_umath.py b/tests/unittest_brain_numpy_core_umath.py index 12c1e3bbbc..5559bdd581 100644 --- a/tests/unittest_brain_numpy_core_umath.py +++ b/tests/unittest_brain_numpy_core_umath.py @@ -14,7 +14,7 @@ except ImportError: HAS_NUMPY = False -from astroid import bases, builder, nodes, util +from astroid import bases, builder, nodes @unittest.skipUnless(HAS_NUMPY, "This test requires the numpy library.") @@ -220,9 +220,7 @@ def test_numpy_core_umath_functions_return_type(self): with self.subTest(typ=func_): inferred_values = list(self._inferred_numpy_func_call(func_)) self.assertTrue( - len(inferred_values) == 1 - or len(inferred_values) == 2 - and inferred_values[-1].pytype() is util.Uninferable, + len(inferred_values) == 1, msg="Too much inferred values ({}) for {:s}".format( inferred_values[-1].pytype(), func_ ), diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py index a48d341999..5159978d9a 100644 --- a/tests/unittest_builder.py +++ b/tests/unittest_builder.py @@ -28,7 +28,7 @@ import pytest -from astroid import builder, exceptions, manager, nodes, test_utils, util +from astroid import Instance, builder, exceptions, manager, nodes, test_utils, util from . import resources @@ -476,6 +476,53 @@ def A_assign_type(self): self.assertIn("assign_type", lclass.locals) self.assertIn("type", lclass.locals) + def test_infer_can_assign_regular_object(self): + mod = builder.parse( + """ + class A: + pass + a = A() + a.value = "is set" + a.other = "is set" + """ + ) + obj = list(mod.igetattr("a")) + self.assertEqual(len(obj), 1) + obj = obj[0] + self.assertIsInstance(obj, Instance) + self.assertIn("value", obj.instance_attrs) + self.assertIn("other", obj.instance_attrs) + + def test_infer_can_assign_has_slots(self): + mod = builder.parse( + """ + class A: + __slots__ = ('value',) + a = A() + a.value = "is set" + a.other = "not set" + """ + ) + obj = list(mod.igetattr("a")) + self.assertEqual(len(obj), 1) + obj = obj[0] + self.assertIsInstance(obj, Instance) + self.assertIn("value", obj.instance_attrs) + self.assertNotIn("other", obj.instance_attrs) + + def test_infer_can_assign_no_classdict(self): + mod = builder.parse( + """ + a = object() + a.value = "not set" + """ + ) + obj = list(mod.igetattr("a")) + self.assertEqual(len(obj), 1) + obj = obj[0] + self.assertIsInstance(obj, Instance) + self.assertNotIn("value", obj.instance_attrs) + def test_augassign_attr(self): builder.parse( """ @@ -525,7 +572,7 @@ def func(): return 'None' """ astroid = builder.parse(code) - none, nothing, chain = [ret.value for ret in astroid.body[0].body] + none, nothing, chain = (ret.value for ret in astroid.body[0].body) self.assertIsInstance(none, nodes.Const) self.assertIsNone(none.value) self.assertIsNone(nothing) diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py index fb361f35f4..2e88891637 100644 --- a/tests/unittest_inference.py +++ b/tests/unittest_inference.py @@ -706,14 +706,6 @@ class InvalidGetitem2(object): NoGetitem()[4] #@ InvalidGetitem()[5] #@ InvalidGetitem2()[10] #@ - """ - ) - for node in ast_nodes[:3]: - self.assertRaises(InferenceError, next, node.infer()) - for node in ast_nodes[3:]: - self.assertEqual(next(node.infer()), util.Uninferable) - ast_nodes = extract_node( - """ [1, 2, 3][None] #@ 'lala'['bala'] #@ """ @@ -1229,7 +1221,6 @@ def __init__(self): self.assertEqual(len(foo_class.instance_attrs["attr"]), 1) self.assertEqual(bar_class.instance_attrs, {"attr": [assattr]}) - @pytest.mark.xfail(reason="Relying on path copy") def test_nonregr_multi_referential_addition(self): """Regression test for https://github.com/PyCQA/astroid/issues/483 Make sure issue where referring to the same variable @@ -1243,7 +1234,6 @@ def test_nonregr_multi_referential_addition(self): variable_a = extract_node(code) self.assertEqual(variable_a.inferred()[0].value, 2) - @pytest.mark.xfail(reason="Relying on path copy") def test_nonregr_layed_dictunpack(self): """Regression test for https://github.com/PyCQA/astroid/issues/483 Make sure multiple dictunpack references are inferable @@ -1704,7 +1694,8 @@ def __init__(self): """ ast = extract_node(code, __name__) expr = ast.func.expr - self.assertIs(next(expr.infer()), util.Uninferable) + with pytest.raises(exceptions.InferenceError): + next(expr.infer()) def test_tuple_builtin_inference(self): code = """ @@ -2328,7 +2319,6 @@ def no_yield_mgr(): self.assertRaises(InferenceError, next, module["other_decorators"].infer()) self.assertRaises(InferenceError, next, module["no_yield"].infer()) - @pytest.mark.xfail(reason="Relying on path copy") def test_nested_contextmanager(self): """Make sure contextmanager works with nested functions @@ -3896,6 +3886,65 @@ class Clazz(metaclass=_Meta): ).inferred()[0] assert isinstance(cls, nodes.ClassDef) and cls.name == "Clazz" + def test_infer_subclass_attr_outer_class(self): + node = extract_node( + """ + class Outer: + data = 123 + + class Test(Outer): + pass + Test.data + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value == 123 + + def test_infer_subclass_attr_inner_class_works_indirectly(self): + node = extract_node( + """ + class Outer: + class Inner: + data = 123 + Inner = Outer.Inner + + class Test(Inner): + pass + Test.data + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value == 123 + + def test_infer_subclass_attr_inner_class(self): + clsdef_node, attr_node = extract_node( + """ + class Outer: + class Inner: + data = 123 + + class Test(Outer.Inner): + pass + Test #@ + Test.data #@ + """ + ) + clsdef = next(clsdef_node.infer()) + assert isinstance(clsdef, nodes.ClassDef) + inferred = next(clsdef.igetattr("data")) + assert isinstance(inferred, nodes.Const) + assert inferred.value == 123 + # Inferring the value of .data via igetattr() worked before the + # old_boundnode fixes in infer_subscript, so it should have been + # possible to infer the subscript directly. It is the difference + # between these two cases that led to the discovery of the cause of the + # bug in https://github.com/PyCQA/astroid/issues/904 + inferred = next(attr_node.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value == 123 + def test_delayed_attributes_without_slots(self): ast_node = extract_node( """ @@ -3986,6 +4035,106 @@ def test(): with self.assertRaises(exceptions.AstroidTypeError): inferred.getitem(nodes.Const("4")) + def test_infer_arg_called_type_is_uninferable(self): + node = extract_node( + """ + def func(type): + type #@ + """ + ) + inferred = next(node.infer()) + assert inferred is util.Uninferable + + def test_infer_arg_called_object_when_used_as_index_is_uninferable(self): + node = extract_node( + """ + def func(object): + ['list'][ + object #@ + ] + """ + ) + inferred = next(node.infer()) + assert inferred is util.Uninferable + + @test_utils.require_version(minver="3.9") + def test_infer_arg_called_type_when_used_as_index_is_uninferable(self): + # https://github.com/PyCQA/astroid/pull/958 + node = extract_node( + """ + def func(type): + ['list'][ + type #@ + ] + """ + ) + inferred = next(node.infer()) + assert not isinstance(inferred, nodes.ClassDef) # was inferred as builtins.type + assert inferred is util.Uninferable + + @test_utils.require_version(minver="3.9") + def test_infer_arg_called_type_when_used_as_subscript_is_uninferable(self): + # https://github.com/PyCQA/astroid/pull/958 + node = extract_node( + """ + def func(type): + type[0] #@ + """ + ) + inferred = next(node.infer()) + assert not isinstance(inferred, nodes.ClassDef) # was inferred as builtins.type + assert inferred is util.Uninferable + + @test_utils.require_version(minver="3.9") + def test_infer_arg_called_type_defined_in_outer_scope_is_uninferable(self): + # https://github.com/PyCQA/astroid/pull/958 + node = extract_node( + """ + def outer(type): + def inner(): + type[0] #@ + """ + ) + inferred = next(node.infer()) + assert not isinstance(inferred, nodes.ClassDef) # was inferred as builtins.type + assert inferred is util.Uninferable + + def test_infer_subclass_attr_instance_attr_indirect(self): + node = extract_node( + """ + class Parent: + def __init__(self): + self.data = 123 + + class Test(Parent): + pass + t = Test() + t + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, Instance) + const = next(inferred.igetattr("data")) + assert isinstance(const, nodes.Const) + assert const.value == 123 + + def test_infer_subclass_attr_instance_attr(self): + node = extract_node( + """ + class Parent: + def __init__(self): + self.data = 123 + + class Test(Parent): + pass + t = Test() + t.data + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value == 123 + class GetattrTest(unittest.TestCase): def test_yes_when_unknown(self): @@ -4789,7 +4938,6 @@ class instance(object): self.assertIsInstance(inferred, Instance) -@pytest.mark.xfail(reason="Relying on path copy") def test_augassign_recursion(): """Make sure inference doesn't throw a RecursionError @@ -5248,26 +5396,25 @@ class Cls: def test_prevent_recursion_error_in_igetattr_and_context_manager_inference(): code = """ class DummyContext(object): - def method(self, msg): # pylint: disable=C0103 - pass def __enter__(self): - pass + return self def __exit__(self, ex_type, ex_value, ex_tb): return True - class CallMeMaybe(object): - def __call__(self): - while False: - with DummyContext() as con: - f_method = con.method - break + if False: + with DummyContext() as con: + pass - with DummyContext() as con: - con #@ - f_method = con.method + with DummyContext() as con: + con.__enter__ #@ """ node = extract_node(code) - assert next(node.infer()) is util.Uninferable + # According to the original issue raised that introduced this test + # (https://github.com/PyCQA/astroid/663, see 55076ca), this test was a + # non-regression check for StopIteration leaking out of inference and + # causing a RuntimeError. Hence, here just consume the inferred value + # without checking it and rely on pytest to fail on raise + next(node.infer()) def test_infer_context_manager_with_unknown_args(): @@ -5968,5 +6115,37 @@ def test_infer_list_of_uninferables_does_not_crash(): assert not inferred.elts +# https://github.com/PyCQA/astroid/issues/926 +def test_issue926_infer_stmts_referencing_same_name_is_not_uninferable(): + code = """ + pair = [1, 2] + ex = pair[0] + if 1 + 1 == 2: + ex = pair[1] + ex + """ + node = extract_node(code) + inferred = list(node.infer()) + assert len(inferred) == 2 + assert isinstance(inferred[0], nodes.Const) + assert inferred[0].value == 1 + assert isinstance(inferred[1], nodes.Const) + assert inferred[1].value == 2 + + +# https://github.com/PyCQA/astroid/issues/926 +def test_issue926_binop_referencing_same_name_is_not_uninferable(): + code = """ + pair = [1, 2] + ex = pair[0] + pair[1] + ex + """ + node = extract_node(code) + inferred = list(node.infer()) + assert len(inferred) == 1 + assert isinstance(inferred[0], nodes.Const) + assert inferred[0].value == 3 + + if __name__ == "__main__": unittest.main() diff --git a/tests/unittest_modutils.py b/tests/unittest_modutils.py index a4f3e9082d..248a88cdb9 100644 --- a/tests/unittest_modutils.py +++ b/tests/unittest_modutils.py @@ -76,7 +76,7 @@ def test_find_distutils_submodules_in_virtualenv(self): class LoadModuleFromNameTest(unittest.TestCase): - """ load a python module from it's name """ + """load a python module from it's name""" def test_knownValues_load_module_from_name_1(self): self.assertEqual(modutils.load_module_from_name("sys"), sys) @@ -126,7 +126,7 @@ def test_get_module_part_exception(self): class ModPathFromFileTest(unittest.TestCase): - """ given an absolute file path return the python module's path as a list """ + """given an absolute file path return the python module's path as a list""" def test_knownValues_modpath_from_file_1(self): self.assertEqual( @@ -301,6 +301,18 @@ def test_knownValues_is_relative_1(self): def test_knownValues_is_relative_3(self): self.assertFalse(modutils.is_relative("astroid", astroid.__path__[0])) + def test_knownValues_is_relative_4(self): + self.assertTrue( + modutils.is_relative("util", astroid.interpreter._import.spec.__file__) + ) + + def test_knownValues_is_relative_5(self): + self.assertFalse( + modutils.is_relative( + "objectmodel", astroid.interpreter._import.spec.__file__ + ) + ) + def test_deep_relative(self): self.assertTrue(modutils.is_relative("ElementTree", xml.etree.__path__[0])) diff --git a/tests/unittest_nodes.py b/tests/unittest_nodes.py index 4ce73be9b8..14f713a51b 100644 --- a/tests/unittest_nodes.py +++ b/tests/unittest_nodes.py @@ -542,6 +542,19 @@ def test_str(self): def test_unicode(self): self._test("a") + @pytest.mark.skipif( + not PY38, reason="kind attribute for ast.Constant was added in 3.8" + ) + def test_str_kind(self): + node = builder.extract_node( + """ + const = u"foo" + """ + ) + assert isinstance(node.value, nodes.Const) + assert node.value.value == "foo" + assert node.value.kind, "u" + def test_copy(self): """ Make sure copying a Const object doesn't result in infinite recursion diff --git a/tests/unittest_object_model.py b/tests/unittest_object_model.py index 5d438a65fb..64e49609b9 100644 --- a/tests/unittest_object_model.py +++ b/tests/unittest_object_model.py @@ -358,7 +358,6 @@ def test(self): return 42 with self.assertRaises(exceptions.InferenceError): next(node.infer()) - @pytest.mark.xfail(reason="Relying on path copy") def test_descriptor_error_regression(self): """Make sure the following code does node cause an exception""" diff --git a/tests/unittest_regrtest.py b/tests/unittest_regrtest.py index 29febfb8f6..acabde135e 100644 --- a/tests/unittest_regrtest.py +++ b/tests/unittest_regrtest.py @@ -99,7 +99,7 @@ def test_numpy_crash(self): astroid = builder.string_build(data, __name__, __file__) callfunc = astroid.body[1].value.func inferred = callfunc.inferred() - self.assertEqual(len(inferred), 2) + self.assertEqual(len(inferred), 1) def test_nameconstant(self): # used to fail for Python 3.4 diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py index dd6102c6ff..a298803c91 100644 --- a/tests/unittest_scoped_nodes.py +++ b/tests/unittest_scoped_nodes.py @@ -466,6 +466,55 @@ def func(): self.assertIsInstance(func_vals[0], nodes.Const) self.assertIsNone(func_vals[0].value) + def test_no_returns_is_implicitly_none(self): + code = """ + def f(): + print('non-empty, non-pass, no return statements') + value = f() + value + """ + node = builder.extract_node(code) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value is None + + def test_only_raises_is_not_implicitly_none(self): + code = """ + def f(): + raise SystemExit() + f() + """ + node = builder.extract_node(code) # type: nodes.Call + inferred = next(node.infer()) + assert inferred is util.Uninferable + + def test_abstract_methods_are_not_implicitly_none(self): + code = """ + from abc import ABCMeta, abstractmethod + + class Abstract(metaclass=ABCMeta): + @abstractmethod + def foo(self): + pass + def bar(self): + print('non-empty, non-pass, no return statements') + Abstract().foo() #@ + Abstract().bar() #@ + + class Concrete(Abstract): + def foo(self): + return 123 + Concrete().foo() #@ + Concrete().bar() #@ + """ + afoo, abar, cfoo, cbar = builder.extract_node(code) + + assert next(afoo.infer()) is util.Uninferable + for node, value in [(abar, None), (cfoo, 123), (cbar, None)]: + inferred = next(node.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value == value + def test_func_instance_attr(self): """test instance attributes for functions""" data = """ @@ -1430,7 +1479,9 @@ class A: pass class B: pass - scope = object() + class Scope: + pass + scope = Scope() scope.A = A scope.B = B class C(scope.A, scope.B): @@ -1921,6 +1972,153 @@ def update(self): builder.parse(data) +def test_issue940_metaclass_subclass_property(): + node = builder.extract_node( + """ + class BaseMeta(type): + @property + def __members__(cls): + return ['a', 'property'] + class Parent(metaclass=BaseMeta): + pass + class Derived(Parent): + pass + Derived.__members__ + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.List) + assert [c.value for c in inferred.elts] == ["a", "property"] + + +def test_issue940_property_grandchild(): + node = builder.extract_node( + """ + class Grandparent: + @property + def __members__(self): + return ['a', 'property'] + class Parent(Grandparent): + pass + class Child(Parent): + pass + Child().__members__ + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.List) + assert [c.value for c in inferred.elts] == ["a", "property"] + + +def test_issue940_metaclass_property(): + node = builder.extract_node( + """ + class BaseMeta(type): + @property + def __members__(cls): + return ['a', 'property'] + class Parent(metaclass=BaseMeta): + pass + Parent.__members__ + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.List) + assert [c.value for c in inferred.elts] == ["a", "property"] + + +def test_issue940_with_metaclass_class_context_property(): + node = builder.extract_node( + """ + class BaseMeta(type): + pass + class Parent(metaclass=BaseMeta): + @property + def __members__(self): + return ['a', 'property'] + class Derived(Parent): + pass + Derived.__members__ + """ + ) + inferred = next(node.infer()) + assert not isinstance(inferred, nodes.List) + assert isinstance(inferred, objects.Property) + + +def test_issue940_metaclass_values_funcdef(): + node = builder.extract_node( + """ + class BaseMeta(type): + def __members__(cls): + return ['a', 'func'] + class Parent(metaclass=BaseMeta): + pass + Parent.__members__() + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.List) + assert [c.value for c in inferred.elts] == ["a", "func"] + + +def test_issue940_metaclass_derived_funcdef(): + node = builder.extract_node( + """ + class BaseMeta(type): + def __members__(cls): + return ['a', 'func'] + class Parent(metaclass=BaseMeta): + pass + class Derived(Parent): + pass + Derived.__members__() + """ + ) + inferred_result = next(node.infer()) + assert isinstance(inferred_result, nodes.List) + assert [c.value for c in inferred_result.elts] == ["a", "func"] + + +def test_issue940_metaclass_funcdef_is_not_datadescriptor(): + node = builder.extract_node( + """ + class BaseMeta(type): + def __members__(cls): + return ['a', 'property'] + class Parent(metaclass=BaseMeta): + @property + def __members__(cls): + return BaseMeta.__members__() + class Derived(Parent): + pass + Derived.__members__ + """ + ) + # Here the function is defined on the metaclass, but the property + # is defined on the base class. When loading the attribute in a + # class context, this should return the property object instead of + # resolving the data descriptor + inferred = next(node.infer()) + assert isinstance(inferred, objects.Property) + + +def test_issue940_enums_as_a_real_world_usecase(): + node = builder.extract_node( + """ + from enum import Enum + class Sounds(Enum): + bee = "buzz" + cat = "meow" + Sounds.__members__ + """ + ) + inferred_result = next(node.infer()) + assert isinstance(inferred_result, nodes.Dict) + actual = [k.value for k, _ in inferred_result.items] + assert sorted(actual) == ["bee", "cat"] + + def test_metaclass_cannot_infer_call_yields_an_instance(): node = builder.extract_node( """ @@ -2005,5 +2203,31 @@ def func(a, b=1, /, c=2): pass assert first_param.value == 1 +@test_utils.require_version(minver="3.7") +def test_ancestor_with_generic(): + # https://github.com/PyCQA/astroid/issues/942 + tree = builder.parse( + """ + from typing import TypeVar, Generic + T = TypeVar("T") + class A(Generic[T]): + def a_method(self): + print("hello") + class B(A[T]): pass + class C(B[str]): pass + """ + ) + inferred_b = next(tree["B"].infer()) + assert [cdef.name for cdef in inferred_b.ancestors()] == ["A", "Generic", "object"] + + inferred_c = next(tree["C"].infer()) + assert [cdef.name for cdef in inferred_c.ancestors()] == [ + "B", + "A", + "Generic", + "object", + ] + + if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini index da9e174523..ab10fd838e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{36,37,38,39} +envlist = py{36,37,38,39,310} skip_missing_interpreters = true [testenv:pylint]