PY-86002 Revert also PY-25989 PY-84544 to fix new test failures after the revert to weak unions

Namely, `generics_basic.py` in the conformance test suite and
Py3TypeCheckerInspectionTest.testTypeVarWidening.

Revert 07803c6243370d55df0d870e2e32b38170c1fb49


(cherry picked from commit 9c334ef32597be1d1e8fbc3251ac267be46555c8)

IJ-MR-184619

GitOrigin-RevId: 4b2440c973d9c7a7a42590665bd473c4e7a4c9cf
This commit is contained in:
Mikhail Golubev
2025-12-03 20:58:50 +00:00
committed by intellij-monorepo-bot
parent 7c1fa29f39
commit 060b900cf9
4 changed files with 20 additions and 81 deletions
@@ -280,8 +280,10 @@ public final class PyTypeChecker {
substitutedRef = null;
}
}
final PyType substitution = Ref.deref(substitutedRef);
PyType bound = expected.getBound();
List<@Nullable PyType> constraints = expected.getConstraints();
int matchedConstraintIndex = -1;
// Promote int in Type[TypeVar('T', int)] to Type[int] before checking that bounds match
if (expected.isDefinition()) {
bound = toClass(bound);
@@ -291,25 +293,6 @@ public final class PyTypeChecker {
// Remove value-specific components from the actual type to make it safe to propagate
PyType safeActual = constraints.isEmpty() && bound instanceof PyLiteralStringType ? actual : replaceLiteralStringWithStr(actual);
if (substitutedRef != null) {
final PyType substitution = substitutedRef.get();
if (expected.equals(safeActual) || expected.equals(substitution)) {
return true;
}
MyMatchHelper matchHelper = new MyMatchHelper(expected, context);
if (matchHelper.match(substitution, safeActual)) {
return true;
}
if (context.mySubstitutions.frozenTypeVars.contains(expected)) {
return false;
}
if (!matchHelper.match(safeActual, substitution)) {
safeActual = PyUnionType.union(safeActual, substitution);
}
}
int matchedConstraintIndex = -1;
if (constraints.isEmpty()) {
Optional<Boolean> match = match(bound, safeActual, context);
if (match.isPresent() && !match.get()) {
@@ -317,13 +300,25 @@ public final class PyTypeChecker {
}
}
else {
final PyType finalSafeActual = safeActual;
matchedConstraintIndex = ContainerUtil.indexOf(constraints, constraint -> match(constraint, finalSafeActual, context).orElse(true));
matchedConstraintIndex = ContainerUtil.indexOf(constraints, constraint -> match(constraint, safeActual, context).orElse(true));
if (matchedConstraintIndex == -1) {
return false;
}
}
if (substitutedRef != null) {
if (expected.equals(safeActual) || expected.equals(substitution)) {
return true;
}
Optional<Boolean> recursiveMatch = RecursionManager.doPreventingRecursion(
expected, false, context.reversedSubstitutions
? () -> match(safeActual, substitution, context)
: () -> match(substitution, safeActual, context)
);
return recursiveMatch != null ? recursiveMatch.orElse(false) : false;
}
if (safeActual != null) {
PyType type = constraints.isEmpty() ? safeActual : constraints.get(matchedConstraintIndex);
context.mySubstitutions.typeVars.put(expected, Ref.create(type));
@@ -338,25 +333,6 @@ public final class PyTypeChecker {
return true;
}
private static final class MyMatchHelper {
private final @NotNull Object myKey;
private final @NotNull MatchContext myContext;
MyMatchHelper(@NotNull Object key, @NotNull MatchContext context) {
myKey = key;
myContext = context;
}
boolean match(@Nullable PyType expected, @Nullable PyType actual) {
Optional<Boolean> result = RecursionManager.doPreventingRecursion(
myKey, false, myContext.reversedSubstitutions
? () -> PyTypeChecker.match(actual, expected, myContext)
: () -> PyTypeChecker.match(expected, actual, myContext)
);
return result != null && result.orElse(false);
}
}
private static @Nullable PyType toClass(@Nullable PyType type) {
return PyTypeUtil.toStream(type)
.map(t -> t instanceof PyInstantiableType<?> instantiableType ? instantiableType.toClass() : t)
@@ -1610,7 +1586,6 @@ public final class PyTypeChecker {
}
});
}
substitutions.frozenTypeVars = new HashSet<>(substitutions.typeVars.keySet());
return substitutions;
}
@@ -1920,6 +1895,8 @@ public final class PyTypeChecker {
@ApiStatus.Experimental
public static class GenericSubstitutions {
// Nullable-Nullable because of com.jetbrains.python.psi.types.PyTypeChecker.collectTypeSubstitutions
private final @NotNull Map<PyTypeVarType, @Nullable Ref<@Nullable PyType>> typeVars;
private final @NotNull Map<PyTypeVarTupleType, @Nullable PyPositionalVariadicType> typeVarTuples;
@@ -1928,8 +1905,6 @@ public final class PyTypeChecker {
private @Nullable PyType qualifierType;
private @NotNull Set<PyTypeVarType> frozenTypeVars = Collections.emptySet();
public GenericSubstitutions(@NotNull Map<? extends PyTypeParameterType, PyType> typeParameters) {
this(
EntryStream.of(typeParameters)
@@ -40,11 +40,7 @@ def test():
print(result)
print(result + <warning descr="Expected type 'int', got 'str' instead">'foo'</warning>)
# Bug: Expected error.
# Generics are considered to be covariant.
# I.e. `list[str]` is assignable to `list[int | str]`.
# Thus, substitution `T` -> `int | str` is considered valid.
f2(1, ['foo'], 'bar')
f2(1, <warning descr="Expected type 'List[int]' (matched generic type 'List[T]'), got 'List[str]' instead">['foo']</warning>, 'bar')
result = f3(1, 'foo', True)
f4(<warning descr="Expected type 'Tuple[bool, int, str]', got 'Tuple[int, str, bool]' instead">result</warning>)
@@ -3332,33 +3332,4 @@ public class Py3TypeCheckerInspectionTest extends PyInspectionTestCase {
var: Template = Concrete()
""");
}
// PY-25989 PY-84544
public void testTypeVarWidening() {
myFixture.enableInspections(PyAssertTypeInspection.class);
doTestByText("""
from collections.abc import Iterable
from typing import assert_type
# PY-84544
def foo(iterable: Iterable[int] | Iterable[str]) -> None:
assert_type(next(iter(iterable)), int | str)
# PY-25989
assert_type(max(1, 2.6), float)
assert_type(max(2.6, 1), float)
max(1, <warning descr="Expected type 'int' (matched generic type 'SupportsRichComparisonT ≤: SupportsDunderLT[Any] | SupportsDunderGT[Any]'), got 'object' instead">object()</warning>)
def bar[T: int, str](v1: T, v2: T) -> T:
if (bool(input())):
return v1
return v2
_ = bar(1, <warning descr="Expected type 'int' (matched generic type 'T ≤: int'), got 'str' instead">"a"</warning>)
""");
}
}
@@ -1347,10 +1347,7 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase {
def accepts_anything(x: str) -> None:
pass
# Bug: Expected error.
# `Callable[[str], None]` is assignable to `Callable[[int | str], None]`.
# Thus, substitution `T` -> `int | str` is considered valid.
func(42, accepts_anything)""")
func(42, <warning descr="Expected type '(int) -> None' (matched generic type '(T) -> None'), got '(x: str) -> None' instead">accepts_anything</warning>)""")
);
}