diff --git a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.kt b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.kt index 23fbbc35cb66..720073c9e0bb 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.kt +++ b/python/python-psi-impl/src/com/jetbrains/python/codeInsight/typing/PyTypingTypeProvider.kt @@ -1190,7 +1190,7 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { } private fun typeHasOverloadedBitwiseOr( - type: PyType, expression: PyExpression, + type: PyType, expression: PyExpression, operator: String = "__or__", context: Context, ): Boolean { if (type !is PyClassType) { @@ -1202,7 +1202,7 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { return false } val resolved = metaClassType - .resolveMember("__or__", expression, AccessDirection.READ, PyResolveContext.defaultContext(typeContext)) + .resolveMember(operator, expression, AccessDirection.READ, PyResolveContext.defaultContext(typeContext)) if (resolved.isNullOrEmpty()) return false return StreamEx.of(resolved) @@ -1279,11 +1279,11 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { if (neverType != null) { return Ref(neverType) } - val unionType: Ref? = getUnionType(resolved, context) + val unionType = getUnionType(resolved, context) if (unionType != null) { return unionType } - val intersectionType: Ref? = getIntersectionType(resolved, context) + val intersectionType = getIntersectionType(resolved, context) if (intersectionType != null) { return intersectionType } @@ -1467,19 +1467,16 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { } private fun getIntersectionType(resolved: PsiElement, context: Context): Ref? { - if (resolved is PyBinaryExpression && resolved.operator === PyTokenTypes.AND) { - val left = resolved.leftExpression - val right = resolved.rightExpression - if (left == null || right == null) return null + if (resolved !is PyBinaryExpression || resolved.operator !== PyTokenTypes.AND) return null + val left = resolved.leftExpression ?: return null + val right = resolved.rightExpression ?: return null - val leftTypeRef: Ref? = getType(left, context) - val rightTypeRef: Ref? = getType(right, context) - if (leftTypeRef == null || rightTypeRef == null) return null + val leftTypeRef = getType(left, context) + val rightTypeRef = getType(right, context) + if (leftTypeRef == null && rightTypeRef == null) return null - val intersection = intersection(leftTypeRef.get(), rightTypeRef.get()) - return if (intersection != null) Ref(intersection) else null - } - return null + val intersection = intersection(leftTypeRef?.get(), rightTypeRef?.get()) + return intersection?.let { Ref(it) } } private fun getNoneType(typeHint: PyExpression, resolved: PsiElement): Ref? { @@ -2095,18 +2092,20 @@ class PyTypingTypeProvider : PyTypeProviderWithCustomContext() { } } else if (element is PyBinaryExpression && element.operator === PyTokenTypes.OR) { - val left = element.leftExpression - val right = element.rightExpression - if (left == null || right == null) return null + val left = element.leftExpression ?: return null + val right = element.rightExpression ?: return null - val leftTypeRef: Ref? = getType(left, context) - val rightTypeRef: Ref? = getType(right, context) - if (leftTypeRef == null || rightTypeRef == null) return null + val leftTypeRef = getType(left, context) + val rightTypeRef = getType(right, context) + if (leftTypeRef == null && rightTypeRef == null) return null - val leftType = leftTypeRef.get() - if (leftType != null && typeHasOverloadedBitwiseOr(leftType, left, context)) return null + // if the class type defines __or__ then don't create a union + val leftType = leftTypeRef?.get() + if (leftType != null && typeHasOverloadedBitwiseOr(leftType, left, context = context)) return null + val rightType = rightTypeRef?.get() + if (rightType != null && typeHasOverloadedBitwiseOr(rightType, left, "__ror__", context)) return null - val union = PyUnionType.union(leftType, rightTypeRef.get()) + val union = PyUnionType.union(leftType, rightType) return if (union != null) Ref(union) else null } return null diff --git a/python/python-psi-impl/src/com/jetbrains/python/validation/PyCompatibilityVisitor.java b/python/python-psi-impl/src/com/jetbrains/python/validation/PyCompatibilityVisitor.java index c905bdda249a..78f445f9f5ff 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/validation/PyCompatibilityVisitor.java +++ b/python/python-psi-impl/src/com/jetbrains/python/validation/PyCompatibilityVisitor.java @@ -920,6 +920,7 @@ public abstract class PyCompatibilityVisitor extends PyElementVisitor { node, new ReplaceWithOldStyleUnionQuickFix(), new AddFromFutureImportAnnotationsQuickFix()); } else { + if (node.getLeftExpression().getType(context) == null) return; registerForAllMatchingVersions(level -> level.isOlderThan(LanguageLevel.PYTHON310), PyPsiBundle.message("INSP.compatibility.new.union.syntax.not.available.in.earlier.version"), node, new ReplaceWithOldStyleUnionQuickFix()); diff --git a/python/testSrc/com/jetbrains/python/Py3HighlightingTest.java b/python/testSrc/com/jetbrains/python/Py3HighlightingTest.java index dc7323f2e377..808c3e804b8b 100644 --- a/python/testSrc/com/jetbrains/python/Py3HighlightingTest.java +++ b/python/testSrc/com/jetbrains/python/Py3HighlightingTest.java @@ -1,18 +1,4 @@ -/* - * Copyright 2000-2016 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python; import com.jetbrains.python.fixtures.PyTestCase; @@ -119,6 +105,30 @@ public class Py3HighlightingTest extends PyTestCase { doTestWithLanguageLevel(LanguageLevel.PYTHON39, false, false); } + public void testNoErrorMetaClassOverloadBitwiseRorChain() { + runWithLanguageLevel( + LanguageLevel.PYTHON39, + () -> { + myFixture.configureByText( + "test.py", """ + class M(type): + def __ror__(self, other): + return other + + class A(metaclass=M): + ... + class B(metaclass=M): + ... + class C(metaclass=M): + ... + + print(A | B | C) + """); + myFixture.testHighlighting(false, false, true); + } + ); + } + // PY-32067 public void testAwaitInNonAsyncFunction() { doHighlightingQuickfixTest("Convert to async function"); diff --git a/python/testSrc/com/jetbrains/python/Py3TypeTest.java b/python/testSrc/com/jetbrains/python/Py3TypeTest.java index 831cc6d673ba..a5d10806c2c6 100644 --- a/python/testSrc/com/jetbrains/python/Py3TypeTest.java +++ b/python/testSrc/com/jetbrains/python/Py3TypeTest.java @@ -5229,6 +5229,32 @@ public class Py3TypeTest extends PyTestCase { """); } + @TestFor(issues = "PY-88281") + public void testUnionPartialUnresolved() { + doTest("int | Any", """ + expr: int | asdf + """); + } + + @TestFor(issues = "PY-88281") + public void testIntersectionPartialUnresolved() { + doTest("int & Any", """ + expr: int & asdf + """); + } + + public void testRightHandOrClass() { + doTest("UnionType | type[str] | int", """ + class M(type): + def __ror__(self, other: object) -> int: + return 1 + + class A(metaclass=M): ... + + expr = str | A + """); + } + private void doTest(final String expectedType, final String text) { myFixture.configureByText(PythonFileType.INSTANCE, text); final PyExpression expr = myFixture.findElementByText("expr", PyExpression.class); diff --git a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java index d2325b453778..41e1c997dfd3 100644 --- a/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java +++ b/python/testSrc/com/jetbrains/python/inspections/PyTypeHintsInspectionTest.java @@ -1,4 +1,4 @@ -// Copyright 2000-2018 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. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.inspections; import com.jetbrains.python.fixtures.PyInspectionTestCase; @@ -1549,7 +1549,7 @@ public class PyTypeHintsInspectionTest extends PyInspectionTestCase { doTestByText(""" from typing import Self - something: Self | None = None + something: Self | None = None """); }