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

Make revealed type of Final vars distinct from non-Final vars #7955

Merged
merged 3 commits into from
Nov 15, 2019
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
42 changes: 33 additions & 9 deletions docs/source/literal_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,16 @@ you can instead change the variable to be ``Final`` (see :ref:`final_attrs`):

c: Final = 19

reveal_type(c) # Revealed type is 'int'
expects_literal(c) # ...but this type checks!
reveal_type(c) # Revealed type is 'Literal[19]?'
expects_literal(c) # ...and this type checks!

If you do not provide an explicit type in the ``Final``, the type of ``c`` becomes
context-sensitive: mypy will basically try "substituting" the original assigned
value whenever it's used before performing type checking. So, mypy will type-check
the above program almost as if it were written like so:
*context-sensitive*: mypy will basically try "substituting" the original assigned
value whenever it's used before performing type checking. This is why the revealed
type of ``c`` is ``Literal[19]?``: the question mark at the end reflects this
context-sensitive nature.

For example, mypy will type check the above program almost as if it were written like so:

.. code-block:: python

Expand All @@ -138,11 +141,32 @@ the above program almost as if it were written like so:
reveal_type(19)
expects_literal(19)

This is why ``expects_literal(19)`` type-checks despite the fact that ``reveal_type(c)``
reports ``int``.
This means that while changing a variable to be ``Final`` is not quite the same thing
as adding an explicit ``Literal[...]`` annotation, it often leads to the same effect
in practice.

The main cases where the behavior of context-sensitive vs true literal types differ are
when you try using those types in places that are not explicitly expecting a ``Literal[...]``.
For example, compare and contrast what happens when you try appending these types to a list:

.. code-block:: python

from typing_extensions import Final, Literal

a: Final = 19
b: Literal[19] = 19

# Mypy will chose to infer List[int] here.
list_of_ints = []
list_of_ints.append(a)
reveal_type(list_of_ints) # Revealed type is 'List[int]'

# But if the variable you're appending is an explicit Literal, mypy
# will infer List[Literal[19]].
list_of_lits = []
list_of_lits.append(b)
reveal_type(list_of_lits) # Revealed type is 'List[Literal[19]]'

So while changing a variable to be ``Final`` is not quite the same thing as adding
an explicit ``Literal[...]`` annotation, it often leads to the same effect in practice.

Limitations
***********
Expand Down
1 change: 1 addition & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4099,6 +4099,7 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance:
the name refers to a compatible generic type.
"""
info = self.lookup_typeinfo(name)
args = [remove_instance_last_known_values(arg) for arg in args]
# TODO: assert len(args) == len(info.defn.type_vars)
return Instance(info, args)

Expand Down
15 changes: 8 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import mypy.checker
from mypy import types
from mypy.sametypes import is_same_type
from mypy.erasetype import replace_meta_vars, erase_type
from mypy.erasetype import replace_meta_vars, erase_type, remove_instance_last_known_values
from mypy.maptype import map_instance_to_supertype
from mypy.messages import MessageBuilder
from mypy import message_registry
Expand Down Expand Up @@ -3045,12 +3045,13 @@ def check_lst_expr(self, items: List[Expression], fullname: str,
self.named_type('builtins.function'),
name=tag,
variables=[tvdef])
return self.check_call(constructor,
[(i.expr if isinstance(i, StarExpr) else i)
for i in items],
[(nodes.ARG_STAR if isinstance(i, StarExpr) else nodes.ARG_POS)
for i in items],
context)[0]
out = self.check_call(constructor,
[(i.expr if isinstance(i, StarExpr) else i)
for i in items],
[(nodes.ARG_STAR if isinstance(i, StarExpr) else nodes.ARG_POS)
for i in items],
context)[0]
return remove_instance_last_known_values(out)

def visit_tuple_expr(self, e: TupleExpr) -> Type:
"""Type check a tuple expression."""
Expand Down
7 changes: 6 additions & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,12 @@ def analyze_class_attribute_access(itype: Instance,

if info.is_enum and not (mx.is_lvalue or is_decorated or is_method):
enum_literal = LiteralType(name, fallback=itype)
return itype.copy_modified(last_known_value=enum_literal)
# When we analyze enums, the corresponding Instance is always considered to be erased
# due to how the signature of Enum.__new__ is `(cls: Type[_T], value: object) -> _T`
# in typeshed. However, this is really more of an implementation detail of how Enums
# are typed, and we really don't want to treat every single Enum value as if it were
# from type variable substitution. So we reset the 'erased' field here.
return itype.copy_modified(erased=False, last_known_value=enum_literal)

t = node.type
if t:
Expand Down
9 changes: 6 additions & 3 deletions mypy/erasetype.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,12 @@ class LastKnownValueEraser(TypeTranslator):
Instance types."""

def visit_instance(self, t: Instance) -> Type:
if t.last_known_value:
return t.copy_modified(last_known_value=None)
return t
if not t.last_known_value and not t.args:
return t
return t.copy_modified(
args=[a.accept(self) for a in t.args],
last_known_value=None,
)
Copy link
Member

Choose a reason for hiding this comment

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

Good catch! Did you add a test for this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point -- I initially didn't since we already had several tests unrelated to literal types that were breaking because of this.

But I think it'd be good to exercise this more directly, so I added a test to check-literals.test.


def visit_type_alias_type(self, t: TypeAliasType) -> Type:
# Type aliases can't contain literal values, because they are
Expand Down
11 changes: 9 additions & 2 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,13 +830,14 @@ def deserialize(cls, data: Union[JsonDict, str]) -> 'Instance':

def copy_modified(self, *,
args: Bogus[List[Type]] = _dummy,
erased: Bogus[bool] = _dummy,
last_known_value: Bogus[Optional['LiteralType']] = _dummy) -> 'Instance':
return Instance(
self.type,
args if args is not _dummy else self.args,
self.line,
self.column,
self.erased,
erased if erased is not _dummy else self.erased,
last_known_value if last_known_value is not _dummy else self.last_known_value,
)

Expand Down Expand Up @@ -1988,7 +1989,13 @@ def visit_deleted_type(self, t: DeletedType) -> str:
return "<Deleted '{}'>".format(t.source)

def visit_instance(self, t: Instance) -> str:
s = t.type.fullname or t.type.name or '<???>'
if t.last_known_value and not t.args:
# Instances with a literal fallback should never be generic. If they are,
# something went wrong so we fall back to showing the full Instance repr.
s = '{}?'.format(t.last_known_value)
else:
s = t.type.fullname or t.type.name or '<???>'

if t.erased:
s += '*'
if t.args != []:
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-columns.test
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ if int():

[case testColumnRevealedType]
if int():
reveal_type(1) # N:17: Revealed type is 'builtins.int'
reveal_type(1) # N:17: Revealed type is 'Literal[1]?'

[case testColumnNonOverlappingEqualityCheck]
# flags: --strict-equality
Expand Down
66 changes: 33 additions & 33 deletions test-data/unit/check-enum.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Medal(Enum):
gold = 1
silver = 2
bronze = 3
reveal_type(Medal.bronze) # N: Revealed type is '__main__.Medal*'
reveal_type(Medal.bronze) # N: Revealed type is 'Literal[__main__.Medal.bronze]?'
m = Medal.gold
if int():
m = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "Medal")
Expand All @@ -20,7 +20,7 @@ class Medal(metaclass=EnumMeta):
# Without __init__ the definition fails at runtime, but we want to verify that mypy
# uses `enum.EnumMeta` and not `enum.Enum` as the definition of what is enum.
def __init__(self, *args): pass
reveal_type(Medal.bronze) # N: Revealed type is '__main__.Medal'
reveal_type(Medal.bronze) # N: Revealed type is 'Literal[__main__.Medal.bronze]?'
m = Medal.gold
if int():
m = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "Medal")
Expand All @@ -34,7 +34,7 @@ class Medal(Achievement):
bronze = None
# See comment in testEnumFromEnumMetaBasics
def __init__(self, *args): pass
reveal_type(Medal.bronze) # N: Revealed type is '__main__.Medal'
reveal_type(Medal.bronze) # N: Revealed type is 'Literal[__main__.Medal.bronze]?'
m = Medal.gold
if int():
m = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "Medal")
Expand All @@ -53,7 +53,7 @@ class Truth(Enum):
false = False
x = ''
x = Truth.true.name
reveal_type(Truth.true.name) # N: Revealed type is 'builtins.str'
reveal_type(Truth.true.name) # N: Revealed type is 'Literal['true']?'
reveal_type(Truth.false.value) # N: Revealed type is 'builtins.bool'
[builtins fixtures/bool.pyi]

Expand Down Expand Up @@ -246,7 +246,7 @@ class A:
a = A()
reveal_type(a.x)
[out]
main:8: note: Revealed type is '__main__.E@4*'
main:8: note: Revealed type is '__main__.E@4'

[case testEnumInClassBody]
from enum import Enum
Expand All @@ -270,9 +270,9 @@ reveal_type(E.bar.value)
reveal_type(I.bar)
reveal_type(I.baz.value)
[out]
main:4: note: Revealed type is '__main__.E*'
main:4: note: Revealed type is 'Literal[__main__.E.foo]?'
main:5: note: Revealed type is 'Any'
main:6: note: Revealed type is '__main__.I*'
main:6: note: Revealed type is 'Literal[__main__.I.bar]?'
main:7: note: Revealed type is 'builtins.int'

[case testFunctionalEnumListOfStrings]
Expand All @@ -282,8 +282,8 @@ F = IntEnum('F', ['bar', 'baz'])
reveal_type(E.foo)
reveal_type(F.baz)
[out]
main:4: note: Revealed type is '__main__.E*'
main:5: note: Revealed type is '__main__.F*'
main:4: note: Revealed type is 'Literal[__main__.E.foo]?'
main:5: note: Revealed type is 'Literal[__main__.F.baz]?'

[case testFunctionalEnumListOfPairs]
from enum import Enum, IntEnum
Expand All @@ -294,10 +294,10 @@ reveal_type(F.baz)
reveal_type(E.foo.value)
reveal_type(F.bar.name)
[out]
main:4: note: Revealed type is '__main__.E*'
main:5: note: Revealed type is '__main__.F*'
main:6: note: Revealed type is 'builtins.int'
main:7: note: Revealed type is 'builtins.str'
main:4: note: Revealed type is 'Literal[__main__.E.foo]?'
main:5: note: Revealed type is 'Literal[__main__.F.baz]?'
main:6: note: Revealed type is 'Literal[1]?'
main:7: note: Revealed type is 'Literal['bar']?'

[case testFunctionalEnumDict]
from enum import Enum, IntEnum
Expand All @@ -308,10 +308,10 @@ reveal_type(F.baz)
reveal_type(E.foo.value)
reveal_type(F.bar.name)
[out]
main:4: note: Revealed type is '__main__.E*'
main:5: note: Revealed type is '__main__.F*'
main:6: note: Revealed type is 'builtins.int'
main:7: note: Revealed type is 'builtins.str'
main:4: note: Revealed type is 'Literal[__main__.E.foo]?'
main:5: note: Revealed type is 'Literal[__main__.F.baz]?'
main:6: note: Revealed type is 'Literal[1]?'
main:7: note: Revealed type is 'Literal['bar']?'

[case testFunctionalEnumErrors]
from enum import Enum, IntEnum
Expand Down Expand Up @@ -363,10 +363,10 @@ main:22: error: "Type[W]" has no attribute "c"
from enum import Flag, IntFlag
A = Flag('A', 'x y')
B = IntFlag('B', 'a b')
reveal_type(A.x) # N: Revealed type is '__main__.A*'
reveal_type(B.a) # N: Revealed type is '__main__.B*'
reveal_type(A.x.name) # N: Revealed type is 'builtins.str'
reveal_type(B.a.name) # N: Revealed type is 'builtins.str'
reveal_type(A.x) # N: Revealed type is 'Literal[__main__.A.x]?'
reveal_type(B.a) # N: Revealed type is 'Literal[__main__.B.a]?'
reveal_type(A.x.name) # N: Revealed type is 'Literal['x']?'
reveal_type(B.a.name) # N: Revealed type is 'Literal['a']?'

# TODO: The revealed type should be 'int' here
reveal_type(A.x.value) # N: Revealed type is 'Any'
Expand All @@ -381,7 +381,7 @@ class A:
a = A()
reveal_type(a.x)
[out]
main:7: note: Revealed type is '__main__.A.E@4*'
main:7: note: Revealed type is '__main__.A.E@4'

[case testFunctionalEnumInClassBody]
from enum import Enum
Expand Down Expand Up @@ -451,19 +451,19 @@ F = Enum('F', 'a b')
[rechecked]
[stale]
[out1]
main:2: note: Revealed type is 'm.E*'
main:3: note: Revealed type is 'm.F*'
main:2: note: Revealed type is 'Literal[m.E.a]?'
main:3: note: Revealed type is 'Literal[m.F.b]?'
[out2]
main:2: note: Revealed type is 'm.E*'
main:3: note: Revealed type is 'm.F*'
main:2: note: Revealed type is 'Literal[m.E.a]?'
main:3: note: Revealed type is 'Literal[m.F.b]?'

[case testEnumAuto]
from enum import Enum, auto
class Test(Enum):
a = auto()
b = auto()

reveal_type(Test.a) # N: Revealed type is '__main__.Test*'
reveal_type(Test.a) # N: Revealed type is 'Literal[__main__.Test.a]?'
[builtins fixtures/primitives.pyi]

[case testEnumAttributeAccessMatrix]
Expand Down Expand Up @@ -689,31 +689,31 @@ else:

if x is z:
reveal_type(x) # N: Revealed type is 'Literal[__main__.Foo.A]'
reveal_type(z) # N: Revealed type is '__main__.Foo*'
reveal_type(z) # N: Revealed type is 'Literal[__main__.Foo.A]?'
accepts_foo_a(z)
else:
reveal_type(x) # N: Revealed type is 'Union[Literal[__main__.Foo.B], Literal[__main__.Foo.C]]'
reveal_type(z) # N: Revealed type is '__main__.Foo*'
reveal_type(z) # N: Revealed type is 'Literal[__main__.Foo.A]?'
accepts_foo_a(z)
if z is x:
reveal_type(x) # N: Revealed type is 'Literal[__main__.Foo.A]'
reveal_type(z) # N: Revealed type is '__main__.Foo*'
reveal_type(z) # N: Revealed type is 'Literal[__main__.Foo.A]?'
accepts_foo_a(z)
else:
reveal_type(x) # N: Revealed type is 'Union[Literal[__main__.Foo.B], Literal[__main__.Foo.C]]'
reveal_type(z) # N: Revealed type is '__main__.Foo*'
reveal_type(z) # N: Revealed type is 'Literal[__main__.Foo.A]?'
accepts_foo_a(z)

if y is z:
reveal_type(y) # N: Revealed type is 'Literal[__main__.Foo.A]'
reveal_type(z) # N: Revealed type is '__main__.Foo*'
reveal_type(z) # N: Revealed type is 'Literal[__main__.Foo.A]?'
accepts_foo_a(z)
else:
reveal_type(y) # No output: this branch is unreachable
reveal_type(z) # No output: this branch is unreachable
if z is y:
reveal_type(y) # N: Revealed type is 'Literal[__main__.Foo.A]'
reveal_type(z) # N: Revealed type is '__main__.Foo*'
reveal_type(z) # N: Revealed type is 'Literal[__main__.Foo.A]?'
accepts_foo_a(z)
else:
reveal_type(y) # No output: this branch is unreachable
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class A:
pass

[case testErrorCodeNoteHasNoCode]
reveal_type(1) # N: Revealed type is 'builtins.int'
reveal_type(1) # N: Revealed type is 'Literal[1]?'

[case testErrorCodeSyntaxError]
1 '' # E: invalid syntax [syntax]
Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-expressions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1911,7 +1911,7 @@ from typing import Union
reveal_type(1 if bool() else 2) # N: Revealed type is 'builtins.int'
reveal_type(1 if bool() else '') # N: Revealed type is 'builtins.object'
x: Union[int, str] = reveal_type(1 if bool() else '') \
# N: Revealed type is 'Union[builtins.int, builtins.str]'
# N: Revealed type is 'Union[Literal[1]?, Literal['']?]'
class A:
pass
class B(A):
Expand All @@ -1934,7 +1934,7 @@ reveal_type(d if bool() else b) # N: Revealed type is '__main__.A'
[case testConditionalExpressionUnionWithAny]
from typing import Union, Any
a: Any
x: Union[int, str] = reveal_type(a if int() else 1) # N: Revealed type is 'Union[Any, builtins.int]'
x: Union[int, str] = reveal_type(a if int() else 1) # N: Revealed type is 'Union[Any, Literal[1]?]'
reveal_type(a if int() else 1) # N: Revealed type is 'Any'


Expand Down Expand Up @@ -2207,7 +2207,7 @@ d() # E: "D[str, int]" not callable
[builtins fixtures/dict.pyi]

[case testRevealType]
reveal_type(1) # N: Revealed type is 'builtins.int'
reveal_type(1) # N: Revealed type is 'Literal[1]?'

[case testRevealLocals]
x = 1
Expand Down
Loading