Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emit warning for invalid use of quoted types in union syntax #18183

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
44 changes: 43 additions & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1354,7 +1354,49 @@ def visit_union_type(self, t: UnionType) -> Type:
and not self.options.python_version >= (3, 10)
):
self.fail("X | Y syntax for unions requires Python 3.10", t, code=codes.SYNTAX)
return UnionType(self.anal_array(t.items), t.line, uses_pep604_syntax=t.uses_pep604_syntax)
items = self.anal_array(t.items)
# Check for invalid quoted type inside union syntax.
if (
t.original_str_expr is None
and not self.api.is_stub_file
and (not self.api.is_future_flag_set("annotations") or self.defining_alias)
):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: This new if statement now accounts for most the code in this function. What about moving it to a helper function to make the basic logic clearer?

# Whether each type can be OR'd with a string.
item_ors_with_str = [
# Is a TypeVar?
isinstance(typ, (TypeVarType, UnboundType))
# Is a 'typing._UnionGenericAlias'?
or (
isinstance(typ, TypeAliasType)
and typ.alias is not None
# "type: ignore" comment needed to satisfy the ProperTypePlugin.
# Calling get_proper_type here would give the wrong result in the edge
# case where a non-PEP-695 alias has a PEP-695 alias as its target.
and isinstance(typ.alias.target, UnionType) # type: ignore[misc]
and typ.alias.alias_tvars
and not typ.alias.python_3_12_type_alias
)
for typ in items
]
for idx, itm in enumerate(t.items):
if (
isinstance(itm, UnboundType)
# Comes from a quoted expression.
and itm.original_str_expr is not None
# Not preceded by type that makes OR-ing with string valid.
and not any(item_ors_with_str[:idx])
# Not the first item immediately followed by a type that
# can be OR'd with a string.
# Accessing index 1 is safe because union syntax guarantees
# that there are at least 2 items.
and not (idx == 0 and item_ors_with_str[1])
):
self.fail(
"X | Y syntax for unions cannot use quoted operands; use quotes"
" around the entire expression instead",
itm,
)
return UnionType(items, t.line, uses_pep604_syntax=t.uses_pep604_syntax)

def visit_partial_type(self, t: PartialType) -> Type:
assert False, "Internal error: Unexpected partial type"
Expand Down
28 changes: 28 additions & 0 deletions test-data/unit/check-python312.test
Original file line number Diff line number Diff line change
Expand Up @@ -1949,3 +1949,31 @@ class D:
class G[Q]:
def g(self, x: Q): ...
d: G[str]

[case testPEP695TypeAliasInUnionOrSyntaxWithQuotedOperands]
import types

type A1 = int
type A2 = int | str
type A3[T] = list[T]
type A4[T] = T | str

x1: A1 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
x2: A2 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
x3: A3[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
x4: A4[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-full.pyi]

[case testPEP695TypeAliasInUnionOrSyntaxWithQuotedOperandsNestedAlias]
import types
from typing import TypeVar
from typing_extensions import TypeAlias

T = TypeVar("T")
type A1[T] = T | str
A2: TypeAlias = A1[T]

x: A2[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
[builtins fixtures/tuple.pyi]
[typing fixtures/typing-full.pyi]
62 changes: 62 additions & 0 deletions test-data/unit/check-union-or-syntax.test
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,65 @@ foo: ReadableBuffer
[file was_mmap.py]
from was_builtins import *
class mmap: ...

[case testUnionOrSyntaxWithQuotedOperandsNotAllowed]
# flags: --python-version 3.10
from typing import Union, assert_type

x1: "int" | str # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
x2: int | "str" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
x3: int | str | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
assert_type(x1, Union[int, str])
assert_type(x2, Union[int, str])
assert_type(x3, Union[int, str, bytes])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test also valid cases, like Callable[[], None] | "int", which work at runtime? Or are these tested in existing tests?


[case testUnionOrSyntaxWithQuotedOperandsWithTypeVar]
# flags: --python-version 3.10
import types
from typing import TypeVar, Union, assert_type
from typing_extensions import TypeAlias

T = TypeVar("T")

ok1: TypeAlias = T | "int"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to have more tests for things like "int | str" | T or "int | str", here and elsewhere?

ok2: TypeAlias = "int" | T
ok3: TypeAlias = int | T | "str"
ok4: TypeAlias = "int" | T | "str"
ok5: TypeAlias = T | "int" | str
ok6: TypeAlias = T | int | "str"
ok7: TypeAlias = list["str" | T]

bad1: TypeAlias = "T" | "int" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
bad2: TypeAlias = int | "str" | T # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
bad3: TypeAlias = list["str" | int] # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
[builtins fixtures/tuple.pyi]

[case testUnionOrSyntaxWithQuotedOperandsWithAlias]
# flags: --python-version 3.10
import types
from typing import TypeVar
from typing_extensions import TypeAlias

T = TypeVar("T")

A1: TypeAlias = int
A2: TypeAlias = int | str
A3: TypeAlias = list[T]
A4: TypeAlias = T | str

x1: A1 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
x2: A2 | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
x3: A3[int] | "bytes" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
x4: A4[int] | "bytes" # ok
[builtins fixtures/tuple.pyi]

[case testUnionOrSyntaxWithQuotedOperandsFutureAnnotations]
# flags: --python-version 3.10
from __future__ import annotations
import types
from typing_extensions import TypeAlias

x1: int | "str" # ok
def f(x: int | "str"): pass # ok
x2: TypeAlias = int | "str" # E: X | Y syntax for unions cannot use quoted operands; use quotes around the entire expression instead
[builtins fixtures/tuple.pyi]
Loading