PY-9342 Distinguish between function and method types

This commit is contained in:
Mikhail Golubev
2014-08-14 18:27:28 +04:00
parent a135c36544
commit a1ae56b0f9
12 changed files with 234 additions and 25 deletions

View File

@@ -1009,6 +1009,8 @@ class ModuleRedeclarator(object):
self.classes_buf.out(0, txt)
txt = create_function()
self.classes_buf.out(0, txt)
txt = create_method()
self.classes_buf.out(0, txt)
# Fake <type 'namedtuple'>
if version[0] >= 3 or (version[0] == 2 and version[1] >= 6):

View File

@@ -115,6 +115,25 @@ class __function(object):
"""
return txt
def create_method():
txt = """
class __method(object):
'''A mock class representing method type.'''
def __init__(self):
"""
if version[0] == 2:
txt += """
self.im_class = None
self.im_self = None
self.im_func = None
"""
if version[0] >= 3 or (version[0] == 2 and version[1] >= 6):
txt += """
self.__func__ = None
self.__self__ = None
"""
return txt
def _searchbases(cls, accum):
# logic copied from inspect.py

View File

@@ -60,6 +60,7 @@ public class PyNames {
public static final String FAKE_OLD_BASE = "___Classobj";
public static final String FAKE_GENERATOR = "__generator";
public static final String FAKE_FUNCTION = "__function";
public static final String FAKE_METHOD = "__method";
public static final String FAKE_NAMEDTUPLE = "__namedtuple";
public static final String FUTURE_MODULE = "__future__";
@@ -471,4 +472,33 @@ public class PyNames {
public static boolean isRightOperatorName(@Nullable String name) {
return name != null && name.matches("__r[a-z]+__");
}
/**
* Available in Python 3 and Python 2 starting from 2.6.
* <p/>
* Attributes {@code __doc__}, {@code __dict__} and {@code __module__} should be inherited from object.
*/
public static final ImmutableSet<String> FUNCTION_SPECIAL_ATTRIBUTES = ImmutableSet.of(
"__defaults__",
"__globals__",
"__closure__",
"__code__",
"__name__"
);
public static final ImmutableSet<String> LEGACY_FUNCTION_SPECIAL_ATTRIBUTES = ImmutableSet.of(
"func_defaults",
"func_globals",
"func_closure",
"func_code",
"func_name",
"func_doc",
"func_dict"
);
public static final ImmutableSet<String> PY3_ONLY_FUNCTION_SPECIAL_ATTRIBUTES = ImmutableSet.of("__annotations__", "__kwdefaults__");
public static final ImmutableSet<String> METHOD_SPECIAL_ATTRIBUTES = ImmutableSet.of("__func__", "__self__");
public static final ImmutableSet<String> LEGACY_METHOD_SPECIAL_ATTRIBUTES = ImmutableSet.of("im_func", "im_self", "im_class");
}

View File

@@ -30,6 +30,9 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.jetbrains.python.psi.PyFunction.Modifier.STATICMETHOD;
import static com.jetbrains.python.psi.PyUtil.as;
/**
* Type of a particular function that is represented as a {@link Callable} in the PSI tree.
*
@@ -74,20 +77,68 @@ public class PyFunctionType implements PyCallableType {
@Nullable PyExpression location,
@NotNull AccessDirection direction,
@NotNull PyResolveContext resolveContext) {
final PyClassTypeImpl functionType = PyBuiltinCache.getInstance(getCallable()).getObjectType(PyNames.FAKE_FUNCTION);
if (functionType == null) {
final PyClassType delegate = selectFakeType(location, resolveContext.getTypeEvalContext());
if (delegate == null) {
return Collections.emptyList();
}
return functionType.resolveMember(name, location, direction, resolveContext);
return delegate.resolveMember(name, location, direction, resolveContext);
}
@Override
public Object[] getCompletionVariants(String completionPrefix, PsiElement location, ProcessingContext context) {
final PyClassTypeImpl functionType = PyBuiltinCache.getInstance(getCallable()).getObjectType(PyNames.FAKE_FUNCTION);
if (functionType == null) {
final TypeEvalContext typeEvalContext = TypeEvalContext.userInitiated(location.getContainingFile());
final PyClassType delegate;
if (location instanceof PyReferenceExpression) {
delegate = selectFakeType(((PyReferenceExpression)location).getQualifier(), typeEvalContext);
}
else {
delegate = PyBuiltinCache.getInstance(getCallable()).getObjectType(PyNames.FAKE_FUNCTION);
}
if (delegate == null) {
return ArrayUtil.EMPTY_OBJECT_ARRAY;
}
return functionType.getCompletionVariants(completionPrefix, location, context);
return delegate.getCompletionVariants(completionPrefix, location, context);
}
/**
* Select either {@link PyNames#FAKE_FUNCTION} or {@link PyNames#FAKE_METHOD} fake class depending on concrete reference used and
* language level. Will fallback to fake function type.
*/
@Nullable
private PyClassTypeImpl selectFakeType(@Nullable PyExpression location, @NotNull TypeEvalContext context) {
if (location instanceof PyReferenceExpression && isBoundMethodReference(((PyReferenceExpression)location), context)) {
return PyBuiltinCache.getInstance(getCallable()).getObjectType(PyNames.FAKE_METHOD);
}
return PyBuiltinCache.getInstance(getCallable()).getObjectType(PyNames.FAKE_FUNCTION);
}
private boolean isBoundMethodReference(@NotNull PyReferenceExpression location, @NotNull TypeEvalContext context) {
final PyFunction function = as(getCallable(), PyFunction.class);
final boolean isNonStaticMethod = function != null && function.getContainingClass() != null && function.getModifier() != STATICMETHOD;
if (isNonStaticMethod) {
if (LanguageLevel.forElement(location).isOlderThan(LanguageLevel.PYTHON30)) {
return true;
}
if (location.isQualified()) {
//noinspection ConstantConditions
final PyType qualifierType = PyTypeChecker.toNonWeakType(context.getType(location.getQualifier()), context);
if (isInstanceType(qualifierType)) {
return true;
}
else if (qualifierType instanceof PyUnionType) {
for (PyType type : ((PyUnionType)qualifierType).getMembers()) {
if (isInstanceType(type)) {
return true;
}
}
}
}
}
return false;
}
private static boolean isInstanceType(@Nullable PyType type) {
return type instanceof PyClassType && !((PyClassType)type).isDefinition();
}
@Override

View File

@@ -652,6 +652,19 @@ class __function(object):
self.__name__ = ''
class __method(object):
'''A mock class representing method type.'''
def __init__(self):
self.im_class = None
self.im_self = None
self.im_func = None
self.__func__ = None
self.__self__ = None
class __namedtuple(tuple):
'''A mock base class for named tuples.'''

View File

@@ -613,14 +613,24 @@ class __function(object):
self.__dict__ = ''
self.__module__ = ''
self.__annotations__ = {}
self.__defaults__ = {}
self.__globals__ = {}
self.__kwdefaults__ = {}
self.__closure__ = None
self.__code__ = None
self.__name__ = ''
self.__annotations__ = {}
self.__kwdefaults__ = {}
class __method(object):
'''A mock class representing method type.'''
def __init__(self):
self.__func__ = None
self.__self__ = None
class __namedtuple(tuple):
'''A mock base class for named tuples.'''

View File

@@ -0,0 +1,5 @@
class MyClass(object):
def method(self):
pass
MyClass().method.__<caret>

View File

@@ -0,0 +1 @@
(lambda: 42).__<caret>

View File

@@ -0,0 +1,6 @@
class MyClass(object):
@staticmethod
def method(self):
pass
MyClass().method.__<caret>

View File

@@ -0,0 +1,5 @@
class MyClass(object):
def method(self):
pass
MyClass.method.__<caret>

View File

@@ -0,0 +1,10 @@
class MyClass(object):
def method(self):
pass
if True:
inst = MyClass()
else:
inst = unresolved
inst.method.__<caret>

View File

@@ -626,23 +626,80 @@ public class PythonCompletionTest extends PyTestCase {
}
// PY-4073
public void testSpecialFunctionAttributes() throws Exception {
setLanguageLevel(LanguageLevel.PYTHON27);
try {
List<String> suggested = doTestByText("def func(): pass; func.func_<caret>");
assertNotNull(suggested);
assertContainsElements(suggested,
"func_defaults", "func_globals", "func_closure",
"func_code", "func_name", "func_doc", "func_dict");
public void testFunctionSpecialAttributes() {
runWithLanguageLevel(LanguageLevel.PYTHON27, new Runnable() {
@Override
public void run() {
List<String> suggested = doTestByText("def func(): pass; func.func_<caret>");
assertNotNull(suggested);
assertContainsElements(suggested, PyNames.LEGACY_FUNCTION_SPECIAL_ATTRIBUTES);
suggested = doTestByText("def func(): pass; func.__<caret>");
assertNotNull(suggested);
assertContainsElements(suggested, "__defaults__", "__globals__", "__closure__",
"__code__", "__name__", "__doc__", "__dict__", "__module__");
assertDoesntContain(suggested, "__annotations__", "__kwdefaults__");
}
finally {
setLanguageLevel(null);
}
suggested = doTestByText("def func(): pass; func.__<caret>");
assertNotNull(suggested);
assertContainsElements(suggested, PyNames.FUNCTION_SPECIAL_ATTRIBUTES);
assertDoesntContain(suggested, PyNames.PY3_ONLY_FUNCTION_SPECIAL_ATTRIBUTES);
}
});
}
// PY-9342
public void testBoundMethodSpecialAttributes() {
List<String> suggested = doTestByText("{}.update.im_<caret>");
assertNotNull(suggested);
assertContainsElements(suggested, PyNames.LEGACY_METHOD_SPECIAL_ATTRIBUTES);
suggested = doTestByText("{}.update.__<caret>");
assertNotNull(suggested);
assertContainsElements(suggested, PyNames.METHOD_SPECIAL_ATTRIBUTES);
assertDoesntContain(suggested, PyNames.FUNCTION_SPECIAL_ATTRIBUTES);
}
// PY-9342
public void testWeakQualifierBoundMethodAttributes() {
assertUnderscoredMethodSpecialAttributesSuggested();
}
private void assertUnderscoredMethodSpecialAttributesSuggested() {
myFixture.configureByFile("completion/" + getTestName(true) + ".py");
myFixture.completeBasic();
final List<String> suggested = myFixture.getLookupElementStrings();
assertNotNull(suggested);
assertContainsElements(suggested, PyNames.METHOD_SPECIAL_ATTRIBUTES);
assertDoesntContain(suggested, PyNames.FUNCTION_SPECIAL_ATTRIBUTES);
}
// PY-9342
public void testUnboundMethodSpecialAttributes() {
runWithLanguageLevel(LanguageLevel.PYTHON27, new Runnable() {
@Override
public void run() {
assertUnderscoredMethodSpecialAttributesSuggested();
}
});
runWithLanguageLevel(LanguageLevel.PYTHON32, new Runnable() {
@Override
public void run() {
assertUnderscoredFunctionAttributesSuggested();
}
});
}
// PY-9342
public void testStaticMethodSpecialAttributes() {
assertUnderscoredFunctionAttributesSuggested();
}
// PY-9342
public void testLambdaSpecialAttributes() {
assertUnderscoredFunctionAttributesSuggested();
}
private void assertUnderscoredFunctionAttributesSuggested() {
myFixture.configureByFile("completion/" + getTestName(true) + ".py");
myFixture.completeBasic();
final List<String> suggested = myFixture.getLookupElementStrings();
assertNotNull(suggested);
assertContainsElements(suggested, PyNames.FUNCTION_SPECIAL_ATTRIBUTES);
assertDoesntContain(suggested, PyNames.METHOD_SPECIAL_ATTRIBUTES);
}
}