From e0b0343a78c4b98173c4f82a0438928fbb03ae8d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 30 Apr 2017 15:23:43 +0200 Subject: [PATCH 01/38] Remove expanduser from the parser path. Not sure if that makes sense so I'd rather remove it. --- jedi/parser/python/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/jedi/parser/python/__init__.py b/jedi/parser/python/__init__.py index f4ef81de..d60531fa 100644 --- a/jedi/parser/python/__init__.py +++ b/jedi/parser/python/__init__.py @@ -74,9 +74,6 @@ def parse(code=None, path=None, grammar=None, error_recovery=True, if grammar is None: grammar = load_grammar() - if path is not None: - path = os.path.expanduser(path) - if cache and not code and path is not None: # In this case we do actual caching. We just try to load it. module_node = load_module(grammar, path) From 63679aabd97658b06f8439d23161e9d2fff0410f Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 1 May 2017 02:19:42 +0200 Subject: [PATCH 02/38] Replace Param.get_description with get_code and a parameter include_coma. --- jedi/api/classes.py | 9 +++++++-- jedi/parser/python/tree.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 24b1bee3..0aab4f9f 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -15,7 +15,7 @@ from jedi.evaluate import representation as er from jedi.evaluate import instance from jedi.evaluate import imports from jedi.evaluate import compiled -from jedi.evaluate.filters import ParamName, TreeNameDefinition +from jedi.evaluate.filters import ParamName from jedi.evaluate.imports import ImportName from jedi.api.keywords import KeywordName @@ -538,7 +538,12 @@ class Definition(BaseDefinition): typ = 'def' return typ + ' ' + u(self._name.string_name) elif typ == 'param': - return typ + ' ' + tree_name.get_definition().get_description() + code = tree_name.get_definition().get_code( + include_prefix=False, + include_comma=False + ) + return typ + ' ' + code + definition = tree_name.get_definition() # Remove the prefix, because that's not what we want for get_code diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index e37e2b55..a13c1d7d 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -983,12 +983,18 @@ class Param(PythonBaseNode): """ return search_ancestor(self, ('funcdef', 'lambdef')) - def get_description(self): - # TODO Remove? + def get_code(self, normalized=False, include_prefix=True, include_comma=True): + if include_comma: + return super(Param, self).get_code(normalized, include_prefix) + children = self.children if children[-1] == ',': children = children[:-1] - return self._get_code_for_children(children, False, False) + return self._get_code_for_children( + children, + normalized=False, + include_prefix=include_prefix + ) def __repr__(self): default = '' if self.default is None else '=%s' % self.default.get_code() From c1c3f35e08a2404814d0463a648c562c15f1beb6 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 1 May 2017 02:26:24 +0200 Subject: [PATCH 03/38] Docstring for Param.get_code(). --- jedi/parser/python/tree.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index a13c1d7d..62335f13 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -984,6 +984,12 @@ class Param(PythonBaseNode): return search_ancestor(self, ('funcdef', 'lambdef')) def get_code(self, normalized=False, include_prefix=True, include_comma=True): + """ + Like all the other get_code functions, but includes the param + `include_comma`. + + :param include_comma bool: If enabled includes the comma in the string output. + """ if include_comma: return super(Param, self).get_code(normalized, include_prefix) From e96bb29d189b16fbd84daf4a2196b743fb72269b Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 2 May 2017 08:43:46 +0200 Subject: [PATCH 04/38] Param docstring. --- jedi/parser/python/tree.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 62335f13..45ef4fa8 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -925,6 +925,10 @@ class Param(PythonBaseNode): @property def star_count(self): + """ + Is `0` in case of `foo`, `1` in case of `*foo` or `2` in case of + `**foo`. + """ first = self.children[0] if first in ('*', '**'): return len(first.value) @@ -932,6 +936,10 @@ class Param(PythonBaseNode): @property def default(self): + """ + The default is the test node that appears after the `=`. Is `None` in + case no default is present. + """ try: return self.children[int(self.children[0] in ('*', '**')) + 2] except IndexError: @@ -939,6 +947,10 @@ class Param(PythonBaseNode): @property def annotation(self): + """ + The default is the test node that appears after `->`. Is `None` in case + no annotation is present. + """ tfpdef = self._tfpdef() if tfpdef.type == 'tfpdef': assert tfpdef.children[1] == ":" @@ -957,6 +969,9 @@ class Param(PythonBaseNode): @property def name(self): + """ + The `Name` leaf of the param. + """ if self._tfpdef().type == 'tfpdef': return self._tfpdef().children[0] else: @@ -965,7 +980,7 @@ class Param(PythonBaseNode): @property def position_index(self): """ - Returns the positional index of a paramter. + Property for the positional index of a paramter. """ index = self.parent.children.index(self) try: @@ -979,7 +994,7 @@ class Param(PythonBaseNode): def get_parent_function(self): """ - Returns the function/lambda a paramter is defined in. + Returns the function/lambda of a parameter. """ return search_ancestor(self, ('funcdef', 'lambdef')) From fc7cc1c81480a16306edbc1203fd9b07bfba3f36 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 2 May 2017 08:50:52 +0200 Subject: [PATCH 05/38] Docstrings for get_defined_names. --- jedi/parser/python/tree.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 45ef4fa8..33f889c5 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -25,8 +25,6 @@ Any subclasses of :class:`Scope`, including :class:`Module` has an attribute [] """ -from itertools import chain - from jedi._compatibility import utf8_repr, unicode from jedi.parser.tree import Node, BaseNode, Leaf, ErrorNode, ErrorLeaf, \ search_ancestor @@ -650,6 +648,10 @@ class WithStmt(Flow): __slots__ = () def get_defined_names(self): + """ + Returns the a list of `Name` that the with statement defines. The + defined names are set after `as`. + """ names = [] for with_item in self.children[1:-2:2]: # Check with items for 'as' names. @@ -692,6 +694,11 @@ class ImportFrom(Import): __slots__ = () def get_defined_names(self): + """ + Returns the a list of `Name` that the import defines. The + defined names are set after `import` or in case an alias - `as` - is + present that name is returned. + """ return [alias or name for name, alias in self._as_name_tuples()] def aliases(self): @@ -762,6 +769,11 @@ class ImportName(Import): __slots__ = () def get_defined_names(self): + """ + Returns the a list of `Name` that the import defines. The defined names + is always the first name after `import` or in case an alias - `as` - is + present that name is returned. + """ return [alias or path[0] for path, alias in self._dotted_as_names()] @property @@ -880,14 +892,18 @@ class ExprStmt(PythonBaseNode, DocstringMixin): __slots__ = () def get_defined_names(self): + """ + Returns a list of `Name` defined before the `=` sign. + """ names = [] if self.children[1].type == 'annassign': names = _defined_names(self.children[0]) - return list(chain.from_iterable( - _defined_names(self.children[i]) + return [ + name for i in range(0, len(self.children) - 2, 2) - if '=' in self.children[i + 1].value) - ) + names + if '=' in self.children[i + 1].value + for name in _defined_names(self.children[i]) + ] + names def get_rhs(self): """Returns the right-hand-side of the equals.""" @@ -1030,4 +1046,7 @@ class CompFor(PythonBaseNode): return True def get_defined_names(self): + """ + Returns the a list of `Name` that the comprehension defines. + """ return _defined_names(self.children[1]) From 5c836a72b6e3d04c9bebbb25ebda7fbfb221ad1d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 2 May 2017 08:57:03 +0200 Subject: [PATCH 06/38] Lambda and Function docstrings render better. --- docs/docs/parser.rst | 2 +- jedi/parser/python/tree.py | 27 ++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/docs/parser.rst b/docs/docs/parser.rst index d9ae642a..9ec82012 100644 --- a/docs/docs/parser.rst +++ b/docs/docs/parser.rst @@ -22,7 +22,7 @@ All nodes and leaves have these methods/properties: Python Parser Tree ------------------ +------------------ .. automodule:: jedi.parser.python.tree :members: diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 33f889c5..46cb2556 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -450,14 +450,15 @@ class Function(ClassOrFunc): """ Used to store the parsed contents of a python function. - Children: - 0. - 1. - 2. parameter list (including open-paren and close-paren s) - 3. or 5. - 4. or 6. Node() representing function body - 3. -> (if annotation is also present) - 4. annotation (if present) + Children:: + + 0. + 1. + 2. parameter list (including open-paren and close-paren s) + 3. or 5. + 4. or 6. Node() representing function body + 3. -> (if annotation is also present) + 4. annotation (if present) """ type = 'funcdef' @@ -520,12 +521,12 @@ class Lambda(Function): """ Lambdas are basically trimmed functions, so give it the same interface. - Children: + Children:: - 0. - *. for each argument x - -2. - -1. Node() representing body + 0. + *. for each argument x + -2. + -1. Node() representing body """ type = 'lambdef' __slots__ = () From 6ea06fdfd8d78bd15916f5153ee49c45133f7b5e Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 2 May 2017 08:59:07 +0200 Subject: [PATCH 07/38] Even if static analysis is not working well, we can at least write it correctly. --- docs/docs/{static_analsysis.rst => static_analysis.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/docs/{static_analsysis.rst => static_analysis.rst} (100%) diff --git a/docs/docs/static_analsysis.rst b/docs/docs/static_analysis.rst similarity index 100% rename from docs/docs/static_analsysis.rst rename to docs/docs/static_analysis.rst From 336b8a46d065c509810dc78f2abecd752bab1c0e Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Tue, 2 May 2017 19:19:07 +0200 Subject: [PATCH 08/38] search_ancestor now uses *node_types as a parameter instead of a mix of tuple and simple string like isinstance. --- jedi/evaluate/__init__.py | 2 +- jedi/evaluate/imports.py | 2 +- jedi/evaluate/representation.py | 4 ++-- jedi/parser/python/tree.py | 2 +- jedi/parser/tree.py | 9 +++------ 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/jedi/evaluate/__init__.py b/jedi/evaluate/__init__.py index 0fdd5202..94b5f2b0 100644 --- a/jedi/evaluate/__init__.py +++ b/jedi/evaluate/__init__.py @@ -337,7 +337,7 @@ class Evaluator(object): # This is the first global lookup. stmt = atom.get_definition() if stmt.type == 'comp_for': - stmt = tree.search_ancestor(stmt, ('expr_stmt', 'lambdef', 'funcdef', 'classdef')) + stmt = tree.search_ancestor(stmt, 'expr_stmt', 'lambdef', 'funcdef', 'classdef') if stmt is None or stmt.type != 'expr_stmt': # We only need to adjust the start_pos for statements, because # there the name cannot be used. diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 529d0b54..b0f5bedc 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -37,7 +37,7 @@ from jedi.evaluate.filters import AbstractNameDefinition @memoize_default(default=set()) def infer_import(context, tree_name, is_goto=False): module_context = context.get_root_context() - import_node = search_ancestor(tree_name, ('import_name', 'import_from')) + import_node = search_ancestor(tree_name, 'import_name', 'import_from') import_path = import_node.get_path_for_name(tree_name) from_import_name = None evaluator = context.evaluator diff --git a/jedi/evaluate/representation.py b/jedi/evaluate/representation.py index 5f648953..9e7ab312 100644 --- a/jedi/evaluate/representation.py +++ b/jedi/evaluate/representation.py @@ -350,8 +350,8 @@ class FunctionExecutionContext(context.TreeContext): @recursion.execution_recursion_decorator(default=iter([])) def get_yield_values(self): - for_parents = [(y, tree.search_ancestor(y, ('for_stmt', 'funcdef', - 'while_stmt', 'if_stmt'))) + for_parents = [(y, tree.search_ancestor(y, 'for_stmt', 'funcdef', + 'while_stmt', 'if_stmt')) for y in self.tree_node.iter_yield_exprs()] # Calculate if the yields are placed within the same for loop. diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 46cb2556..46b503da 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -1013,7 +1013,7 @@ class Param(PythonBaseNode): """ Returns the function/lambda of a parameter. """ - return search_ancestor(self, ('funcdef', 'lambdef')) + return search_ancestor(self, 'funcdef', 'lambdef') def get_code(self, normalized=False, include_prefix=True, include_comma=True): """ diff --git a/jedi/parser/tree.py b/jedi/parser/tree.py index 1b6a2666..690698aa 100644 --- a/jedi/parser/tree.py +++ b/jedi/parser/tree.py @@ -2,21 +2,18 @@ from abc import abstractmethod, abstractproperty from jedi._compatibility import utf8_repr, encoding, is_py3 -def search_ancestor(node, node_type_or_types): +def search_ancestor(node, *node_types): """ Recursively looks at the parents of a node and checks if the type names match. :param node: The node that is looked at. - :param node_type_or_types: A tuple or a string of type names that are + :param node_types: A tuple or a string of type names that are searched for. """ - if not isinstance(node_type_or_types, (list, tuple)): - node_type_or_types = (node_type_or_types,) - while True: node = node.parent - if node is None or node.type in node_type_or_types: + if node is None or node.type in node_types: return node From f916b9b054acfe316238ee4e7d4ce633ef951d95 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 5 May 2017 09:21:42 +0200 Subject: [PATCH 09/38] More docstrings. --- jedi/parser/python/tree.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 46b503da..f8924d28 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -326,6 +326,10 @@ class Module(Scope): return False def get_used_names(self): + """ + Returns all the `Name` leafs that exist in this module. Tihs includes + both definitions and references of names. + """ if self._used_names is None: # Don't directly use self._used_names to eliminate a lookup. dct = {} @@ -356,9 +360,15 @@ class ClassOrFunc(Scope): @property def name(self): + """ + Returns the `Name` leaf that defines the function or class name. + """ return self.children[1] def get_decorators(self): + """ + :return list of Decorator: + """ decorated = self.parent if decorated.type == 'decorated': if decorated.children[0].type == 'decorators': @@ -387,6 +397,10 @@ class Class(ClassOrFunc): super(Class, self).__init__(children) def get_super_arglist(self): + """ + Returns the `arglist` node that defines the super classes. It returns + None if there are no arguments. + """ if self.children[2] != '(': # Has no parentheses return None else: From 14eeb60240b9320028287eb5a79c4ed21eb19d6e Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 5 May 2017 09:23:50 +0200 Subject: [PATCH 10/38] Remove is_scope from CompiledObject. It's not needed according to tests. --- jedi/evaluate/compiled/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/jedi/evaluate/compiled/__init__.py b/jedi/evaluate/compiled/__init__.py index 6597990d..ab22a257 100644 --- a/jedi/evaluate/compiled/__init__.py +++ b/jedi/evaluate/compiled/__init__.py @@ -222,9 +222,6 @@ class CompiledObject(Context): for result in self.evaluator.execute(bltn_obj, params): yield result - def is_scope(self): - return True - def get_self_attributes(self): return [] # Instance compatibility From a78769954db5101f1202ce747697dc74b1ba13a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Gouzien?= Date: Sat, 6 May 2017 13:33:36 +0200 Subject: [PATCH 11/38] Check whether inspect.getfile will raise a TypeError and anticipate it. Anticipate the raise of TypeError from inspect.getfile to prevent the computation of repr() for the error message wich is not used. Useful for some big pandas arrays. Fix tentative of #919. --- jedi/evaluate/compiled/mixed.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/jedi/evaluate/compiled/mixed.py b/jedi/evaluate/compiled/mixed.py index dfb49657..1390ea47 100644 --- a/jedi/evaluate/compiled/mixed.py +++ b/jedi/evaluate/compiled/mixed.py @@ -116,8 +116,21 @@ def _load_module(evaluator, path, python_object): return module +def source_findable(python_object): + """Check if inspect.getfile has a chance to find the source.""" + return (inspect.ismodule(python_object) or + inspect.isclass(python_object) or + inspect.ismethod(python_object) or + inspect.isfunction(python_object) or + inspect.istraceback(python_object) or + inspect.isframe(python_object) or + inspect.iscode(python_object)) + + def find_syntax_node_name(evaluator, python_object): try: + if not source_findable(python_object): + raise TypeError # Prevents computation of `repr` within inspect. path = inspect.getsourcefile(python_object) except TypeError: # The type might not be known (e.g. class_with_dict.__weakref__) From 9d5cc0be065bf45286a6d160478c2cf4d8a96b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Gouzien?= Date: Sat, 6 May 2017 14:14:56 +0200 Subject: [PATCH 12/38] Test that no repr() can slow down completion. Was reported with issue #919. --- test/test_speed.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/test_speed.py b/test/test_speed.py index dcb69840..73dda181 100644 --- a/test/test_speed.py +++ b/test/test_speed.py @@ -51,3 +51,19 @@ class TestSpeed(TestCase): with open('speed/precedence.py') as f: line = len(f.read().splitlines()) assert jedi.Script(line=line, path='speed/precedence.py').goto_definitions() + + @_check_speed(0.1) + def test_no_repr_computation(self): + """ + For Interpreter completion aquisition of sourcefile can trigger + unwanted computation of repr(). Exemple : big pandas data. + See issue #919. + """ + class Slow_repr(): + "class to test what happens if __repr__ is very slow." + def some_method(self): + pass + def __repr__(self): + time.sleep(0.2) + test = Slow_repr() + jedi.Interpreter('test.som', [locals()]).completions() From 405a3397193401e89893f92ba725857547b89224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Gouzien?= Date: Sat, 6 May 2017 14:16:06 +0200 Subject: [PATCH 13/38] =?UTF-8?q?Add=20author=20=C3=89lie=20Gouzien.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUTHORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index b5619dcb..52204a8b 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -43,6 +43,7 @@ bstaint (@bstaint) Mathias Rav (@Mortal) Daniel Fiterman (@dfit99) Simon Ruggier (@sruggier) +Élie Gouzien (@ElieGouzien) Note: (@user) means a github user name. From 80fbdec1da486fbada57e9a470222943c5301ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89lie=20Gouzien?= Date: Sat, 6 May 2017 14:28:11 +0200 Subject: [PATCH 14/38] Corrected test class name. --- test/test_speed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_speed.py b/test/test_speed.py index 73dda181..8d5bc9a3 100644 --- a/test/test_speed.py +++ b/test/test_speed.py @@ -59,11 +59,11 @@ class TestSpeed(TestCase): unwanted computation of repr(). Exemple : big pandas data. See issue #919. """ - class Slow_repr(): + class SlowRepr(): "class to test what happens if __repr__ is very slow." def some_method(self): pass def __repr__(self): time.sleep(0.2) - test = Slow_repr() + test = SlowRepr() jedi.Interpreter('test.som', [locals()]).completions() From 0882849e65c172ea8ac9774b813b675952d91460 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 14:52:46 +0200 Subject: [PATCH 15/38] Don't do a simple_stmt error recovery in the parser, because it makes it more complicated. --- jedi/parser/python/parser.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/jedi/parser/python/parser.py b/jedi/parser/python/parser.py index 2fb9d2f3..7b91e268 100644 --- a/jedi/parser/python/parser.py +++ b/jedi/parser/python/parser.py @@ -144,22 +144,9 @@ class Parser(BaseParser): elif symbol == 'suite' and len(nodes) > 1: # suites without an indent in them get discarded. break - elif symbol == 'simple_stmt' and len(nodes) > 1: - # simple_stmt can just be turned into a PythonNode, if - # there are enough statements. Ignore the rest after that. - break return index, symbol, nodes index, symbol, nodes = current_suite(stack) - if symbol == 'simple_stmt': - index -= 2 - (_, _, (type_, suite_nodes)) = stack[index] - symbol = grammar.number2symbol[type_] - suite_nodes.append(tree.PythonNode(symbol, list(nodes))) - # Remove - nodes[:] = [] - nodes = suite_nodes - stack[index] # print('err', token.tok_name[typ], repr(value), start_pos, len(stack), index) if self._stack_removal(grammar, stack, arcs, index + 1, value, start_pos): From 536e62e67d6213a8d88954bf56bfd288855e1221 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 14:58:53 +0200 Subject: [PATCH 16/38] Move is_scope and get_parent_scope out of the parser. --- jedi/evaluate/__init__.py | 8 ++++---- jedi/evaluate/dynamic.py | 3 ++- jedi/evaluate/filters.py | 3 ++- jedi/evaluate/finder.py | 7 ++++--- jedi/evaluate/flow_analysis.py | 12 +++++++----- jedi/evaluate/helpers.py | 3 ++- jedi/evaluate/instance.py | 7 ++++--- jedi/evaluate/representation.py | 2 +- jedi/parser/python/tree.py | 23 ----------------------- jedi/parser_utils.py | 18 ++++++++++++++++++ 10 files changed, 44 insertions(+), 42 deletions(-) diff --git a/jedi/evaluate/__init__.py b/jedi/evaluate/__init__.py index 94b5f2b0..1f1537e6 100644 --- a/jedi/evaluate/__init__.py +++ b/jedi/evaluate/__init__.py @@ -194,7 +194,7 @@ class Evaluator(object): if_stmt = if_stmt.parent if if_stmt.type in ('if_stmt', 'for_stmt'): break - if if_stmt.is_scope(): + if parser_utils.is_scope(if_stmt): if_stmt = None break predefined_if_name_dict = context.predefined_names.get(if_stmt) @@ -537,7 +537,7 @@ class Evaluator(object): while True: node = node.parent - if node.is_scope(): + if parser_utils.is_scope(node): return node elif node.type in ('argument', 'testlist_comp'): if node.children[1].type == 'comp_for': @@ -553,7 +553,7 @@ class Evaluator(object): return base_context is_funcdef = scope_node.type in ('funcdef', 'lambdef') - parent_scope = scope_node.get_parent_scope() + parent_scope = parser_utils.get_parent_scope(scope_node) parent_context = from_scope_node(parent_scope, child_is_funcdef=is_funcdef) if is_funcdef: @@ -586,7 +586,7 @@ class Evaluator(object): base_node = base_context.tree_node - if node_is_context and node.is_scope(): + if node_is_context and parser_utils.is_scope(node): scope_node = node else: if node.parent.type in ('funcdef', 'classdef'): diff --git a/jedi/evaluate/dynamic.py b/jedi/evaluate/dynamic.py index 6f294ff7..53977fd2 100644 --- a/jedi/evaluate/dynamic.py +++ b/jedi/evaluate/dynamic.py @@ -24,6 +24,7 @@ from jedi.evaluate.cache import memoize_default from jedi.evaluate import imports from jedi.evaluate.param import TreeArguments, create_default_param from jedi.common import to_list, unite +from jedi.parser_utils import get_parent_scope MAX_PARAM_SEARCHES = 20 @@ -103,7 +104,7 @@ def _search_function_executions(evaluator, module_context, funcdef): func_string_name = funcdef.name.value compare_node = funcdef if func_string_name == '__init__': - cls = funcdef.get_parent_scope() + cls = get_parent_scope(funcdef) if isinstance(cls, tree.Class): func_string_name = cls.name.value compare_node = cls diff --git a/jedi/evaluate/filters.py b/jedi/evaluate/filters.py index 3b9950a2..198389a1 100644 --- a/jedi/evaluate/filters.py +++ b/jedi/evaluate/filters.py @@ -7,6 +7,7 @@ from abc import abstractmethod from jedi.parser.tree import search_ancestor from jedi.evaluate import flow_analysis from jedi.common import to_list, unite +from jedi.parser_utils import get_parent_scope class AbstractNameDefinition(object): @@ -189,7 +190,7 @@ class ParserTreeFilter(AbstractUsedNamesFilter): if parent.type == 'trailer': return False base_node = parent if parent.type in ('classdef', 'funcdef') else name - return base_node.get_parent_scope() == self._parser_scope + return get_parent_scope(base_node) == self._parser_scope def _check_flows(self, names): for name in sorted(names, key=lambda name: name.start_pos, reverse=True): diff --git a/jedi/evaluate/finder.py b/jedi/evaluate/finder.py index 74bc1ee8..1eb94c67 100644 --- a/jedi/evaluate/finder.py +++ b/jedi/evaluate/finder.py @@ -32,6 +32,7 @@ from jedi.evaluate import param from jedi.evaluate import helpers from jedi.evaluate.filters import get_global_filters from jedi.evaluate.context import ContextualizedName, ContextualizedNode +from jedi.parser_utils import is_scope, get_parent_scope class NameFinder(object): @@ -106,7 +107,7 @@ class NameFinder(object): if self._context.predefined_names: # TODO is this ok? node might not always be a tree.Name node = self._name - while node is not None and not node.is_scope(): + while node is not None and not is_scope(node): node = node.parent if node.type in ("if_stmt", "for_stmt", "comp_for"): try: @@ -160,7 +161,7 @@ class NameFinder(object): if base_node.type == 'comp_for': return types while True: - flow_scope = flow_scope.get_parent_scope(include_flows=True) + flow_scope = get_parent_scope(flow_scope, include_flows=True) n = _check_flow_information(self._name_context, flow_scope, self._name, self._position) if n is not None: @@ -298,7 +299,7 @@ def _check_flow_information(context, flow, search_name, pos): return None result = None - if flow.is_scope(): + if is_scope(flow): # Check for asserts. module_node = flow.get_root_node() try: diff --git a/jedi/evaluate/flow_analysis.py b/jedi/evaluate/flow_analysis.py index 06de185a..670b7a71 100644 --- a/jedi/evaluate/flow_analysis.py +++ b/jedi/evaluate/flow_analysis.py @@ -1,4 +1,6 @@ -from jedi.parser_utils import get_flow_branch_keyword +from jedi.parser_utils import get_flow_branch_keyword, is_scope, get_parent_scope + + class Status(object): lookup_table = {} @@ -32,14 +34,14 @@ UNSURE = Status(None, 'unsure') def _get_flow_scopes(node): while True: - node = node.get_parent_scope(include_flows=True) - if node is None or node.is_scope(): + node = get_parent_scope(node, include_flows=True) + if node is None or is_scope(node): return yield node def reachability_check(context, context_scope, node, origin_scope=None): - first_flow_scope = node.get_parent_scope(include_flows=True) + first_flow_scope = get_parent_scope(node, include_flows=True) if origin_scope is not None: origin_flow_scopes = list(_get_flow_scopes(origin_scope)) node_flow_scopes = list(_get_flow_scopes(node)) @@ -95,7 +97,7 @@ def _break_check(context, context_scope, flow_scope, node): return reachable if context_scope != flow_scope and context_scope != flow_scope.parent: - flow_scope = flow_scope.get_parent_scope(include_flows=True) + flow_scope = get_parent_scope(flow_scope, include_flows=True) return reachable & _break_check(context, context_scope, flow_scope, node) else: return reachable diff --git a/jedi/evaluate/helpers.py b/jedi/evaluate/helpers.py index 12739bbc..d85e94f8 100644 --- a/jedi/evaluate/helpers.py +++ b/jedi/evaluate/helpers.py @@ -3,6 +3,7 @@ from itertools import chain from contextlib import contextmanager from jedi.parser.python import tree +from jedi.parser_utils import get_parent_scope def deep_ast_copy(obj): @@ -143,7 +144,7 @@ def get_module_names(module, all_scopes): # parent_scope. There's None as a parent, because nodes in the module # node have the parent module and not suite as all the others. # Therefore it's important to catch that case. - names = [n for n in names if n.get_parent_scope().parent in (module, None)] + names = [n for n in names if get_parent_scope(n).parent in (module, None)] return names diff --git a/jedi/evaluate/instance.py b/jedi/evaluate/instance.py index d0cd812a..9b131f84 100644 --- a/jedi/evaluate/instance.py +++ b/jedi/evaluate/instance.py @@ -11,6 +11,7 @@ from jedi.cache import memoize_method from jedi.evaluate import representation as er from jedi.evaluate.dynamic import search_params from jedi.evaluate import iterable +from jedi.parser_utils import get_parent_scope class AbstractInstanceContext(Context): @@ -151,7 +152,7 @@ class AbstractInstanceContext(Context): def create_instance_context(self, class_context, node): if node.parent.type in ('funcdef', 'classdef'): node = node.parent - scope = node.get_parent_scope() + scope = get_parent_scope(node) if scope == class_context.tree_node: return class_context else: @@ -189,7 +190,7 @@ class CompiledInstance(AbstractInstanceContext): return compiled.CompiledContextName(self, self.class_context.name.string_name) def create_instance_context(self, class_context, node): - if node.get_parent_scope().type == 'classdef': + if get_parent_scope(node).type == 'classdef': return class_context else: return super(CompiledInstance, self).create_instance_context(class_context, node) @@ -332,7 +333,7 @@ class InstanceClassFilter(filters.ParserTreeFilter): while node is not None: if node == self._parser_scope or node == self.context: return True - node = node.get_parent_scope() + node = get_parent_scope(node) return False def _access_possible(self, name): diff --git a/jedi/evaluate/representation.py b/jedi/evaluate/representation.py index 9e7ab312..fb505780 100644 --- a/jedi/evaluate/representation.py +++ b/jedi/evaluate/representation.py @@ -276,7 +276,7 @@ class FunctionContext(use_metaclass(CachedMetaClass, context.TreeContext)): def py__class__(self): # This differentiation is only necessary for Python2. Python3 does not # use a different method class. - if isinstance(self.tree_node.get_parent_scope(), tree.Class): + if isinstance(parser_utils.get_parent_scope(self.tree_node), tree.Class): name = 'METHOD_CLASS' else: name = 'FUNCTION_CLASS' diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index f8924d28..a07ace95 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -59,19 +59,6 @@ class DocstringMixin(object): class PythonMixin(object): - def get_parent_scope(self, include_flows=False): - """ - Returns the underlying scope. - """ - scope = self.parent - while scope is not None: - if include_flows and isinstance(scope, Flow): - return scope - if scope.is_scope(): - break - scope = scope.parent - return scope - def get_definition(self): if self.type in ('newline', 'endmarker'): raise ValueError('Cannot get the indentation of whitespace or indentation.') @@ -90,10 +77,6 @@ class PythonMixin(object): break return scope - def is_scope(self): - # Default is not being a scope. Just inherit from Scope. - return False - def get_name_of_position(self, position): for c in self.children: if isinstance(c, Leaf): @@ -267,9 +250,6 @@ class Scope(PythonBaseNode, DocstringMixin): return scan(self.children) - def is_scope(self): - return True - def get_suite(self): """ Returns the part that is executed by the function. @@ -1057,9 +1037,6 @@ class CompFor(PythonBaseNode): type = 'comp_for' __slots__ = () - def is_scope(self): - return True - def get_defined_names(self): """ Returns the a list of `Name` that the comprehension defines. diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index 47a9a17c..df340f6b 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -217,3 +217,21 @@ def get_following_comment_same_line(node): comment = comment[:comment.index("\n")] return comment + +def is_scope(node): + return node.type in ('file_input', 'classdef', 'funcdef', 'lambdef', 'comp_for') + + +def get_parent_scope(node, include_flows=False): + """ + Returns the underlying scope. + """ + scope = node.parent + while scope is not None: + if include_flows and isinstance(scope, tree.Flow): + return scope + if is_scope(scope): + break + scope = scope.parent + return scope + From fe49fc9b990293fe9b187d95298568750579bd1b Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 15:06:34 +0200 Subject: [PATCH 17/38] Add slots to the PythonMixin. --- jedi/parser/python/tree.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index a07ace95..43b5e977 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -59,6 +59,11 @@ class DocstringMixin(object): class PythonMixin(object): + """ + Some Python specific utitilies. + """ + __slots__ = () + def get_definition(self): if self.type in ('newline', 'endmarker'): raise ValueError('Cannot get the indentation of whitespace or indentation.') From 66b28ca84068216359674c4b4539db3d58329e92 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 15:22:45 +0200 Subject: [PATCH 18/38] Small cleanup. --- jedi/parser/python/tree.py | 8 -------- jedi/parser_utils.py | 7 ++++++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 43b5e977..5310dbfd 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -512,10 +512,6 @@ class Function(ClassOrFunc): except IndexError: return None - def _get_paramlist_code(self): - return self.children[2].get_code() - - class Lambda(Function): """ Lambdas are basically trimmed functions, so give it the same interface. @@ -543,9 +539,6 @@ class Lambda(Function): """ raise AttributeError("lambda is not named.") - def _get_paramlist_code(self): - return '(' + ''.join(param.get_code() for param in self.params).strip() + ')' - def _get_param_nodes(self): return self.children[1:-2] @@ -554,7 +547,6 @@ class Lambda(Function): """ Returns `None`, lambdas don't have annotations. """ - # lambda functions do not support annotations return None def __repr__(self): diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index df340f6b..a3d97be9 100644 --- a/jedi/parser_utils.py +++ b/jedi/parser_utils.py @@ -154,7 +154,12 @@ def get_call_signature(funcdef, width=72, call_string=None): call_string = '' else: call_string = funcdef.name.value - code = call_string + funcdef._get_paramlist_code() + if funcdef.type == 'lambdef': + p = '(' + ''.join(param.get_code() for param in funcdef.params).strip() + ')' + else: + p = funcdef.children[2].get_code() + code = call_string + p + return '\n'.join(textwrap.wrap(code, width)) From 9bf66b6149c32118b8b3948176240cf10579c571 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 15:38:03 +0200 Subject: [PATCH 19/38] Make Import.aliases private. --- jedi/parser/python/tree.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 5310dbfd..2f541d4f 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -663,9 +663,14 @@ class Import(PythonBaseNode): __slots__ = () def get_path_for_name(self, name): + """ + The path is the list of names that leads to the searched name. + + :return list of Name: + """ try: # The name may be an alias. If it is, just map it back to the name. - name = self.aliases()[name] + name = self._aliases()[name] except KeyError: pass @@ -693,7 +698,7 @@ class ImportFrom(Import): """ return [alias or name for name, alias in self._as_name_tuples()] - def aliases(self): + def _aliases(self): """Mapping from alias to its corresponding name.""" return dict((alias, name) for name, alias in self._as_name_tuples() if alias is not None) @@ -803,10 +808,13 @@ class ImportName(Import): import foo.bar """ - return [1 for path, alias in self._dotted_as_names() - if alias is None and len(path) > 1] + return bool([1 for path, alias in self._dotted_as_names() + if alias is None and len(path) > 1]) - def aliases(self): + def _aliases(self): + """ + :return list of Name: Returns all the alias + """ return dict((alias, path[-1]) for path, alias in self._dotted_as_names() if alias is not None) From 84d82790893fe1e767bb420ef92f3a23ba982587 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 15:47:34 +0200 Subject: [PATCH 20/38] Import.paths -> Import.get_paths. --- jedi/api/__init__.py | 2 +- jedi/parser/python/tree.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 084a3483..e470fafe 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -342,7 +342,7 @@ class Script(object): elif isinstance(node, tree.Import): import_names = set(node.get_defined_names()) if node.is_nested(): - import_names |= set(path[-1] for path in node.paths()) + import_names |= set(path[-1] for path in node.get_paths()) for n in import_names: imports.infer_import(context, n) elif node.type == 'expr_stmt': diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 2f541d4f..1eaaf894 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -294,7 +294,7 @@ class Module(Scope): # the future print statement). for imp in self.iter_imports(): if imp.type == 'import_from' and imp.level == 0: - for path in imp.paths(): + for path in imp.get_paths(): names = [name.value for name in path] if len(names) == 2 and names[0] == '__future__': yield names[1] @@ -674,7 +674,7 @@ class Import(PythonBaseNode): except KeyError: pass - for path in self.paths(): + for path in self.get_paths(): if name in path: return path[:path.index(name) + 1] raise ValueError('Name should be defined in the import itself') @@ -746,9 +746,9 @@ class ImportFrom(Import): """ The last name defined in a star import. """ - return self.paths()[-1][-1] + return self.get_paths()[-1][-1] - def paths(self): + def get_paths(self): """ The import paths defined in an import statement. Typically an array like this: ``[, ]``. @@ -778,7 +778,7 @@ class ImportName(Import): """The level parameter of ``__import__``.""" return 0 # Obviously 0 for imports without from. - def paths(self): + def get_paths(self): return [path for path, alias in self._dotted_as_names()] def _dotted_as_names(self): From 6c95f73d77d80b2a1fb281d460a7a06e0ff4bca3 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 15:59:55 +0200 Subject: [PATCH 21/38] Remove a function that was not really needed. --- jedi/evaluate/representation.py | 2 +- jedi/parser/python/tree.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/jedi/evaluate/representation.py b/jedi/evaluate/representation.py index fb505780..a47cfad6 100644 --- a/jedi/evaluate/representation.py +++ b/jedi/evaluate/representation.py @@ -476,7 +476,7 @@ class ModuleContext(use_metaclass(CachedMetaClass, context.TreeContext)): modules = [] for i in self.tree_node.iter_imports(): if i.is_star_import(): - name = i.star_import_name() + name = i.get_paths()[-1][-1] new = imports.infer_import(self, name) for module in new: if isinstance(module, ModuleContext): diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index 1eaaf894..8257f0a5 100644 --- a/jedi/parser/python/tree.py +++ b/jedi/parser/python/tree.py @@ -742,16 +742,12 @@ class ImportFrom(Import): else: yield as_name.children[::2] # yields x, y -> ``x as y`` - def star_import_name(self): - """ - The last name defined in a star import. - """ - return self.get_paths()[-1][-1] - def get_paths(self): """ The import paths defined in an import statement. Typically an array like this: ``[, ]``. + + :return list of list of Name: """ dotted = self.get_from_names() From 6b7376bc5daf5b7b13b15e3dcdb4ccd220144a26 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 16:06:01 +0200 Subject: [PATCH 22/38] Move some stdlib tests. --- test/{test_integration_stdlib.py => test_evaluate/test_stdlib.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{test_integration_stdlib.py => test_evaluate/test_stdlib.py} (100%) diff --git a/test/test_integration_stdlib.py b/test/test_evaluate/test_stdlib.py similarity index 100% rename from test/test_integration_stdlib.py rename to test/test_evaluate/test_stdlib.py From f9f60177bf08a455f0ed78248fa8e737355b60f5 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 16:14:21 +0200 Subject: [PATCH 23/38] Move an analysis test. --- test/{test_integration_analysis.py => test_api/test_analysis.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{test_integration_analysis.py => test_api/test_analysis.py} (100%) diff --git a/test/test_integration_analysis.py b/test/test_api/test_analysis.py similarity index 100% rename from test/test_integration_analysis.py rename to test/test_api/test_analysis.py From d717c3bf403331edb8cd151906631261e742cd66 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 16:20:49 +0200 Subject: [PATCH 24/38] Merge some import tests. --- test/test_evaluate/test_imports.py | 82 +++++++++++++++++++++++++++++ test/test_integration_import.py | 83 ------------------------------ 2 files changed, 82 insertions(+), 83 deletions(-) delete mode 100644 test/test_integration_import.py diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index 6402a7b0..5a9569b6 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -1,3 +1,8 @@ +""" +Tests of various import related things that could not be tested with "Black Box +Tests". +""" + import os import sys @@ -7,6 +12,9 @@ import jedi from jedi._compatibility import find_module_py33, find_module from ..helpers import cwd_at +from jedi import Script +from jedi._compatibility import is_py26 + @pytest.mark.skipif('sys.version_info < (3,3)') def test_find_module_py33(): @@ -127,3 +135,77 @@ def test_import_completion_docstring(): # However for performance reasons not all modules are loaded and the # docstring is empty in this case. assert completions[0].docstring() == '' + + +def test_goto_definition_on_import(): + assert Script("import sys_blabla", 1, 8).goto_definitions() == [] + assert len(Script("import sys", 1, 8).goto_definitions()) == 1 + + +@cwd_at('jedi') +def test_complete_on_empty_import(): + assert Script("from datetime import").completions()[0].name == 'import' + # should just list the files in the directory + assert 10 < len(Script("from .", path='whatever.py').completions()) < 30 + + # Global import + assert len(Script("from . import", 1, 5, 'whatever.py').completions()) > 30 + # relative import + assert 10 < len(Script("from . import", 1, 6, 'whatever.py').completions()) < 30 + + # Global import + assert len(Script("from . import classes", 1, 5, 'whatever.py').completions()) > 30 + # relative import + assert 10 < len(Script("from . import classes", 1, 6, 'whatever.py').completions()) < 30 + + wanted = set(['ImportError', 'import', 'ImportWarning']) + assert set([c.name for c in Script("import").completions()]) == wanted + if not is_py26: # python 2.6 doesn't always come with a library `import*`. + assert len(Script("import import", path='').completions()) > 0 + + # 111 + assert Script("from datetime import").completions()[0].name == 'import' + assert Script("from datetime import ").completions() + + +def test_imports_on_global_namespace_without_path(): + """If the path is None, there shouldn't be any import problem""" + completions = Script("import operator").completions() + assert [c.name for c in completions] == ['operator'] + completions = Script("import operator", path='example.py').completions() + assert [c.name for c in completions] == ['operator'] + + # the first one has a path the second doesn't + completions = Script("import keyword", path='example.py').completions() + assert [c.name for c in completions] == ['keyword'] + completions = Script("import keyword").completions() + assert [c.name for c in completions] == ['keyword'] + + +def test_named_import(): + """named import - jedi-vim issue #8""" + s = "import time as dt" + assert len(Script(s, 1, 15, '/').goto_definitions()) == 1 + assert len(Script(s, 1, 10, '/').goto_definitions()) == 1 + + +@pytest.mark.skipif('True', reason='The nested import stuff is still very messy.') +def test_goto_following_on_imports(): + s = "import multiprocessing.dummy; multiprocessing.dummy" + g = Script(s).goto_assignments() + assert len(g) == 1 + assert (g[0].line, g[0].column) != (0, 0) + + +def test_after_from(): + def check(source, result, column=None): + completions = Script(source, column=column).completions() + assert [c.name for c in completions] == result + + check('\nfrom os. ', ['path']) + check('\nfrom os ', ['import']) + check('from os ', ['import']) + check('\nfrom os import whatever', ['import'], len('from os im')) + + check('from os\\\n', ['import']) + check('from os \\\n', ['import']) diff --git a/test/test_integration_import.py b/test/test_integration_import.py deleted file mode 100644 index d961666c..00000000 --- a/test/test_integration_import.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Tests of various import related things that could not be tested with "Black Box -Tests". -""" -from jedi import Script -from .helpers import cwd_at -from jedi._compatibility import is_py26 - -import pytest - - -def test_goto_definition_on_import(): - assert Script("import sys_blabla", 1, 8).goto_definitions() == [] - assert len(Script("import sys", 1, 8).goto_definitions()) == 1 - - -@cwd_at('jedi') -def test_complete_on_empty_import(): - assert Script("from datetime import").completions()[0].name == 'import' - # should just list the files in the directory - assert 10 < len(Script("from .", path='whatever.py').completions()) < 30 - - # Global import - assert len(Script("from . import", 1, 5, 'whatever.py').completions()) > 30 - # relative import - assert 10 < len(Script("from . import", 1, 6, 'whatever.py').completions()) < 30 - - # Global import - assert len(Script("from . import classes", 1, 5, 'whatever.py').completions()) > 30 - # relative import - assert 10 < len(Script("from . import classes", 1, 6, 'whatever.py').completions()) < 30 - - wanted = set(['ImportError', 'import', 'ImportWarning']) - assert set([c.name for c in Script("import").completions()]) == wanted - if not is_py26: # python 2.6 doesn't always come with a library `import*`. - assert len(Script("import import", path='').completions()) > 0 - - # 111 - assert Script("from datetime import").completions()[0].name == 'import' - assert Script("from datetime import ").completions() - - -def test_imports_on_global_namespace_without_path(): - """If the path is None, there shouldn't be any import problem""" - completions = Script("import operator").completions() - assert [c.name for c in completions] == ['operator'] - completions = Script("import operator", path='example.py').completions() - assert [c.name for c in completions] == ['operator'] - - # the first one has a path the second doesn't - completions = Script("import keyword", path='example.py').completions() - assert [c.name for c in completions] == ['keyword'] - completions = Script("import keyword").completions() - assert [c.name for c in completions] == ['keyword'] - - -def test_named_import(): - """named import - jedi-vim issue #8""" - s = "import time as dt" - assert len(Script(s, 1, 15, '/').goto_definitions()) == 1 - assert len(Script(s, 1, 10, '/').goto_definitions()) == 1 - - -@pytest.mark.skipif('True', reason='The nested import stuff is still very messy.') -def test_goto_following_on_imports(): - s = "import multiprocessing.dummy; multiprocessing.dummy" - g = Script(s).goto_assignments() - assert len(g) == 1 - assert (g[0].line, g[0].column) != (0, 0) - - -def test_after_from(): - def check(source, result, column=None): - completions = Script(source, column=column).completions() - assert [c.name for c in completions] == result - - check('\nfrom os. ', ['path']) - check('\nfrom os ', ['import']) - check('from os ', ['import']) - check('\nfrom os import whatever', ['import'], len('from os im')) - - check('from os\\\n', ['import']) - check('from os \\\n', ['import']) From ab71c943ee77e7423200a87478765505a31c82ea Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 16:29:48 +0200 Subject: [PATCH 25/38] Move a parser test to the correct place. --- test/test_new_parser.py | 13 ------------- test/test_parser/test_parser.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 13 deletions(-) delete mode 100644 test/test_new_parser.py diff --git a/test/test_new_parser.py b/test/test_new_parser.py deleted file mode 100644 index 453f65ae..00000000 --- a/test/test_new_parser.py +++ /dev/null @@ -1,13 +0,0 @@ -from jedi._compatibility import u -from jedi.parser.python import parse - - -def test_basic_parsing(): - def compare(string): - """Generates the AST object and then regenerates the code.""" - assert parse(string).get_code() == string - - compare(u('\na #pass\n')) - compare(u('wblabla* 1\t\n')) - compare(u('def x(a, b:3): pass\n')) - compare(u('assert foo\n')) diff --git a/test/test_parser/test_parser.py b/test/test_parser/test_parser.py index 2251c0a8..e96112eb 100644 --- a/test/test_parser/test_parser.py +++ b/test/test_parser/test_parser.py @@ -13,6 +13,16 @@ from jedi.parser_utils import get_statement_of_position, \ clean_scope_docstring, safe_literal_eval +def test_basic_parsing(): + def compare(string): + """Generates the AST object and then regenerates the code.""" + assert parse(string).get_code() == string + + compare('\na #pass\n') + compare('wblabla* 1\t\n') + compare('def x(a, b:3): pass\n') + compare('assert foo\n') + def test_user_statement_on_import(): """github #285""" s = "from datetime import (\n" \ From 3b033bb276b6d81fd02265e62c11f4b7e4571925 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 16:33:24 +0200 Subject: [PATCH 26/38] Remove two tests that are not necessary anymore because the code that made them necessary was removed (some import hacks). --- test/test_jedi_system.py | 61 ---------------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 test/test_jedi_system.py diff --git a/test/test_jedi_system.py b/test/test_jedi_system.py deleted file mode 100644 index ea0d1e9d..00000000 --- a/test/test_jedi_system.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Test the Jedi "System" which means for example to test if imports are -correctly used. -""" - -import os -import inspect - -import jedi - - -def test_settings_module(): - """ - jedi.settings and jedi.cache.settings must be the same module. - """ - from jedi import cache - from jedi import settings - assert cache.settings is settings - - -def test_no_duplicate_modules(): - """ - Make sure that import hack works as expected. - - Jedi does an import hack (see: jedi/__init__.py) to have submodules - with circular dependencies. The modules in this circular dependency - "loop" must be imported by ``import `` rather than normal - ``from jedi import `` (or ``from . jedi ...``). This test - make sure that this is satisfied. - - See also: - - - `#160 `_ - - `#161 `_ - """ - import sys - jedipath = os.path.dirname(os.path.abspath(jedi.__file__)) - - def is_submodule(m): - try: - filepath = m.__file__ - except AttributeError: - return False - return os.path.abspath(filepath).startswith(jedipath) - - modules = list(filter(is_submodule, sys.modules.values())) - top_modules = [m for m in modules if not m.__name__.startswith('jedi.')] - for m in modules: - if m is jedi: - # py.test automatically improts `jedi.*` when --doctest-modules - # is given. So this test cannot succeeds. - continue - for tm in top_modules: - try: - imported = getattr(m, tm.__name__) - except AttributeError: - continue - if inspect.ismodule(imported): - # module could have a function with the same name, e.g. - # `keywords.keywords`. - assert imported is tm From 9fb7fb66da2f8eef536f41ae2c122af89921e5ec Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 7 May 2017 16:39:32 +0200 Subject: [PATCH 27/38] Move another test to delete a file. --- test/test_evaluate/test_imports.py | 8 ++++++++ test/test_windows.py | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 test/test_windows.py diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index 5a9569b6..32026a1e 100644 --- a/test/test_evaluate/test_imports.py +++ b/test/test_evaluate/test_imports.py @@ -209,3 +209,11 @@ def test_after_from(): check('from os\\\n', ['import']) check('from os \\\n', ['import']) + + +def test_path_issues(): + """ + See pull request #684 for details. + """ + source = '''from datetime import ''' + assert jedi.Script(source).completions() diff --git a/test/test_windows.py b/test/test_windows.py deleted file mode 100644 index e5d8cd0d..00000000 --- a/test/test_windows.py +++ /dev/null @@ -1,8 +0,0 @@ -import jedi - -def test_path_issues(): - """ - See pull request #684 for details. - """ - source = '''from datetime import ''' - assert jedi.Script(source).completions() From 0b3590ce20dff6e66004296d01af06eac4993d0e Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 8 May 2017 19:55:35 +0200 Subject: [PATCH 28/38] Python 3.6 was not tested in the default configuration of tox. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 97b77a14..a2847586 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26, py27, py33, py34, py35 +envlist = py26, py27, py33, py34, py35, py36 [testenv] deps = pytest>=2.3.5 From 0da875281b5b578187e83096592ada0af9f73913 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Thu, 11 May 2017 16:22:11 -0400 Subject: [PATCH 29/38] Remove an unused compatibility function that was overriden by the same name lower in the same file. --- jedi/_compatibility.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 65e08855..bb427c81 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -166,14 +166,6 @@ try: except NameError: unicode = str -if is_py3: - u = lambda s: s -else: - u = lambda s: s.decode('utf-8') - -u.__doc__ = """ -Decode a raw string into unicode object. Do nothing in Python 3. -""" # exec function if is_py3: From 90b531a3b34317ef25856d1330eeca146a389060 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 15 May 2017 11:10:22 -0400 Subject: [PATCH 30/38] Correcting a sentence. --- jedi/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/settings.py b/jedi/settings.py index 13eaf60d..ccb6889a 100644 --- a/jedi/settings.py +++ b/jedi/settings.py @@ -115,7 +115,7 @@ else: 'jedi') cache_directory = os.path.expanduser(_cache_directory) """ -The path where all the caches can be found. +The path where the cache is stored. On Linux, this defaults to ``~/.cache/jedi/``, on OS X to ``~/Library/Caches/Jedi/`` and on Windows to ``%APPDATA%\\Jedi\\Jedi\\``. From f8b5aab6f4e0781722a291fdb965998ccb233dfc Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 15 May 2017 13:57:26 -0400 Subject: [PATCH 31/38] Move some parser tests. --- test/test_parser/test_parser.py | 77 ------------------- .../test_basic.py} | 0 .../test_error_correction.py | 12 +++ .../test_parser_utils.py | 69 +++++++++++++++++ 4 files changed, 81 insertions(+), 77 deletions(-) rename test/{test_parser/test_user_context.py => test_parso_integration/test_basic.py} (100%) create mode 100644 test/test_parso_integration/test_error_correction.py create mode 100644 test/test_parso_integration/test_parser_utils.py diff --git a/test/test_parser/test_parser.py b/test/test_parser/test_parser.py index e96112eb..2cfc0c42 100644 --- a/test/test_parser/test_parser.py +++ b/test/test_parser/test_parser.py @@ -4,13 +4,10 @@ from textwrap import dedent import pytest -import jedi from jedi._compatibility import u, is_py3 from jedi.parser.python import parse, load_grammar from jedi.parser.python import tree from jedi.common import splitlines -from jedi.parser_utils import get_statement_of_position, \ - clean_scope_docstring, safe_literal_eval def test_basic_parsing(): @@ -23,53 +20,6 @@ def test_basic_parsing(): compare('def x(a, b:3): pass\n') compare('assert foo\n') -def test_user_statement_on_import(): - """github #285""" - s = "from datetime import (\n" \ - " time)" - - for pos in [(2, 1), (2, 4)]: - p = parse(s) - stmt = get_statement_of_position(p, pos) - assert isinstance(stmt, tree.Import) - assert [n.value for n in stmt.get_defined_names()] == ['time'] - - -class TestCallAndName(): - def get_call(self, source): - # Get the simple_stmt and then the first one. - simple_stmt = parse(source).children[0] - return simple_stmt.children[0] - - def test_name_and_call_positions(self): - name = self.get_call('name\nsomething_else') - assert name.value == 'name' - assert name.start_pos == (1, 0) - assert name.end_pos == (1, 4) - - leaf = self.get_call('1.0\n') - assert leaf.value == '1.0' - assert safe_literal_eval(leaf.value) == 1.0 - assert leaf.start_pos == (1, 0) - assert leaf.end_pos == (1, 3) - - def test_call_type(self): - call = self.get_call('hello') - assert isinstance(call, tree.Name) - - def test_literal_type(self): - literal = self.get_call('1.0') - assert isinstance(literal, tree.Literal) - assert type(safe_literal_eval(literal.value)) == float - - literal = self.get_call('1') - assert isinstance(literal, tree.Literal) - assert type(safe_literal_eval(literal.value)) == int - - literal = self.get_call('"hello"') - assert isinstance(literal, tree.Literal) - assert safe_literal_eval(literal.value) == 'hello' - class TestSubscopes(): def get_sub(self, source): @@ -135,33 +85,6 @@ def test_incomplete_list_comprehension(): ['error_node', 'error_node', 'newline', 'endmarker'] -def test_hex_values_in_docstring(): - source = r''' - def foo(object): - """ - \xff - """ - return 1 - ''' - - doc = clean_scope_docstring(next(parse(source).iter_funcdefs())) - if is_py3: - assert doc == '\xff' - else: - assert doc == u('�') - - -def test_error_correction_with(): - source = """ - with open() as f: - try: - f.""" - comps = jedi.Script(source).completions() - assert len(comps) > 30 - # `open` completions have a closed attribute. - assert [1 for c in comps if c.name == 'closed'] - - def test_newline_positions(): endmarker = parse('a\n').children[-1] assert endmarker.end_pos == (2, 0) diff --git a/test/test_parser/test_user_context.py b/test/test_parso_integration/test_basic.py similarity index 100% rename from test/test_parser/test_user_context.py rename to test/test_parso_integration/test_basic.py diff --git a/test/test_parso_integration/test_error_correction.py b/test/test_parso_integration/test_error_correction.py new file mode 100644 index 00000000..153c34d9 --- /dev/null +++ b/test/test_parso_integration/test_error_correction.py @@ -0,0 +1,12 @@ +import jedi + + +def test_error_correction_with(): + source = """ + with open() as f: + try: + f.""" + comps = jedi.Script(source).completions() + assert len(comps) > 30 + # `open` completions have a closed attribute. + assert [1 for c in comps if c.name == 'closed'] diff --git a/test/test_parso_integration/test_parser_utils.py b/test/test_parso_integration/test_parser_utils.py new file mode 100644 index 00000000..f944133b --- /dev/null +++ b/test/test_parso_integration/test_parser_utils.py @@ -0,0 +1,69 @@ +from jedi._compatibility import u, is_py3 +from jedi.parser_utils import get_statement_of_position, \ + clean_scope_docstring, safe_literal_eval +from jedi.parser.python import parse +from jedi.parser.python import tree + + +class TestCallAndName(): + def get_call(self, source): + # Get the simple_stmt and then the first one. + simple_stmt = parse(source).children[0] + return simple_stmt.children[0] + + def test_name_and_call_positions(self): + name = self.get_call('name\nsomething_else') + assert name.value == 'name' + assert name.start_pos == (1, 0) + assert name.end_pos == (1, 4) + + leaf = self.get_call('1.0\n') + assert leaf.value == '1.0' + assert safe_literal_eval(leaf.value) == 1.0 + assert leaf.start_pos == (1, 0) + assert leaf.end_pos == (1, 3) + + def test_call_type(self): + call = self.get_call('hello') + assert isinstance(call, tree.Name) + + def test_literal_type(self): + literal = self.get_call('1.0') + assert isinstance(literal, tree.Literal) + assert type(safe_literal_eval(literal.value)) == float + + literal = self.get_call('1') + assert isinstance(literal, tree.Literal) + assert type(safe_literal_eval(literal.value)) == int + + literal = self.get_call('"hello"') + assert isinstance(literal, tree.Literal) + assert safe_literal_eval(literal.value) == 'hello' + + +def test_user_statement_on_import(): + """github #285""" + s = "from datetime import (\n" \ + " time)" + + for pos in [(2, 1), (2, 4)]: + p = parse(s) + stmt = get_statement_of_position(p, pos) + assert isinstance(stmt, tree.Import) + assert [n.value for n in stmt.get_defined_names()] == ['time'] + + +def test_hex_values_in_docstring(): + source = r''' + def foo(object): + """ + \xff + """ + return 1 + ''' + + doc = clean_scope_docstring(next(parse(source).iter_funcdefs())) + if is_py3: + assert doc == '\xff' + else: + assert doc == u('�') From 6848762f7cf485223928f988cb51c6b5385129d8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 15 May 2017 14:51:25 -0400 Subject: [PATCH 32/38] Move some more tests. --- test/test_parser/test_parser_tree.py | 9 ------ .../test_parser_utils.py | 31 ++++++++++++++----- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/test/test_parser/test_parser_tree.py b/test/test_parser/test_parser_tree.py index 1451231a..e137c904 100644 --- a/test/test_parser/test_parser_tree.py +++ b/test/test_parser/test_parser_tree.py @@ -6,7 +6,6 @@ import pytest from jedi.parser.python import parse from jedi.parser.python import tree -from jedi.parser_utils import get_doc_with_call_signature, get_call_signature class TestsFunctionAndLambdaParsing(object): @@ -14,13 +13,11 @@ class TestsFunctionAndLambdaParsing(object): FIXTURES = [ ('def my_function(x, y, z) -> str:\n return x + y * z\n', { 'name': 'my_function', - 'call_sig': 'my_function(x, y, z)', 'params': ['x', 'y', 'z'], 'annotation': "str", }), ('lambda x, y, z: x + y * z\n', { 'name': '', - 'call_sig': '(x, y, z)', 'params': ['x', 'y', 'z'], }), ] @@ -62,9 +59,3 @@ class TestsFunctionAndLambdaParsing(object): assert node.annotation is None else: assert node.annotation.value == expected_annotation - - def test_get_call_signature(self, node, expected): - assert get_call_signature(node) == expected['call_sig'] - - def test_doc(self, node, expected): - assert get_doc_with_call_signature(node) == (expected['call_sig'] + '\n\n') diff --git a/test/test_parso_integration/test_parser_utils.py b/test/test_parso_integration/test_parser_utils.py index f944133b..55494a74 100644 --- a/test/test_parso_integration/test_parser_utils.py +++ b/test/test_parso_integration/test_parser_utils.py @@ -1,9 +1,10 @@ from jedi._compatibility import u, is_py3 -from jedi.parser_utils import get_statement_of_position, \ - clean_scope_docstring, safe_literal_eval +from jedi import parser_utils from jedi.parser.python import parse from jedi.parser.python import tree +import pytest + class TestCallAndName(): def get_call(self, source): @@ -19,7 +20,7 @@ class TestCallAndName(): leaf = self.get_call('1.0\n') assert leaf.value == '1.0' - assert safe_literal_eval(leaf.value) == 1.0 + assert parser_utils.safe_literal_eval(leaf.value) == 1.0 assert leaf.start_pos == (1, 0) assert leaf.end_pos == (1, 3) @@ -30,15 +31,15 @@ class TestCallAndName(): def test_literal_type(self): literal = self.get_call('1.0') assert isinstance(literal, tree.Literal) - assert type(safe_literal_eval(literal.value)) == float + assert type(parser_utils.safe_literal_eval(literal.value)) == float literal = self.get_call('1') assert isinstance(literal, tree.Literal) - assert type(safe_literal_eval(literal.value)) == int + assert type(parser_utils.safe_literal_eval(literal.value)) == int literal = self.get_call('"hello"') assert isinstance(literal, tree.Literal) - assert safe_literal_eval(literal.value) == 'hello' + assert parser_utils.safe_literal_eval(literal.value) == 'hello' def test_user_statement_on_import(): @@ -48,7 +49,7 @@ def test_user_statement_on_import(): for pos in [(2, 1), (2, 4)]: p = parse(s) - stmt = get_statement_of_position(p, pos) + stmt = parser_utils.get_statement_of_position(p, pos) assert isinstance(stmt, tree.Import) assert [n.value for n in stmt.get_defined_names()] == ['time'] @@ -62,8 +63,22 @@ def test_hex_values_in_docstring(): return 1 ''' - doc = clean_scope_docstring(next(parse(source).iter_funcdefs())) + doc = parser_utils.clean_scope_docstring(next(parse(source).iter_funcdefs())) if is_py3: assert doc == '\xff' else: assert doc == u('�') + + +@pytest.mark.parametrize( + 'code,call_signature', [ + ('def my_function(x, y, z) -> str:\n return', 'my_function(x, y, z)'), + ('lambda x, y, z: x + y * z\n', '(x, y, z)') + ]) +def test_get_call_signature(code, call_signature): + node = parse(code).children[0] + if node.type == 'simple_stmt': + node = node.children[0] + assert parser_utils.get_call_signature(node) == call_signature + + assert parser_utils.get_doc_with_call_signature(node) == (call_signature + '\n\n') From 0a8c96cd227668a9ccbf61efc7c781eeb13537fc Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 15 May 2017 14:53:50 -0400 Subject: [PATCH 33/38] Remove a test that is really not necessary anymore, because the issues that it was covering back then are not issues anymore with the new infrastructure. --- test/test_parser/test_old_fast_parser.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/test_parser/test_old_fast_parser.py b/test/test_parser/test_old_fast_parser.py index 6fe989b4..e8fc725d 100644 --- a/test/test_parser/test_old_fast_parser.py +++ b/test/test_parser/test_old_fast_parser.py @@ -28,17 +28,6 @@ def test_carriage_return_splitting(): assert [n.value for lst in module.get_used_names().values() for n in lst] == ['Foo'] -def test_class_in_docstr(): - """ - Regression test for a problem with classes in docstrings. - """ - a = '"\nclasses\n"' - jedi.Script(a, 1, 0)._get_module() - - b = a + '\nimport os' - assert jedi.Script(b, 4, 8).goto_assignments() - - def check_p(src, number_parsers_used, number_of_splits=None, number_of_misses=0): if number_of_splits is None: number_of_splits = number_parsers_used From 882ddbf8acc63b72e10e5e86a86f0c8854f65902 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 15 May 2017 15:00:34 -0400 Subject: [PATCH 34/38] Move some more parser tests. --- test/test_parser/test_old_fast_parser.py | 85 +------------------ test/test_parso_integration/test_basic.py | 46 ++++++++++ .../test_error_correction.py | 40 +++++++++ 3 files changed, 89 insertions(+), 82 deletions(-) diff --git a/test/test_parser/test_old_fast_parser.py b/test/test_parser/test_old_fast_parser.py index e8fc725d..796b947f 100644 --- a/test/test_parser/test_old_fast_parser.py +++ b/test/test_parser/test_old_fast_parser.py @@ -8,13 +8,11 @@ However the tests might still be relevant for the parser. from textwrap import dedent -import jedi -from jedi._compatibility import u from jedi.parser.python import parse def test_carriage_return_splitting(): - source = u(dedent(''' + source = dedent(''' @@ -22,39 +20,18 @@ def test_carriage_return_splitting(): class Foo(): pass - ''')) + ''') source = source.replace('\n', '\r\n') module = parse(source) assert [n.value for lst in module.get_used_names().values() for n in lst] == ['Foo'] -def check_p(src, number_parsers_used, number_of_splits=None, number_of_misses=0): - if number_of_splits is None: - number_of_splits = number_parsers_used - +def check_p(src, number_parsers_used): module_node = parse(src) - assert src == module_node.get_code() return module_node -def test_if(): - src = dedent('''\ - def func(): - x = 3 - if x: - def y(): - return x - return y() - - func() - ''') - - # Two parsers needed, one for pass and one for the function. - check_p(src, 2) - assert [d.name for d in jedi.Script(src, 8, 6).goto_definitions()] == ['int'] - - def test_for(): src = dedent("""\ for a in [1,2]: @@ -111,24 +88,6 @@ def test_nested_funcs(): check_p(src, 3) -def test_class_and_if(): - src = dedent("""\ - class V: - def __init__(self): - pass - - if 1: - c = 3 - - def a_func(): - return 1 - - # COMMENT - a_func()""") - check_p(src, 5, 5) - assert [d.name for d in jedi.Script(src).goto_definitions()] == ['int'] - - def test_multi_line_params(): src = dedent("""\ def x(a, @@ -226,44 +185,6 @@ def test_additional_indent(): check_p(source, 2) -def test_incomplete_function(): - source = '''return ImportErr''' - - script = jedi.Script(dedent(source), 1, 3) - assert script.completions() - - -def test_string_literals(): - """Simplified case of jedi-vim#377.""" - source = dedent(""" - x = ur''' - - def foo(): - pass - """) - - script = jedi.Script(dedent(source)) - assert script._get_module().tree_node.end_pos == (6, 0) - assert script.completions() - - -def test_decorator_string_issue(): - """ - Test case from #589 - """ - source = dedent('''\ - """ - @""" - def bla(): - pass - - bla.''') - - s = jedi.Script(source) - assert s.completions() - assert s._get_module().tree_node.get_code() == source - - def test_round_trip(): code = dedent(''' def x(): diff --git a/test/test_parso_integration/test_basic.py b/test/test_parso_integration/test_basic.py index 01aff796..a9ec14e6 100644 --- a/test/test_parso_integration/test_basic.py +++ b/test/test_parso_integration/test_basic.py @@ -1,6 +1,52 @@ +from textwrap import dedent + +from jedi.parser.python import parse import jedi def test_form_feed_characters(): s = "\f\nclass Test(object):\n pass" jedi.Script(s, line=2, column=18).call_signatures() + + +def check_p(src, number_parsers_used): + module_node = parse(src) + assert src == module_node.get_code() + return module_node + + +def test_if(): + src = dedent('''\ + def func(): + x = 3 + if x: + def y(): + return x + return y() + + func() + ''') + + # Two parsers needed, one for pass and one for the function. + check_p(src, 2) + assert [d.name for d in jedi.Script(src, 8, 6).goto_definitions()] == ['int'] + + +def test_class_and_if(): + src = dedent("""\ + class V: + def __init__(self): + pass + + if 1: + c = 3 + + def a_func(): + return 1 + + # COMMENT + a_func()""") + check_p(src, 5, 5) + assert [d.name for d in jedi.Script(src).goto_definitions()] == ['int'] + + diff --git a/test/test_parso_integration/test_error_correction.py b/test/test_parso_integration/test_error_correction.py index 153c34d9..49814733 100644 --- a/test/test_parso_integration/test_error_correction.py +++ b/test/test_parso_integration/test_error_correction.py @@ -1,3 +1,5 @@ +from textwrap import dedent + import jedi @@ -10,3 +12,41 @@ def test_error_correction_with(): assert len(comps) > 30 # `open` completions have a closed attribute. assert [1 for c in comps if c.name == 'closed'] + + +def test_string_literals(): + """Simplified case of jedi-vim#377.""" + source = dedent(""" + x = ur''' + + def foo(): + pass + """) + + script = jedi.Script(dedent(source)) + assert script._get_module().tree_node.end_pos == (6, 0) + assert script.completions() + + +def test_incomplete_function(): + source = '''return ImportErr''' + + script = jedi.Script(dedent(source), 1, 3) + assert script.completions() + + +def test_decorator_string_issue(): + """ + Test case from #589 + """ + source = dedent('''\ + """ + @""" + def bla(): + pass + + bla.''') + + s = jedi.Script(source) + assert s.completions() + assert s._get_module().tree_node.get_code() == source From f4548d127ca967d1b189387569c6336870a922ee Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 15 May 2017 15:02:45 -0400 Subject: [PATCH 35/38] Some simplifications for the parsers. --- test/test_parser/test_old_fast_parser.py | 30 +++++++++++------------ test/test_parso_integration/test_basic.py | 6 ++--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/test/test_parser/test_old_fast_parser.py b/test/test_parser/test_old_fast_parser.py index 796b947f..03998b98 100644 --- a/test/test_parser/test_old_fast_parser.py +++ b/test/test_parser/test_old_fast_parser.py @@ -26,7 +26,7 @@ def test_carriage_return_splitting(): assert [n.value for lst in module.get_used_names().values() for n in lst] == ['Foo'] -def check_p(src, number_parsers_used): +def check_p(src): module_node = parse(src) assert src == module_node.get_code() return module_node @@ -40,7 +40,7 @@ def test_for(): for a1 in 1,"": a1 """) - check_p(src, 1) + check_p(src) def test_class_with_class_var(): @@ -51,7 +51,7 @@ def test_class_with_class_var(): self.foo = 4 pass """) - check_p(src, 3) + check_p(src) def test_func_with_if(): @@ -65,7 +65,7 @@ def test_func_with_if(): else: return a """) - check_p(src, 1) + check_p(src) def test_decorator(): @@ -75,7 +75,7 @@ def test_decorator(): def dec(self, a): return a """) - check_p(src, 2) + check_p(src) def test_nested_funcs(): @@ -85,7 +85,7 @@ def test_nested_funcs(): return func(*args, **kwargs) return wrapper """) - check_p(src, 3) + check_p(src) def test_multi_line_params(): @@ -96,7 +96,7 @@ def test_multi_line_params(): foo = 1 """) - check_p(src, 2) + check_p(src) def test_class_func_if(): @@ -110,7 +110,7 @@ def test_class_func_if(): pass """) - check_p(src, 3) + check_p(src) def test_multi_line_for(): @@ -121,7 +121,7 @@ def test_multi_line_for(): pass """) - check_p(src, 1) + check_p(src) def test_wrong_indentation(): @@ -131,7 +131,7 @@ def test_wrong_indentation(): b a """) - #check_p(src, 1) + check_p(src) src = dedent("""\ def complex(): @@ -143,7 +143,7 @@ def test_wrong_indentation(): def other(): pass """) - check_p(src, 3) + check_p(src) def test_strange_parentheses(): @@ -154,7 +154,7 @@ def test_strange_parentheses(): def x(): pass """) - check_p(src, 2) + check_p(src) def test_fake_parentheses(): @@ -172,7 +172,7 @@ def test_fake_parentheses(): def z(): pass """) - check_p(src, 3, 2, 1) + check_p(src) def test_additional_indent(): @@ -182,7 +182,7 @@ def test_additional_indent(): pass ''') - check_p(source, 2) + check_p(source) def test_round_trip(): @@ -202,4 +202,4 @@ def test_parentheses_in_string(): import abc abc.''') - check_p(code, 2, 1, 1) + check_p(code) diff --git a/test/test_parso_integration/test_basic.py b/test/test_parso_integration/test_basic.py index a9ec14e6..e3c3da15 100644 --- a/test/test_parso_integration/test_basic.py +++ b/test/test_parso_integration/test_basic.py @@ -9,7 +9,7 @@ def test_form_feed_characters(): jedi.Script(s, line=2, column=18).call_signatures() -def check_p(src, number_parsers_used): +def check_p(src): module_node = parse(src) assert src == module_node.get_code() return module_node @@ -28,7 +28,7 @@ def test_if(): ''') # Two parsers needed, one for pass and one for the function. - check_p(src, 2) + check_p(src) assert [d.name for d in jedi.Script(src, 8, 6).goto_definitions()] == ['int'] @@ -46,7 +46,7 @@ def test_class_and_if(): # COMMENT a_func()""") - check_p(src, 5, 5) + check_p(src) assert [d.name for d in jedi.Script(src).goto_definitions()] == ['int'] From 3c57f781dd03ebea483e83f35eefc813c1763050 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 15 May 2017 15:18:42 -0400 Subject: [PATCH 36/38] Move another few tests. --- test/test_parser/test_diff_parser.py | 33 --------------------- test/test_parser/test_tokenize.py | 6 ---- test/test_parso_integration/test_basic.py | 35 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/test/test_parser/test_diff_parser.py b/test/test_parser/test_diff_parser.py index 8231a9ee..a8127fc0 100644 --- a/test/test_parser/test_diff_parser.py +++ b/test/test_parser/test_diff_parser.py @@ -2,7 +2,6 @@ from textwrap import dedent import pytest -import jedi from jedi import debug from jedi.common import splitlines from jedi import cache @@ -386,38 +385,6 @@ def test_node_insertion(differ): differ.parse(code2, parsers=1, copies=2) -def test_add_to_end(): - """ - fast_parser doesn't parse everything again. It just updates with the - help of caches, this is an example that didn't work. - """ - - a = dedent("""\ - class Abc(): - def abc(self): - self.x = 3 - - class Two(Abc): - def g(self): - self - """) # ^ here is the first completion - - b = " def h(self):\n" \ - " self." - - def complete(code, line=None, column=None): - script = jedi.Script(code, line, column, 'example.py') - assert script.completions() - _assert_valid_graph(script._get_module()) - - complete(a, 7, 12) - complete(a + b) - - a = a[:-1] + '.\n' - complete(a, 7, 13) - complete(a + b) - - def test_whitespace_at_end(differ): code = dedent('str\n\n') diff --git a/test/test_parser/test_tokenize.py b/test/test_parser/test_tokenize.py index 4cddf9c1..a2563a06 100644 --- a/test/test_parser/test_tokenize.py +++ b/test/test_parser/test_tokenize.py @@ -147,12 +147,6 @@ class TokenTest(unittest.TestCase): assert safe_literal_eval(string_tok.value) == 'test' -def test_tokenizer_with_string_literal_backslash(): - import jedi - c = jedi.Script("statement = u'foo\\\n'; statement").goto_definitions() - assert c[0]._name._context.obj == 'foo' - - def test_ur_literals(): """ Decided to parse `u''` literals regardless of Python version. This makes diff --git a/test/test_parso_integration/test_basic.py b/test/test_parso_integration/test_basic.py index e3c3da15..8840ca1f 100644 --- a/test/test_parso_integration/test_basic.py +++ b/test/test_parso_integration/test_basic.py @@ -50,3 +50,38 @@ def test_class_and_if(): assert [d.name for d in jedi.Script(src).goto_definitions()] == ['int'] +def test_add_to_end(): + """ + The diff parser doesn't parse everything again. It just updates with the + help of caches, this is an example that didn't work. + """ + + a = dedent("""\ + class Abc(): + def abc(self): + self.x = 3 + + class Two(Abc): + def g(self): + self + """) # ^ here is the first completion + + b = " def h(self):\n" \ + " self." + + def complete(code, line=None, column=None): + script = jedi.Script(code, line, column, 'example.py') + assert script.completions() + + complete(a, 7, 12) + complete(a + b) + + a = a[:-1] + '.\n' + complete(a, 7, 13) + complete(a + b) + + +def test_tokenizer_with_string_literal_backslash(): + import jedi + c = jedi.Script("statement = u'foo\\\n'; statement").goto_definitions() + assert c[0]._name._context.obj == 'foo' From f5248250d80ea78152f381d0a9aeba4ddb6e2a5f Mon Sep 17 00:00:00 2001 From: micbou Date: Wed, 12 Jul 2017 21:34:39 +0200 Subject: [PATCH 37/38] Fix keyword docstring --- jedi/api/keywords.py | 11 +++++++---- test/test_evaluate/test_docstring.py | 10 +++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/jedi/api/keywords.py b/jedi/api/keywords.py index 1ee92919..b624d73a 100644 --- a/jedi/api/keywords.py +++ b/jedi/api/keywords.py @@ -76,12 +76,16 @@ class KeywordName(AbstractNameDefinition): api_type = 'keyword' def __init__(self, evaluator, name): + self.evaluator = evaluator self.string_name = name self.parent_context = evaluator.BUILTINS def eval(self): return set() + def infer(self): + return [Keyword(self.evaluator, self.string_name, (0, 0))] + class Keyword(object): api_type = 'keyword' @@ -100,9 +104,8 @@ class Keyword(object): """ For a `parsing.Name` like comparision """ return [self.name] - @property - def docstr(self): - return imitate_pydoc(self.name) + def py__doc__(self, include_call_signature=False): + return imitate_pydoc(self.name.string_name) def __repr__(self): return '<%s: %s>' % (type(self).__name__, self.name) @@ -136,6 +139,6 @@ def imitate_pydoc(string): return '' try: - return pydoc_topics.topics[label] if pydoc_topics else '' + return pydoc_topics.topics[label].strip() if pydoc_topics else '' except KeyError: return '' diff --git a/test/test_evaluate/test_docstring.py b/test/test_evaluate/test_docstring.py index 6dbd1cac..98858b91 100644 --- a/test/test_evaluate/test_docstring.py +++ b/test/test_evaluate/test_docstring.py @@ -20,7 +20,7 @@ class TestDocstring(unittest.TestCase): def func(): '''Docstring of `func`.''' func""").goto_definitions() - self.assertEqual(defs[0].raw_doc, 'Docstring of `func`.') + self.assertEqual(defs[0].docstring(), 'func()\n\nDocstring of `func`.') @unittest.skip('need evaluator class for that') def test_attribute_docstring(self): @@ -28,7 +28,7 @@ class TestDocstring(unittest.TestCase): x = None '''Docstring of `x`.''' x""").goto_definitions() - self.assertEqual(defs[0].raw_doc, 'Docstring of `x`.') + self.assertEqual(defs[0].docstring(), 'Docstring of `x`.') @unittest.skip('need evaluator class for that') def test_multiple_docstrings(self): @@ -38,7 +38,7 @@ class TestDocstring(unittest.TestCase): x = func '''Docstring of `x`.''' x""").goto_definitions() - docs = [d.raw_doc for d in defs] + docs = [d.docstring() for d in defs] self.assertEqual(docs, ['Original docstring.', 'Docstring of `x`.']) def test_completion(self): @@ -105,6 +105,10 @@ class TestDocstring(unittest.TestCase): assert '__init__' in names assert 'mro' not in names # Exists only for types. + def test_docstring_keyword(self): + completions = jedi.Script('assert').completions() + self.assertIn('assert', completions[0].docstring()) + @unittest.skipIf(numpydoc_unavailable, 'numpydoc module is unavailable') def test_numpydoc_docstring(self): s = dedent(''' From 175e57214eb0f608d77fbbefc555ab9554f28d6b Mon Sep 17 00:00:00 2001 From: micbou Date: Thu, 13 Jul 2017 01:42:09 +0200 Subject: [PATCH 38/38] Fix instance docstring --- jedi/evaluate/instance.py | 5 +++++ test/test_evaluate/test_docstring.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/jedi/evaluate/instance.py b/jedi/evaluate/instance.py index 9b131f84..d48169e5 100644 --- a/jedi/evaluate/instance.py +++ b/jedi/evaluate/instance.py @@ -197,6 +197,11 @@ class CompiledInstance(AbstractInstanceContext): class TreeInstance(AbstractInstanceContext): + def __init__(self, evaluator, parent_context, class_context, var_args): + super(TreeInstance, self).__init__(evaluator, parent_context, + class_context, var_args) + self.tree_node = class_context.tree_node + @property def name(self): return filters.ContextName(self, self.class_context.name.tree_name) diff --git a/test/test_evaluate/test_docstring.py b/test/test_evaluate/test_docstring.py index 98858b91..efd61941 100644 --- a/test/test_evaluate/test_docstring.py +++ b/test/test_evaluate/test_docstring.py @@ -22,6 +22,21 @@ class TestDocstring(unittest.TestCase): func""").goto_definitions() self.assertEqual(defs[0].docstring(), 'func()\n\nDocstring of `func`.') + def test_class_doc(self): + defs = jedi.Script(""" + class TestClass(): + '''Docstring of `TestClass`.''' + TestClass""").goto_definitions() + self.assertEqual(defs[0].docstring(), 'Docstring of `TestClass`.') + + def test_instance_doc(self): + defs = jedi.Script(""" + class TestClass(): + '''Docstring of `TestClass`.''' + tc = TestClass() + tc""").goto_definitions() + self.assertEqual(defs[0].docstring(), 'Docstring of `TestClass`.') + @unittest.skip('need evaluator class for that') def test_attribute_docstring(self): defs = jedi.Script("""