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

Use X | Y union syntax in error messages #15102

Merged
merged 8 commits into from
Apr 24, 2023
Merged
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
4 changes: 4 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@ def add_invertible_flag(
"--force-uppercase-builtins", default=False, help=argparse.SUPPRESS, group=none_group
)

add_invertible_flag(
"--force-union-syntax", default=False, help=argparse.SUPPRESS, group=none_group
)

lint_group = parser.add_argument_group(
title="Configuring warnings",
description="Detect code that is sound but redundant or problematic.",
Expand Down
31 changes: 26 additions & 5 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2359,6 +2359,12 @@ def format(typ: Type) -> str:
def format_list(types: Sequence[Type]) -> str:
return ", ".join(format(typ) for typ in types)

def format_union(types: Sequence[Type]) -> str:
formatted = [format(typ) for typ in types if format(typ) != "None"]
if any(format(typ) == "None" for typ in types):
formatted.append("None")
return " | ".join(formatted)

def format_literal_value(typ: LiteralType) -> str:
if typ.is_enum_literal():
underlying_type = format(typ.fallback)
Expand Down Expand Up @@ -2457,9 +2463,17 @@ def format_literal_value(typ: LiteralType) -> str:
)

if len(union_items) == 1 and isinstance(get_proper_type(union_items[0]), NoneType):
return f"Optional[{literal_str}]"
return (
f"{literal_str} | None"
if options.use_or_syntax()
else f"Optional[{literal_str}]"
)
elif union_items:
return f"Union[{format_list(union_items)}, {literal_str}]"
return (
f"{literal_str} | {format_union(union_items)}"
if options.use_or_syntax()
else f"Union[{format_list(union_items)}, {literal_str}]"
)
else:
return literal_str
else:
Expand All @@ -2470,10 +2484,17 @@ def format_literal_value(typ: LiteralType) -> str:
)
if print_as_optional:
rest = [t for t in typ.items if not isinstance(get_proper_type(t), NoneType)]
return f"Optional[{format(rest[0])}]"
return (
f"{format(rest[0])} | None"
if options.use_or_syntax()
else f"Optional[{format(rest[0])}]"
)
else:
s = f"Union[{format_list(typ.items)}]"

s = (
format_union(typ.items)
if options.use_or_syntax()
else f"Union[{format_list(typ.items)}]"
)
return s
elif isinstance(typ, NoneType):
return "None"
Expand Down
6 changes: 6 additions & 0 deletions mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,18 @@ def __init__(self) -> None:
self.disable_memoryview_promotion = False

self.force_uppercase_builtins = False
self.force_union_syntax = False

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

def use_or_syntax(self) -> bool:
if self.python_version >= (3, 10):
return not self.force_union_syntax
return False

# To avoid breaking plugin compatibility, keep providing new_semantic_analyzer
@property
def new_semantic_analyzer(self) -> bool:
Expand Down
1 change: 1 addition & 0 deletions mypy/test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ def parse_options(
options.error_summary = False
options.hide_error_codes = True
options.force_uppercase_builtins = True
options.force_union_syntax = True

# Allow custom python version to override testfile_pyversion.
if all(flag.split("=")[0] not in ["--python-version", "-2", "--py2"] for flag in flag_list):
Expand Down
2 changes: 2 additions & 0 deletions mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ def run_case_once(
options.allow_empty_bodies = not testcase.name.endswith("_no_empty")
if "lowercase" not in testcase.file:
options.force_uppercase_builtins = True
if "union-error" not in testcase.file:
options.force_union_syntax = True

if incremental_step and options.incremental:
# Don't overwrite # flags: --no-incremental in incremental test cases
Expand Down
2 changes: 2 additions & 0 deletions mypy/test/testcmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ def test_python_cmdline(testcase: DataDrivenTestCase, step: int) -> None:
args.append("--allow-empty-bodies")
if "--no-force-uppercase-builtins" not in args:
args.append("--force-uppercase-builtins")
if "--no-force-union-syntax" not in args:
args.append("--force-union-syntax")
# Type check the program.
fixed = [python3_path, "-m", "mypy"]
env = os.environ.copy()
Expand Down
61 changes: 61 additions & 0 deletions test-data/unit/check-union-error-syntax.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[case testUnionErrorSyntax]
# flags: --python-version 3.10 --no-force-union-syntax
from typing import Union
x : Union[bool, str]
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "bool | str")

[case testOrErrorSyntax]
# flags: --python-version 3.10 --force-union-syntax
from typing import Union
x : Union[bool, str]
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "Union[bool, str]")

[case testOrNoneErrorSyntax]
# flags: --python-version 3.10 --no-force-union-syntax
from typing import Union
x : Union[bool, None]
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "bool | None")

[case testOptionalErrorSyntax]
# flags: --python-version 3.10 --force-union-syntax
from typing import Union
x : Union[bool, None]
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "Optional[bool]")

[case testNoneAsFinalItem]
# flags: --python-version 3.10 --no-force-union-syntax
from typing import Union
x : Union[bool, None, str]
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type "bool | str | None")

[case testLiteralOrErrorSyntax]
# flags: --python-version 3.10 --no-force-union-syntax
from typing import Union
from typing_extensions import Literal
x : Union[Literal[1], Literal[2], str]
x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1, 2] | str")
[builtins fixtures/tuple.pyi]

[case testLiteralUnionErrorSyntax]
# flags: --python-version 3.10 --force-union-syntax
from typing import Union
from typing_extensions import Literal
x : Union[Literal[1], Literal[2], str]
x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Union[str, Literal[1, 2]]")
[builtins fixtures/tuple.pyi]

[case testLiteralOrNoneErrorSyntax]
# flags: --python-version 3.10 --no-force-union-syntax
from typing import Union
from typing_extensions import Literal
x : Union[Literal[1], None]
x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Literal[1] | None")
[builtins fixtures/tuple.pyi]

[case testLiteralOptionalErrorSyntax]
# flags: --python-version 3.10 --force-union-syntax
from typing import Union
from typing_extensions import Literal
x : Union[Literal[1], None]
x = 3 # E: Incompatible types in assignment (expression has type "Literal[3]", variable has type "Optional[Literal[1]]")
[builtins fixtures/tuple.pyi]
2 changes: 1 addition & 1 deletion test-data/unit/daemon.test
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ def bar() -> None:
foo(arg='xyz')

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