diff --git a/python/src/com/jetbrains/python/psi/PyClass.java b/python/src/com/jetbrains/python/psi/PyClass.java index 06f73a10c368..d90c101648fd 100644 --- a/python/src/com/jetbrains/python/psi/PyClass.java +++ b/python/src/com/jetbrains/python/psi/PyClass.java @@ -3,6 +3,7 @@ package com.jetbrains.python.psi; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiNamedElement; import com.intellij.psi.StubBasedPsiElement; +import com.intellij.util.Processor; import com.jetbrains.python.codeInsight.controlflow.ScopeOwner; import com.jetbrains.python.psi.stubs.PyClassStub; import org.jetbrains.annotations.NonNls; @@ -37,6 +38,24 @@ public interface PyClass extends PsiNamedElement, PyStatement, NameDefiner, PyDo @Nullable PyFunction findMethodByName(@NotNull @NonNls final String name, boolean inherited); + /** + * Finds either __init__ or __new__, whichever is defined for given class. + * If __init__ is defined, it is found first. This mimics the way initialization methods + * are searched for and called by Python when a constructor call is made. + * Since __new__ only makes sense for new-style classes, an old-style class never finds it with this method. + * @param inherited true: search in superclasses, too. + * @return a method that would be called first when an instance of this class is instantiated. + */ + @Nullable + PyFunction findInitOrNew(boolean inherited); + + /** + * Apply a processor to every method, looking at superclasses in method resolution order as needed. + * @param processor what to apply + * @param inherited true: search in superclasses, too. + */ + void scanMethods(Processor processor, boolean inherited); + PyTargetExpression[] getClassAttributes(); PyTargetExpression[] getInstanceAttributes(); diff --git a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java index b2c5b95adf3b..24525b168aed 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java +++ b/python/src/com/jetbrains/python/psi/impl/PyCallExpressionHelper.java @@ -45,7 +45,7 @@ public class PyCallExpressionHelper { PsiElement redefining_func = refex.getReference().resolve(); if (redefining_func != null) { PsiNamedElement true_func = PyBuiltinCache.getInstance(us).getByName(refname, PsiNamedElement.class); - if (true_func instanceof PyClass) true_func = ((PyClass)true_func).findMethodByName(PyNames.INIT, true); + if (true_func instanceof PyClass) true_func = ((PyClass)true_func).findInitOrNew(true); if (true_func == redefining_func) { // yes, really a case of "foo = classmethod(foo)" PyArgumentList arglist = redefining_call.getArgumentList(); @@ -73,10 +73,14 @@ public class PyCallExpressionHelper { public static PyCallExpression.PyMarkedFunction resolveCallee(PyCallExpression us) { PyExpression callee = us.getCallee(); PyFunction.Flag wrapped_flag = null; + boolean is_constructor_call = false; if (callee instanceof PyReferenceExpression) { PyReferenceExpression ref = (PyReferenceExpression)callee; PsiElement resolved = ref.followAssignmentsChain(); - if (resolved instanceof PyClass) resolved = ((PyClass)resolved).findMethodByName(PyNames.INIT, true); // class to constructor call + if (resolved instanceof PyClass) { + resolved = ((PyClass)resolved).findInitOrNew(true); // class to constructor call + is_constructor_call = true; + } else if (resolved instanceof PyCallExpression) { // is it a case of "foo = classmethod(foo)"? PyCallExpression redefining_call = (PyCallExpression)resolved; @@ -91,6 +95,9 @@ public class PyCallExpressionHelper { if (resolved instanceof PyFunction) { EnumSet flags = EnumSet.noneOf(PyFunction.Flag.class); int implicit_offset = getImplicitArgumentCount(us.getCallee(), (PyFunction) resolved, wrapped_flag, flags); + if (! is_constructor_call && PyNames.NEW.equals(((PyFunction)resolved).getName())) { + implicit_offset = Math.min(implicit_offset-1, 0); // case of Class.__new__ + } return new PyCallExpression.PyMarkedFunction((PyFunction)resolved, flags, implicit_offset); } } @@ -117,6 +124,7 @@ public class PyCallExpressionHelper { if (wrapped_flag == PyFunction.Flag.STATICMETHOD && implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self' if (wrapped_flag == PyFunction.Flag.CLASSMETHOD && ! is_by_instance) implicit_offset += 1; // Both Foo.method() and foo.method() have implicit the first arg } + if (! is_by_instance && PyNames.NEW.equals(method.getName())) implicit_offset += 1; // constructor call // decorators? if (PyNames.INIT.equals(method.getName())) { String refName = callReference instanceof PyReferenceExpression @@ -139,7 +147,7 @@ public class PyCallExpressionHelper { if (flags != null) { flags.add(PyFunction.Flag.STATICMETHOD); } - if (implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self' + if (is_by_instance && implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self' } else if (PyNames.CLASSMETHOD.equals(deconame)) { if (flags != null) { diff --git a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java index d30f21a4cf70..d42287b18ae7 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyClassImpl.java @@ -13,6 +13,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.reference.SoftReference; import com.intellij.util.Icons; import com.intellij.util.IncorrectOperationException; +import com.intellij.util.Processor; import com.jetbrains.python.PyElementTypes; import com.jetbrains.python.PyNames; import com.jetbrains.python.PyTokenTypes; @@ -261,22 +262,59 @@ public class PyClassImpl extends PyPresentableElementImpl implement return result.toArray(new PyFunction[result.size()]); } + private static class NameFindingProcessor implements Processor { + private PyFunction myResult; + private String[] myNames; + + public NameFindingProcessor(String... names) { + myNames = names; + myResult = null; + } + + public PyFunction getResult() { + return myResult; + } + + public boolean process(PyFunction pyFunction) { + String fname = pyFunction.getName(); + for (String name: myNames) { + if (name.equals(fname)) { + myResult = pyFunction; + return false; + } + } + return true; + } + } + public PyFunction findMethodByName(@NotNull final String name, boolean inherited) { + NameFindingProcessor proc = new NameFindingProcessor(name); + scanMethods(proc, inherited); + return proc.getResult(); + } + + @Nullable + public PyFunction findInitOrNew(boolean inherited) { + NameFindingProcessor proc; + if (isNewStyleClass()) proc = new NameFindingProcessor(PyNames.INIT, PyNames.NEW); + else proc = new NameFindingProcessor(PyNames.INIT); + scanMethods(proc, inherited); + return proc.getResult(); + } + + public void scanMethods(Processor processor, boolean inherited) { PyFunction[] methods = getMethods(); for(PyFunction method: methods) { - if (name.equals(method.getName())) { - return method; - } + if (! processor.process(method)) return; } if (inherited) { for (PyClass ancestor : iterateAncestors()) { - PyFunction candidate = ancestor.findMethodByName(name, false); // not recursively, we want MRI in MI cases - if (candidate != null) return candidate; + ancestor.scanMethods(processor, false); } } - return null; } + public PyTargetExpression[] getClassAttributes() { PyClassStub stub = getStub(); if (stub != null) { diff --git a/python/testData/paramInfo/IgnoreNewInOldStyleClass.py b/python/testData/paramInfo/IgnoreNewInOldStyleClass.py new file mode 100644 index 000000000000..9de86eeacaf0 --- /dev/null +++ b/python/testData/paramInfo/IgnoreNewInOldStyleClass.py @@ -0,0 +1,10 @@ +# makes sense for python 2.x +class A: + def __init__(self, one): + pass + +class B(A): + def __new__(cls, one, two): + pass + +b = B("only_one") diff --git a/python/testData/paramInfo/RedefinedNewConstructorCall.py b/python/testData/paramInfo/RedefinedNewConstructorCall.py new file mode 100644 index 000000000000..783c52132fc5 --- /dev/null +++ b/python/testData/paramInfo/RedefinedNewConstructorCall.py @@ -0,0 +1,7 @@ +# signature of overridden __new__ + +class A(object): + def __new__(cls, a, b): + pass + +A(1, 2) diff --git a/python/testData/paramInfo/RedefinedNewDirectCall.py b/python/testData/paramInfo/RedefinedNewDirectCall.py new file mode 100644 index 000000000000..1355f367ecf9 --- /dev/null +++ b/python/testData/paramInfo/RedefinedNewDirectCall.py @@ -0,0 +1,7 @@ +# signature of overridden __new__ + +class A(object): + def __new__(cls, a, b): + pass + +A.__new__(A, 1, 2) diff --git a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java index d0b6e96a3102..66c7de076e09 100644 --- a/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java +++ b/python/testSrc/com/jetbrains/python/PyParameterInfoTest.java @@ -247,6 +247,30 @@ public class PyParameterInfoTest extends LightMarkedTestCase { feignCtrlP(marks.get("").getTextOffset()).check("self,a,b", new String[]{"b"}, new String[]{"self,"}); } + public void testRedefinedNewConstructorCall() throws Exception { + Map marks = loadTest(); + assertEquals("Test data sanity", marks.size(), 2); + + feignCtrlP(marks.get("").getTextOffset()).check("cls,a,b", new String[]{"a,"}, new String[]{"cls,"}); + feignCtrlP(marks.get("").getTextOffset()).check("cls,a,b", new String[]{"b"}, new String[]{"cls,"}); + } + + public void testRedefinedNewDirectCall() throws Exception { + Map marks = loadTest(); + assertEquals("Test data sanity", marks.size(), 3); + + feignCtrlP(marks.get("").getTextOffset()).check("cls,a,b", new String[]{"cls,"}); + feignCtrlP(marks.get("").getTextOffset()).check("cls,a,b", new String[]{"a,"}); + feignCtrlP(marks.get("").getTextOffset()).check("cls,a,b", new String[]{"b"}); + } + + public void testIgnoreNewInOldStyleClass() throws Exception { + Map marks = loadTest(); + assertEquals("Test data sanity", marks.size(), 1); + + feignCtrlP(marks.get("").getTextOffset()).check("self,one", new String[]{"one"}, new String[]{"self,"}); + } + // TODO: add method tests with decorators when a mock SDK is available /**