Skip to content
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
20 changes: 18 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *).
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)):
Expand Down Expand Up @@ -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)
Expand Down
70 changes: 69 additions & 1 deletion test-data/unit/check-newsemanal.test
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -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"
1 change: 1 addition & 0 deletions test-data/unit/fixtures/ops.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading