Restrict matching against callables (PY-29993, PY-28364)

Expected class-like types are not considered as callables (exclusion: bare typing.Callable).
Fix matching definition against metaclass.

GitOrigin-RevId: a2fe59c3bc7f56ef08dfb960a9ed841a86a65815
This commit is contained in:
Semyon Proshev
2020-04-16 17:05:07 +00:00
committed by intellij-monorepo-bot
parent 9f32d57ace
commit 17d5a4bbce
3 changed files with 58 additions and 1 deletions
@@ -11,6 +11,7 @@ import com.jetbrains.python.PyNames;
import com.jetbrains.python.PythonRuntimeService;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.typing.PyProtocolsKt;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyTypeProvider;
@@ -450,6 +451,12 @@ public class PyTypeChecker {
private static Optional<Boolean> match(@NotNull PyCallableType expected,
@NotNull PyCallableType actual,
@NotNull MatchContext matchContext) {
if (expected instanceof PyClassLikeType) {
return PyTypingTypeProvider.CALLABLE.equals(((PyClassLikeType)expected).getClassQName())
? Optional.of(actual.isCallable())
: Optional.empty();
}
if (expected.isCallable() && actual.isCallable()) {
final TypeEvalContext context = matchContext.context;
final List<PyCallableParameter> expectedParameters = expected.getParameters(context);
@@ -489,7 +496,7 @@ public class PyTypeChecker {
return false;
}
final PyClassLikeType metaClass = actual.getMetaClassType(context.context, true);
return metaClass != null && match(expected, metaClass, context).orElse(true);
return metaClass != null && match(expected, metaClass.toInstance(), context).orElse(true);
}
private static boolean typeVarAcceptsBothClassAndInstanceTypes(@NotNull PyGenericType typeVar) {
@@ -6,6 +6,10 @@ class SubMetaClass(MetaClass):
pass
class MetaClass2(type):
pass
class MyClass(object):
__metaclass__ = MetaClass
pass
@@ -26,6 +30,15 @@ class MyClass3(Generated):
pass
class MyClass4:
__metaclass__ = MetaClass2
pass
class MyClass5:
pass
def f(x):
# type: (MetaClass) -> None
pass
@@ -35,3 +48,5 @@ f(MyClass)
f(MyClass2)
f(Generated)
f(MyClass3)
f(<warning descr="Expected type 'MetaClass', got 'Type[MyClass4]' instead">MyClass4</warning>)
f(<warning descr="Expected type 'MetaClass', got 'Type[MyClass5]' instead">MyClass5</warning>)
@@ -1083,4 +1083,39 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase {
public void testTypedDictInStub() {
runWithLanguageLevel(LanguageLevel.getLatest(), this::doMultiFileTest);
}
// PY-28364
public void testDefinitionAgainstCallableInstance() {
runWithLanguageLevel(
LanguageLevel.getLatest(),
() -> doTestByText("class B:\n" +
" def __call__(self, *args, **kwargs):\n" +
" pass\n" +
"\n" +
"def some_fn(arg: B):\n" +
" pass\n" +
"\n" +
"some_fn(<warning descr=\"Expected type 'B', got 'Type[B]' instead\">B</warning>)")
);
}
// PY-29993
public void testCallableInstanceAgainstOtherCallableInstance() {
runWithLanguageLevel(
LanguageLevel.getLatest(),
() -> doTestByText("class MyCls:\n" +
" def __call__(self):\n" +
" return True\n" +
"\n" +
"class DifferentCls:\n" +
" def __call__(self):\n" +
" return True\n" +
"\n" +
"def foo(arg: MyCls):\n" +
" pass\n" +
"\n" +
"foo(MyCls())\n" +
"foo(<warning descr=\"Expected type 'MyCls', got 'DifferentCls' instead\">DifferentCls()</warning>)")
);
}
}