From fb0c5cc58ba09453200639931a74518eee70d999 Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Mon, 15 Jan 2018 18:55:10 +0300 Subject: [PATCH] Support module with `__getattr__` while resolving and type checking (PY-28017) --- .../PyUnresolvedReferencesInspection.java | 95 ++++++++++++------- .../python/psi/types/PyTypeChecker.java | 47 +++++---- .../ModuleWithGetAttr/a.py | 6 ++ .../ModuleWithGetAttr/mymod.py | 7 ++ .../FromPackageImportBuiltin/a.py | 2 +- .../ModuleWithGetAttr/a.py | 5 + .../ModuleWithGetAttr/mymod.py | 7 ++ .../OneUnsedOneMarked/a.py | 2 +- .../UnusedUnresolvedNameImported/a.py | 2 +- .../a.py | 4 +- .../a.py | 4 +- .../NestedPackageNamedAsSourceRoot/a.py | 2 +- .../hiddenPyiImports/HiddenPyiImports.py | 6 +- .../package_with_stub_in_path/a.pyi | 2 +- .../PyTypeCheckerInspectionTest.java | 5 + .../PyUnresolvedReferencesInspectionTest.java | 5 + 16 files changed, 140 insertions(+), 61 deletions(-) create mode 100644 python/testData/inspections/PyTypeCheckerInspection/ModuleWithGetAttr/a.py create mode 100644 python/testData/inspections/PyTypeCheckerInspection/ModuleWithGetAttr/mymod.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/ModuleWithGetAttr/a.py create mode 100644 python/testData/inspections/PyUnresolvedReferencesInspection/ModuleWithGetAttr/mymod.py diff --git a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java index 76f79711fccd..d65308539bb3 100644 --- a/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java +++ b/python/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesInspection.java @@ -51,6 +51,7 @@ import com.jetbrains.python.psi.impl.PyBuiltinCache; import com.jetbrains.python.psi.impl.PyImportStatementNavigator; import com.jetbrains.python.psi.impl.PyImportedModule; import com.jetbrains.python.psi.impl.PyPsiUtils; +import com.jetbrains.python.psi.impl.references.PyFromImportNameReference; import com.jetbrains.python.psi.impl.references.PyImportReference; import com.jetbrains.python.psi.impl.references.PyOperatorReference; import com.jetbrains.python.psi.resolve.ImportedResolveResult; @@ -557,43 +558,43 @@ public class PyUnresolvedReferencesInspection extends PyInspection { if (PyNames.COMPARISON_OPERATORS.contains(expr.getReferencedName())) { return; } - final PyExpression qualifier = expr.getQualifier(); - if (qualifier != null) { - PyType type = myTypeEvalContext.getType(qualifier); - if (type != null) { - if (ignoreUnresolvedMemberForType(type, reference, refName)) { - return; - } - addCreateMemberFromUsageFixes(type, reference, refText, actions); - if (type instanceof PyClassType) { - final PyClassType classType = (PyClassType)type; - if (reference instanceof PyOperatorReference) { - String className = type.getName(); - if (classType.isDefinition()) { - final PyClassLikeType metaClassType = classType.getMetaClassType(myTypeEvalContext, true); - if (metaClassType != null) { - className = metaClassType.getName(); - } + } + final PyExpression qualifier = getReferenceQualifier(reference); + if (qualifier != null) { + final PyType type = myTypeEvalContext.getType(qualifier); + if (type != null) { + if (ignoreUnresolvedMemberForType(type, reference, refName)) { + return; + } + addCreateMemberFromUsageFixes(type, reference, refText, actions); + if (type instanceof PyClassType) { + final PyClassType classType = (PyClassType)type; + if (reference instanceof PyOperatorReference) { + String className = type.getName(); + if (classType.isDefinition()) { + final PyClassLikeType metaClassType = classType.getMetaClassType(myTypeEvalContext, true); + if (metaClassType != null) { + className = metaClassType.getName(); } - description = PyBundle.message("INSP.unresolved.operator.ref", - className, refName, - ((PyOperatorReference)reference).getReadableOperatorName()); } - else { - final List slots = classType.getPyClass().getOwnSlots(); - - if (slots != null && slots.contains(refName)) { - return; - } - - description = PyBundle.message("INSP.unresolved.ref.$0.for.class.$1", refText, type.getName()); - } - markedQualified = true; + description = PyBundle.message("INSP.unresolved.operator.ref", + className, refName, + ((PyOperatorReference)reference).getReadableOperatorName()); } else { - description = PyBundle.message("INSP.cannot.find.$0.in.$1", refText, type.getName()); - markedQualified = true; + final List slots = classType.getPyClass().getOwnSlots(); + + if (slots != null && slots.contains(refName)) { + return; + } + + description = PyBundle.message("INSP.unresolved.ref.$0.for.class.$1", refText, type.getName()); } + markedQualified = true; + } + else { + description = PyBundle.message("INSP.cannot.find.$0.in.$1", refText, type.getName()); + markedQualified = true; } } } @@ -657,6 +658,30 @@ public class PyUnresolvedReferencesInspection extends PyInspection { registerProblem(node, description, hl_type, null, rangeInElement, actions.toArray(LocalQuickFix.EMPTY_ARRAY)); } + @Nullable + private static PyExpression getReferenceQualifier(@NotNull PsiReference reference) { + final PsiElement element = reference.getElement(); + + if (element instanceof PyQualifiedExpression) { + final PyExpression qualifier = ((PyQualifiedExpression)element).getQualifier(); + if (qualifier != null) { + return qualifier; + } + } + + if (reference instanceof PyFromImportNameReference) { + final PyFromImportStatement statement = PsiTreeUtil.getParentOfType(element, PyFromImportStatement.class); + if (statement != null) { + final PyReferenceExpression source = statement.getImportSource(); + if (source != null) { + return source; + } + } + } + + return null; + } + private static void addInstallPackageAction(List actions, String packageName, Module module, Sdk sdk) { final List requirements = Collections.singletonList(new PyRequirement(packageName)); final String name = "Install package " + packageName; @@ -808,6 +833,12 @@ public class PyUnresolvedReferencesInspection extends PyInspection { if (type instanceof PyUnionType) { return ContainerUtil.exists(((PyUnionType)type).getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name)); } + if (type instanceof PyModuleType) { + final PyFile module = ((PyModuleType)type).getModule(); + if (module.getLanguageLevel().isAtLeast(LanguageLevel.PYTHON37)) { + return PyTypeChecker.definesGetAttr(module, myTypeEvalContext); + } + } for (PyInspectionExtension extension : Extensions.getExtensions(PyInspectionExtension.EP_NAME)) { if (extension.ignoreUnresolvedMember(type, name, myTypeEvalContext)) { return true; diff --git a/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java index 337370cfb2d5..8c6939df6176 100644 --- a/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java +++ b/python/src/com/jetbrains/python/psi/types/PyTypeChecker.java @@ -247,6 +247,12 @@ public class PyTypeChecker { final Set actualAttributes = actualClassType.getMemberNames(true, context); return actualAttributes.containsAll(((PyStructuralType)expected).getAttributeNames()); } + if (expected instanceof PyStructuralType && actual instanceof PyModuleType) { + final PyFile module = ((PyModuleType)actual).getModule(); + if (module.getLanguageLevel().isAtLeast(LanguageLevel.PYTHON37) && definesGetAttr(module, context)) { + return true; + } + } if (expected instanceof PyStructuralType) { final Set expectedAttributes = ((PyStructuralType)expected).getAttributeNames(); final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); @@ -702,29 +708,36 @@ public class PyTypeChecker { return false; } - public static boolean overridesGetAttr(@NotNull PyClass cls, @NotNull TypeEvalContext context) { - PsiElement method = resolveClassMember(cls, PyNames.GETATTR, context); - if (method != null) { - return true; + public static boolean definesGetAttr(@NotNull PyFile file, @NotNull TypeEvalContext context) { + if (file instanceof PyTypedElement) { + final PyType type = context.getType((PyTypedElement)file); + if (type != null) { + return resolveTypeMember(type, PyNames.GETATTR, context) != null; + } } - method = resolveClassMember(cls, PyNames.GETATTRIBUTE, context); - if (method != null && !PyBuiltinCache.getInstance(cls).isBuiltin(method)) { - return true; + + return false; + } + + public static boolean overridesGetAttr(@NotNull PyClass cls, @NotNull TypeEvalContext context) { + final PyType type = context.getType(cls); + if (type != null) { + if (resolveTypeMember(type, PyNames.GETATTR, context) != null) { + return true; + } + final PsiElement method = resolveTypeMember(type, PyNames.GETATTRIBUTE, context); + if (method != null && !PyBuiltinCache.getInstance(cls).isBuiltin(method)) { + return true; + } } return false; } @Nullable - private static PsiElement resolveClassMember(@NotNull PyClass cls, @NotNull String name, @NotNull TypeEvalContext context) { - final PyType type = context.getType(cls); - if (type != null) { - final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); - final List results = type.resolveMember(name, null, AccessDirection.READ, resolveContext); - if (results != null && !results.isEmpty()) { - return results.get(0).getElement(); - } - } - return null; + private static PsiElement resolveTypeMember(@NotNull PyType type, @NotNull String name, @NotNull TypeEvalContext context) { + final PyResolveContext resolveContext = PyResolveContext.noImplicits().withTypeEvalContext(context); + final List results = type.resolveMember(name, null, AccessDirection.READ, resolveContext); + return !ContainerUtil.isEmpty(results) ? results.get(0).getElement() : null; } @Nullable diff --git a/python/testData/inspections/PyTypeCheckerInspection/ModuleWithGetAttr/a.py b/python/testData/inspections/PyTypeCheckerInspection/ModuleWithGetAttr/a.py new file mode 100644 index 000000000000..283d3d8f0f9a --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/ModuleWithGetAttr/a.py @@ -0,0 +1,6 @@ +import mymod + +def foo(mod): + return mod.myfunc + +print(foo(mymod)()) \ No newline at end of file diff --git a/python/testData/inspections/PyTypeCheckerInspection/ModuleWithGetAttr/mymod.py b/python/testData/inspections/PyTypeCheckerInspection/ModuleWithGetAttr/mymod.py new file mode 100644 index 000000000000..bd0ca7e81b10 --- /dev/null +++ b/python/testData/inspections/PyTypeCheckerInspection/ModuleWithGetAttr/mymod.py @@ -0,0 +1,7 @@ +def myhiddenfunc(): + return "ok" + +def __getattr__(name): + if name == "myfunc": + return myhiddenfunc + raise AttributeError \ No newline at end of file diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/FromPackageImportBuiltin/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/FromPackageImportBuiltin/a.py index 8880a48e3db9..324fbdedb299 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/FromPackageImportBuiltin/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/FromPackageImportBuiltin/a.py @@ -1,3 +1,3 @@ -from importSource import len +from importSource import len len() diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/ModuleWithGetAttr/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/ModuleWithGetAttr/a.py new file mode 100644 index 000000000000..65c3ccf65662 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/ModuleWithGetAttr/a.py @@ -0,0 +1,5 @@ +from mymod import myfunc +import mymod + +print(myfunc()) +print(mymod.myfunc()) \ No newline at end of file diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/ModuleWithGetAttr/mymod.py b/python/testData/inspections/PyUnresolvedReferencesInspection/ModuleWithGetAttr/mymod.py new file mode 100644 index 000000000000..bd0ca7e81b10 --- /dev/null +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/ModuleWithGetAttr/mymod.py @@ -0,0 +1,7 @@ +def myhiddenfunc(): + return "ok" + +def __getattr__(name): + if name == "myfunc": + return myhiddenfunc + raise AttributeError \ No newline at end of file diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/OneUnsedOneMarked/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/OneUnsedOneMarked/a.py index baae7fc5fe65..423ed3675c9b 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/OneUnsedOneMarked/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/OneUnsedOneMarked/a.py @@ -1,4 +1,4 @@ -from library import foo,bar +from library import foo,bar print(foo) \ No newline at end of file diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/UnusedUnresolvedNameImported/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/UnusedUnresolvedNameImported/a.py index b0ad821f96b9..4c2e80e6d68f 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/UnusedUnresolvedNameImported/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/UnusedUnresolvedNameImported/a.py @@ -1 +1 @@ -from my_module import eggs +from my_module import eggs diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/UnusedUnresolvedNameImportedSeveralTimes/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/UnusedUnresolvedNameImportedSeveralTimes/a.py index 5e1c928b579d..41807d3b8586 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/UnusedUnresolvedNameImportedSeveralTimes/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/UnusedUnresolvedNameImportedSeveralTimes/a.py @@ -1,2 +1,2 @@ -from my_module import eggs -from my_module import eggs +from my_module import eggs +from my_module import eggs diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/UsedUnresolvedNameImportedSeveralTimes/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/UsedUnresolvedNameImportedSeveralTimes/a.py index 820a0f0ad6f3..120e766a149f 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection/UsedUnresolvedNameImportedSeveralTimes/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection/UsedUnresolvedNameImportedSeveralTimes/a.py @@ -1,4 +1,4 @@ -from my_package.my_module import eggs -from my_package.my_module import eggs +from my_package.my_module import eggs +from my_package.my_module import eggs eggs() diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/NestedPackageNamedAsSourceRoot/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/NestedPackageNamedAsSourceRoot/a.py index 8e76d4d6ea5d..ab865bfa3c27 100644 --- a/python/testData/inspections/PyUnresolvedReferencesInspection3K/NestedPackageNamedAsSourceRoot/a.py +++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/NestedPackageNamedAsSourceRoot/a.py @@ -1,6 +1,6 @@ from lib1 import m1 from lib1.m1 import x -from lib1.m1 import something +from lib1.m1 import something print(m1, x, something) diff --git a/python/testData/pyi/inspections/hiddenPyiImports/HiddenPyiImports.py b/python/testData/pyi/inspections/hiddenPyiImports/HiddenPyiImports.py index d37ed81f8e4d..afc95ff16b69 100644 --- a/python/testData/pyi/inspections/hiddenPyiImports/HiddenPyiImports.py +++ b/python/testData/pyi/inspections/hiddenPyiImports/HiddenPyiImports.py @@ -1,7 +1,7 @@ -from m1 import foo -from m1 import bar +from m1 import foo +from m1 import bar from m1 import bar_imported -from m1 import m2 +from m1 import m2 from m1 import m2_imported print(foo, bar, bar_imported, m2, m2_imported) diff --git a/python/testData/pyi/inspections/pyiRelativeImports/package_with_stub_in_path/a.pyi b/python/testData/pyi/inspections/pyiRelativeImports/package_with_stub_in_path/a.pyi index 69389da213da..bb877b1b3c73 100644 --- a/python/testData/pyi/inspections/pyiRelativeImports/package_with_stub_in_path/a.pyi +++ b/python/testData/pyi/inspections/pyiRelativeImports/package_with_stub_in_path/a.pyi @@ -1,5 +1,5 @@ from .m1 import foo -from .m1 import bar +from .m1 import bar foo2 = foo bar2 = bar \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java index 992f846afafb..669421673762 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java @@ -465,4 +465,9 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase { public void testDefinitionAgainstStructural() { runWithLanguageLevel(LanguageLevel.PYTHON36, this::doTest); } + + // PY-28017 + public void testModuleWithGetAttr() { + runWithLanguageLevel(LanguageLevel.PYTHON37, this::doMultiFileTest); + } } diff --git a/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java index 458008a7f870..696de7efde0f 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyUnresolvedReferencesInspectionTest.java @@ -660,6 +660,11 @@ public class PyUnresolvedReferencesInspectionTest extends PyInspectionTestCase { doMultiFileTest(); } + // PY-28017 + public void testModuleWithGetAttr() { + runWithLanguageLevel(LanguageLevel.PYTHON37, this::doMultiFileTest); + } + @NotNull @Override protected Class getInspectionClass() {