From 13b25ecd3c51f5c8670687f04b14a9a157cdad2c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 2 Nov 2025 13:34:11 +0100 Subject: [PATCH] Respect force-union-syntax flag in error hint --- mypy/checker.py | 14 +++++--------- mypy/messages.py | 7 +++---- mypy/suggestions.py | 3 ++- mypy/test/testcheck.py | 2 +- test-data/unit/check-inference.test | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 63e128f78310..bfdd69ca7831 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4432,7 +4432,7 @@ def infer_variable_type( # partial type which will be made more specific later. A partial type # gets generated in assignment like 'x = []' where item type is not known. if name.name != "_" and not self.infer_partial_type(name, lvalue, init_type): - self.msg.need_annotation_for_var(name, context, self.options.python_version) + self.msg.need_annotation_for_var(name, context, self.options) self.set_inference_error_fallback_type(name, lvalue, init_type) elif ( isinstance(lvalue, MemberExpr) @@ -4442,7 +4442,7 @@ def infer_variable_type( and not is_same_type(self.inferred_attribute_types[lvalue.def_var], init_type) ): # Multiple, inconsistent types inferred for an attribute. - self.msg.need_annotation_for_var(name, context, self.options.python_version) + self.msg.need_annotation_for_var(name, context, self.options) name.type = AnyType(TypeOfAny.from_error) else: # Infer type of the target. @@ -4639,9 +4639,7 @@ def check_simple_assignment( rvalue, type_context=lvalue_type, always_allow_any=always_allow_any ) if not is_valid_inferred_type(rvalue_type, self.options) and inferred is not None: - self.msg.need_annotation_for_var( - inferred, context, self.options.python_version - ) + self.msg.need_annotation_for_var(inferred, context, self.options) rvalue_type = rvalue_type.accept(SetNothingToAny()) if ( @@ -7663,7 +7661,7 @@ def enter_partial_types( var.type = NoneType() else: if var not in self.partial_reported and not permissive: - self.msg.need_annotation_for_var(var, context, self.options.python_version) + self.msg.need_annotation_for_var(var, context, self.options) self.partial_reported.add(var) if var.type: fixed = fixup_partial_type(var.type) @@ -7690,9 +7688,7 @@ def handle_partial_var_type( if in_scope: context = partial_types[node] if is_local or not self.options.allow_untyped_globals: - self.msg.need_annotation_for_var( - node, context, self.options.python_version - ) + self.msg.need_annotation_for_var(node, context, self.options) self.partial_reported.add(node) else: # Defer the node -- we might get a better type in the outer scope diff --git a/mypy/messages.py b/mypy/messages.py index c6378c264757..a868d0d84fef 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1803,18 +1803,17 @@ def unimported_type_becomes_any(self, prefix: str, typ: Type, ctx: Context) -> N ) def need_annotation_for_var( - self, node: SymbolNode, context: Context, python_version: tuple[int, int] | None = None + self, node: SymbolNode, context: Context, options: Options | None = None ) -> None: hint = "" - pep604_supported = not python_version or python_version >= (3, 10) # type to recommend the user adds recommended_type = None # Only gives hint if it's a variable declaration and the partial type is a builtin type - if python_version and isinstance(node, Var) and isinstance(node.type, PartialType): + if options and isinstance(node, Var) and isinstance(node.type, PartialType): type_dec = "" if not node.type.type: # partial None - if pep604_supported: + if options.use_or_syntax(): recommended_type = f"{type_dec} | None" else: recommended_type = f"Optional[{type_dec}]" diff --git a/mypy/suggestions.py b/mypy/suggestions.py index 45aa5ade47a4..756cf6ae2ccd 100644 --- a/mypy/suggestions.py +++ b/mypy/suggestions.py @@ -890,7 +890,8 @@ def visit_typeddict_type(self, t: TypedDictType) -> str: def visit_union_type(self, t: UnionType) -> str: if len(t.items) == 2 and is_overlapping_none(t): - return f"Optional[{remove_optional(t).accept(self)}]" + s = remove_optional(t).accept(self) + return f"{s} | None" if self.options.use_or_syntax() else f"Optional[{s}]" else: return super().visit_union_type(t) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index f59cce701ea6..f2b7057d9f20 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -140,7 +140,7 @@ def run_case_once( options.hide_error_codes = False if "abstract" not in testcase.file: options.allow_empty_bodies = not testcase.name.endswith("_no_empty") - if "union-error" not in testcase.file: + if "union-error" not in testcase.file and "Pep604" not in testcase.name: options.force_union_syntax = True if incremental_step and options.incremental: diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 9ed9c5e9ec78..ecaf4b3cf5b9 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3548,7 +3548,7 @@ if x: [builtins fixtures/dict.pyi] [case testSuggestPep604AnnotationForPartialNone] -# flags: --local-partial-types --python-version 3.10 +# flags: --local-partial-types --python-version 3.10 --no-force-union-syntax x = None # E: Need type annotation for "x" (hint: "x: | None = ...") [case testTupleContextFromIterable]