Skip to content

Commit 720b77e

Browse files
authored
Supports __future__.annotations (#7963)
This PR partially addresses #7907.
1 parent bbd6677 commit 720b77e

File tree

6 files changed

+70
-4
lines changed

6 files changed

+70
-4
lines changed

mypy/semanal.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@
124124
T = TypeVar('T')
125125

126126

127+
FUTURE_IMPORTS = {
128+
'__future__.nested_scopes': 'nested_scopes',
129+
'__future__.generators': 'generators',
130+
'__future__.division': 'division',
131+
'__future__.absolute_import': 'absolute_import',
132+
'__future__.with_statement': 'with_statement',
133+
'__future__.print_function': 'print_function',
134+
'__future__.unicode_literals': 'unicode_literals',
135+
'__future__.barry_as_FLUFL': 'barry_as_FLUFL',
136+
'__future__.generator_stop': 'generator_stop',
137+
'__future__.annotations': 'annotations',
138+
} # type: Final
139+
140+
127141
# Special cased built-in classes that are needed for basic functionality and need to be
128142
# available very early on.
129143
CORE_BUILTIN_CLASSES = ['object', 'bool', 'function'] # type: Final
@@ -200,6 +214,7 @@ class SemanticAnalyzer(NodeVisitor[None],
200214
errors = None # type: Errors # Keeps track of generated errors
201215
plugin = None # type: Plugin # Mypy plugin for special casing of library features
202216
statement = None # type: Optional[Statement] # Statement/definition being analyzed
217+
future_import_flags = None # type: Set[str]
203218

204219
# Mapping from 'async def' function definitions to their return type wrapped as a
205220
# 'Coroutine[Any, Any, T]'. Used to keep track of whether a function definition's
@@ -261,6 +276,8 @@ def __init__(self,
261276
# current SCC or top-level function.
262277
self.deferral_debug_context = [] # type: List[Tuple[str, int]]
263278

279+
self.future_import_flags = set() # type: Set[str]
280+
264281
# mypyc doesn't properly handle implementing an abstractproperty
265282
# with a regular attribute so we make them properties
266283
@property
@@ -1704,6 +1721,7 @@ def visit_import_from(self, imp: ImportFrom) -> None:
17041721
module = self.modules.get(module_id)
17051722
for id, as_id in imp.names:
17061723
fullname = module_id + '.' + id
1724+
self.set_future_import_flags(fullname)
17071725
if module is None:
17081726
node = None
17091727
elif module_id == self.cur_mod_id and fullname in self.modules:
@@ -1863,6 +1881,8 @@ def visit_import_all(self, i: ImportAll) -> None:
18631881
# namespace is incomplete.
18641882
self.mark_incomplete('*', i)
18651883
for name, node in m.names.items():
1884+
fullname = i_id + '.' + name
1885+
self.set_future_import_flags(fullname)
18661886
if node is None:
18671887
continue
18681888
# if '__all__' exists, all nodes not included have had module_public set to
@@ -4909,6 +4929,13 @@ def parse_bool(self, expr: Expression) -> Optional[bool]:
49094929
return False
49104930
return None
49114931

4932+
def set_future_import_flags(self, module_name: str) -> None:
4933+
if module_name in FUTURE_IMPORTS:
4934+
self.future_import_flags.add(FUTURE_IMPORTS[module_name])
4935+
4936+
def is_future_flag_set(self, flag: str) -> bool:
4937+
return flag in self.future_import_flags
4938+
49124939

49134940
class HasPlaceholders(TypeQuery[bool]):
49144941
def __init__(self) -> None:

mypy/semanal_shared.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ def final_iteration(self) -> bool:
7373
"""Is this the final iteration of semantic analysis?"""
7474
raise NotImplementedError
7575

76+
@abstractmethod
77+
def is_future_flag_set(self, flag: str) -> bool:
78+
"""Is the specific __future__ feature imported"""
79+
raise NotImplementedError
80+
7681

7782
@trait
7883
class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface):

mypy/test/testcheck.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
'check-selftype.test',
6666
'check-python2.test',
6767
'check-columns.test',
68+
'check-future.test',
6869
'check-functions.test',
6970
'check-tuples.test',
7071
'check-expressions.test',

mypy/typeanal.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
196196
return hook(AnalyzeTypeContext(t, t, self))
197197
if (fullname in nongen_builtins
198198
and t.args and
199-
not self.allow_unnormalized):
199+
not self.allow_unnormalized and
200+
not self.api.is_future_flag_set("annotations")):
200201
self.fail(no_subscript_builtin_alias(fullname,
201202
propose_alt=not self.defining_alias), t)
202203
tvar_def = self.tvar_scope.get_binding(sym)
@@ -291,12 +292,14 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
291292
return make_optional_type(item)
292293
elif fullname == 'typing.Callable':
293294
return self.analyze_callable_type(t)
294-
elif fullname == 'typing.Type':
295+
elif (fullname == 'typing.Type' or
296+
(fullname == 'builtins.type' and self.api.is_future_flag_set('annotations'))):
295297
if len(t.args) == 0:
296298
any_type = self.get_omitted_any(t)
297299
return TypeType(any_type, line=t.line, column=t.column)
300+
type_str = 'Type[...]' if fullname == 'typing.Type' else 'type[...]'
298301
if len(t.args) != 1:
299-
self.fail('Type[...] must have exactly one type argument', t)
302+
self.fail(type_str + ' must have exactly one type argument', t)
300303
item = self.anal_type(t.args[0])
301304
return TypeType.make_normalized(item, line=t.line)
302305
elif fullname == 'typing.ClassVar':

test-data/unit/check-future.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- Test cases for __future__ imports
2+
3+
[case testFutureAnnotationsImportCollections]
4+
# flags: --python-version 3.7
5+
from __future__ import annotations
6+
from collections import defaultdict, ChainMap, Counter, deque
7+
8+
t1: defaultdict[int, int]
9+
t2: ChainMap[int, int]
10+
t3: Counter[int]
11+
t4: deque[int]
12+
13+
[builtins fixtures/tuple.pyi]
14+
15+
[case testFutureAnnotationsImportBuiltIns]
16+
# flags: --python-version 3.7
17+
from __future__ import annotations
18+
19+
t1: type[int]
20+
t2: list[int]
21+
t3: dict[int, int]
22+
t4: tuple[int, str, int]
23+
24+
[builtins fixtures/dict.pyi]

test-data/unit/lib-stub/collections.pyi

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Iterable, Union, Optional, Dict, TypeVar, overload, Optional, Callable
1+
from typing import Any, Iterable, Union, Optional, Dict, TypeVar, overload, Optional, Callable, Sized
22

33
def namedtuple(
44
typename: str,
@@ -17,3 +17,9 @@ class OrderedDict(Dict[KT, VT]): ...
1717

1818
class defaultdict(Dict[KT, VT]):
1919
def __init__(self, default_factory: Optional[Callable[[], VT]]) -> None: ...
20+
21+
class Counter(Dict[KT, int], Generic[KT]): ...
22+
23+
class deque(Sized, Iterable[KT], Reversible[KT], Generic[KT]): ...
24+
25+
class ChainMap(MutableMapping[KT, VT], Generic[KT, VT]): ...

0 commit comments

Comments
 (0)