diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml
index 91691a259553..c7ba406e7bca 100644
--- a/python/pluginResources/intellij.python.community.impl.xml
+++ b/python/pluginResources/intellij.python.community.impl.xml
@@ -669,6 +669,8 @@
+
+
diff --git a/python/python-psi-impl/resources/messages/PyPsiBundle.properties b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
index 93c08f396718..8f2621d46a31 100644
--- a/python/python-psi-impl/resources/messages/PyPsiBundle.properties
+++ b/python/python-psi-impl/resources/messages/PyPsiBundle.properties
@@ -936,6 +936,7 @@ INSP.unresolved.refs.class.object.has.no.attribute=''{0}'' object has no attribu
INSP.unresolved.refs.import.resolves.to.its.containing.file=Import resolves to its containing file
INSP.unresolved.refs.class.does.not.define.operator=Class ''{0}'' does not define ''{1}'', so the ''{2}'' operator cannot be used on its instances
INSP.unresolved.refs.ignore.references.label=Ignored references:
+INSP.unresolved.refs.unresolved.attribute.in.union.type=Some members of ''{0}'' don''t have attribute ''{1}''
unresolved.docstring.param.reference=Function ''{0}'' does not have a parameter ''{1}''
unresolved.import.reference=No module named ''{0}''
diff --git a/python/python-psi-impl/src/com/jetbrains/python/documentation/PyTypeRenderer.java b/python/python-psi-impl/src/com/jetbrains/python/documentation/PyTypeRenderer.java
index 6efa1e553d87..fdf9d8d6cc76 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/documentation/PyTypeRenderer.java
+++ b/python/python-psi-impl/src/com/jetbrains/python/documentation/PyTypeRenderer.java
@@ -297,7 +297,7 @@ public abstract class PyTypeRenderer extends PyTypeVisitorExt<@NotNull HtmlChunk
if (ContainerUtil.all(unionType.getMembers(), t -> t instanceof PyClassType ct && ct.isDefinition())) {
return wrapInTypingType(render(unionType.map(type -> type != null ? ((PyClassType)type).toInstance() : null)));
}
- if (PyTypeChecker.isUnknown(unionType, false, myTypeEvalContext)) {
+ if (unionType.isWeak()) {
// Always put Any at the end of the union
return renderUnion(List.of(render(unionType.excludeNull()), visitUnknownType()));
}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesVisitor.java
index f4c5e3960e3a..f3befebd9c19 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesVisitor.java
+++ b/python/python-psi-impl/src/com/jetbrains/python/inspections/unresolvedReference/PyUnresolvedReferencesVisitor.java
@@ -12,6 +12,7 @@ import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.Version;
+import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
@@ -26,6 +27,7 @@ import com.jetbrains.python.codeInsight.PyCustomMember;
import com.jetbrains.python.codeInsight.PySubstitutionChunkReference;
import com.jetbrains.python.codeInsight.controlflow.PyDataFlowKt;
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider;
+import com.jetbrains.python.documentation.PythonDocumentationProvider;
import com.jetbrains.python.documentation.docstrings.DocStringParameterReference;
import com.jetbrains.python.documentation.docstrings.DocStringTypeReference;
import com.jetbrains.python.inspections.PyInspectionExtension;
@@ -152,6 +154,29 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
!isContainingFileImportAllowed(node, (PsiFile)target)) {
registerProblem(node, PyPsiBundle.message("INSP.unresolved.refs.import.resolves.to.its.containing.file"));
}
+ else if (Registry.is("python.typing.strict.unions", true) && node instanceof PyQualifiedExpression qualifiedExpression) {
+ String referencedName = qualifiedExpression.getReferencedName();
+ PyExpression qualifier = qualifiedExpression.getQualifier();
+ if (referencedName != null && qualifier != null) {
+ PyType qualifierType = myTypeEvalContext.getType(qualifier);
+ if (qualifierType instanceof PyUnionType unionType) {
+ boolean unresolvedInSomeUnionMembers = ContainerUtil.exists(unionType.getMembers(), t -> {
+ return !PyTypeChecker.isUnknown(t, false, myTypeEvalContext) &&
+ ContainerUtil.isEmpty(t.resolveMember(referencedName, qualifiedExpression, AccessDirection.READ, getResolveContext()));
+ });
+ if (unresolvedInSomeUnionMembers) {
+ String qualifierTypeRender = PythonDocumentationProvider.getTypeName(qualifierType, myTypeEvalContext);
+ registerProblem(
+ node,
+ PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.in.union.type", qualifierTypeRender, referencedName),
+ ProblemHighlightType.WEAK_WARNING,
+ null,
+ reference.getRangeInElement()
+ );
+ }
+ }
+ }
+ }
}
private boolean isAwaitCallToImportedNonAsyncFunction(@NotNull PsiReference reference) {
@@ -285,6 +310,11 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
description = PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.for.class", refText, type.getName());
}
}
+ else if (unresolvedInUnionTypeContainingUnknownMembers(type, refName)) {
+ String typeRender = PythonDocumentationProvider.getTypeName(type, myTypeEvalContext);
+ description = PyPsiBundle.message("INSP.unresolved.refs.unresolved.attribute.in.union.type", typeRender, refName);
+ severity = HighlightSeverity.WEAK_WARNING;
+ }
else {
description = PyPsiBundle.message("INSP.unresolved.refs.cannot.find.reference.in.type", refText, type.getName());
}
@@ -310,6 +340,9 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
if (severity == HighlightSeverity.WARNING) {
hlType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING;
}
+ if (severity == HighlightSeverity.WEAK_WARNING) {
+ hlType = ProblemHighlightType.WEAK_WARNING;
+ }
else if (severity == HighlightSeverity.ERROR) {
hlType = ProblemHighlightType.GENERIC_ERROR;
}
@@ -354,6 +387,12 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
if (type instanceof PyTypeVarType typeVarType) {
return typeVarType.getBound() == null && typeVarType.getDefaultType() == null && typeVarType.getConstraints().isEmpty();
}
+ if (type instanceof PyUnionType unionType) {
+ if (unresolvedInUnionTypeContainingUnknownMembers(unionType, name)) {
+ return false;
+ }
+ return ContainerUtil.exists(unionType.getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name));
+ }
if (PyTypeChecker.isUnknown(type, myTypeEvalContext)) {
// this almost always means that we don't know the type, so don't show an error in this case
return true;
@@ -403,9 +442,6 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
return true;
}
}
- if (type instanceof PyUnionType) {
- return ContainerUtil.exists(((PyUnionType)type).getMembers(), member -> ignoreUnresolvedMemberForType(member, reference, name));
- }
if (type instanceof PyModuleType) {
final PyFile module = ((PyModuleType)type).getModule();
if (module.getLanguageLevel().isAtLeast(LanguageLevel.PYTHON37)) {
@@ -420,6 +456,17 @@ public abstract class PyUnresolvedReferencesVisitor extends PyInspectionVisitor
return false;
}
+ private boolean unresolvedInUnionTypeContainingUnknownMembers(@NotNull PyType type, @NotNull String name) {
+ if (!(type instanceof PyUnionType unionType)) {
+ return false;
+ }
+ boolean unresolvedInSomeUnionMembers = ContainerUtil.exists(unionType.getMembers(), t -> {
+ return !PyTypeChecker.isUnknown(t, false, myTypeEvalContext) &&
+ ContainerUtil.isEmpty(t.resolveMember(name, null, AccessDirection.READ, getResolveContext()));
+ });
+ return unresolvedInSomeUnionMembers && unionType.isWeak();
+ }
+
private boolean isDecoratedAsDynamic(@NotNull PyClass cls, boolean inherited) {
if (inherited) {
if (isDecoratedAsDynamic(cls, false)) {
diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyABCUtil.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyABCUtil.java
index d19b04f79bf8..4093b2d279ee 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyABCUtil.java
+++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyABCUtil.java
@@ -15,6 +15,7 @@
*/
package com.jetbrains.python.psi.types;
+import com.intellij.openapi.util.registry.Registry;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.psi.PyClass;
import org.jetbrains.annotations.NotNull;
@@ -124,7 +125,10 @@ public final class PyABCUtil {
}
}
if (type instanceof PyUnionType) {
- return PyTypeUtil.toStream(type).nonNull().anyMatch(it -> isSubtype(it, superClassName, context));
+ if (!Registry.is("python.typing.strict.unions", true)) {
+ return PyTypeUtil.toStream(type).nonNull().anyMatch(it -> isSubtype(it, superClassName, context));
+ }
+ return PyTypeUtil.toStream(type).nonNull().allMatch(it -> isSubtype(it, superClassName, context));
}
return false;
}
diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java
index 140212f1f415..f8dc63ebc375 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java
+++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypeChecker.java
@@ -2,6 +2,7 @@
package com.jetbrains.python.psi.types;
import com.intellij.openapi.util.*;
+import com.intellij.openapi.util.registry.Registry;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.ArrayUtil;
@@ -466,12 +467,13 @@ public final class PyTypeChecker {
}
}
- // checking strictly separately until PY-24834 gets implemented
- if (ContainerUtil.exists(actual.getMembers(), x -> x instanceof PyLiteralStringType || x instanceof PyLiteralType)) {
- return ContainerUtil.and(actual.getMembers(), type -> match(expected, type, context).orElse(false));
+ if (!Registry.is("python.typing.strict.unions", true)) {// checking strictly separately until PY-24834 gets implemented
+ if (ContainerUtil.exists(actual.getMembers(), x -> x instanceof PyLiteralStringType || x instanceof PyLiteralType)) {
+ return ContainerUtil.and(actual.getMembers(), type -> match(expected, type, context).orElse(false));
+ }
+ return ContainerUtil.or(actual.getMembers(), type -> match(expected, type, context).orElse(false));
}
-
- return ContainerUtil.or(actual.getMembers(), type -> match(expected, type, context).orElse(false));
+ return ContainerUtil.and(actual.getMembers(), type -> match(expected, type, context).orElse(false));
}
private static @NotNull Optional match(@NotNull PyTupleType expected, @NotNull PyUnionType actual, @NotNull MatchContext context) {
@@ -1039,11 +1041,10 @@ public final class PyTypeChecker {
}
}
if (type instanceof PyUnionType union) {
- for (PyType t : union.getMembers()) {
- if (isUnknown(t, genericsAreUnknown, context)) {
- return true;
- }
+ if (!Registry.is("python.typing.strict.unions", true)) {
+ return ContainerUtil.exists(union.getMembers(), member -> isUnknown(member, genericsAreUnknown, context));
}
+ return ContainerUtil.all(union.getMembers(), member -> isUnknown(member, genericsAreUnknown, context));
}
return false;
}
@@ -1418,37 +1419,52 @@ public final class PyTypeChecker {
@Nullable PyType actualType,
@NotNull GenericSubstitutions substitutions,
@NotNull TypeEvalContext context) {
- // TODO find out a better way to pass the corresponding function inside
- final PyParameter param = paramWrapper.getParameter();
- final PyFunction function = as(ScopeUtil.getScopeOwner(param), PyFunction.class);
- assert function != null;
- if (function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD) {
- actualType = PyTypeUtil.toStream(actualType)
- .select(PyClassLikeType.class)
- .map(PyClassLikeType::toClass)
- .select(PyType.class)
- .foldLeft(PyUnionType::union)
- .orElse(actualType);
- }
- else if (PyUtil.isInitMethod(function)) {
- actualType = PyTypeUtil.toStream(actualType)
- .select(PyInstantiableType.class)
- .map(PyInstantiableType::toInstance)
- .select(PyType.class)
- .foldLeft(PyUnionType::union)
- .orElse(actualType);
- }
+ // TODO find out a better way to pass the corresponding function inside
+ final PyParameter param = paramWrapper.getParameter();
+ final PyFunction function = as(ScopeUtil.getScopeOwner(param), PyFunction.class);
+ assert function != null;
+ if (function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD) {
+ actualType = PyTypeUtil.toStream(actualType)
+ .select(PyClassLikeType.class)
+ .map(PyClassLikeType::toClass)
+ .select(PyType.class)
+ .foldLeft(PyUnionType::union)
+ .orElse(actualType);
+ }
+ else if (PyUtil.isInitMethod(function)) {
+ actualType = PyTypeUtil.toStream(actualType)
+ .select(PyInstantiableType.class)
+ .map(PyInstantiableType::toInstance)
+ .select(PyType.class)
+ .foldLeft(PyUnionType::union)
+ .orElse(actualType);
+ }
+ if (Registry.is("python.typing.strict.unions", true)) {
+ PyClass pyClass = function.getContainingClass();
+ assert pyClass != null;
+ PyClassLikeType classType = as(context.getType(pyClass), PyClassLikeType.class);
+ assert classType != null;
+ PyClassLikeType superType =
+ function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD || PyUtil.isNewMethod(function) ? classType : classType.toInstance();
+ // In a union receiver type, leave only members that actually have this function
+ // TODO how does it work with qualified calls, e.g. SomeClass.method(receiver, arg1, arg2)
+ // TODO how does it work with @classmethods?
+ actualType = PyTypeUtil.toStream(actualType)
+ .filter(type -> match(superType, type, context))
+ .collect(PyTypeUtil.toUnion());
+ }
- PyClass containingClass = function.getContainingClass();
- assert containingClass != null;
- PyType genericClass = findGenericDefinitionType(containingClass, context);
- if (genericClass instanceof PyInstantiableType> instantiableType && (isNewMethod(function) || function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD)) {
- genericClass = instantiableType.toClass();
- }
- if (genericClass != null && !match(genericClass, expectedType, context, substitutions)) {
- return null;
- }
- return actualType;
+ PyClass containingClass = function.getContainingClass();
+ assert containingClass != null;
+ PyType genericClass = findGenericDefinitionType(containingClass, context);
+ if (genericClass instanceof PyInstantiableType> instantiableType &&
+ (isNewMethod(function) || function.getModifier() == PyAstFunction.Modifier.CLASSMETHOD)) {
+ genericClass = instantiableType.toClass();
+ }
+ if (genericClass != null && !match(genericClass, expectedType, context, substitutions)) {
+ return null;
+ }
+ return actualType;
}
private static boolean matchParameterArgumentTypes(@NotNull PyCallableParameter paramWrapper,
diff --git a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt
index 39aa2cf177f3..e528b6a5e3af 100644
--- a/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt
+++ b/python/python-psi-impl/src/com/jetbrains/python/psi/types/PyTypedDictType.kt
@@ -1,6 +1,7 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.psi.types
+import com.intellij.openapi.util.registry.Registry
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.python.PyNames
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
@@ -206,7 +207,10 @@ class PyTypedDictType @JvmOverloads constructor(
}
private fun strictUnionMatch(expected: PyType?, actual: PyType?, context: TypeEvalContext): Boolean {
- return PyTypeUtil.toStream(actual).allMatch { type -> PyTypeChecker.match(expected, type, context) }
+ if (!Registry.`is`("python.typing.strict.unions", true)) {
+ return PyTypeUtil.toStream(actual).allMatch { type -> PyTypeChecker.match(expected, type, context) }
+ }
+ return PyTypeChecker.match(expected, actual, context)
}
/**
diff --git a/python/testData/inspections/PyTypeCheckerInspection/ForLoopIteration.py b/python/testData/inspections/PyTypeCheckerInspection/ForLoopIteration.py
index 807bc4926a77..2196ff6c65e3 100644
--- a/python/testData/inspections/PyTypeCheckerInspection/ForLoopIteration.py
+++ b/python/testData/inspections/PyTypeCheckerInspection/ForLoopIteration.py
@@ -5,7 +5,7 @@ def test(p1):
for x in 42:
pass
- for x in f('foo', p1):
+ for x in f('foo', p1):
pass
diff --git a/python/testData/inspections/PyTypeCheckerInspection/IterateOverDictValueWhenItsTypeIsUnion.py b/python/testData/inspections/PyTypeCheckerInspection/IterateOverDictValueWhenItsTypeIsUnion.py
index 7f786045c9d1..665923f96eaf 100644
--- a/python/testData/inspections/PyTypeCheckerInspection/IterateOverDictValueWhenItsTypeIsUnion.py
+++ b/python/testData/inspections/PyTypeCheckerInspection/IterateOverDictValueWhenItsTypeIsUnion.py
@@ -3,5 +3,5 @@ KWARGS = {
"little_list": ['WORLD_RET_BP_IMPALA_AB.Control', 'WORLD_RET_BP_IMPALA_AB.Impala_WS'],
}
-for element in KWARGS["little_list"]:
+for element in KWARGS["little_list"]:
print(element)
\ No newline at end of file
diff --git a/python/testData/inspections/PyTypeCheckerInspection/ListLiteralAgainstTypingLiteral.py b/python/testData/inspections/PyTypeCheckerInspection/ListLiteralAgainstTypingLiteral.py
index 29a5800a4451..2799f9a3d9cf 100644
--- a/python/testData/inspections/PyTypeCheckerInspection/ListLiteralAgainstTypingLiteral.py
+++ b/python/testData/inspections/PyTypeCheckerInspection/ListLiteralAgainstTypingLiteral.py
@@ -15,5 +15,4 @@ list_union_literal_incorrect: List[Union[L2, L1]] = ['a', 'r']
list_tuple: List[Tuple[L2, L1]] = [('b', 'test'), (5, 'test')]
-# TODO false negative due to PY-24834
-list_tuple_incorrect: List[Tuple[L2, L1]] = [('a',), (5, 'test')]
\ No newline at end of file
+list_tuple_incorrect: List[Tuple[L2, L1]] = [('a',), (5, 'test')]
\ No newline at end of file
diff --git a/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py b/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py
index 4f180addc5c1..0c32344b04da 100644
--- a/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py
+++ b/python/testData/inspections/PyTypeCheckerInspection/MapReturnElementType.py
@@ -1,5 +1,5 @@
def test():
xs = map(lambda x: x + 1, [1, 2, 3])
- print('foo' + xs[0])
+ print('foo' + xs[0])
ys = map(tuple, iter([[1, 2, 3]]))
print(1 + ys[0], 'bar' + ys[1])
diff --git a/python/testData/inspections/PyTypeCheckerInspection/NotNone.py b/python/testData/inspections/PyTypeCheckerInspection/NotNone.py
index dd9f2d8806ab..2f5942613b9c 100644
--- a/python/testData/inspections/PyTypeCheckerInspection/NotNone.py
+++ b/python/testData/inspections/PyTypeCheckerInspection/NotNone.py
@@ -14,8 +14,8 @@ def test():
x1 = f1()
x2 = f2()
x3 = 1
- f(x1) # Weaker union types
- f(x2) # Weaker union types
+ f(x1)
+ f(x2)
f(x3)
if x1:
f(x1)
diff --git a/python/testData/inspections/PyTypeCheckerInspection/SetLiteralAgainstTypingLiteral.py b/python/testData/inspections/PyTypeCheckerInspection/SetLiteralAgainstTypingLiteral.py
index 23e2a28ea656..a903a0af30ce 100644
--- a/python/testData/inspections/PyTypeCheckerInspection/SetLiteralAgainstTypingLiteral.py
+++ b/python/testData/inspections/PyTypeCheckerInspection/SetLiteralAgainstTypingLiteral.py
@@ -15,5 +15,4 @@ set_union_literal_incorrect: Set[Union[L2, L1]] = {'a', 'r'}
set_of_tuple_and_list: Set[Union[Tuple[L2, L1], List[L1]]] = {('b', 'test'), ['test', 'test'], (5, 'test')}
-# TODO false negative due to PY-24834
-set_of_tuple_and_list_incorrect: Set[Union[Tuple[L2, L1], List[L1]]] = {('b', 'r'), ['test', 'test'], (5, 'test')}
\ No newline at end of file
+set_of_tuple_and_list_incorrect: Set[Union[Tuple[L2, L1], List[L1]]] = {('b', 'r'), ['test', 'test'], (5, 'test')}
\ No newline at end of file
diff --git a/python/testData/inspections/PyTypeCheckerInspection/StrictUnionImplicitProtocolMatching.py b/python/testData/inspections/PyTypeCheckerInspection/StrictUnionImplicitProtocolMatching.py
new file mode 100644
index 000000000000..90f071cfb01c
--- /dev/null
+++ b/python/testData/inspections/PyTypeCheckerInspection/StrictUnionImplicitProtocolMatching.py
@@ -0,0 +1,51 @@
+from typing import Any
+
+
+class A:
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ return 42
+
+
+class B:
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ return 42
+
+
+class C:
+ pass
+
+
+def all_union_members_match_no_any(iterable: A | B):
+ for _ in iterable:
+ pass
+
+
+def some_union_members_match_no_any(iterable: A | B | None):
+ for _ in iterable:
+ pass
+
+
+def all_union_members_dont_match_no_any(iterable: C | None):
+ for _ in iterable:
+ pass
+
+
+def all_union_members_match_with_any(iterable: A | B | Any):
+ for _ in iterable:
+ pass
+
+
+def some_union_members_match_with_any(iterable: A | B | None | Any):
+ for _ in iterable:
+ pass
+
+
+def all_union_members_dont_match_with_any(iterable: C | None | Any):
+ for _ in iterable:
+ pass
diff --git a/python/testData/inspections/PyTypeCheckerInspection/TypeAssertions.py b/python/testData/inspections/PyTypeCheckerInspection/TypeAssertions.py
index 99a34191b5d4..50307998814c 100644
--- a/python/testData/inspections/PyTypeCheckerInspection/TypeAssertions.py
+++ b/python/testData/inspections/PyTypeCheckerInspection/TypeAssertions.py
@@ -37,8 +37,8 @@ def test():
:type x: int or str
"""
x_1 = f_1()
- print_int(x_1) # Weaker union types
- print_int_or_str(x_1) # Weaker union types
+ print_int(x_1)
+ print_int_or_str(x_1)
if isinstance(x_1, int):
print_int(x_1)
if isinstance(x_1, str):
diff --git a/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py b/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py
index 0a9a0263507b..5e146505ae5c 100644
--- a/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py
+++ b/python/testData/inspections/PyTypeCheckerInspection/UnionReturnTypes.py
@@ -16,7 +16,7 @@ def test(c):
:type x: int
"""
x1 = f1(c)
- f2(x1) # Weaker union types
+ f2(x1)
f3(x1)
f2(x1.count(''))
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFun/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFun/a.py
index 7135b2811861..5cf05c2b8c2e 100644
--- a/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFun/a.py
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFun/a.py
@@ -6,7 +6,7 @@ async def expect_no_warning():
async def expect_new_warning():
- await fun_non_async()
+ await fun_non_async()
def local_fun_non_async():
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFunReturnAwaitable/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFunReturnAwaitable/a.py
index 403734c64dd0..fe94a40833d7 100644
--- a/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFunReturnAwaitable/a.py
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection/AsyncAwaitWarningOnImportedFunReturnAwaitable/a.py
@@ -2,7 +2,7 @@ from b import fun_awaitable_imported, MyAwaitable
async def expect_false_positive_warning():
- await fun_awaitable_imported()
+ await fun_awaitable_imported()
async def expect_pass_1():
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/CustomNewReturnInAnotherModule/a.py b/python/testData/inspections/PyUnresolvedReferencesInspection/CustomNewReturnInAnotherModule/a.py
index 5e6d4a745397..33d78e10e71f 100644
--- a/python/testData/inspections/PyUnresolvedReferencesInspection/CustomNewReturnInAnotherModule/a.py
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection/CustomNewReturnInAnotherModule/a.py
@@ -1,4 +1,4 @@
from b import C
c = C()
-c.foo()
+c.foo()
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py b/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py
index d736117e174f..b534020f25e7 100644
--- a/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection/listIndexedByUnknownType.py
@@ -1,6 +1,6 @@
def f(i):
xs = []
- xs[i].items()
+ xs[i].items()
def g(index):
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py
new file mode 100644
index 000000000000..a40b256e9844
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberAttributeAccess.py
@@ -0,0 +1,31 @@
+from typing import Any
+
+
+class A:
+ def method(self):
+ pass
+
+class B(A):
+ pass
+
+class C:
+ def method(self):
+ pass
+
+def union_with_all_compatible_types(x: A | B | C):
+ x.method()
+
+def union_with_some_incompatible_types(x: A | None):
+ x.method()
+
+def union_with_all_incompatible_types(x: object | None):
+ x.method()
+
+def union_with_some_incompatible_types_and_any(x: Any | None):
+ x.method()
+
+def narrowing_union_with_some_incompatible_types_after(x: Any | None):
+ if isinstance(x, A):
+ x.method()
+ assert isinstance(x, B)
+ x.method()
\ No newline at end of file
diff --git a/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py
new file mode 100644
index 000000000000..ddadd640ada4
--- /dev/null
+++ b/python/testData/inspections/PyUnresolvedReferencesInspection3K/strictUnionMemberOperatorAccess.py
@@ -0,0 +1,63 @@
+from typing import Any
+
+
+class A:
+ def __pos__(self):
+ pass
+
+ def __add__(self, other):
+ pass
+
+ def __getitem__(self, item):
+ pass
+
+
+class B:
+ def __pos__(self):
+ pass
+
+ def __add__(self, other):
+ pass
+
+ def __getitem__(self, item):
+ pass
+
+
+class C:
+ pass
+
+
+def all_union_members_match_no_any(x: A | B):
+ print(+x)
+ print(x + 1)
+ print(x[42])
+
+
+def some_union_members_match_no_any(x: A | B | None):
+ print(+x)
+ print(x + 1)
+ print(x[42])
+
+
+def all_union_members_dont_match_no_any(x: C | None):
+ print(+x)
+ print(x + 1)
+ print(x[42])
+
+
+def all_union_members_match_with_any(x: A | B | Any):
+ print(+x)
+ print(x + 1)
+ print(x[42])
+
+
+def some_union_members_match_with_any(x: A | B | None | Any):
+ print(+x)
+ print(x + 1)
+ print(x[42])
+
+
+def all_union_members_dont_match_with_any(x: C | None | Any):
+ print(+x)
+ print(x + 1)
+ print(x[42])
diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java
index 27a35457dd49..e437033f8ad7 100644
--- a/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java
+++ b/python/testSrc/com/jetbrains/python/inspections/Py3TypeCheckerInspectionTest.java
@@ -3085,7 +3085,7 @@ def foo(param: str | int) -> TypeGuard[str]:
call = empty
""");
}
-
+
public void testNoWarningIfUnreachable() {
doTestByText("""
def foo() -> int:
@@ -3093,4 +3093,9 @@ def foo(param: str | int) -> TypeGuard[str]:
return "42" # no warning here, because it is unreachable
""");
}
+
+ // PY-24834
+ public void testStrictUnionImplicitProtocolMatching() {
+ doTest();
+ }
}
diff --git a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java
index 453540b9383f..f82b97d6ace0 100644
--- a/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java
+++ b/python/testSrc/com/jetbrains/python/inspections/Py3UnresolvedReferencesInspectionTest.java
@@ -476,4 +476,14 @@ public class Py3UnresolvedReferencesInspectionTest extends PyInspectionTestCase
""");
}
+
+ // PY-24834
+ public void testStrictUnionMemberAttributeAccess() {
+ doTest();
+ }
+
+ // PY-24834
+ public void testStrictUnionMemberOperatorAccess() {
+ doTest();
+ }
}
diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java
index 089af5e572c4..8cce3b44e7e3 100644
--- a/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java
+++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeCheckerInspectionTest.java
@@ -1526,10 +1526,10 @@ public class PyTypeCheckerInspectionTest extends PyInspectionTestCase {
title: str
year: int
- movies1: list[Movie] = [
+ movies1: list[Movie] = [
{"title": "Blade Runner", "year": 1982}, # OK
{"title": "The Matrix"},
- ]
+ ]
movies2: list[Movie] = [
{"title": "The Matrix"},
]