Skip to content

Commit

Permalink
Fix crash when using member type aliases in runtime contexts (#13602)
Browse files Browse the repository at this point in the history
This pull request:

- Fixes #10357
- Fixes #9908

Currently, checkmember.py attempts handling only type aliases with Instance targets 
and ignores type aliases with a special form target such as `Union[...]`, `Literal[...]`, 
or `Callable[...]`, causing either a crash or odd runtime behavior.

This diff replaces that logic (the `instance_alias_type` function) with the more 
general-purpose `alias_type_in_runtime_context` function found in checkexpr.py.

I'm not actually 100% sure if the latter is a perfect substitute for the former -- the 
two functions seem to handle Instance type aliases a little differently. But I think this 
is probably fine: the long-term benefits of consolidating mypy's logic is probably 
worth some short-term risk.
  • Loading branch information
Michael0x2a authored Sep 5, 2022
1 parent 8eb9cdc commit 71e19e8
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 33 deletions.
4 changes: 1 addition & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2309,9 +2309,7 @@ def determine_type_of_member(self, sym: SymbolTableNode) -> Type | None:
with self.msg.filter_errors():
# Suppress any errors, they will be given when analyzing the corresponding node.
# Here we may have incorrect options and location context.
return self.expr_checker.alias_type_in_runtime_context(
sym.node, sym.node.no_args, sym.node
)
return self.expr_checker.alias_type_in_runtime_context(sym.node, ctx=sym.node)
# TODO: handle more node kinds here.
return None

Expand Down
11 changes: 5 additions & 6 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type:
# Note that we suppress bogus errors for alias redefinitions,
# they are already reported in semanal.py.
result = self.alias_type_in_runtime_context(
node, node.no_args, e, alias_definition=e.is_alias_rvalue or lvalue
node, ctx=e, alias_definition=e.is_alias_rvalue or lvalue
)
elif isinstance(node, (TypeVarExpr, ParamSpecExpr)):
result = self.object_type()
Expand Down Expand Up @@ -3805,12 +3805,10 @@ def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type:
both `reveal_type` instances will reveal the same type `def (...) -> builtins.list[Any]`.
Note that type variables are implicitly substituted with `Any`.
"""
return self.alias_type_in_runtime_context(
alias.node, alias.no_args, alias, alias_definition=True
)
return self.alias_type_in_runtime_context(alias.node, ctx=alias, alias_definition=True)

def alias_type_in_runtime_context(
self, alias: TypeAlias, no_args: bool, ctx: Context, *, alias_definition: bool = False
self, alias: TypeAlias, *, ctx: Context, alias_definition: bool = False
) -> Type:
"""Get type of a type alias (could be generic) in a runtime expression.
Expand Down Expand Up @@ -3842,7 +3840,7 @@ class LongName(Generic[T]): ...
# Normally we get a callable type (or overloaded) with .is_type_obj() true
# representing the class's constructor
tp = type_object_type(item.type, self.named_type)
if no_args:
if alias.no_args:
return tp
return self.apply_type_arguments_to_callable(tp, item.args, ctx)
elif (
Expand All @@ -3860,6 +3858,7 @@ class LongName(Generic[T]): ...
if alias_definition:
return AnyType(TypeOfAny.special_form)
# This type is invalid in most runtime contexts, give it an 'object' type.
# TODO: Use typing._SpecialForm instead?
return self.named_type("builtins.object")

def apply_type_arguments_to_callable(
Expand Down
30 changes: 8 additions & 22 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
is_final_node,
)
from mypy.plugin import AttributeContext
from mypy.typeanal import set_any_tvars
from mypy.typeops import (
bind_self,
class_callable,
Expand Down Expand Up @@ -458,14 +457,16 @@ def analyze_member_var_access(
v = Var(name, type=type_object_type(vv, mx.named_type))
v.info = info

if isinstance(vv, TypeAlias) and isinstance(get_proper_type(vv.target), Instance):
if isinstance(vv, TypeAlias):
# Similar to the above TypeInfo case, we allow using
# qualified type aliases in runtime context if it refers to an
# instance type. For example:
# class C:
# A = List[int]
# x = C.A() <- this is OK
typ = instance_alias_type(vv, mx.named_type)
typ = mx.chk.expr_checker.alias_type_in_runtime_context(
vv, ctx=mx.context, alias_definition=mx.is_lvalue
)
v = Var(name, type=typ)
v.info = info

Expand Down Expand Up @@ -657,21 +658,6 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
return inferred_dunder_get_type.ret_type


def instance_alias_type(alias: TypeAlias, named_type: Callable[[str], Instance]) -> Type:
"""Type of a type alias node targeting an instance, when appears in runtime context.
As usual, we first erase any unbound type variables to Any.
"""
target: Type = get_proper_type(alias.target)
assert isinstance(
get_proper_type(target), Instance
), "Must be called only with aliases to classes"
target = get_proper_type(set_any_tvars(alias, alias.line, alias.column))
assert isinstance(target, Instance)
tp = type_object_type(target.type, named_type)
return expand_type_by_instance(tp, target)


def is_instance_var(var: Var, info: TypeInfo) -> bool:
"""Return if var is an instance variable according to PEP 526."""
return (
Expand Down Expand Up @@ -980,10 +966,10 @@ def analyze_class_attribute_access(
# Reference to a module object.
return mx.named_type("types.ModuleType")

if isinstance(node.node, TypeAlias) and isinstance(
get_proper_type(node.node.target), Instance
):
return instance_alias_type(node.node, mx.named_type)
if isinstance(node.node, TypeAlias):
return mx.chk.expr_checker.alias_type_in_runtime_context(
node.node, ctx=mx.context, alias_definition=mx.is_lvalue
)

if is_decorated:
assert isinstance(node.node, Decorator)
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/check-dataclasses.test
Original file line number Diff line number Diff line change
Expand Up @@ -632,8 +632,8 @@ class Two:
S: TypeAlias = Callable[[int], str] # E: Type aliases inside dataclass definitions are not supported at runtime

c = Two()
x = c.S # E: Member "S" is not assignable
reveal_type(x) # N: Revealed type is "Any"
x = c.S
reveal_type(x) # N: Revealed type is "builtins.object"
[builtins fixtures/dataclasses.pyi]

[case testDataclassOrdering]
Expand Down
151 changes: 151 additions & 0 deletions test-data/unit/check-type-aliases.test
Original file line number Diff line number Diff line change
Expand Up @@ -796,3 +796,154 @@ S = TypeVar("S")
class C(Generic[S], List[Defer]): ...
class Defer: ...
[builtins fixtures/list.pyi]

[case testClassLevelTypeAliasesInUnusualContexts]
from typing import Union
from typing_extensions import TypeAlias

class Foo: pass

NormalImplicit = Foo
NormalExplicit: TypeAlias = Foo
SpecialImplicit = Union[int, str]
SpecialExplicit: TypeAlias = Union[int, str]

class Parent:
NormalImplicit = Foo
NormalExplicit: TypeAlias = Foo
SpecialImplicit = Union[int, str]
SpecialExplicit: TypeAlias = Union[int, str]

class Child(Parent): pass

p = Parent()
c = Child()

# Use type aliases in a runtime context

reveal_type(NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(SpecialExplicit) # N: Revealed type is "builtins.object"

reveal_type(Parent.NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(Parent.NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(Parent.SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(Parent.SpecialExplicit) # N: Revealed type is "builtins.object"

reveal_type(Child.NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(Child.NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(Child.SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(Child.SpecialExplicit) # N: Revealed type is "builtins.object"

reveal_type(p.NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(p.NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(p.SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(p.SpecialExplicit) # N: Revealed type is "builtins.object"

reveal_type(c.NormalImplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(p.NormalExplicit) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(c.SpecialImplicit) # N: Revealed type is "builtins.object"
reveal_type(c.SpecialExplicit) # N: Revealed type is "builtins.object"

# Use type aliases in a type alias context in a plausible way

def plausible_top_1() -> NormalImplicit: pass
def plausible_top_2() -> NormalExplicit: pass
def plausible_top_3() -> SpecialImplicit: pass
def plausible_top_4() -> SpecialExplicit: pass
reveal_type(plausible_top_1) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(plausible_top_2) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(plausible_top_3) # N: Revealed type is "def () -> Union[builtins.int, builtins.str]"
reveal_type(plausible_top_4) # N: Revealed type is "def () -> Union[builtins.int, builtins.str]"

def plausible_parent_1() -> Parent.NormalImplicit: pass # E: Variable "__main__.Parent.NormalImplicit" is not valid as a type \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
def plausible_parent_2() -> Parent.NormalExplicit: pass
def plausible_parent_3() -> Parent.SpecialImplicit: pass
def plausible_parent_4() -> Parent.SpecialExplicit: pass
reveal_type(plausible_parent_1) # N: Revealed type is "def () -> Parent.NormalImplicit?"
reveal_type(plausible_parent_2) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(plausible_parent_3) # N: Revealed type is "def () -> Union[builtins.int, builtins.str]"
reveal_type(plausible_parent_4) # N: Revealed type is "def () -> Union[builtins.int, builtins.str]"

def plausible_child_1() -> Child.NormalImplicit: pass # E: Variable "__main__.Parent.NormalImplicit" is not valid as a type \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
def plausible_child_2() -> Child.NormalExplicit: pass
def plausible_child_3() -> Child.SpecialImplicit: pass
def plausible_child_4() -> Child.SpecialExplicit: pass
reveal_type(plausible_child_1) # N: Revealed type is "def () -> Child.NormalImplicit?"
reveal_type(plausible_child_2) # N: Revealed type is "def () -> __main__.Foo"
reveal_type(plausible_child_3) # N: Revealed type is "def () -> Union[builtins.int, builtins.str]"
reveal_type(plausible_child_4) # N: Revealed type is "def () -> Union[builtins.int, builtins.str]"

# Use type aliases in a type alias context in an implausible way

def weird_parent_1() -> p.NormalImplicit: pass # E: Name "p.NormalImplicit" is not defined
def weird_parent_2() -> p.NormalExplicit: pass # E: Name "p.NormalExplicit" is not defined
def weird_parent_3() -> p.SpecialImplicit: pass # E: Name "p.SpecialImplicit" is not defined
def weird_parent_4() -> p.SpecialExplicit: pass # E: Name "p.SpecialExplicit" is not defined
reveal_type(weird_parent_1) # N: Revealed type is "def () -> Any"
reveal_type(weird_parent_2) # N: Revealed type is "def () -> Any"
reveal_type(weird_parent_3) # N: Revealed type is "def () -> Any"
reveal_type(weird_parent_4) # N: Revealed type is "def () -> Any"

def weird_child_1() -> c.NormalImplicit: pass # E: Name "c.NormalImplicit" is not defined
def weird_child_2() -> c.NormalExplicit: pass # E: Name "c.NormalExplicit" is not defined
def weird_child_3() -> c.SpecialImplicit: pass # E: Name "c.SpecialImplicit" is not defined
def weird_child_4() -> c.SpecialExplicit: pass # E: Name "c.SpecialExplicit" is not defined
reveal_type(weird_child_1) # N: Revealed type is "def () -> Any"
reveal_type(weird_child_2) # N: Revealed type is "def () -> Any"
reveal_type(weird_child_3) # N: Revealed type is "def () -> Any"
reveal_type(weird_child_4) # N: Revealed type is "def () -> Any"
[builtins fixtures/tuple.pyi]

[case testMalformedTypeAliasRuntimeReassignments]
from typing import Union
from typing_extensions import TypeAlias

class Foo: pass

NormalImplicit = Foo
NormalExplicit: TypeAlias = Foo
SpecialImplicit = Union[int, str]
SpecialExplicit: TypeAlias = Union[int, str]

class Parent:
NormalImplicit = Foo
NormalExplicit: TypeAlias = Foo
SpecialImplicit = Union[int, str]
SpecialExplicit: TypeAlias = Union[int, str]

class Child(Parent): pass

p = Parent()
c = Child()

NormalImplicit = 4 # E: Cannot assign multiple types to name "NormalImplicit" without an explicit "Type[...]" annotation \
# E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
NormalExplicit = 4 # E: Cannot assign multiple types to name "NormalExplicit" without an explicit "Type[...]" annotation \
# E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
SpecialImplicit = 4 # E: Cannot assign multiple types to name "SpecialImplicit" without an explicit "Type[...]" annotation
SpecialExplicit = 4 # E: Cannot assign multiple types to name "SpecialExplicit" without an explicit "Type[...]" annotation

Parent.NormalImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
Parent.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
Parent.SpecialImplicit = 4
Parent.SpecialExplicit = 4

Child.NormalImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
Child.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
Child.SpecialImplicit = 4
Child.SpecialExplicit = 4

p.NormalImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
p.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
p.SpecialImplicit = 4
p.SpecialExplicit = 4

c.NormalImplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
c.NormalExplicit = 4 # E: Incompatible types in assignment (expression has type "int", variable has type "Type[Foo]")
c.SpecialImplicit = 4
c.SpecialExplicit = 4
[builtins fixtures/tuple.pyi]

0 comments on commit 71e19e8

Please sign in to comment.