-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
[Reverted] Fixes to union simplification, isinstance and more #3025
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
Changes from 10 commits
c53e0ca
dc62a99
07d64f5
0beffee
e3ff2c7
4cb8673
fc57a76
9cf9114
d406157
d745a50
a638e72
ca511d8
807924c
ef46cfa
6cd09f1
39f94a1
0826cc3
baafcd1
a06b41c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
) | ||
import mypy.applytype | ||
import mypy.constraints | ||
from mypy.erasetype import erase_type | ||
# Circular import; done in the function instead. | ||
# import mypy.solve | ||
from mypy import messages, sametypes | ||
|
@@ -496,52 +497,157 @@ def unify_generic_callable(type: CallableType, target: CallableType, | |
|
||
|
||
def restrict_subtype_away(t: Type, s: Type) -> Type: | ||
"""Return a supertype of (t intersect not s) | ||
"""Return t minus s. | ||
|
||
Currently just remove elements of a union type. | ||
If we can't determine a precise result, return a supertype of the | ||
ideal result (just t is a valid result). | ||
|
||
This is used for type inference of runtime type checks such as | ||
isinstance. | ||
|
||
Currently this just removes elements of a union type. | ||
""" | ||
if isinstance(t, UnionType): | ||
new_items = [item for item in t.items if (not is_subtype(item, s) | ||
or isinstance(item, AnyType))] | ||
# Since runtime type checks will ignore type arguments, erase the types. | ||
erased_s = erase_type(s) | ||
new_items = [item for item in t.items | ||
if (not is_proper_subtype(erase_type(item), erased_s) | ||
or isinstance(item, AnyType))] | ||
return UnionType.make_union(new_items) | ||
else: | ||
return t | ||
|
||
|
||
def is_proper_subtype(t: Type, s: Type) -> bool: | ||
def is_proper_subtype(left: Type, right: Type) -> bool: | ||
"""Check if t is a proper subtype of s? | ||
|
||
For proper subtypes, there's no need to rely on compatibility due to | ||
Any types. Any instance type t is also a proper subtype of t. | ||
""" | ||
# FIX tuple types | ||
if isinstance(s, UnionType): | ||
if isinstance(t, UnionType): | ||
return all([is_proper_subtype(item, s) for item in t.items]) | ||
else: | ||
return any([is_proper_subtype(t, item) for item in s.items]) | ||
if isinstance(right, UnionType) and not isinstance(left, UnionType): | ||
return any([is_proper_subtype(left, item) | ||
for item in right.items]) | ||
return left.accept(ProperSubtypeVisitor(right)) | ||
|
||
|
||
class ProperSubtypeVisitor(TypeVisitor[bool]): | ||
def __init__(self, right: Type) -> None: | ||
self.right = right | ||
|
||
def visit_unbound_type(self, left: UnboundType) -> bool: | ||
return True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe assert False instead? |
||
|
||
def visit_error_type(self, left: ErrorType) -> bool: | ||
return False | ||
|
||
def visit_type_list(self, left: TypeList) -> bool: | ||
assert False, 'Should not happen' | ||
|
||
def visit_any(self, left: AnyType) -> bool: | ||
return isinstance(self.right, AnyType) | ||
|
||
def visit_none_type(self, left: NoneTyp) -> bool: | ||
if experiments.STRICT_OPTIONAL: | ||
return (isinstance(self.right, NoneTyp) or | ||
is_named_instance(self.right, 'builtins.object')) | ||
return True | ||
|
||
def visit_uninhabited_type(self, left: UninhabitedType) -> bool: | ||
return True | ||
|
||
def visit_erased_type(self, left: ErasedType) -> bool: | ||
return True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe assert False instead? |
||
|
||
if isinstance(t, Instance): | ||
if isinstance(s, Instance): | ||
if not t.type.has_base(s.type.fullname()): | ||
def visit_deleted_type(self, left: DeletedType) -> bool: | ||
return True | ||
|
||
def visit_instance(self, left: Instance) -> bool: | ||
if isinstance(self.right, Instance): | ||
for base in left.type.mro: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This breaks on the code snippet from #3069: when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
if base._promote and is_proper_subtype(base._promote, self.right): | ||
return True | ||
|
||
if not left.type.has_base(self.right.type.fullname()): | ||
return False | ||
|
||
def check_argument(left: Type, right: Type, variance: int) -> bool: | ||
def check_argument(leftarg: Type, rightarg: Type, variance: int) -> bool: | ||
if variance == COVARIANT: | ||
return is_proper_subtype(left, right) | ||
return is_proper_subtype(leftarg, rightarg) | ||
elif variance == CONTRAVARIANT: | ||
return is_proper_subtype(right, left) | ||
return is_proper_subtype(rightarg, leftarg) | ||
else: | ||
return sametypes.is_same_type(left, right) | ||
return sametypes.is_same_type(leftarg, rightarg) | ||
|
||
# Map left type to corresponding right instances. | ||
t = map_instance_to_supertype(t, s.type) | ||
left = map_instance_to_supertype(left, self.right.type) | ||
|
||
return all(check_argument(ta, ra, tvar.variance) for ta, ra, tvar in | ||
zip(t.args, s.args, s.type.defn.type_vars)) | ||
zip(left.args, self.right.args, self.right.type.defn.type_vars)) | ||
return False | ||
|
||
def visit_type_var(self, left: TypeVarType) -> bool: | ||
if isinstance(self.right, TypeVarType) and left.id == self.right.id: | ||
return True | ||
return is_proper_subtype(left.upper_bound, self.right) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: value restrictions? |
||
|
||
def visit_callable_type(self, left: CallableType) -> bool: | ||
# TODO: Implement this properly | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's just do this. |
||
return is_subtype(left, self.right) | ||
|
||
def visit_tuple_type(self, left: TupleType) -> bool: | ||
right = self.right | ||
if isinstance(right, Instance): | ||
if (is_named_instance(right, 'builtins.tuple') or | ||
is_named_instance(right, 'typing.Iterable') or | ||
is_named_instance(right, 'typing.Container') or | ||
is_named_instance(right, 'typing.Sequence') or | ||
is_named_instance(right, 'typing.Reversible')): | ||
if not right.args: | ||
return False | ||
iter_type = right.args[0] | ||
if is_named_instance(right, 'builtins.tuple') and isinstance(iter_type, AnyType): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verify if this is still needed. |
||
# Special case plain 'tuple' which is needed for isinstance(x, tuple). | ||
return True | ||
return all(is_proper_subtype(li, iter_type) for li in left.items) | ||
return is_proper_subtype(left.fallback, right) | ||
elif isinstance(right, TupleType): | ||
if len(left.items) != len(right.items): | ||
return False | ||
for l, r in zip(left.items, right.items): | ||
if not is_proper_subtype(l, r): | ||
return False | ||
return is_proper_subtype(left.fallback, right.fallback) | ||
return False | ||
|
||
def visit_typeddict_type(self, left: TypedDictType) -> bool: | ||
# TODO: Does it make sense to support TypedDict here? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is supposed to be used only with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They're also used for union simplification so we still need to do this. |
||
return False | ||
|
||
def visit_overloaded(self, left: Overloaded) -> bool: | ||
# TODO: What's the right thing to do here? | ||
return False | ||
|
||
def visit_union_type(self, left: UnionType) -> bool: | ||
return all([is_proper_subtype(item, self.right) for item in left.items]) | ||
|
||
def visit_partial_type(self, left: PartialType) -> bool: | ||
# TODO: What's the right thing to do here? | ||
return False | ||
|
||
def visit_type_type(self, left: TypeType) -> bool: | ||
right = self.right | ||
if isinstance(right, TypeType): | ||
return is_proper_subtype(left.item, right.item) | ||
if isinstance(right, CallableType): | ||
# This is unsound, we don't check the __init__ signature. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (And so is the previous case (TypeType).) |
||
return right.is_type_obj() and is_proper_subtype(left.item, right.ret_type) | ||
if isinstance(right, Instance): | ||
if right.type.fullname() in ('builtins.type', 'builtins.object'): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've got a feeling this means that |
||
return True | ||
item = left.item | ||
return isinstance(item, Instance) and is_proper_subtype(item, | ||
right.type.metaclass_type) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a test for this case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This actually looks wrong so I'm going to remove this and add a TODO item. |
||
return False | ||
else: | ||
return sametypes.is_same_type(t, s) | ||
|
||
|
||
def is_more_precise(t: Type, s: Type) -> bool: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,7 +109,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: | |
t.optional = False | ||
# We don't need to worry about double-wrapping Optionals or | ||
# wrapping Anys: Union simplification will take care of that. | ||
return UnionType.make_simplified_union([self.visit_unbound_type(t), NoneTyp()]) | ||
return make_optional_type(self.visit_unbound_type(t)) | ||
sym = self.lookup(t.name, t) | ||
if sym is not None: | ||
if sym.node is None: | ||
|
@@ -151,7 +151,7 @@ def visit_unbound_type(self, t: UnboundType) -> Type: | |
self.fail('Optional[...] must have exactly one type argument', t) | ||
return AnyType() | ||
item = self.anal_type(t.args[0]) | ||
return UnionType.make_simplified_union([item, NoneTyp()]) | ||
return make_optional_type(item) | ||
elif fullname == 'typing.Callable': | ||
return self.analyze_callable_type(t) | ||
elif fullname == 'typing.Type': | ||
|
@@ -557,3 +557,20 @@ def visit_partial_type(self, t: PartialType) -> None: | |
|
||
def visit_type_type(self, t: TypeType) -> None: | ||
pass | ||
|
||
|
||
def make_optional_type(t: Type) -> Type: | ||
"""Return the type corresponding to Optional[t]. | ||
|
||
Note that we can't use normal union simplification, since this function | ||
is called during semantic analysis and simplification only works during | ||
type checking. | ||
""" | ||
if not experiments.STRICT_OPTIONAL: | ||
return t | ||
if isinstance(t, NoneTyp): | ||
return t | ||
if isinstance(t, UnionType) and any(isinstance(item, NoneTyp) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we always flatten unions? Otherwise this could be fooled by |
||
for item in t.items): | ||
return t | ||
return UnionType([t, NoneTyp()], t.line, t.column) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -986,6 +986,21 @@ def make_union(items: List[Type], line: int = -1, column: int = -1) -> Type: | |
|
||
@staticmethod | ||
def make_simplified_union(items: List[Type], line: int = -1, column: int = -1) -> Type: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: make this a function somewhere else. |
||
"""Build union type with redundant union items removed. | ||
|
||
If only a single item remains, this may return a non-union type. | ||
|
||
Examples: | ||
|
||
* [int, str] -> Union[int, str] | ||
* [int, object] -> object | ||
* [int, int] -> int | ||
* [int, Any] -> Union[int, Any] (Any types are not simplified away!) | ||
* [Any, Any] -> Any | ||
|
||
Note: This must NOT be used during semantic analysis, since TypeInfos may not | ||
be fully initialized. | ||
""" | ||
while any(isinstance(typ, UnionType) for typ in items): | ||
all_items = [] # type: List[Type] | ||
for typ in items: | ||
|
@@ -995,22 +1010,16 @@ def make_simplified_union(items: List[Type], line: int = -1, column: int = -1) - | |
all_items.append(typ) | ||
items = all_items | ||
|
||
if any(isinstance(typ, AnyType) for typ in items): | ||
return AnyType() | ||
|
||
from mypy.subtypes import is_subtype | ||
from mypy.sametypes import is_same_type | ||
from mypy.subtypes import is_proper_subtype | ||
|
||
removed = set() # type: Set[int] | ||
for i, ti in enumerate(items): | ||
if i in removed: continue | ||
# Keep track of the truishness info for deleted subtypes which can be relevant | ||
cbt = cbf = False | ||
for j, tj in enumerate(items): | ||
if (i != j | ||
and is_subtype(tj, ti) | ||
and (not (isinstance(tj, Instance) and tj.type.fallback_to_any) | ||
or is_same_type(ti, tj))): | ||
if (i != j and is_proper_subtype(tj, ti)): | ||
# We found a redundant item in the union. | ||
removed.add(j) | ||
cbt = cbt or tj.can_be_true | ||
cbf = cbf or tj.can_be_false | ||
|
@@ -1714,7 +1723,7 @@ def set_typ_args(tp: Type, new_args: List[Type], line: int = -1, column: int = - | |
if isinstance(tp, TupleType): | ||
return tp.copy_modified(items=new_args) | ||
if isinstance(tp, UnionType): | ||
return UnionType.make_simplified_union(new_args, line, column) | ||
return UnionType(new_args, line, column) | ||
if isinstance(tp, CallableType): | ||
return tp.copy_modified(arg_types=new_args[:-1], ret_type=new_args[-1], | ||
line=line, column=column) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
t
ands
should also be replaced withleft
andright
in the docstring below.