Fix issues with Python 3.6's f strings and underscores in numbers.

This commit is contained in:
Dave Halter
2017-01-08 19:38:57 +01:00
parent 00a9f1ec0a
commit 7300f3e7ef
6 changed files with 101 additions and 22 deletions

View File

@@ -7,6 +7,7 @@ from collections import namedtuple
from jedi._compatibility import u from jedi._compatibility import u
from jedi.evaluate.helpers import evaluate_call_of_leaf from jedi.evaluate.helpers import evaluate_call_of_leaf
from jedi import parser from jedi import parser
from jedi.parser import tree
from jedi.parser import tokenize from jedi.parser import tokenize
from jedi.cache import time_cache from jedi.cache import time_cache
from jedi import common from jedi import common
@@ -203,6 +204,8 @@ def evaluate_goto_definition(evaluator, context, leaf):
return context.eval_node(leaf.parent) return context.eval_node(leaf.parent)
elif parent.type == 'trailer': elif parent.type == 'trailer':
return evaluate_call_of_leaf(context, leaf) return evaluate_call_of_leaf(context, leaf)
elif isinstance(leaf, tree.Literal):
return context.evaluator.eval_atom(context, leaf)
return [] return []

View File

@@ -270,7 +270,7 @@ class Evaluator(object):
debug.dbg('eval_element %s@%s', element, element.start_pos) debug.dbg('eval_element %s@%s', element, element.start_pos)
types = set() types = set()
if isinstance(element, (tree.Name, tree.Literal)) or tree.is_node(element, 'atom'): if isinstance(element, (tree.Name, tree.Literal)) or tree.is_node(element, 'atom'):
types = self._eval_atom(context, element) types = self.eval_atom(context, element)
elif isinstance(element, tree.Keyword): elif isinstance(element, tree.Keyword):
# For False/True/None # For False/True/None
if element.value in ('False', 'True', 'None'): if element.value in ('False', 'True', 'None'):
@@ -283,7 +283,7 @@ class Evaluator(object):
elif element.type in ('power', 'atom_expr'): elif element.type in ('power', 'atom_expr'):
first_child = element.children[0] first_child = element.children[0]
if not (first_child.type == 'keyword' and first_child.value == 'await'): if not (first_child.type == 'keyword' and first_child.value == 'await'):
types = self._eval_atom(context, first_child) types = self.eval_atom(context, first_child)
for trailer in element.children[1:]: for trailer in element.children[1:]:
if trailer == '**': # has a power operation. if trailer == '**': # has a power operation.
right = self.eval_element(context, element.children[2]) right = self.eval_element(context, element.children[2])
@@ -306,7 +306,7 @@ class Evaluator(object):
assert element.value == '...' assert element.value == '...'
types = set([compiled.create(self, Ellipsis)]) types = set([compiled.create(self, Ellipsis)])
elif element.type == 'dotted_name': elif element.type == 'dotted_name':
types = self._eval_atom(context, element.children[0]) types = self.eval_atom(context, element.children[0])
for next_name in element.children[2::2]: for next_name in element.children[2::2]:
# TODO add search_global=True? # TODO add search_global=True?
types = unite( types = unite(
@@ -325,7 +325,7 @@ class Evaluator(object):
debug.dbg('eval_element result %s', types) debug.dbg('eval_element result %s', types)
return types return types
def _eval_atom(self, context, atom): def eval_atom(self, context, atom):
""" """
Basically to process ``atom`` nodes. The parser sometimes doesn't Basically to process ``atom`` nodes. The parser sometimes doesn't
generate the node (because it has just one child). In that case an atom generate the node (because it has just one child). In that case an atom
@@ -351,9 +351,9 @@ class Evaluator(object):
c = atom.children c = atom.children
if c[0].type == 'string': if c[0].type == 'string':
# Will be one string. # Will be one string.
types = self._eval_atom(context, c[0]) types = self.eval_atom(context, c[0])
for string in c[1:]: for string in c[1:]:
right = self._eval_atom(context, string) right = self.eval_atom(context, string)
types = precedence.calculate(self, context, types, '+', right) types = precedence.calculate(self, context, types, '+', right)
return types return types
# Parentheses without commas are not tuples. # Parentheses without commas are not tuples.

View File

@@ -59,21 +59,32 @@ Whitespace = r'[ \f\t]*'
Comment = r'#[^\r\n]*' Comment = r'#[^\r\n]*'
Name = r'\w+' Name = r'\w+'
Hexnumber = r'0[xX](?:_?[0-9a-fA-F])+' if py_version >= 36:
Binnumber = r'0[bB](?:_?[01])+' Hexnumber = r'0[xX](?:_?[0-9a-fA-F])+'
if is_py3: Binnumber = r'0[bB](?:_?[01])+'
Octnumber = r'0[oO](?:_?[0-7])+' Octnumber = r'0[oO](?:_?[0-7])+'
Decnumber = r'(?:0(?:_?0)*|[1-9](?:_?[0-9])*)'
Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber)
Exponent = r'[eE][-+]?[0-9](?:_?[0-9])*'
Pointfloat = group(r'[0-9](?:_?[0-9])*\.(?:[0-9](?:_?[0-9])*)?',
r'\.[0-9](?:_?[0-9])*') + maybe(Exponent)
Expfloat = r'[0-9](?:_?[0-9])*' + Exponent
Floatnumber = group(Pointfloat, Expfloat)
Imagnumber = group(r'[0-9](?:_?[0-9])*[jJ]', Floatnumber + r'[jJ]')
else: else:
Octnumber = '0[0-7]+' Hexnumber = r'0[xX][0-9a-fA-F]+'
Binnumber = r'0[bB][01]+'
Decnumber = r'(?:0(?:_?0)*|[1-9](?:_?[0-9])*)' if is_py3:
Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber) Octnumber = r'0[oO][0-7]+'
Exponent = r'[eE][-+]?[0-9](?:_?[0-9])*' else:
Pointfloat = group(r'[0-9](?:_?[0-9])*\.(?:[0-9](?:_?[0-9])*)?', Octnumber = '0[0-7]+'
r'\.[0-9](?:_?[0-9])*') + maybe(Exponent) Decnumber = r'(?:0+|[1-9][0-9]*)'
Expfloat = r'[0-9](?:_?[0-9])*' + Exponent Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber)
Floatnumber = group(Pointfloat, Expfloat) Exponent = r'[eE][-+]?[0-9]+'
Imagnumber = group(r'[0-9](?:_?[0-9])*[jJ]', Floatnumber + r'[jJ]') Pointfloat = group(r'[0-9]+\.[0-9]*', r'\.[0-9]+') + maybe(Exponent)
Expfloat = r'[0-9]+' + Exponent
Floatnumber = group(Pointfloat, Expfloat)
Imagnumber = group(r'[0-9]+[jJ]', Floatnumber + r'[jJ]')
Number = group(Imagnumber, Floatnumber, Intnumber) Number = group(Imagnumber, Floatnumber, Intnumber)
# Return the empty string, plus all of the valid string prefixes. # Return the empty string, plus all of the valid string prefixes.

View File

@@ -41,10 +41,27 @@ import abc
from jedi._compatibility import (Python3Method, encoding, is_py3, utf8_repr, from jedi._compatibility import (Python3Method, encoding, is_py3, utf8_repr,
literal_eval, use_metaclass, unicode) literal_eval, use_metaclass, unicode)
from jedi.parser import token
from jedi.parser.utils import underscore_memoization from jedi.parser.utils import underscore_memoization
def _safe_literal_eval(value):
first_two = value[:2].lower()
if first_two[0] == 'f' or first_two in ('fr', 'rf'):
# literal_eval is not able to resovle f literals. We have to do that
# manually in a later stage
return ''
try:
return literal_eval(value)
except SyntaxError:
# It's possible to create syntax errors with literals like rb'' in
# Python 2. This should not be possible and in that case just return an
# empty string.
# Before Python 3.3 there was a more strict definition in which order
# you could define literals.
return ''
def is_node(node, *symbol_names): def is_node(node, *symbol_names):
try: try:
type = node.type type = node.type
@@ -98,7 +115,7 @@ class DocstringMixin(object):
# leaves anymore that might be part of the docstring. A # leaves anymore that might be part of the docstring. A
# docstring can also look like this: ``'foo' 'bar' # docstring can also look like this: ``'foo' 'bar'
# Returns a literal cleaned version of the ``Token``. # Returns a literal cleaned version of the ``Token``.
cleaned = cleandoc(literal_eval(node.value)) cleaned = cleandoc(_safe_literal_eval(node.value))
# Since we want the docstr output to be always unicode, just # Since we want the docstr output to be always unicode, just
# force it. # force it.
if is_py3 or isinstance(cleaned, unicode): if is_py3 or isinstance(cleaned, unicode):
@@ -422,7 +439,7 @@ class Literal(LeafWithNewLines):
__slots__ = () __slots__ = ()
def eval(self): def eval(self):
return literal_eval(self.value) return _safe_literal_eval(self.value)
class Number(Literal): class Number(Literal):

View File

@@ -19,6 +19,17 @@ str..
#? [] #? []
a(0):. a(0):.
# -----------------
# _ separators (for older versions than 3.6, a = 1_2_3 will just be 1, the rest
# gets ignored.)
# -----------------
#? int()
1_2_3
#? int()
123_345_345
#? int()
0x3_4
# ----------------- # -----------------
# if/else/elif # if/else/elif
# ----------------- # -----------------

View File

@@ -0,0 +1,37 @@
import pytest
import jedi
from jedi._compatibility import py_version
def _eval_literal(value):
def_, = jedi.Script(value).goto_definitions()
return def_._name._context
@pytest.mark.skipif('sys.version_info[:2] < (3, 6)')
def test_f_strings():
"""
f literals are not really supported in Jedi. They just get ignored and an
empty string is returned.
"""
context = _eval_literal('f"asdf"')
assert context.obj == ''
context = _eval_literal('f"{asdf}"')
assert context.obj == ''
context = _eval_literal('F"{asdf}"')
assert context.obj == ''
context = _eval_literal('rF"{asdf}"')
assert context.obj == ''
def test_rb_strings():
context = _eval_literal('br"asdf"')
assert context.obj == b'asdf'
context = _eval_literal('rb"asdf"')
if py_version < 33:
# Before Python 3.3 there was a more strict definition in which order
# you could define literals.
assert context.obj == ''
else:
assert context.obj == b'asdf'