From 7300f3e7ef7dcdc53d230fd1d5c537df3abecd61 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 8 Jan 2017 19:38:57 +0100 Subject: [PATCH] Fix issues with Python 3.6's f strings and underscores in numbers. --- jedi/api/helpers.py | 3 +++ jedi/evaluate/__init__.py | 12 +++++----- jedi/parser/tokenize.py | 37 +++++++++++++++++++---------- jedi/parser/tree.py | 23 +++++++++++++++--- test/completion/basic.py | 11 +++++++++ test/test_evaluate/test_literals.py | 37 +++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 22 deletions(-) create mode 100644 test/test_evaluate/test_literals.py diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index c95ea2dd..565e8435 100644 --- a/jedi/api/helpers.py +++ b/jedi/api/helpers.py @@ -7,6 +7,7 @@ from collections import namedtuple from jedi._compatibility import u from jedi.evaluate.helpers import evaluate_call_of_leaf from jedi import parser +from jedi.parser import tree from jedi.parser import tokenize from jedi.cache import time_cache from jedi import common @@ -203,6 +204,8 @@ def evaluate_goto_definition(evaluator, context, leaf): return context.eval_node(leaf.parent) elif parent.type == 'trailer': return evaluate_call_of_leaf(context, leaf) + elif isinstance(leaf, tree.Literal): + return context.evaluator.eval_atom(context, leaf) return [] diff --git a/jedi/evaluate/__init__.py b/jedi/evaluate/__init__.py index a5b5b5ff..eac4bdea 100644 --- a/jedi/evaluate/__init__.py +++ b/jedi/evaluate/__init__.py @@ -270,7 +270,7 @@ class Evaluator(object): debug.dbg('eval_element %s@%s', element, element.start_pos) types = set() 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): # For False/True/None if element.value in ('False', 'True', 'None'): @@ -283,7 +283,7 @@ class Evaluator(object): elif element.type in ('power', 'atom_expr'): first_child = element.children[0] 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:]: if trailer == '**': # has a power operation. right = self.eval_element(context, element.children[2]) @@ -306,7 +306,7 @@ class Evaluator(object): assert element.value == '...' types = set([compiled.create(self, Ellipsis)]) 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]: # TODO add search_global=True? types = unite( @@ -325,7 +325,7 @@ class Evaluator(object): debug.dbg('eval_element result %s', 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 generate the node (because it has just one child). In that case an atom @@ -351,9 +351,9 @@ class Evaluator(object): c = atom.children if c[0].type == 'string': # Will be one string. - types = self._eval_atom(context, c[0]) + types = self.eval_atom(context, c[0]) for string in c[1:]: - right = self._eval_atom(context, string) + right = self.eval_atom(context, string) types = precedence.calculate(self, context, types, '+', right) return types # Parentheses without commas are not tuples. diff --git a/jedi/parser/tokenize.py b/jedi/parser/tokenize.py index a2d789a9..3080c5f5 100644 --- a/jedi/parser/tokenize.py +++ b/jedi/parser/tokenize.py @@ -59,21 +59,32 @@ Whitespace = r'[ \f\t]*' Comment = r'#[^\r\n]*' Name = r'\w+' -Hexnumber = r'0[xX](?:_?[0-9a-fA-F])+' -Binnumber = r'0[bB](?:_?[01])+' -if is_py3: +if py_version >= 36: + Hexnumber = r'0[xX](?:_?[0-9a-fA-F])+' + Binnumber = r'0[bB](?:_?[01])+' 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: - Octnumber = '0[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]') + Hexnumber = r'0[xX][0-9a-fA-F]+' + Binnumber = r'0[bB][01]+' + if is_py3: + Octnumber = r'0[oO][0-7]+' + else: + Octnumber = '0[0-7]+' + Decnumber = r'(?:0+|[1-9][0-9]*)' + Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber) + Exponent = r'[eE][-+]?[0-9]+' + 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) # Return the empty string, plus all of the valid string prefixes. diff --git a/jedi/parser/tree.py b/jedi/parser/tree.py index da00dc16..ba2470e4 100644 --- a/jedi/parser/tree.py +++ b/jedi/parser/tree.py @@ -41,10 +41,27 @@ import abc from jedi._compatibility import (Python3Method, encoding, is_py3, utf8_repr, literal_eval, use_metaclass, unicode) -from jedi.parser import token 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): try: type = node.type @@ -98,7 +115,7 @@ class DocstringMixin(object): # leaves anymore that might be part of the docstring. A # docstring can also look like this: ``'foo' 'bar' # 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 # force it. if is_py3 or isinstance(cleaned, unicode): @@ -422,7 +439,7 @@ class Literal(LeafWithNewLines): __slots__ = () def eval(self): - return literal_eval(self.value) + return _safe_literal_eval(self.value) class Number(Literal): diff --git a/test/completion/basic.py b/test/completion/basic.py index 5b4f9091..9336649e 100644 --- a/test/completion/basic.py +++ b/test/completion/basic.py @@ -19,6 +19,17 @@ str.. #? [] 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 # ----------------- diff --git a/test/test_evaluate/test_literals.py b/test/test_evaluate/test_literals.py new file mode 100644 index 00000000..88f4671d --- /dev/null +++ b/test/test_evaluate/test_literals.py @@ -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'