PY-76885 Conformance test failure: constructors_call_metaclass.py

(cherry picked from commit d4ae8eb0303ec141c9766599ede8be1d3bb538fb)

GitOrigin-RevId: 90a67c7cc6266752ab8cdfa3379e95123c3959f9
This commit is contained in:
Petr
2025-04-15 15:43:41 +02:00
committed by intellij-monorepo-bot
parent c52f4bbadf
commit 9b70a7cf4a
6 changed files with 180 additions and 15 deletions

View File

@@ -923,25 +923,55 @@ public final class PyCallExpressionHelper {
}
private static @NotNull List<? extends RatedResolveResult> resolveConstructors(@NotNull PyClassType type,
@Nullable PyExpression location,
@Nullable PyCallSiteExpression callSite,
@NotNull PyResolveContext resolveContext) {
final var metaclassDunderCall = resolveMetaclassDunderCall(type, location, resolveContext);
if (!metaclassDunderCall.isEmpty()) {
// When evaluating a constructor call, a type checker should first check if the class has a custom metaclass (a
// subclass of type) that defines a __call__ method. If so, it should evaluate the call of this method using the
// supplied arguments. If the metaclass is type, this step can be skipped.
//
// If the evaluated return type of the __call__ method indicates something other than an instance of the class
// being constructed, a type checker should assume that the metaclass __call__ method is overriding
// type.__call__ in some special manner, and it should not attempt to evaluate the __new__ or __init__
// methods on the class.
final var metaclassDunderCall = resolveMetaclassDunderCall(type, callSite, resolveContext);
final var context = resolveContext.getTypeEvalContext();
boolean skipNewAndInitEvaluation = StreamEx.of(metaclassDunderCall)
.map(RatedResolveResult::getElement)
.select(PyTypedElement.class)
.map(context::getType)
.select(PyCallableType.class)
.anyMatch(callableType -> {
if (isReturnTypeAnnotated(callableType, context)) {
PyType callType = callSite != null ? callableType.getCallType(context, callSite) : callableType.getReturnType(context);
return !(callType instanceof PyClassType classType && classType.getPyClass() == type.getPyClass());
}
return false;
});
if (skipNewAndInitEvaluation) {
return metaclassDunderCall;
}
final var context = resolveContext.getTypeEvalContext();
final var initAndNew = type.getPyClass().multiFindInitOrNew(true, context);
return ContainerUtil.map(preferInitOverNew(initAndNew), e -> new RatedResolveResult(PyReferenceImpl.getRate(e, context), e));
}
private static boolean isReturnTypeAnnotated(@NotNull PyCallableType callableType, @NotNull TypeEvalContext context) {
PyCallable callable = callableType.getCallable();
if (callable instanceof PyFunction function) {
PyExpression returnTypeAnnotation = PyTypingTypeProvider.getReturnTypeAnnotation(function, context);
return returnTypeAnnotation != null;
}
return false;
}
private static @NotNull Collection<? extends PyFunction> preferInitOverNew(@NotNull List<PyFunction> initAndNew) {
final MultiMap<String, PyFunction> functions = ContainerUtil.groupBy(initAndNew, PyFunction::getName);
return functions.containsKey(PyNames.INIT) ? functions.get(PyNames.INIT) : functions.values();
}
private static @NotNull List<? extends RatedResolveResult> resolveMetaclassDunderCall(@NotNull PyClassType type,
@Nullable PyExpression location,
@Nullable PyCallSiteExpression callSite,
@NotNull PyResolveContext resolveContext) {
final var context = resolveContext.getTypeEvalContext();
@@ -951,7 +981,7 @@ public final class PyCallExpressionHelper {
final PyClassType typeType = PyBuiltinCache.getInstance(type.getPyClass()).getTypeType();
if (metaClassType == typeType) return Collections.emptyList();
final var results = resolveDunderCall(metaClassType, location, resolveContext);
final var results = resolveDunderCall(metaClassType, callSite, resolveContext);
if (results.isEmpty()) return Collections.emptyList();
final Set<PsiElement> typeDunderCall =
@@ -959,13 +989,7 @@ public final class PyCallExpressionHelper {
? Collections.emptySet()
: ContainerUtil.map2SetNotNull(resolveDunderCall(typeType, null, resolveContext), RatedResolveResult::getElement);
return ContainerUtil.filter(
results,
it -> {
final var element = it.getElement();
return !typeDunderCall.contains(element) && !ParamHelper.isSelfArgsKwargsCallable(element, context);
}
);
return ContainerUtil.filter(results, it -> !typeDunderCall.contains(it.getElement()));
}
private static @NotNull List<? extends RatedResolveResult> resolveDunderCall(@NotNull PyClassLikeType type,

View File

@@ -15,7 +15,6 @@ callables_subtyping.py
classes_classvar.py
classes_override.py
constructors_call_init.py
constructors_call_metaclass.py
constructors_call_new.py
constructors_call_type.py
constructors_callable.py

View File

@@ -3492,6 +3492,67 @@ public class Py3TypeTest extends PyTestCase {
""");
}
public void testMetaclassHavingDunderCall() {
doTest("object", """
from typing import Self
class Meta(type):
def call(cls, *args, **kwargs) -> object: ...
__call__ = call
class MyClass(metaclass=Meta):
def __new__(cls, p) -> Self: ...
expr = MyClass()
""");
doTest("MyClass", """
from typing import Self
class Meta(type):
def __call__(cls): ...
class MyClass(metaclass=Meta):
def __new__(cls, p: int) -> Self: ...
expr = MyClass(1)
""");
doTest("MyClass", """
from typing import Self
class Meta(type):
def __call__[T](cls: type[T], *args, **kwargs) -> T: ...
class MyClass(metaclass=Meta):
def __new__(cls, p) -> Self: ...
expr = MyClass(1)
""");
doTest("int", """
from typing import Self
class Meta(type):
def __call__[T](cls, x: T) -> T: ...
class MyClass(metaclass=Meta):
def __new__(cls, p1, p2) -> Self: ...
expr = MyClass(1)
""");
}
private void doTest(final String expectedType, final String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class);

View File

@@ -135,7 +135,7 @@ class PyNavigationTest : PyTestCase() {
myFixture.configureByText(
"a.py",
"class MyMeta(type):\n" +
" def __call__(self, p1, p2):\n" +
" def __call__(self, p1, p2) -> object:\n" +
" pass\n" +
"class MyClass(metaclass=MyMeta):\n" +
" def __init__(self, p3, p4):\n" +

View File

@@ -401,4 +401,65 @@ public class Py3ArgumentListInspectionTest extends PyInspectionTestCase {
"""
);
}
public void testMetaclassHavingDunderCall() {
doTestByText("""
from typing import Self
class Meta(type):
def call(cls, *args, **kwargs) -> object: ...
__call__ = call
class MyClass(metaclass=Meta):
def __new__(cls, p) -> Self: ...
expr = MyClass()
""");
doTestByText("""
from typing import Self
class Meta(type):
def __call__(cls): ...
class MyClass(metaclass=Meta):
def __new__(cls, p) -> Self: ...
c = MyClass(<warning descr="Parameter 'p' unfilled">)</warning>
""");
doTestByText("""
from typing import Self
class Meta(type):
def __call__[T](cls: type[T], *args, **kwargs) -> T: ...
class MyClass(metaclass=Meta):
def __new__(cls, p) -> Self: ...
c = MyClass(<warning descr="Parameter 'p' unfilled">)</warning>
""");
doTestByText("""
from typing import Self
class Meta(type):
def __call__[T](cls, x: T) -> T: ...
class MyClass(metaclass=Meta):
def __new__(cls, p1, p2) -> Self: ...
c = MyClass(1)
""");
}
}

View File

@@ -416,11 +416,31 @@ public class PyArgumentListInspectionTest extends PyInspectionTestCase {
// PY-17877
public void testMetaclassHavingDunderCall() {
// TODO Metaclass's `__call__` has to be validated as well as `__init__` / `__new__`
//runWithLanguageLevel(
// LanguageLevel.getLatest(),
// () -> doTestByText("""
// class MetaFoo(type):
// def __call__(cls, p3, p4):
// print(f'MetaFoo.__call__: {cls}, {p3}, {p4}')
//
// class Foo(metaclass=MetaFoo):
// pass
//
// class SubFoo(Foo):
// def __new__(self, p1, p2):
// # This never gets called
// print(f'SubFoo.__new__: {p1}, {p2}')
//
// sub = SubFoo(1<warning descr="Parameter 'p4' unfilled">)</warning>
// foo = Foo(3<warning descr="Parameter 'p4' unfilled">)</warning>""")
//);
runWithLanguageLevel(
LanguageLevel.getLatest(),
() -> doTestByText("""
class MetaFoo(type):
def __call__(cls, p3, p4):
# type: (...) -> object
print(f'MetaFoo.__call__: {cls}, {p3}, {p4}')
class Foo(metaclass=MetaFoo):