diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java index 8d59d75df9fa..50201cdceffa 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java +++ b/python/python-psi-impl/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java @@ -10,6 +10,7 @@ import com.intellij.psi.PsiReference; import com.intellij.psi.ResolveResult; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ObjectUtils; +import com.intellij.util.ThreeState; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import com.jetbrains.python.PyNames; @@ -486,25 +487,28 @@ public final class PyCallExpressionHelper { return true; // unqualified + method = implicit constructor call } for (PyExpression qualifier : qualifiers) { - if (qualifier != null && isQualifiedByInstance(resolved, qualifier, context)) { - return true; + if (qualifier != null) { + ThreeState byInstance = isQualifiedByInstance(resolved, qualifier, context); + if (byInstance != ThreeState.UNSURE) { + return byInstance.toBoolean(); + } } } - return false; + return true; } - private static boolean isQualifiedByInstance(@Nullable PyCallable resolved, - @NotNull PyExpression qualifier, - @NotNull TypeEvalContext context) { + private static @NotNull ThreeState isQualifiedByInstance(@Nullable PyCallable resolved, + @NotNull PyExpression qualifier, + @NotNull TypeEvalContext context) { if (isQualifiedByClass(resolved, qualifier, context)) { - return false; + return ThreeState.NO; } final PyType qualifierType = context.getType(qualifier); - if (qualifierType != null) { - // TODO: handle UnionType - if (qualifierType instanceof PyModuleType) return false; // qualified by module, not instance. + // TODO: handle UnionType + if (qualifierType instanceof PyModuleType) { + return ThreeState.UNSURE; } - return true; // NOTE. best guess: unknown qualifier is more probably an instance. + return ThreeState.YES; // NOTE. best guess: unknown qualifier is more probably an instance. } private static boolean isQualifiedByClass(@Nullable PyCallable resolved, diff --git a/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/a.py b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/a.py new file mode 100644 index 000000000000..9c5fa2502320 --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/a.py @@ -0,0 +1,3 @@ +from mod import func + +func(1, 2) diff --git a/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/mod.pyi b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/mod.pyi new file mode 100644 index 000000000000..d91d087fb4f2 --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithFromImport/mod.pyi @@ -0,0 +1,5 @@ +class C: + def method(self, a: int, b: int) -> int: ... + +_inst: C = ... +func = _inst.method \ No newline at end of file diff --git a/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py new file mode 100644 index 000000000000..c66aa063d76c --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py @@ -0,0 +1,3 @@ +import mod + +mod.func(1, 2) \ No newline at end of file diff --git a/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi new file mode 100644 index 000000000000..d91d087fb4f2 --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/BoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi @@ -0,0 +1,5 @@ +class C: + def method(self, a: int, b: int) -> int: ... + +_inst: C = ... +func = _inst.method \ No newline at end of file diff --git a/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py b/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py new file mode 100644 index 000000000000..c66aa063d76c --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/a.py @@ -0,0 +1,3 @@ +import mod + +mod.func(1, 2) \ No newline at end of file diff --git a/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi b/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi new file mode 100644 index 000000000000..690e7e972bc7 --- /dev/null +++ b/python/testData/inspections/PyArgumentListInspection/StaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport/mod.pyi @@ -0,0 +1,6 @@ +class C: + @staticmethod + def method(a: int, b: int) -> int: ... + +_inst: C = ... +func = _inst.method diff --git a/python/testData/inspections/changeSignatureOfExportedBoundMethod/changeSignatureOfExportedBoundMethod.py b/python/testData/inspections/changeSignatureOfExportedBoundMethod/changeSignatureOfExportedBoundMethod.py new file mode 100644 index 000000000000..4757445c5264 --- /dev/null +++ b/python/testData/inspections/changeSignatureOfExportedBoundMethod/changeSignatureOfExportedBoundMethod.py @@ -0,0 +1,3 @@ +from mod import func + +func(1, 2, 3) \ No newline at end of file diff --git a/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod.py b/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod.py new file mode 100644 index 000000000000..a3b7d022b6f6 --- /dev/null +++ b/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod.py @@ -0,0 +1,6 @@ +class C: + def method(self, a, b): + pass + +_inst: C = C() +func = _inst.method \ No newline at end of file diff --git a/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod_after.py b/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod_after.py new file mode 100644 index 000000000000..0e9bd16c1026 --- /dev/null +++ b/python/testData/inspections/changeSignatureOfExportedBoundMethod/mod_after.py @@ -0,0 +1,6 @@ +class C: + def method(self, a, b, i): + pass + +_inst: C = C() +func = _inst.method \ No newline at end of file diff --git a/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/a.py b/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/a.py new file mode 100644 index 000000000000..ff45bddfdb33 --- /dev/null +++ b/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/a.py @@ -0,0 +1,4 @@ +import mod + + +mod.func(42) \ No newline at end of file diff --git a/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/mod.py b/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/mod.py new file mode 100644 index 000000000000..4df3d92d0ae0 --- /dev/null +++ b/python/testData/paramInfo/BoundMethodReassignedAndImportedWithQualifiedImport/mod.py @@ -0,0 +1,7 @@ +class C: + def method(self, param): + pass + + +_inst: C = C() +func = _inst.method diff --git a/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/a.py b/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/a.py new file mode 100644 index 000000000000..ff45bddfdb33 --- /dev/null +++ b/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/a.py @@ -0,0 +1,4 @@ +import mod + + +mod.func(42) \ No newline at end of file diff --git a/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/mod.py b/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/mod.py new file mode 100644 index 000000000000..644f81777a92 --- /dev/null +++ b/python/testData/paramInfo/UnboundMethodReassignedAndImportedWithQualifiedImport/mod.py @@ -0,0 +1,6 @@ +class C: + def method(self, param): + pass + + +func = C.method diff --git a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java index 559c8395e49b..149ce4a771a9 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -311,6 +311,18 @@ public class PyParameterInfoTest extends LightMarkedTestCase { feignCtrlP(marks.get("").getTextOffset()).check("self: A, a, b", new String[]{"b"}, new String[]{"self: A, "}); } + // PY-53671 + public void testUnboundMethodReassignedAndImportedWithQualifiedImport() { + Map marks = loadMultiFileTest(1); + feignCtrlP(marks.get("").getTextOffset()).check("self: C, param", new String[]{"self: C, "}, ArrayUtil.EMPTY_STRING_ARRAY); + } + + // PY-53671 + public void testBoundMethodReassignedAndImportedWithQualifiedImport() { + Map marks = loadMultiFileTest(1); + feignCtrlP(marks.get("").getTextOffset()).check("self: C, param", new String[]{"param"}, new String[]{"self: C, "}); + } + public void testConstructorFactory() { Map marks = loadTest(1); diff --git a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java index 97b9a05be626..9d57240451cc 100644 --- a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java +++ b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java @@ -747,6 +747,13 @@ public class PyQuickFixTest extends PyTestCase { doInspectionTest(PyArgumentListInspection.class, "Change the signature of func(i1, i, i3, num)", true, true); } + // PY-53671 + public void testChangeSignatureOfExportedBoundMethod() { + runWithLanguageLevel(LanguageLevel.getLatest(), () -> { + doMultiFilesInspectionTest(PyArgumentListInspection.class, "Change the signature of method(self, a, b, i)", "mod.py"); + }); + } + // PY-8174 public void testChangeSignatureParametersDefaultValues() { doInspectionTest(PyArgumentListInspection.class, "Change the signature of func(i, foo)", true, true); diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java index 6c913772e4c3..68be755d346d 100644 --- a/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/Py3ArgumentListInspectionTest.java @@ -52,4 +52,26 @@ public class Py3ArgumentListInspectionTest extends PyInspectionTestCase { "a = A()\n" + "b = B()"); } + + // PY-53671 + public void testBoundMethodExportedAsTopLevelFunctionImportedWithQualifiedImport() { + doMultiFileTest(); + } + + // PY-53671 + public void testBoundMethodExportedAsTopLevelFunctionImportedWithFromImport() { + doMultiFileTest(); + } + + // PY-53671 + public void testStaticMethodExportedAsTopLevelFunctionImportedWithQualifiedImport() { + doMultiFileTest(); + } + + // PY-53671 + public void testRandomRandint() { + doTestByText("import random\n" + + "\n" + + "random.randint(1, 2)"); + } }