[python] Special-case typing.Generic while calculating a class MRO

typing.Generic is a magical class that can be specified in any position
in the list of base classes, not affecting the MRO consistency. It's done by
the custom __mro_entries__ implementation in typing._BaseGenericAlias (Python < 3.12),
which skips this Generic entry if there are other generic classes following
it on the list of superclasses. Namely, it's possible to do the following:

```
class Base(Generic[T]):
    pass

class MyClass(Generic[T], Base[T]):
    pass
```

which would cause a TypeError for regular classes. Since it broke our implementation
of the C3 algorithm in PyClassImpl.getMROAncestorTypes, we now special-case it by
always moving typing.Generic to the very end of the base class list while constructing
MRO.

See https://github.com/python/cpython/blob/3.11/Lib/typing.py#L1298 for a pure-Python
version of typing._BaseGenericAlias.__mro_entries__ and a relevant discussion in
https://github.com/python/cpython/issues/106102.

GitOrigin-RevId: e7d765193d532ab8457133e8fb5ad06840d89225
This commit is contained in:
Mikhail Golubev
2024-10-01 11:38:26 +03:00
committed by intellij-monorepo-bot
parent 5622587ae3
commit 190a55438e
3 changed files with 31 additions and 5 deletions

View File

@@ -25,15 +25,13 @@ import com.intellij.util.IncorrectOperationException;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.PyStubElementTypes;
import com.jetbrains.python.PythonDialectsTokenSetProvider;
import com.jetbrains.python.*;
import com.jetbrains.python.ast.PyAstFunction.Modifier;
import com.jetbrains.python.ast.impl.PyUtilCore;
import com.jetbrains.python.codeInsight.controlflow.ControlFlowCache;
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
import com.jetbrains.python.documentation.docstrings.DocStringUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.stubs.PyClassElementType;
@@ -383,7 +381,7 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
cache.put(type, Ref.create());
List<PyClassLikeType> result = null;
try {
final List<PyClassLikeType> bases = removeNotNullDuplicates(type.getSuperClassTypes(context));
final List<PyClassLikeType> bases = moveTypingGenericToTheEnd(removeNotNullDuplicates(type.getSuperClassTypes(context)));
final List<List<PyClassLikeType>> lines = new ArrayList<>();
for (PyClassLikeType base : bases) {
if (base != null) {
@@ -424,6 +422,20 @@ public class PyClassImpl extends PyBaseElementImpl<PyClassStub> implements PyCla
return result;
}
private static @NotNull List<PyClassLikeType> moveTypingGenericToTheEnd(@NotNull List<PyClassLikeType> bases) {
int genericPos = ContainerUtil.indexOf(bases,
base -> base instanceof PyCustomType customType &&
PyTypingTypeProvider.GENERIC.equals(customType.getClassQName()));
if (genericPos >= 0) {
List<PyClassLikeType> reorderedBases = new ArrayList<>(bases.size());
reorderedBases.addAll(bases.subList(0, genericPos));
reorderedBases.addAll(bases.subList(genericPos + 1, bases.size()));
reorderedBases.add(bases.get(genericPos));
return reorderedBases;
}
return bases;
}
@NotNull
private static <T> List<T> removeNotNullDuplicates(@NotNull List<T> list) {
final Set<T> distinct = new HashSet<>();

View File

@@ -0,0 +1,9 @@
from typing import Generic, TypeVar
T = TypeVar('T')
class Base(Generic[T]):
pass
class MyClass(Generic[T], Base[T]):
pass

View File

@@ -152,6 +152,11 @@ public class PyClassMROTest extends PyTestCase {
});
}
public void testTypingGenericAsFirstBaseClass() {
PyClass pyClass = getClass("MyClass");
assertMRO(pyClass, "Base", "Generic", "object");
}
// PY-21837
public void testClassImportedFromUnstubbedFileAndSuperImportedWithAs() {
myFixture.copyDirectoryToProject("codeInsight/classMRO/" + getTestName(false), "");