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. 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/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 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: 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/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/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/jedi/evaluate/__init__.py b/jedi/evaluate/__init__.py index 0fdd5202..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) @@ -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. @@ -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/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 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__) 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/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/instance.py b/jedi/evaluate/instance.py index d0cd812a..d48169e5 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,13 +190,18 @@ 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) 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) @@ -332,7 +338,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 5f648953..a47cfad6 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' @@ -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. @@ -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/__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) 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): diff --git a/jedi/parser/python/tree.py b/jedi/parser/python/tree.py index e37e2b55..8257f0a5 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 @@ -61,18 +59,10 @@ 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 + """ + Some Python specific utitilies. + """ + __slots__ = () def get_definition(self): if self.type in ('newline', 'endmarker'): @@ -92,10 +82,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): @@ -269,9 +255,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. @@ -311,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] @@ -328,6 +311,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 = {} @@ -358,9 +345,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': @@ -389,6 +382,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: @@ -452,14 +449,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' @@ -514,20 +512,16 @@ 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. - 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__ = () @@ -545,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] @@ -556,7 +547,6 @@ class Lambda(Function): """ Returns `None`, lambdas don't have annotations. """ - # lambda functions do not support annotations return None def __repr__(self): @@ -650,6 +640,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. @@ -669,13 +663,18 @@ 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 - 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') @@ -692,9 +691,14 @@ 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): + 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) @@ -738,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.paths()[-1][-1] - - def paths(self): + 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() @@ -762,6 +762,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 @@ -769,7 +774,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): @@ -799,10 +804,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) @@ -880,14 +888,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.""" @@ -925,6 +937,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 +948,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 +959,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 +981,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 +992,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,16 +1006,28 @@ 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')) + 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) - def get_description(self): - # TODO Remove? 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() @@ -999,8 +1038,8 @@ 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. + """ return _defined_names(self.children[1]) 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 diff --git a/jedi/parser_utils.py b/jedi/parser_utils.py index 47a9a17c..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)) @@ -217,3 +222,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 + 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\\``. 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 diff --git a/test/test_evaluate/test_docstring.py b/test/test_evaluate/test_docstring.py index 6dbd1cac..efd61941 100644 --- a/test/test_evaluate/test_docstring.py +++ b/test/test_evaluate/test_docstring.py @@ -20,7 +20,22 @@ 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`.') + + 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): @@ -28,7 +43,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 +53,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 +120,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(''' diff --git a/test/test_evaluate/test_imports.py b/test/test_evaluate/test_imports.py index 6402a7b0..32026a1e 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,85 @@ 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']) + + +def test_path_issues(): + """ + See pull request #684 for details. + """ + source = '''from datetime import ''' + assert jedi.Script(source).completions() 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 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']) 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 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_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_old_fast_parser.py b/test/test_parser/test_old_fast_parser.py index 6fe989b4..03998b98 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,50 +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 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 - +def check_p(src): 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]: @@ -74,7 +40,7 @@ def test_for(): for a1 in 1,"": a1 """) - check_p(src, 1) + check_p(src) def test_class_with_class_var(): @@ -85,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(): @@ -99,7 +65,7 @@ def test_func_with_if(): else: return a """) - check_p(src, 1) + check_p(src) def test_decorator(): @@ -109,7 +75,7 @@ def test_decorator(): def dec(self, a): return a """) - check_p(src, 2) + check_p(src) def test_nested_funcs(): @@ -119,25 +85,7 @@ def test_nested_funcs(): return func(*args, **kwargs) return wrapper """) - 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'] + check_p(src) def test_multi_line_params(): @@ -148,7 +96,7 @@ def test_multi_line_params(): foo = 1 """) - check_p(src, 2) + check_p(src) def test_class_func_if(): @@ -162,7 +110,7 @@ def test_class_func_if(): pass """) - check_p(src, 3) + check_p(src) def test_multi_line_for(): @@ -173,7 +121,7 @@ def test_multi_line_for(): pass """) - check_p(src, 1) + check_p(src) def test_wrong_indentation(): @@ -183,7 +131,7 @@ def test_wrong_indentation(): b a """) - #check_p(src, 1) + check_p(src) src = dedent("""\ def complex(): @@ -195,7 +143,7 @@ def test_wrong_indentation(): def other(): pass """) - check_p(src, 3) + check_p(src) def test_strange_parentheses(): @@ -206,7 +154,7 @@ def test_strange_parentheses(): def x(): pass """) - check_p(src, 2) + check_p(src) def test_fake_parentheses(): @@ -224,7 +172,7 @@ def test_fake_parentheses(): def z(): pass """) - check_p(src, 3, 2, 1) + check_p(src) def test_additional_indent(): @@ -234,45 +182,7 @@ def test_additional_indent(): pass ''') - 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 + check_p(source) def test_round_trip(): @@ -292,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_parser/test_parser.py b/test/test_parser/test_parser.py index 2251c0a8..2cfc0c42 100644 --- a/test/test_parser/test_parser.py +++ b/test/test_parser/test_parser.py @@ -4,61 +4,21 @@ 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_user_statement_on_import(): - """github #285""" - s = "from datetime import (\n" \ - " time)" +def test_basic_parsing(): + def compare(string): + """Generates the AST object and then regenerates the code.""" + assert parse(string).get_code() == string - 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' + compare('\na #pass\n') + compare('wblabla* 1\t\n') + compare('def x(a, b:3): pass\n') + compare('assert foo\n') class TestSubscopes(): @@ -125,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_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_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_parser/test_user_context.py b/test/test_parser/test_user_context.py deleted file mode 100644 index 01aff796..00000000 --- a/test/test_parser/test_user_context.py +++ /dev/null @@ -1,6 +0,0 @@ -import jedi - - -def test_form_feed_characters(): - s = "\f\nclass Test(object):\n pass" - jedi.Script(s, line=2, column=18).call_signatures() diff --git a/test/test_parso_integration/test_basic.py b/test/test_parso_integration/test_basic.py new file mode 100644 index 00000000..8840ca1f --- /dev/null +++ b/test/test_parso_integration/test_basic.py @@ -0,0 +1,87 @@ +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): + 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) + 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) + 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' 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..49814733 --- /dev/null +++ b/test/test_parso_integration/test_error_correction.py @@ -0,0 +1,52 @@ +from textwrap import dedent + +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'] + + +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 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..55494a74 --- /dev/null +++ b/test/test_parso_integration/test_parser_utils.py @@ -0,0 +1,84 @@ +from jedi._compatibility import u, is_py3 +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): + # 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 parser_utils.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(parser_utils.safe_literal_eval(literal.value)) == float + + literal = self.get_call('1') + assert isinstance(literal, tree.Literal) + assert type(parser_utils.safe_literal_eval(literal.value)) == int + + literal = self.get_call('"hello"') + assert isinstance(literal, tree.Literal) + assert parser_utils.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 = parser_utils.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 = 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') diff --git a/test/test_speed.py b/test/test_speed.py index dcb69840..8d5bc9a3 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 SlowRepr(): + "class to test what happens if __repr__ is very slow." + def some_method(self): + pass + def __repr__(self): + time.sleep(0.2) + test = SlowRepr() + jedi.Interpreter('test.som', [locals()]).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() 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