Annotations can contain forward references even if they are not a string anymore

Since Python 3.7 this behavior can be imported with from future import __annotations
This commit is contained in:
Dave Halter
2018-07-28 16:35:24 +02:00
parent b073b05aa0
commit 9bba91628a
4 changed files with 28 additions and 5 deletions

View File

@@ -216,5 +216,5 @@ class ModuleContext(TreeContext):
return "<%s: %s@%s-%s is_stub=%s>" % ( return "<%s: %s@%s-%s is_stub=%s>" % (
self.__class__.__name__, self._string_name, self.__class__.__name__, self._string_name,
self.tree_node.start_pos[0], self.tree_node.end_pos[0], self.tree_node.start_pos[0], self.tree_node.end_pos[0],
self._path.endswith('.pyi') self._path is not None and self._path.endswith('.pyi')
) )

View File

@@ -110,7 +110,7 @@ class NameFinder(object):
ancestor = search_ancestor(origin_scope, 'funcdef', 'classdef') ancestor = search_ancestor(origin_scope, 'funcdef', 'classdef')
if ancestor is not None: if ancestor is not None:
colon = ancestor.children[-2] colon = ancestor.children[-2]
if position < colon.start_pos: if position is not None and position < colon.start_pos:
if lambdef is None or position < lambdef.children[-2].start_pos: if lambdef is None or position < lambdef.children[-2].start_pos:
position = ancestor.start_pos position = ancestor.start_pos

View File

@@ -54,7 +54,7 @@ def _evaluate_annotation_string(context, string, index=None):
context_set = context.eval_node(node) context_set = context.eval_node(node)
if index is not None: if index is not None:
context_set = context_set.filter( context_set = context_set.filter(
lambda context: context.array_type == u'tuple' lambda context: context.array_type == u'tuple' # noqa
and len(list(context.py__iter__())) >= index and len(list(context.py__iter__())) >= index
).py__getitem__(index) ).py__getitem__(index)
return context_set.execute_evaluated() return context_set.execute_evaluated()

View File

@@ -196,9 +196,17 @@ def eval_atom(context, atom):
) or atom ) or atom
if stmt.type == 'lambdef': if stmt.type == 'lambdef':
stmt = atom stmt = atom
position = stmt.start_pos
if _is_annotation_name(atom):
# Since Python 3.7 (with from __future__ import annotations),
# annotations are essentially strings and can reference objects
# that are defined further down in code. Therefore just set the
# position to None, so the finder will not try to stop at a certain
# position in the module.
position = None
return context.py__getattribute__( return context.py__getattribute__(
name_or_str=atom, name_or_str=atom,
position=stmt.start_pos, position=position,
search_global=True search_global=True
) )
elif atom.type == 'keyword': elif atom.type == 'keyword':
@@ -410,6 +418,22 @@ def _eval_comparison(evaluator, context, left_contexts, operator, right_contexts
) )
def _is_annotation_name(name):
ancestor = tree.search_ancestor(name, 'param', 'funcdef', 'expr_stmt')
if ancestor is None:
return False
if ancestor.type in ('param', 'funcdef'):
annotation = ancestor.annotation
if annotation is not None:
return annotation.start_pos <= name.start_pos < annotation.end_pos
elif ancestor.type == 'expr_stmt':
c = ancestor.children
if len(c) > 1 and c[1].type == 'annassign':
return c[1].start_pos <= name.start_pos < c[1].end_pos
return True
def _is_tuple(context): def _is_tuple(context):
return isinstance(context, iterable.Sequence) and context.array_type == 'tuple' return isinstance(context, iterable.Sequence) and context.array_type == 'tuple'
@@ -497,7 +521,6 @@ def _remove_statements(evaluator, context, stmt, name):
def tree_name_to_contexts(evaluator, context, tree_name): def tree_name_to_contexts(evaluator, context, tree_name):
context_set = ContextSet() context_set = ContextSet()
module_node = context.get_root_context().tree_node module_node = context.get_root_context().tree_node
if module_node is not None: if module_node is not None: