Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
support | operation between typing.Union and strings
Adds support for performing '|' operation between Union objects and
strings, e.g. forward type references.

For example following code:

    from typing import Union

    U1 = Union[int, str]
    U1 | "float"

The result of the operation above becomes:

   int | str | ForwardRef('float')
  • Loading branch information
Elmir Jagudin authored and elmjag committed Feb 4, 2026
commit 411f549bbea0306cca9afb048e18d9da1a189669
1 change: 0 additions & 1 deletion Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2281,7 +2281,6 @@ class Ints(enum.IntEnum):
self.assertEqual(Union[Literal[1], Literal[Ints.B], Literal[True]].__args__,
(Literal[1], Literal[Ints.B], Literal[True]))

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: types.UnionType[int, str] | float != types.UnionType[int, str, float]
def test_allow_non_types_in_or(self):
# gh-140348: Test that using | with a Union object allows things that are
# not allowed by is_unionable().
Expand Down
7 changes: 1 addition & 6 deletions crates/vm/src/builtins/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2048,12 +2048,7 @@ pub(crate) fn call_slot_new(
}

pub(crate) fn or_(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
if !union_::is_unionable(zelf.clone(), vm) || !union_::is_unionable(other.clone(), vm) {
return Ok(vm.ctx.not_implemented());
}

let tuple = PyTuple::new_ref(vec![zelf, other], &vm.ctx);
union_::make_union(&tuple, vm)
union_::or_op(zelf, other, vm)
}

fn take_next_base(bases: &mut [Vec<PyTypeRef>]) -> Option<PyTypeRef> {
Expand Down
34 changes: 32 additions & 2 deletions crates/vm/src/builtins/union.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
convert::ToPyObject,
function::PyComparisonValue,
protocol::{PyMappingMethods, PyNumberMethods},
stdlib::typing::TypeAliasType,
stdlib::typing::{TypeAliasType, call_typing_func_object},
types::{AsMapping, AsNumber, Comparable, GetAttr, Hashable, PyComparisonOp, Representable},
};
use alloc::fmt;
Expand Down Expand Up @@ -193,7 +193,7 @@ impl PyUnion {
}
}

pub fn is_unionable(obj: PyObjectRef, vm: &VirtualMachine) -> bool {
fn is_unionable(obj: PyObjectRef, vm: &VirtualMachine) -> bool {
let cls = obj.class();
cls.is(vm.ctx.types.none_type)
|| obj.downcastable::<PyType>()
Expand All @@ -202,6 +202,36 @@ pub fn is_unionable(obj: PyObjectRef, vm: &VirtualMachine) -> bool {
|| obj.downcast_ref::<TypeAliasType>().is_some()
}

fn type_check(arg: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
// Fast path to avoid calling into typing.py
if is_unionable(arg.clone(), vm) {
return Ok(arg);
}
let message_str: PyObjectRef = vm
.ctx
.new_str("Union[arg, ...]: each arg must be a type.")
.into();
call_typing_func_object(vm, "_type_check", (arg, message_str))
}

fn has_union_operands(a: PyObjectRef, b: PyObjectRef, vm: &VirtualMachine) -> bool {
let union_type = vm.ctx.types.union_type;
a.class().is(union_type) || b.class().is(union_type)
}

pub fn or_op(zelf: PyObjectRef, other: PyObjectRef, vm: &VirtualMachine) -> PyResult {
if !has_union_operands(zelf.clone(), other.clone(), vm)
&& (!is_unionable(zelf.clone(), vm) || !is_unionable(other.clone(), vm))
{
return Ok(vm.ctx.not_implemented());
}

let left = type_check(zelf, vm)?;
let right = type_check(other, vm)?;
let tuple = PyTuple::new_ref(vec![left, right], &vm.ctx);
make_union(&tuple, vm)
}

fn make_parameters(args: &Py<PyTuple>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
let parameters = genericalias::make_parameters(args, vm);
let result = dedup_and_flatten_args(&parameters, vm)?;
Expand Down