Skip to content
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

Implement meet for literal types; add tests for 'is_same_type' #6043

Merged
merged 4 commits into from
Dec 11, 2018
Merged
Show file tree
Hide file tree
Changes from 3 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
9 changes: 8 additions & 1 deletion mypy/meet.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ def visit_instance(self, t: Instance) -> Type:
return meet_types(t, self.s)
elif isinstance(self.s, TupleType):
return meet_types(t, self.s)
elif isinstance(self.s, LiteralType):
return meet_types(t, self.s)
return self.default(self.s)

def visit_callable_type(self, t: CallableType) -> Type:
Expand Down Expand Up @@ -528,7 +530,12 @@ def visit_typeddict_type(self, t: TypedDictType) -> Type:
return self.default(self.s)

def visit_literal_type(self, t: LiteralType) -> Type:
raise NotImplementedError()
if isinstance(self.s, LiteralType) and self.s == t:
return t
elif isinstance(self.s, Instance) and is_subtype(t.fallback, self.s):
return t
else:
return self.default(self.s)

def visit_partial_type(self, t: PartialType) -> Type:
# We can't determine the meet of partial types. We should never get here.
Expand Down
76 changes: 75 additions & 1 deletion mypy/test/testtypes.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
"""Test cases for mypy types and type operations."""

from typing import List, Tuple
from typing import List, Tuple, cast

from mypy.test.helpers import Suite, assert_equal, assert_true, assert_false, assert_type, skip
from mypy.erasetype import erase_type
from mypy.expandtype import expand_type
from mypy.join import join_types, join_simple
from mypy.meet import meet_types
from mypy.sametypes import is_same_type
from mypy.types import (
UnboundType, AnyType, CallableType, TupleType, TypeVarDef, Type, Instance, NoneTyp, Overloaded,
TypeType, UnionType, UninhabitedType, true_only, false_only, TypeVarId, TypeOfAny, LiteralType
)
from mypy.nodes import ARG_POS, ARG_OPT, ARG_STAR, ARG_STAR2, CONTRAVARIANT, INVARIANT, COVARIANT
from mypy.subtypes import is_subtype, is_more_precise, is_proper_subtype
from mypy.test.typefixture import TypeFixture, InterfaceTypeFixture
from mypy.experiments import strict_optional_set


class TypesSuite(Suite):
Expand Down Expand Up @@ -846,8 +848,31 @@ def test_type_type(self) -> None:
self.assert_meet(self.fx.type_type, self.fx.type_any, self.fx.type_any)
self.assert_meet(self.fx.type_b, self.fx.anyt, self.fx.type_b)

def test_literal_type(self) -> None:
a = self.fx.a
d = self.fx.d
lit1 = LiteralType(1, a)
lit2 = LiteralType(2, a)
lit3 = LiteralType("foo", d)

self.assert_meet(lit1, lit1, lit1)
self.assert_meet(lit1, a, lit1)
self.assert_meet_uninhabited(lit1, lit3)
self.assert_meet_uninhabited(lit1, lit2)
self.assert_meet(UnionType([lit1, lit2]), lit1, lit1)
self.assert_meet(UnionType([lit1, lit2]), UnionType([lit2, lit3]), lit2)
self.assert_meet(UnionType([lit1, lit2]), UnionType([lit1, lit2]), UnionType([lit1, lit2]))
self.assert_meet(lit1, self.fx.anyt, lit1)
self.assert_meet(lit1, self.fx.o, lit1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe test these two in reverse order too? (i.e. if literal is the second one)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of assert_meet already does this for us, thankfully: it's defined as just:

def assert_meet(self, s: Type, t: Type, meet: Type) -> None:
    self.assert_simple_meet(s, t, meet)
    self.assert_simple_meet(t, s, meet)

...where assert_simple_meet is responsible for actually doing the checking.


# FIX generic interfaces + ranges

def assert_meet_uninhabited(self, s: Type, t: Type) -> None:
with strict_optional_set(False):
self.assert_meet(s, t, self.fx.nonet)
with strict_optional_set(True):
self.assert_meet(s, t, self.fx.uninhabited)

def assert_meet(self, s: Type, t: Type, meet: Type) -> None:
self.assert_simple_meet(s, t, meet)
self.assert_simple_meet(t, s, meet)
Expand All @@ -874,3 +899,52 @@ def callable(self, *a: Type) -> CallableType:
return CallableType(list(a[:-1]),
[ARG_POS] * n, [None] * n,
a[-1], self.fx.function)


class SameTypeSuite(Suite):
def setUp(self) -> None:
self.fx = TypeFixture()

def test_literal_type(self) -> None:
a = self.fx.a
b = self.fx.b # Reminder: b is a subclass of a
d = self.fx.d

# Literals are not allowed to contain floats, but we're going to
# test them anyways, just to make sure the semantics are robust
# against these kinds of things.
lit0 = LiteralType(cast(int, 1.0), a)
lit1 = LiteralType(1, b)
lit2 = LiteralType(2, b)
lit3 = LiteralType("foo", d)

self.assert_same(lit1, lit1)
self.assert_same(UnionType([lit1, lit2]), UnionType([lit1, lit2]))
self.assert_same(UnionType([lit1, lit2]), UnionType([lit2, lit1]))
self.assert_not_same(lit1, b)
self.assert_not_same(lit0, lit1)
self.assert_not_same(lit1, lit2)
self.assert_not_same(lit1, lit3)

self.assert_not_same(lit1, self.fx.anyt)
self.assert_not_same(lit1, self.fx.nonet)

def assert_same(self, s: Type, t: Type, strict: bool = True) -> None:
self.assert_simple_is_same(s, t, expected=True, strict=strict)
self.assert_simple_is_same(t, s, expected=True, strict=strict)

def assert_not_same(self, s: Type, t: Type, strict: bool = True) -> None:
self.assert_simple_is_same(s, t, False, strict=strict)
self.assert_simple_is_same(t, s, False, strict=strict)

def assert_simple_is_same(self, s: Type, t: Type, expected: bool, strict: bool) -> None:
actual = is_same_type(s, t)
assert_equal(actual, expected,
'is_same_type({}, {}) is {{}} ({{}} expected)'.format(s, t))

if strict:
actual2 = (s == t)
assert_equal(actual2, expected,
'({} == {}) is {{}} ({{}} expected)'.format(s, t))
assert_equal(hash(s) == hash(t), expected,
'(hash({}) == hash({}) is {{}} ({{}} expected)'.format(s, t))
78 changes: 78 additions & 0 deletions test-data/unit/check-literal.test
Original file line number Diff line number Diff line change
Expand Up @@ -1240,3 +1240,81 @@ indirect.Literal()
[builtins fixtures/isinstancelist.pyi]
[out]


--
-- Other misc interactions
--

[case testLiteralMeetsWithCallablesInLists]
from typing import List, Callable, Union
from typing_extensions import Literal

a: Callable[[Literal[1]], int]
b: Callable[[Literal[2]], str]
c: Callable[[int], str]
d: Callable[[object], str]
e: Callable[[Union[Literal[1], Literal[2]]], str]

arr1 = [a, a]
arr2 = [a, b]
arr3 = [a, c]
arr4 = [a, d]
arr5 = [a, e]

reveal_type(arr1) # E: Revealed type is 'builtins.list[def (Literal[1]) -> builtins.int]'
reveal_type(arr2) # E: Revealed type is 'builtins.list[builtins.function*]'
reveal_type(arr3) # E: Revealed type is 'builtins.list[def (Literal[1]) -> builtins.object]'
reveal_type(arr4) # E: Revealed type is 'builtins.list[def (Literal[1]) -> builtins.object]'
reveal_type(arr5) # E: Revealed type is 'builtins.list[def (Literal[1]) -> builtins.object]'

lit: Literal[1]
reveal_type(arr1[0](lit)) # E: Revealed type is 'builtins.int'
reveal_type(arr2[0](lit)) # E: Revealed type is 'Any' \
# E: Cannot call function of unknown type
reveal_type(arr3[0](lit)) # E: Revealed type is 'builtins.object'
reveal_type(arr4[0](lit)) # E: Revealed type is 'builtins.object'
reveal_type(arr5[0](lit)) # E: Revealed type is 'builtins.object'
[builtins fixtures/list.pyi]
[out]

[case testLiteralMeetsWithTypeVars]
from typing import TypeVar, Callable, Union
from typing_extensions import Literal

T = TypeVar('T')
def unify(func: Callable[[T, T], None]) -> T: pass

def f1(x: Literal[1], y: Literal[1]) -> None: pass
def f2(x: Literal[1], y: Literal[2]) -> None: pass
def f3(x: Literal[1], y: int) -> None: pass
def f4(x: Literal[1], y: object) -> None: pass
def f5(x: Literal[1], y: Union[Literal[1], Literal[2]]) -> None: pass

reveal_type(unify(f1)) # E: Revealed type is 'Literal[1]'
reveal_type(unify(f2)) # E: Revealed type is 'None'
reveal_type(unify(f3)) # E: Revealed type is 'Literal[1]'
reveal_type(unify(f4)) # E: Revealed type is 'Literal[1]'
reveal_type(unify(f5)) # E: Revealed type is 'Literal[1]'
[out]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, having both these tests look a bit redundant (especially that you have very good unit tests). I would combine them into a single tests and check meeting some type one way, and some types another way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consolidated both tests into one and pruned out some probably-overkill reveal_type(...) calls. I think testing the types in both ways looks tidier though, so kept that.


[case testLiteralMeetsWithStrictOptional]
# flags: --strict-optional
from typing import TypeVar, Callable, Union
from typing_extensions import Literal

a: Callable[[Literal[1]], int]
b: Callable[[Literal[2]], str]
lit: Literal[1]

arr = [a, b]
reveal_type(arr) # E: Revealed type is 'builtins.list[builtins.function*]'
reveal_type(arr[0](lit)) # E: Revealed type is 'Any' \
# E: Cannot call function of unknown type

T = TypeVar('T')
def unify(func: Callable[[T, T], None]) -> T: pass
def func(x: Literal[1], y: Literal[2]) -> None: pass

reveal_type(unify(func)) # E: Revealed type is '<nothing>'
[builtins fixtures/list.pyi]
[out]