diff --git a/mypy/semanal.py b/mypy/semanal.py index 52861230af376..eb8964b30218b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3136,7 +3136,7 @@ def process_imported_symbol( node.node, module_public=module_public, module_hidden=module_hidden, - becomes_typeinfo=True, + becomes_typeinfo=node.node.becomes_typeinfo, ) # NOTE: we take the original node even for final `Var`s. This is to support # a common pattern when constants are re-exported (same applies to import *). @@ -3385,6 +3385,10 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: special_form = True elif self.analyze_enum_assign(s): special_form = True + elif isinstance(s.rvalue, OpExpr): + # In rare cases we may need to "backtrack", and make something we thought is + # an alias into a regular variable, see testUnionNewSyntaxUndefinedAttrNoCrash. + s.rvalue.analyzed = None if special_form: self.record_special_form_lvalue(s) @@ -3533,6 +3537,11 @@ def should_wait_rhs(self, rv: Expression) -> bool: return True elif isinstance(rv, IndexExpr) and isinstance(rv.base, RefExpr): return self.should_wait_rhs(rv.base) + if isinstance(rv, OpExpr) and rv.op == "|": + if self.is_stub_file: + return False + if self.should_wait_rhs(rv.left) or self.should_wait_rhs(rv.right): + return True elif isinstance(rv, CallExpr) and isinstance(rv.callee, RefExpr): # This is only relevant for builtin SCC where things like 'TypeVar' # may be not ready. @@ -3543,7 +3552,7 @@ def can_be_type_alias(self, rv: Expression, allow_none: bool = False) -> bool: """Is this a valid r.h.s. for an alias definition? Note: this function should be only called for expressions where self.should_wait_rhs() - returns False. + returns False, keep these two functions "in sync". """ if isinstance(rv, RefExpr) and self.is_type_ref(rv, bare=True): return True @@ -4251,6 +4260,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: # annotations (see the second rule). return False if not pep_613 and not pep_695 and not self.can_be_type_alias(rvalue): + # Backtrack potential alias to a regular variable. + if existing and isinstance(existing.node, TypeAlias): + del self.current_symbol_table()[lvalue.name] return False if existing and not isinstance(existing.node, (PlaceholderNode, TypeAlias)): @@ -6862,6 +6874,10 @@ def lookup_qualified( nextsym = self.get_module_symbol(node, part) namespace = node.fullname elif isinstance(node, PlaceholderNode): + # TODO: find a cleaner way to handle this. + # We don't know if the resolved placeholder will have the attribute. + # But if we try to be strict here, we will break some use cases like: + # Alias = Outer.NestedOne | Outer.NestedTwo return sym elif isinstance(node, TypeAlias) and node.no_args: assert isinstance(node.target, ProperType) diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test index f2dec80051831..8d391d4725e8a 100644 --- a/test-data/unit/check-newsemanal.test +++ b/test-data/unit/check-newsemanal.test @@ -75,7 +75,7 @@ from b import bad # E: Module "b" has no attribute "bad" [file b.py] # TODO: Could we generate an error here as well? from a import bad -[targets a, b, a, b, a, b, a, b, __main__] +[targets a, b, a, b, a, b, __main__] [case testNewAnalyzerExportedValuesInImportAll] from m import * @@ -3248,3 +3248,71 @@ def test() -> None: class Model: pass + +[case testUnionNewSyntaxUndefinedAttrNoCrash] +class Test(Flag): # E: Name "Flag" is not defined + A = auto() # E: Name "auto" is not defined + B = auto() # E: Name "auto" is not defined + +AB = Test.A | Test.B + +[case testUnionNewSyntaxUndefinedAttrRegular] +class Test(Flag): # E: Name "Flag" is not defined + A = 1 + B = 2 + +AB = Test.A | Test.B +reveal_type(AB) # N: Revealed type is "builtins.int" +[builtins fixtures/ops.pyi] + +[case testUnionNewSyntaxUndefinedAttrRegularReassign] +class Test(Flag): # E: Name "Flag" is not defined + A = 1 + B = 2 + +AB = Test.A | Test.B +C = AB +reveal_type(AB) # N: Revealed type is "builtins.int" +reveal_type(C) # N: Revealed type is "builtins.int" +[builtins fixtures/ops.pyi] + +[case testUnionNewSyntaxNested] +class Test(Flag): # E: Name "Flag" is not defined + class A: ... + class B: ... + +AB = Test.A | Test.B +ab: AB +reveal_type(ab) # N: Revealed type is "__main__.Test.A | __main__.Test.B" + +[case testUnionNewSyntaxImportCycleNoCrash] +import m +[file m.py] +from lib import A + +AB = int | A + +[file lib.py] +from m import whatever # type: ignore + +A = whatever.A + +[case testAliasToNestedClassInsideClassDependingOnAlias] +class D(list[A]): + class C: ... + +A = D.C +x: A +reveal_type(x) # N: Revealed type is "__main__.D.C" + +[case testUnionNewSyntaxDeepDeferral] +# flags: --disable-error-code=used-before-def + +A = str | B + +B = C +C = D +class D: ... + +x: A +reveal_type(x) # N: Revealed type is "builtins.str | __main__.D" diff --git a/test-data/unit/fixtures/ops.pyi b/test-data/unit/fixtures/ops.pyi index df615f8f65746..ec38ae740b277 100644 --- a/test-data/unit/fixtures/ops.pyi +++ b/test-data/unit/fixtures/ops.pyi @@ -43,6 +43,7 @@ class int: def __rtruediv__(self, x: 'int') -> 'int': pass def __mod__(self, x: 'int') -> 'int': pass def __floordiv__(self, x: 'int') -> 'int': pass + def __or__(self, x: 'int') -> 'int': pass def __pow__(self, x: 'int', __modulo: Optional[int] = ...) -> Any: pass def __pos__(self) -> 'int': pass def __neg__(self) -> 'int': pass