Skip to content
3 changes: 3 additions & 0 deletions pyrefly/lib/alt/solve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2253,6 +2253,9 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
match t {
Type::ClassDef(_) => true,
Type::Type(box Type::ClassType(cls)) => cls.targs().is_empty(),
// `None` is `NoneType` at runtime, which is a plain type that
// doesn't support `__or__` with string literals.
Type::None => true,
Type::TypeAlias(ta) => {
let ta = me.get_type_alias(&ta);
let t = if ta.style == TypeAliasStyle::Scoped {
Expand Down
61 changes: 60 additions & 1 deletion pyrefly/lib/test/annotation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,27 @@ testcase!(
test_union_operator_with_bare_string_literal,
TestEnv::new_with_version(PythonVersion::new(3, 13, 0)),
r#"
from typing import assert_type, TypeVar, Generic
from typing import assert_type, Any, TypeVar, Generic, Literal, Callable, LiteralString
T = TypeVar("T")
class C(Generic[T]): ...
bad1: int | "str" = "foo" # E: `|` union syntax does not work with string literals
bad2: int | "str" | T = "foo" # E: `|` union syntax does not work with string literals # E: Type variable `T` is not in scope
bad3: "str" | int = "foo" # E: `|` union syntax does not work with string literals
bad4: "str" | int | T = "foo" # E: `|` union syntax does not work with string literals # E: Type variable `T` is not in scope
bad5: C | "str" = "foo" # E: `|` union syntax does not work with string literals
bad6: "str" | None = "foo" # E: `|` union syntax does not work with string literals
bad7: None | "str" = "foo" # E: `|` union syntax does not work with string literals
bad8: "str" | Any = "foo" # E: `|` union syntax does not work with string literals
bad9: Any | "str" = "foo" # E: `|` union syntax does not work with string literals
ok1: T | "str" = "foo" # E: Type variable `T` is not in scope
ok2: "str" | T = "foo" # E: Type variable `T` is not in scope
ok3 = list["str" | T]
ok4 = (int) | (str)
ok5: "str" | C[int] = "foo"
ok6: C[int] | "str" = "foo"
ok7: "str" | Literal[1] = "foo"
ok8: "str" | Callable[[int], str] = "foo"
ok9: "str" | LiteralString = "foo"
"#,
);

Expand All @@ -59,6 +66,58 @@ bad: "int" | "list[str]" = 1 # E: `|` union syntax does not work with string li
"#,
);

testcase!(
test_union_forward_ref_with_special_classes,
TestEnv::new_with_version(PythonVersion::new(3, 13, 0)),
r#"
from typing import TypedDict, NamedTuple, Protocol

class TD(TypedDict):
x: int

class NT(NamedTuple):
x: int

class P(Protocol):
def f(self) -> None: ...

bad1: "str" | TD = "foo" # E: `|` union syntax does not work with string literals
bad2: "str" | NT = NT(x=1) # E: `|` union syntax does not work with string literals
bad3: "str" | P = "foo" # E: `|` union syntax does not work with string literals
"#,
);

testcase!(
test_union_forward_ref_with_none,
TestEnv::new_with_version(PythonVersion::new(3, 13, 0)),
r#"
from dataclasses import dataclass

def test() -> "Foo" | None: # E: `|` union syntax does not work with string literals
return Foo(bar=1)

@dataclass
class Foo:
bar: int
"#,
);

testcase!(
bug = "list[int] (types.GenericAlias) errors at runtime but List[int] (typing._GenericAlias) does not; pyrefly can't distinguish them",
test_union_forward_ref_with_builtin_generics,
TestEnv::new_with_version(PythonVersion::new(3, 13, 0)),
r#"
from typing import List, Dict, Type, Any
ok_but_runtime_error1: "str" | list[int] = "foo"
ok_but_runtime_error2: "str" | dict[str, int] = "foo"
ok_but_runtime_error3: "str" | tuple[int, ...] = "foo"
ok_but_runtime_error4: "str" | type[int] = "foo"
ok1: "str" | List[int] = "foo"
ok2: "str" | Dict[str, int] = "foo"
ok3: "str" | Type[Any] = "foo"
"#,
);

// Test that the error is NOT raised for Python 3.14+
// In Python 3.14+, annotations are not evaluated at runtime by default (PEP 649)
testcase!(
Expand Down
Loading