Skip to content

Commit

Permalink
Make revealed type of Final vars distinct from non-Final vars (#7955)
Browse files Browse the repository at this point in the history
This diff changes how we format Instances with a last known value
when displaying them with `reveal_type`.

Previously, we would always ignore the `last_known_value` field:

```python
x: Final = 3
reveal_type(x)  # N: Revealed type is 'builtins.int'
```

Now, we format it like `Literal[3]?`. Note that we use the question
mark suffix as a way of distinguishing the type from true Literal types.

```python
x: Final = 3
y: Literal[3] = 3
reveal_type(x)  # N: Revealed type is 'Literal[3]?'
reveal_type(y)  # N: Revealed type is 'Literal[3]'
```

While making this change and auditing our tests, I also discovered we
were accidentally copying over the `last_known_value` in a few places
by accident. For example:

```python
from typing_extensions import Final

a = []
a.append(1)
a.append(2)     # Got no error here?
reveal_type(a)  # Incorrect revealed type: got builtins.list[Literal[1]?]

b = [0, None]
b.append(1)     # Got no error here?
reveal_type(b)  # Incorrect revealed type: got builtins.list[Union[Literal[0]?, None]]
```

The other code changes I made were largely cosmetic.

Similarly, most of the remaining test changes were just due to places
where we were doing something like `reveal_type(0)` or
`reveal_type(SomeEnum.BLAH)`.

The main motivation behind this diff is that once this lands, it should
become much simpler for me to write some tests I'll need while
revamping #7169. It also helps
make a somewhat confusing and implicit part of mypy internals
more visible.
  • Loading branch information
Michael0x2a authored Nov 15, 2019
1 parent 3b5a62e commit 384f32c
Show file tree
Hide file tree
Showing 20 changed files with 187 additions and 111 deletions.
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,
)

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

0 comments on commit 384f32c

Please sign in to comment.