Skip to content

Commit 334daca

Browse files
authored
Use X | Y union syntax in error messages (#15102)
This should fix #15082: If the python version is set to 3.10 or later union error reports are going to be displayed using the `X | Y` syntax. If None is found in a union it is shown as the last element in order to improve visibility (`Union[bool, None, str]` becomes `"bool | str | None"`. To achieve this an option `use_or_syntax()` is created that is set to true if the python version is 3.10 or later. For testing a hidden flag --force-union-syntax is used that sets the option to false.
1 parent fe8873f commit 334daca

File tree

8 files changed

+103
-6
lines changed

8 files changed

+103
-6
lines changed

mypy/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,10 @@ def add_invertible_flag(
738738
"--force-uppercase-builtins", default=False, help=argparse.SUPPRESS, group=none_group
739739
)
740740

741+
add_invertible_flag(
742+
"--force-union-syntax", default=False, help=argparse.SUPPRESS, group=none_group
743+
)
744+
741745
lint_group = parser.add_argument_group(
742746
title="Configuring warnings",
743747
description="Detect code that is sound but redundant or problematic.",

mypy/messages.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2359,6 +2359,12 @@ def format(typ: Type) -> str:
23592359
def format_list(types: Sequence[Type]) -> str:
23602360
return ", ".join(format(typ) for typ in types)
23612361

2362+
def format_union(types: Sequence[Type]) -> str:
2363+
formatted = [format(typ) for typ in types if format(typ) != "None"]
2364+
if any(format(typ) == "None" for typ in types):
2365+
formatted.append("None")
2366+
return " | ".join(formatted)
2367+
23622368
def format_literal_value(typ: LiteralType) -> str:
23632369
if typ.is_enum_literal():
23642370
underlying_type = format(typ.fallback)
@@ -2457,9 +2463,17 @@ def format_literal_value(typ: LiteralType) -> str:
24572463
)
24582464

24592465
if len(union_items) == 1 and isinstance(get_proper_type(union_items[0]), NoneType):
2460-
return f"Optional[{literal_str}]"
2466+
return (
2467+
f"{literal_str} | None"
2468+
if options.use_or_syntax()
2469+
else f"Optional[{literal_str}]"
2470+
)
24612471
elif union_items:
2462-
return f"Union[{format_list(union_items)}, {literal_str}]"
2472+
return (
2473+
f"{literal_str} | {format_union(union_items)}"
2474+
if options.use_or_syntax()
2475+
else f"Union[{format_list(union_items)}, {literal_str}]"
2476+
)
24632477
else:
24642478
return literal_str
24652479
else:
@@ -2470,10 +2484,17 @@ def format_literal_value(typ: LiteralType) -> str:
24702484
)
24712485
if print_as_optional:
24722486
rest = [t for t in typ.items if not isinstance(get_proper_type(t), NoneType)]
2473-
return f"Optional[{format(rest[0])}]"
2487+
return (
2488+
f"{format(rest[0])} | None"
2489+
if options.use_or_syntax()
2490+
else f"Optional[{format(rest[0])}]"
2491+
)
24742492
else:
2475-
s = f"Union[{format_list(typ.items)}]"
2476-
2493+
s = (
2494+
format_union(typ.items)
2495+
if options.use_or_syntax()
2496+
else f"Union[{format_list(typ.items)}]"
2497+
)
24772498
return s
24782499
elif isinstance(typ, NoneType):
24792500
return "None"

mypy/options.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,12 +356,18 @@ def __init__(self) -> None:
356356
self.disable_memoryview_promotion = False
357357

358358
self.force_uppercase_builtins = False
359+
self.force_union_syntax = False
359360

360361
def use_lowercase_names(self) -> bool:
361362
if self.python_version >= (3, 9):
362363
return not self.force_uppercase_builtins
363364
return False
364365

366+
def use_or_syntax(self) -> bool:
367+
if self.python_version >= (3, 10):
368+
return not self.force_union_syntax
369+
return False
370+
365371
# To avoid breaking plugin compatibility, keep providing new_semantic_analyzer
366372
@property
367373
def new_semantic_analyzer(self) -> bool:

mypy/test/helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ def parse_options(
383383
options.error_summary = False
384384
options.hide_error_codes = True
385385
options.force_uppercase_builtins = True
386+
options.force_union_syntax = True
386387

387388
# Allow custom python version to override testfile_pyversion.
388389
if all(flag.split("=")[0] not in ["--python-version", "-2", "--py2"] for flag in flag_list):

mypy/test/testcheck.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ def run_case_once(
127127
options.allow_empty_bodies = not testcase.name.endswith("_no_empty")
128128
if "lowercase" not in testcase.file:
129129
options.force_uppercase_builtins = True
130+
if "union-error" not in testcase.file:
131+
options.force_union_syntax = True
130132

131133
if incremental_step and options.incremental:
132134
# Don't overwrite # flags: --no-incremental in incremental test cases

mypy/test/testcmdline.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None:
6363
args.append("--allow-empty-bodies")
6464
if "--no-force-uppercase-builtins" not in args:
6565
args.append("--force-uppercase-builtins")
66+
if "--no-force-union-syntax" not in args:
67+
args.append("--force-union-syntax")
6668
# Type check the program.
6769
fixed = [python3_path, "-m", "mypy"]
6870
env = os.environ.copy()
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
[case testUnionErrorSyntax]
2+
# flags: --python-version 3.10 --no-force-union-syntax
3+
from typing import Union
4+
x : Union[bool, str]
5+
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "bool | str")
6+
7+
[case testOrErrorSyntax]
8+
# flags: --python-version 3.10 --force-union-syntax
9+
from typing import Union
10+
x : Union[bool, str]
11+
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "Union[bool, str]")
12+
13+
[case testOrNoneErrorSyntax]
14+
# flags: --python-version 3.10 --no-force-union-syntax
15+
from typing import Union
16+
x : Union[bool, None]
17+
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "bool | None")
18+
19+
[case testOptionalErrorSyntax]
20+
# flags: --python-version 3.10 --force-union-syntax
21+
from typing import Union
22+
x : Union[bool, None]
23+
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "Optional[bool]")
24+
25+
[case testNoneAsFinalItem]
26+
# flags: --python-version 3.10 --no-force-union-syntax
27+
from typing import Union
28+
x : Union[bool, None, str]
29+
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "bool | str | None")
30+
31+
[case testLiteralOrErrorSyntax]
32+
# flags: --python-version 3.10 --no-force-union-syntax
33+
from typing import Union
34+
from typing_extensions import Literal
35+
x : Union[Literal[1], Literal[2], str]
36+
x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1, 2] | str")
37+
[builtins fixtures/tuple.pyi]
38+
39+
[case testLiteralUnionErrorSyntax]
40+
# flags: --python-version 3.10 --force-union-syntax
41+
from typing import Union
42+
from typing_extensions import Literal
43+
x : Union[Literal[1], Literal[2], str]
44+
x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Union[str, Literal[1, 2]]")
45+
[builtins fixtures/tuple.pyi]
46+
47+
[case testLiteralOrNoneErrorSyntax]
48+
# flags: --python-version 3.10 --no-force-union-syntax
49+
from typing import Union
50+
from typing_extensions import Literal
51+
x : Union[Literal[1], None]
52+
x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1] | None")
53+
[builtins fixtures/tuple.pyi]
54+
55+
[case testLiteralOptionalErrorSyntax]
56+
# flags: --python-version 3.10 --force-union-syntax
57+
from typing import Union
58+
from typing_extensions import Literal
59+
x : Union[Literal[1], None]
60+
x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Optional[Literal[1]]")
61+
[builtins fixtures/tuple.pyi]

test-data/unit/daemon.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ def bar() -> None:
312312
foo(arg='xyz')
313313

314314
[case testDaemonGetType_python38]
315-
$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary
315+
$ dmypy start --log-file log.txt -- --follow-imports=error --no-error-summary --python-version 3.8
316316
Daemon started
317317
$ dmypy inspect foo:1:2:3:4
318318
Command "inspect" is only valid after a "check" command (that produces no parse errors)

0 commit comments

Comments
 (0)