diff --git a/.travis.yml b/.travis.yml index 29c03f1a..adf32985 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,11 @@ env: - TOXENV=py32 - TOXENV=py33 - TOXENV=cov + - TOXENV=sith matrix: allow_failures: - env: TOXENV=cov + - env: TOXENV=sith install: - pip install --quiet --use-mirrors tox script: diff --git a/README.rst b/README.rst index 0abb4a2c..096b3cd4 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,8 @@ Jedi - an awesome autocompletion library for Python :target: https://coveralls.io/r/davidhalter/jedi :alt: Coverage Status +.. image:: https://pypip.in/d/jedi/badge.png + :target: https://crate.io/packages/jedi/ Jedi is an autocompletion tool for Python that can be used in IDEs/editors. Jedi works. Jedi is fast. It understands all of the basic Python syntax @@ -24,12 +26,15 @@ which uses Jedi's autocompletion. I encourage you to use Jedi in your IDEs. It's really easy. If there are any problems (also with licensing), just contact me. -Jedi can be used with the following plugins/software: +Jedi can be used with the following editors: -- `VIM-Plugin `_ -- `Emacs-Plugin `_ -- `Sublime-Plugin `_ -- `wdb (web debugger) `_ +- Vim (jedi-vim_, YouCompleteMe_) +- Emacs (Jedi.el_) +- Sublime Text (SublimeJEDI_) + +And it powers the following projects: + +- wdb_ Here are some pictures: @@ -121,3 +126,10 @@ Tests are also run automatically on `Travis CI For more detailed information visit the `testing documentation `_ + + +.. _jedi-vim: https://github.com/davidhalter/jedi-vim +.. _youcompleteme: http://valloric.github.io/YouCompleteMe/ +.. _Jedi.el: https://github.com/tkf/emacs-jedi +.. _sublimejedi: https://github.com/svaiter/SublimeJEDI +.. _wdb: https://github.com/Kozea/wdb diff --git a/docs/_static/logo.png b/docs/_static/logo.png index f56374e3..0c77f119 100644 Binary files a/docs/_static/logo.png and b/docs/_static/logo.png differ diff --git a/docs/conf.py b/docs/conf.py index fe93a75e..04d7b8e9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ sys.path.append(os.path.abspath('_themes')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.todo', - 'sphinx.ext.inheritance_diagram'] + 'sphinx.ext.intersphinx', 'sphinx.ext.inheritance_diagram'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -141,7 +141,7 @@ html_static_path = ['_static'] # Custom sidebar templates, maps document names to template names. html_sidebars = { '**': [ -# 'sidebarlogo.html', + 'sidebarlogo.html', 'localtoc.html', # 'relations.html', 'ghbuttons.html', @@ -266,6 +266,14 @@ todo_include_todos = False # -- Options for autodoc module ------------------------------------------------ +autoclass_content = 'both' autodoc_member_order = 'bysource' autodoc_default_flags = [] #autodoc_default_flags = ['members', 'undoc-members'] + + +# -- Options for intersphinx module -------------------------------------------- + +intersphinx_mapping = { + 'http://docs.python.org/': None, +} diff --git a/docs/docs/development.rst b/docs/docs/development.rst index a6a8b3d1..9d40c0b0 100644 --- a/docs/docs/development.rst +++ b/docs/docs/development.rst @@ -64,6 +64,8 @@ Parser Representation (parser_representation.py) .. automodule:: parsing_representation +Class inheritance diagram: + .. inheritance-diagram:: SubModule Class diff --git a/docs/docs/installation.rst b/docs/docs/installation.rst index 35cc4bc3..44cedfd6 100644 --- a/docs/docs/installation.rst +++ b/docs/docs/installation.rst @@ -30,8 +30,15 @@ System-wide installation via a package manager Arch Linux ~~~~~~~~~~ -You can install jedi directly from AUR: `python-jedi at AUR -`__. +You can install |jedi| directly from official AUR packages: + +- `python-jedi `__ (Python 3) +- `python2-jedi `__ (Python 2) + +The specified Python version just refers to the *runtime environment* for +|jedi|. Use the Python 2 version if you're running vim (or whatever editor you +use) under Python 2. Otherwise, use the Python 3 version. But whatever version +you choose, both are able to complete both Python 2 and 3 *code*. (There is also a packaged version of the vim plugin available: `vim-jedi at AUR `__.) @@ -45,7 +52,7 @@ Debian packages are available as `experimental packages Others ~~~~~~ -We are in the discussion of adding Jedi to the Fedora repositories. +We are in the discussion of adding |jedi| to the Fedora repositories. Manual installation from a downloaded package @@ -53,7 +60,7 @@ Manual installation from a downloaded package If you prefer not to use an automated package installer, you can `download `__ a current copy of -*Jedi* and install it manually. +|jedi| and install it manually. To install it, navigate to the directory containing `setup.py` on your console and type:: diff --git a/docs/docs/repl.rst b/docs/docs/repl.rst new file mode 100644 index 00000000..e911e46e --- /dev/null +++ b/docs/docs/repl.rst @@ -0,0 +1,8 @@ +.. include:: ../global.rst + +How to use Jedi from Python interpreter +======================================= + +.. automodule:: jedi.replstartup + +.. autofunction:: jedi.utils.setup_readline diff --git a/docs/index.rst b/docs/index.rst index c8e518ea..b6bb2755 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,6 +22,7 @@ Docs docs/installation docs/features + docs/repl docs/recipes docs/plugin-api docs/history @@ -44,9 +45,18 @@ Resources Editor Plugins -------------- -- `Vim `_ -- `Emacs `_ -- `Sublime Text 2 `_ +Vim: + +- `jedi-vim `_ +- `YouCompleteMe `_ + +Emacs: + +- `Jedi.el `_ + +Sublime Text 2: + +- `SublimeJEDI `_ .. _other-software: diff --git a/jedi/__init__.py b/jedi/__init__.py index feff0bba..9c37f07c 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -42,8 +42,8 @@ import sys # imports and circular imports... Just avoid it: sys.path.insert(0, __path__[0]) -from .api import Script, NotFoundError, set_debug_function, _quick_complete, \ - preload_module +from .api import Script, Interpreter, NotFoundError, set_debug_function, \ + preload_module, defined_names from . import settings sys.path.pop(0) diff --git a/jedi/__main__.py b/jedi/__main__.py new file mode 100644 index 00000000..73cde226 --- /dev/null +++ b/jedi/__main__.py @@ -0,0 +1,2 @@ +from os import path +print(path.join(path.dirname(path.abspath(__file__)), 'replstartup.py')) diff --git a/jedi/api.py b/jedi/api.py index edae10ca..5eedd23c 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -20,6 +20,7 @@ from jedi import helpers from jedi import common from jedi import cache from jedi import modules +from jedi import interpret from jedi._compatibility import next, unicode import evaluate import keywords @@ -53,14 +54,18 @@ class Script(object): ``unicode`` object (default ``'utf-8'``). :type source_encoding: str """ - def __init__(self, source, line, column, source_path, - source_encoding='utf-8'): + def __init__(self, source, line=None, column=None, source_path=None, + source_encoding='utf-8'): + lines = source.splitlines() + line = len(lines) if line is None else line + column = len(lines[-1]) if column is None else column + api_classes._clear_caches() debug.reset_time() self.source = modules.source_to_unicode(source, source_encoding) self.pos = line, column - self._module = modules.ModuleWithCursor(source_path, - source=self.source, position=self.pos) + self._module = modules.ModuleWithCursor( + source_path, source=self.source, position=self.pos) self._source_path = source_path self.source_path = None if source_path is None \ else os.path.abspath(source_path) @@ -337,7 +342,8 @@ class Script(object): :rtype: list of :class:`api_classes.Definition` """ - d = [api_classes.Definition(d) for d in set(self._goto()[0])] + d = [api_classes.Definition(d) for d in set(self._goto()[0]) + if not isinstance(d, imports.ImportPath._GlobalNamespace)] return self._sorted_defs(d) def _goto(self, add_import_name=False): @@ -385,7 +391,9 @@ class Script(object): defs, search_name = evaluate.goto(stmt) definitions = follow_inexistent_imports(defs) if isinstance(user_stmt, pr.Statement): - if user_stmt.get_commands()[0].start_pos > self.pos: + call = user_stmt.get_commands()[0] + if not isinstance(call, (str, unicode)) and \ + call.start_pos > self.pos: # The cursor must be after the start, otherwise the # statement is just an assignee. definitions = [user_stmt] @@ -503,6 +511,45 @@ class Script(object): return sorted(d, key=lambda x: (x.module_path or '', x.start_pos)) +class Interpreter(Script): + + """ + Jedi API for Python REPLs. + + In addition to completion of simple attribute access, Jedi + supports code completion based on static code analysis. + Jedi can complete attributes of object which is not initialized + yet. + + >>> from os.path import join + >>> namespace = locals() + >>> script = Interpreter('join().up', [namespace]) + >>> print(script.complete()[0].word) + upper + + """ + + def __init__(self, source, namespaces=[], **kwds): + """ + Parse `source` and mixin interpreted Python objects from `namespaces`. + + :type source: str + :arg source: Code to parse. + :type namespaces: list of dict + :arg namespaces: a list of namespace dictionaries such as the one + returned by :func:`locals`. + + Other optional arguments are same as the ones for :class:`Script`. + If `line` and `column` are None, they are assumed be at the end of + `source`. + """ + super(Interpreter, self).__init__(source, **kwds) + + importer = interpret.ObjectImporter(self._parser.user_scope) + for ns in namespaces: + importer.import_raw_namespace(ns) + + def defined_names(source, source_path=None, source_encoding='utf-8'): """ Get all definitions in `source` sorted by its position. @@ -519,7 +566,7 @@ def defined_names(source, source_path=None, source_encoding='utf-8'): modules.source_to_unicode(source, source_encoding), module_path=source_path, ) - return api_classes._defined_names(parser.scope) + return api_classes._defined_names(parser.module) def preload_module(*modules): @@ -545,25 +592,3 @@ def set_debug_function(func_cb=debug.print_to_stdout, warnings=True, debug.enable_warning = warnings debug.enable_notice = notices debug.enable_speed = speed - - -def _quick_complete(source): - """ - Convenience function to complete a source string at the end. - - Example: - - >>> _quick_complete(''' - ... import datetime - ... datetime.da''') #doctest: +ELLIPSIS - [, , ...] - - :param source: The source code to be completed. - :type source: string - :return: Completion objects as returned by :meth:`complete`. - :rtype: list of :class:`api_classes.Completion` - """ - lines = re.sub(r'[\n\r\s]*$', '', source).splitlines() - pos = len(lines), len(lines[-1]) - script = Script(source, pos[0], pos[1], '') - return script.completions() diff --git a/jedi/api_classes.py b/jedi/api_classes.py index 04be77e7..d9ddbb29 100644 --- a/jedi/api_classes.py +++ b/jedi/api_classes.py @@ -142,9 +142,19 @@ class BaseDefinition(object): def path(self): """The module path.""" path = [] + + def insert_nonnone(x): + if x: + path.insert(0, x) + if not isinstance(self._definition, keywords.Keyword): par = self._definition while par is not None: + if isinstance(par, pr.Import): + insert_nonnone(par.namespace) + insert_nonnone(par.from_ns) + if par.relative_count == 0: + break with common.ignored(AttributeError): path.insert(0, par.name) par = par.parent diff --git a/jedi/evaluate.py b/jedi/evaluate.py index 38e0c86b..5aea7a48 100644 --- a/jedi/evaluate.py +++ b/jedi/evaluate.py @@ -318,7 +318,9 @@ def find_name(scope, name_str, position=None, search_global=False, exc = pr.Class, pr.Function until = lambda: par.parent.parent.get_parent_until(exc) - if par.isinstance(pr.Flow): + if par is None: + pass + elif par.isinstance(pr.Flow): if par.command == 'for': result += handle_for_loops(par) else: @@ -554,16 +556,33 @@ def assign_tuples(tup, results, seek_name): else: r = eval_results(i) - # are there still tuples or is it just a Call. - if isinstance(command, pr.Array): - # These are "sub"-tuples. - result += assign_tuples(command, r, seek_name) - else: - if command.name.names[-1] == seek_name: - result += r + # LHS of tuples can be nested, so resolve it recursively + result += find_assignments(command, r, seek_name) return result +def find_assignments(lhs, results, seek_name): + """ + Check if `seek_name` is in the left hand side `lhs` of assignment. + + `lhs` can simply be a variable (`pr.Call`) or a tuple/list (`pr.Array`) + representing the following cases:: + + a = 1 # lhs is pr.Call + (a, b) = 2 # lhs is pr.Array + + :type lhs: pr.Call + :type results: list + :type seek_name: str + """ + if isinstance(lhs, pr.Array): + return assign_tuples(lhs, results, seek_name) + elif lhs.name.names[-1] == seek_name: + return results + else: + return [] + + @recursion.RecursionDecorator @cache.memoize_default(default=()) def follow_statement(stmt, seek_name=None): @@ -587,7 +606,7 @@ def follow_statement(stmt, seek_name=None): if len(stmt.get_set_vars()) > 1 and seek_name and stmt.assignment_details: new_result = [] for ass_commands, op in stmt.assignment_details: - new_result += assign_tuples(ass_commands[0], result, seek_name) + new_result += find_assignments(ass_commands[0], result, seek_name) result = new_result return set(result) @@ -775,7 +794,10 @@ def goto(stmt, call_path=None): commands = stmt.get_commands() assert len(commands) == 1 call = commands[0] - call_path = list(call.generate_call_path()) + if isinstance(call, (str, unicode)): + call_path = [call] + else: + call_path = list(call.generate_call_path()) scope = stmt.get_parent_until(pr.IsScope) pos = stmt.start_pos diff --git a/jedi/imports.py b/jedi/imports.py index d55d39e6..e6acb7f1 100644 --- a/jedi/imports.py +++ b/jedi/imports.py @@ -162,9 +162,9 @@ class ImportPath(pr.Base): # If you edit e.g. gunicorn, there will be imports like this: # `from gunicorn import something`. But gunicorn is not in the # sys.path. Therefore look if gunicorn is a parent directory, #56. - parts = self.file_path.split(os.path.sep) in_path = [] if self.import_path: + parts = self.file_path.split(os.path.sep) for i, p in enumerate(parts): if p == self.import_path[0]: new = os.path.sep.join(parts[:i]) diff --git a/jedi/interpret.py b/jedi/interpret.py new file mode 100644 index 00000000..6792369c --- /dev/null +++ b/jedi/interpret.py @@ -0,0 +1,171 @@ +""" +Module to handle interpreted Python objects. +""" + +import itertools +import tokenize + +from jedi import parsing_representation as pr + + +class ObjectImporter(object): + + """ + Import objects in "raw" namespace such as :func:`locals`. + """ + + def __init__(self, scope): + self.scope = scope + + count = itertools.count() + self._genname = lambda: '*jedi-%s*' % next(count) + """ + Generate unique variable names to avoid name collision. + To avoid name collision to already defined names, generated + names are invalid as Python identifier. + """ + + def import_raw_namespace(self, raw_namespace): + """ + Import interpreted Python objects in a namespace. + + Three kinds of objects are treated here. + + 1. Functions and classes. The objects imported like this:: + + from os.path import join + + 2. Modules. The objects imported like this:: + + import os + + 3. Instances. The objects created like this:: + + from datetime import datetime + dt = datetime(2013, 1, 1) + + :type raw_namespace: dict + :arg raw_namespace: e.g., the dict given by `locals` + """ + scope = self.scope + for (variable, obj) in raw_namespace.items(): + objname = getattr(obj, '__name__', None) + + # Import functions and classes + module = getattr(obj, '__module__', None) + if module and objname: + fakeimport = self.make_fakeimport(module, objname, variable) + scope.add_import(fakeimport) + continue + + # Import modules + if getattr(obj, '__file__', None) and objname: + fakeimport = self.make_fakeimport(objname) + scope.add_import(fakeimport) + continue + + # Import instances + objclass = getattr(obj, '__class__', None) + module = getattr(objclass, '__module__', None) + if objclass and module: + alias = self._genname() + fakeimport = self.make_fakeimport(module, objclass.__name__, + alias) + fakestmt = self.make_fakestatement(variable, alias, call=True) + scope.add_import(fakeimport) + scope.add_statement(fakestmt) + continue + + def make_fakeimport(self, module, variable=None, alias=None): + """ + Make a fake import object. + + The following statements are created depending on what parameters + are given: + + - only `module`: ``import `` + - `module` and `variable`: ``from import `` + - all: ``from import as `` + + :type module: str + :arg module: ```` part in ``from import ...`` + :type variable: str + :arg variable: ```` part in ``from ... import `` + :type alias: str + :arg alias: ```` part in ``... import ... as ``. + + :rtype: :class:`parsing_representation.Import` + """ + submodule = self.scope._sub_module + if variable: + varname = pr.Name( + module=submodule, + names=[(variable, (-1, 0))], + start_pos=(-1, 0), + end_pos=(None, None)) + else: + varname = None + modname = pr.Name( + module=submodule, + names=[(module, (-1, 0))], + start_pos=(-1, 0), + end_pos=(None, None)) + if alias: + aliasname = pr.Name( + module=submodule, + names=[(alias, (-1, 0))], + start_pos=(-1, 0), + end_pos=(None, None)) + else: + aliasname = None + if varname: + fakeimport = pr.Import( + module=submodule, + namespace=varname, + from_ns=modname, + alias=aliasname, + start_pos=(-1, 0), + end_pos=(None, None)) + else: + fakeimport = pr.Import( + module=submodule, + namespace=modname, + alias=aliasname, + start_pos=(-1, 0), + end_pos=(None, None)) + return fakeimport + + def make_fakestatement(self, lhs, rhs, call=False): + """ + Make a fake statement object that represents ``lhs = rhs``. + + :type call: bool + :arg call: When `call` is true, make a fake statement that represents + ``lhs = rhs()``. + + :rtype: :class:`parsing_representation.Statement` + """ + submodule = self.scope._sub_module + lhsname = pr.Name( + module=submodule, + names=[(lhs, (0, 0))], + start_pos=(0, 0), + end_pos=(None, None)) + rhsname = pr.Name( + module=submodule, + names=[(rhs, (0, 0))], + start_pos=(0, 0), + end_pos=(None, None)) + token_list = [lhsname, (tokenize.OP, '=', (0, 0)), rhsname] + if call: + token_list.extend([ + (tokenize.OP, '(', (0, 0)), + (tokenize.OP, ')', (0, 0)), + ]) + return pr.Statement( + module=submodule, + set_vars=[lhsname], + used_vars=[rhsname], + token_list=token_list, + start_pos=(0, 0), + end_pos=(None, None)) diff --git a/jedi/modules.py b/jedi/modules.py index 37e5bbac..c583ec5f 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -141,6 +141,7 @@ class ModuleWithCursor(Module): last_line = self.get_line(self._line_temp) if last_line and last_line[-1] == '\\': line = last_line[:-1] + ' ' + line + self._line_length = len(last_line) else: break return line[::-1] @@ -187,6 +188,7 @@ class ModuleWithCursor(Module): elif token_type == tokenize.NUMBER: pass else: + self._column_temp = self._line_length - end[1] break self._column_temp = self._line_length - end[1] diff --git a/jedi/parsing.py b/jedi/parsing.py index 5371f526..d402753e 100644 --- a/jedi/parsing.py +++ b/jedi/parsing.py @@ -56,7 +56,7 @@ class Parser(object): self.start_pos = self.end_pos = 1 + offset[0], offset[1] # initialize global Scope self.module = pr.SubModule(module_path, self.start_pos, top_module) - self.scope = self.module + self._scope = self.module self.current = (None, None) source = source + '\n' # end with \n, because the parser needs it @@ -390,7 +390,7 @@ class Parser(object): #print 'new_stat', set_vars, used_vars if self.freshscope and not self.no_docstr and len(tok_list) == 1 \ and self.last_token[0] == tokenize.STRING: - self.scope.add_docstr(self.last_token[1]) + self._scope.add_docstr(self.last_token[1]) return None, tok else: stmt = stmt_class(self.module, set_vars, used_vars, tok_list, @@ -408,7 +408,7 @@ class Parser(object): and len(stmt.token_list) == 1 and first_tok[0] == tokenize.STRING): # ... then set it as a docstring - self.scope.statements[-1].add_docstr(first_tok[1]) + self._scope.statements[-1].add_docstr(first_tok[1]) if tok in always_break + not_first_break: self._gen.push_last_back() @@ -429,7 +429,7 @@ class Parser(object): self.start_pos, self.end_pos = start_pos, end_pos except (StopIteration, common.MultiLevelStopIteration): # on finish, set end_pos correctly - s = self.scope + s = self._scope while s is not None: if isinstance(s, pr.Module) \ and not isinstance(s, pr.SubModule): @@ -443,8 +443,8 @@ class Parser(object): or self.user_scope is None and self.start_pos[0] >= self.user_position[0]): debug.dbg('user scope found [%s] = %s' % - (self.parserline.replace('\n', ''), repr(self.scope))) - self.user_scope = self.scope + (self.parserline.replace('\n', ''), repr(self._scope))) + self.user_scope = self._scope self.last_token = self.current self.current = (typ, tok) return self.current @@ -472,29 +472,29 @@ class Parser(object): #debug.dbg('main: tok=[%s] type=[%s] indent=[%s]'\ # % (tok, tokenize.tok_name[token_type], start_position[0])) - while token_type == tokenize.DEDENT and self.scope != self.module: + while token_type == tokenize.DEDENT and self._scope != self.module: token_type, tok = self.next() - if self.start_pos[1] <= self.scope.start_pos[1]: - self.scope.end_pos = self.start_pos - self.scope = self.scope.parent - if isinstance(self.scope, pr.Module) \ - and not isinstance(self.scope, pr.SubModule): - self.scope = self.module + if self.start_pos[1] <= self._scope.start_pos[1]: + self._scope.end_pos = self.start_pos + self._scope = self._scope.parent + if isinstance(self._scope, pr.Module) \ + and not isinstance(self._scope, pr.SubModule): + self._scope = self.module # check again for unindented stuff. this is true for syntax # errors. only check for names, because thats relevant here. If # some docstrings are not indented, I don't care. - while self.start_pos[1] <= self.scope.start_pos[1] \ + while self.start_pos[1] <= self._scope.start_pos[1] \ and (token_type == tokenize.NAME or tok in ['(', '['])\ - and self.scope != self.module: - self.scope.end_pos = self.start_pos - self.scope = self.scope.parent - if isinstance(self.scope, pr.Module) \ - and not isinstance(self.scope, pr.SubModule): - self.scope = self.module + and self._scope != self.module: + self._scope.end_pos = self.start_pos + self._scope = self._scope.parent + if isinstance(self._scope, pr.Module) \ + and not isinstance(self._scope, pr.SubModule): + self._scope = self.module - use_as_parent_scope = self.top_module if isinstance(self.scope, - pr.SubModule) else self.scope + use_as_parent_scope = self.top_module if isinstance(self._scope, + pr.SubModule) else self._scope first_pos = self.start_pos if tok == 'def': func = self._parse_function() @@ -503,7 +503,7 @@ class Parser(object): self.start_pos[0]) continue self.freshscope = True - self.scope = self.scope.add_scope(func, self._decorators) + self._scope = self._scope.add_scope(func, self._decorators) self._decorators = [] elif tok == 'class': cls = self._parse_class() @@ -511,7 +511,7 @@ class Parser(object): debug.warning("class: syntax error@%s" % self.start_pos[0]) continue self.freshscope = True - self.scope = self.scope.add_scope(cls, self._decorators) + self._scope = self._scope.add_scope(cls, self._decorators) self._decorators = [] # import stuff elif tok == 'import': @@ -522,7 +522,7 @@ class Parser(object): i = pr.Import(self.module, first_pos, end_pos, m, alias, defunct=defunct) self._check_user_stmt(i) - self.scope.add_import(i) + self._scope.add_import(i) if not imports: i = pr.Import(self.module, first_pos, self.end_pos, None, defunct=True) @@ -559,7 +559,7 @@ class Parser(object): alias, mod, star, relative_count, defunct=defunct or defunct2) self._check_user_stmt(i) - self.scope.add_import(i) + self._scope.add_import(i) self.freshscope = False #loops elif tok == 'for': @@ -569,7 +569,7 @@ class Parser(object): if tok == ':': s = [] if statement is None else [statement] f = pr.ForFlow(self.module, s, first_pos, set_stmt) - self.scope = self.scope.add_statement(f) + self._scope = self._scope.add_statement(f) else: debug.warning('syntax err, for flow started @%s', self.start_pos[0]) @@ -612,13 +612,13 @@ class Parser(object): # the flow statement, because a dedent releases the # main scope, so just take the last statement. try: - s = self.scope.statements[-1].set_next(f) + s = self._scope.statements[-1].set_next(f) except (AttributeError, IndexError): # If set_next doesn't exist, just add it. - s = self.scope.add_statement(f) + s = self._scope.add_statement(f) else: - s = self.scope.add_statement(f) - self.scope = s + s = self._scope.add_statement(f) + self._scope = s else: for i in inputs: i.parent = use_as_parent_scope @@ -629,7 +629,7 @@ class Parser(object): s = self.start_pos self.freshscope = False # add returns to the scope - func = self.scope.get_parent_until(pr.Function) + func = self._scope.get_parent_until(pr.Function) if tok == 'yield': func.is_generator = True @@ -646,7 +646,7 @@ class Parser(object): elif tok == 'global': stmt, tok = self._parse_statement(self.current) if stmt: - self.scope.add_statement(stmt) + self._scope.add_statement(stmt) for name in stmt.used_vars: # add the global to the top, because there it is # important. @@ -659,8 +659,9 @@ class Parser(object): continue elif tok == 'assert': stmt, tok = self._parse_statement() - stmt.parent = use_as_parent_scope - self.scope.asserts.append(stmt) + if stmt is not None: + stmt.parent = use_as_parent_scope + self._scope.asserts.append(stmt) # default elif token_type in [tokenize.NAME, tokenize.STRING, tokenize.NUMBER] \ @@ -670,7 +671,7 @@ class Parser(object): # by the statement parser. stmt, tok = self._parse_statement(self.current) if stmt: - self.scope.add_statement(stmt) + self._scope.add_statement(stmt) self.freshscope = False else: if token_type not in [tokenize.COMMENT, tokenize.INDENT, diff --git a/jedi/parsing_representation.py b/jedi/parsing_representation.py index 21cce274..e281c0f1 100644 --- a/jedi/parsing_representation.py +++ b/jedi/parsing_representation.py @@ -16,11 +16,11 @@ is the easiest way to write a parser. The same behaviour applies to ``Param``, which is being used in a function definition. The easiest way to play with this module is to use :class:`parsing.Parser`. -:attr:`parsing.Parser.scope` holds an instance of :class:`SubModule`: +:attr:`parsing.Parser.module` holds an instance of :class:`SubModule`: >>> from jedi.parsing import Parser >>> parser = Parser('import os', 'example.py') ->>> submodule = parser.scope +>>> submodule = parser.module >>> submodule @@ -248,14 +248,14 @@ class Scope(Simple, IsScope): ... b = y ... b.c = z ... ''') - >>> parser.scope.get_defined_names() + >>> parser.module.get_defined_names() [, ] Note that unlike :meth:`get_set_vars`, assignment to object attribute does not change the result because it does not change the defined names in this scope. - >>> parser.scope.get_set_vars() + >>> parser.module.get_set_vars() [, , ] """ diff --git a/jedi/replstartup.py b/jedi/replstartup.py new file mode 100644 index 00000000..40ee77da --- /dev/null +++ b/jedi/replstartup.py @@ -0,0 +1,25 @@ +""" +``PYTHONSTARTUP`` to use Jedi in your Python interpreter. + +To use Jedi completion in Python interpreter, add the following in your shell +setup (e.g., ``.bashrc``):: + + export PYTHONSTARTUP="$(python -m jedi)" + +Then you will be able to use Jedi completer in your Python interpreter:: + + $ python + Python 2.7.2+ (default, Jul 20 2012, 22:15:08) + [GCC 4.6.1] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> import os + >>> os.path.join().split().in # doctest: +SKIP + os.path.join().split().index os.path.join().split().insert + +""" + +import jedi.utils +jedi.utils.setup_readline() +del jedi +# Note: try not to do many things here, as it will contaminate global +# namespace of the interpreter. diff --git a/jedi/settings.py b/jedi/settings.py index 666547c9..3f3598c8 100644 --- a/jedi/settings.py +++ b/jedi/settings.py @@ -127,8 +127,10 @@ cache_directory = os.path.expanduser(_cache_directory) """ The path where all the caches can be found. -On Linux, this defaults to ``~/.cache/jedi/``, on OS X to ``~/.jedi/`` and on -Windows to ``%APPDATA%\\Jedi\\Jedi\\``. +On Linux, this defaults to ``~/.cache/jedi/``, on OS X to +``~/Library/Caches/Jedi/`` and on Windows to ``%APPDATA%\\Jedi\\Jedi\\``. +On Linux, if environment variable ``$XDG_CACHE_HOME`` is set, +``$XDG_CACHE_HOME/jedi`` is used instead of the default one. """ # ---------------- diff --git a/jedi/utils.py b/jedi/utils.py new file mode 100644 index 00000000..37aa2225 --- /dev/null +++ b/jedi/utils.py @@ -0,0 +1,110 @@ +""" +Utilities for end-users. +""" + +from rlcompleter import Completer + +from jedi import Interpreter + + +_NON_DELIMS = ' \t\n()' +""" +:class:`rcompleter.Completer` assumes these characters to be delimiter +(i.e., :meth:`rcompleter.Completer.complete` does not expect these +characters) but :class:`JediRLCompleter` can handle them. +""" + +try: + import readline +except ImportError: + pass +else: + _READLINE_DEFAULT_DELIMS = readline.get_completer_delims() + _READLINE_JEDI_DELIMS = ''.join( + set(_READLINE_DEFAULT_DELIMS) - set(_NON_DELIMS)) + + +class JediRLCompleter(Completer): + + """ + :class:`rlcompleter.Completer` enhanced by Jedi. + + This class tries matchers defined in :class:`.Completer` first. + If they fail, :class:`jedi.Interpreter` is used. + + >>> import os + >>> completer = JediRLCompleter(locals()) + >>> completer.complete('os.path.joi', 0) # completion w/o Jedi + 'os.path.join(' + >>> completer.complete('os.path.join().s', 0) # completion with Jedi + 'os.path.join().split' + + """ + + def _jedi_matches(self, text): + completions = Interpreter(text, [self.namespace]).completions() + return [text + c.complete for c in completions] + + @staticmethod + def _split_for_default_matcher(text, delims=_NON_DELIMS): + """ + Split `text` before passing it to :meth:`Completer.attr_matches` etc. + + >>> JediRLCompleter._split_for_default_matcher('f(') + ('f(', '') + >>> JediRLCompleter._split_for_default_matcher('f().g') + ('f()', '.g') + + """ + import re + m = re.match(r"(.*[{0}])([^{0}]*)".format(re.escape(delims)), text) + if not m: + return ('', text) + return m.groups() + + def _find_matches(self, default_matcher, text): + """ + Common part for :meth:`attr_matches` and :meth:`global_matches`. + + Try `default_matcher` first and return what it returns if + it is not empty. Otherwise, try :meth:`_jedi_matches`. + + :arg default_matcher: :meth:`.Completer.attr_matches` or + :meth:`.Completer.global_matches`. + :arg str text: code to complete + """ + (pre, body) = self._split_for_default_matcher(text) + matches = default_matcher(self, body) + if matches: + return [pre + m for m in matches] + return self._jedi_matches(text) + + def attr_matches(self, text): + # NOTE: Completer is old type class so `super` cannot be used here + return self._find_matches(Completer.attr_matches, text) + + def global_matches(self, text): + # NOTE: Completer is old type class so `super` cannot be used here + return self._find_matches(Completer.global_matches, text) + + +def setup_readline(): + """ + Install Jedi completer to :mod:`readline`. + + This function setups :mod:`readline` to use Jedi in Python interactive + shell. If you want to use custom ``PYTHONSTARTUP`` file, you can call + this function like this: + + >>> from jedi.utils import setup_readline + >>> setup_readline() + + """ + try: + import readline + except ImportError: + print("Module readline not available.") + else: + readline.set_completer(JediRLCompleter().complete) + readline.parse_and_bind("tab: complete") + readline.set_completer_delims(_READLINE_JEDI_DELIMS) diff --git a/sith.py b/sith.py new file mode 100755 index 00000000..6f07d882 --- /dev/null +++ b/sith.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python + +""" +Sith attacks (and helps debugging) Jedi. + +Randomly search Python files and run Jedi on it. Exception and used +arguments are recorded to ``./record.json`` (specified by --record):: + + %(prog)s random /path/to/sourcecode + +Redo recorded exception:: + + %(prog)s redo + +Fallback to pdb when error is raised:: + + %(prog)s --pdb random + %(prog)s --pdb redo + +""" + +from __future__ import print_function, division, unicode_literals +import json +import os +import random +import sys +import traceback + +import jedi + +_unspecified = object() + + +class SourceCode(object): + + def __init__(self, path): + self.path = path + with open(path) as f: + self.source = f.read() + self.lines = self.source.splitlines() + self.maxline = len(self.lines) + + def choose_script_args(self): + line = random.randint(1, self.maxline) + column = random.randint(0, len(self.lines[line - 1])) + return (self.source, line, column, self.path) + + +class SourceFinder(object): + + def __init__(self, rootpath): + self.rootpath = rootpath + self.files = list(self.search_files()) + + def search_files(self): + for root, dirnames, filenames in os.walk(self.rootpath): + for name in filenames: + if name.endswith('.py'): + yield os.path.join(root, name) + + def choose_source(self): + # FIXME: try same file for several times + return SourceCode(random.choice(self.files)) + + +class BaseAttacker(object): + + def __init__(self): + self.record = {'data': []} + + def attack(self, operation, *args): + script = jedi.Script(*args) + op = getattr(script, operation) + op() + + def add_record(self, exc_info, operation, args): + (_type, value, tb) = exc_info + self.record['data'].append({ + 'traceback': traceback.format_tb(tb), + 'error': repr(value), + 'operation': operation, + 'args': args, + }) + + def get_record(self, recid): + return self.record['data'][recid] + + def save_record(self, path): + directory = os.path.dirname(os.path.abspath(path)) + if not os.path.isdir(directory): + os.makedirs(directory) + with open(path, 'w') as f: + json.dump(self.record, f) + + def load_record(self, path): + with open(path) as f: + self.record = json.load(f) + return self.record + + def add_arguments(self, parser): + parser.set_defaults(func=self.do_run) + + def get_help(self): + for line in self.__doc__.splitlines(): + line = line.strip() + if line: + return line + + +class MixinPrinter(object): + + def print_record(self, recid=-1): + data = self.get_record(recid) + print(*data['traceback'], end='') + print(""" +{error} is raised by running Script(...).{operation}() with +line : {args[1]} +column: {args[2]} +path : {args[3]} +""".format(**data)) + + +class MixinLoader(object): + + def add_arguments(self, parser): + super(MixinLoader, self).add_arguments(parser) + parser = parser.add_argument( + 'recid', default=0, nargs='?', type=int, help=""" + This option currently has no effect as random attack record + only one error. + """) + + def do_run(self, record, recid): + self.load_record(record) + + +class AttackReporter(object): + + def __init__(self): + self.tries = 0 + self.errors = 0 + + def __iter__(self): + return self + + def __next__(self): + self.tries += 1 + sys.stderr.write('.') + sys.stderr.flush() + return self.tries + + next = __next__ + + def error(self): + self.errors += 1 + sys.stderr.write('\n') + sys.stderr.flush() + print('{0}th error is encountered after {1} tries.' + .format(self.errors, self.tries)) + + +class RandomAttacker(MixinPrinter, BaseAttacker): + + """ + Randomly run Script().() against files under . + """ + + operations = [ + 'completions', 'goto_assignments', 'goto_definitions', 'usages', + 'call_signatures'] + + def choose_operation(self): + return random.choice(self.operations) + + def generate_attacks(self, maxtries, finder): + for _ in range(maxtries): + src = finder.choose_source() + operation = self.choose_operation() + yield (operation, src.choose_script_args()) + + def do_run(self, record, rootpath, maxtries): + finder = SourceFinder(rootpath) + reporter = AttackReporter() + for (operation, args) in self.generate_attacks(maxtries, finder): + reporter.next() + try: + self.attack(operation, *args) + except jedi.NotFoundError: + pass + except Exception: + self.add_record(sys.exc_info(), operation, args) + reporter.error() + self.print_record() + raise + finally: + self.save_record(record) + + def add_arguments(self, parser): + super(RandomAttacker, self).add_arguments(parser) + parser.add_argument( + '--maxtries', '-l', default=10000, type=int) + parser.add_argument( + 'rootpath', default='.', nargs='?', + help='root directory to look for Python files.') + + +class RedoAttacker(MixinLoader, BaseAttacker): + + """ + Redo recorded attack. + """ + + def do_run(self, record, recid): + super(RedoAttacker, self).do_run(record, recid) + data = self.get_record(recid) + try: + self.attack(data['operation'], *data['args']) + except: + traceback.print_exc() + raise + + +class ShowRecord(MixinLoader, MixinPrinter, BaseAttacker): + + """ + Show recorded errors. + """ + + def do_run(self, record, recid): + super(ShowRecord, self).do_run(record, recid) + self.print_record() + + +class AttackApp(object): + + def __init__(self): + self.parsers = [] + self.attackers = [] + + def run(self, args=None): + parser = self.get_parser() + self.do_run(**vars(parser.parse_args(args))) + + def do_run(self, func, debugger, fs_cache, **kwds): + if fs_cache is _unspecified: + jedi.settings.use_filesystem_cache = False + else: + jedi.settings.cache_directory = fs_cache + + try: + func(**kwds) + except: + if debugger: + einfo = sys.exc_info() + pdb = __import__(debugger) + pdb.post_mortem(einfo if debugger == 'pudb' else einfo[2]) + sys.exit(1) + + def add_parser(self, attacker_class, *args, **kwds): + attacker = attacker_class() + parser = self.subparsers.add_parser( + *args, + help=attacker.get_help(), + description=attacker.__doc__, + **kwds) + attacker.add_arguments(parser) + + # Not required, just fore debugging: + self.parsers.append(parser) + self.attackers.append(attacker) + + def get_parser(self): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + parser.add_argument( + '--record', '-R', default='record.json', + help='Exceptions are recorded in here (default: %(default)s).') + parser.add_argument( + '--pdb', dest='debugger', const='pdb', action='store_const', + help='Launch pdb when error is raised.') + parser.add_argument( + '--ipdb', dest='debugger', const='ipdb', action='store_const', + help='Launch ipdb when error is raised.') + parser.add_argument( + '--pudb', dest='debugger', const='pudb', action='store_const', + help='Launch pudb when error is raised.') + parser.add_argument( + '--fs-cache', '-C', default=_unspecified, + help=""" + By default, file system cache is off for reproducibility. + Pass a temporary directory to use file system cache. + It is set to ``jedi.settings.cache_directory``. + """) + + self.subparsers = parser.add_subparsers() + self.add_parser(RandomAttacker, 'random') + self.add_parser(RedoAttacker, 'redo') + self.add_parser(ShowRecord, 'show') + + return parser + + +if __name__ == '__main__': + try: + import argparse + except ImportError: + print('The argparse module (Python>=2.7) is needed to run sith.') + sys.exit(1) + app = AttackApp() + app.run() diff --git a/test/completion/arrays.py b/test/completion/arrays.py index 5ab96cae..3e22e933 100644 --- a/test/completion/arrays.py +++ b/test/completion/arrays.py @@ -94,6 +94,28 @@ a4 b4 +# ----------------- +# multiple assignments +# ----------------- +a = b = 1 +#? int() +a +#? int() +b + +(a, b) = (c, (e, f)) = ('2', (3, 4)) +#? str() +a +#? tuple() +b +#? str() +c +#? int() +e +#? int() +f + + # ----------------- # unnessecary braces # ----------------- diff --git a/test/test_api_classes.py b/test/test_api_classes.py index 65a9d0d2..714a11ef 100644 --- a/test/test_api_classes.py +++ b/test/test_api_classes.py @@ -3,7 +3,13 @@ import textwrap import pytest from jedi import api +import jedi +def test_is_keyword(): + results = jedi.Script('import ', 1, 1, None).goto_definitions() + assert len(results) == 1 and results[0].is_keyword == True + results = jedi.Script('str', 1, 1, None).goto_definitions() + assert len(results) == 1 and results[0].is_keyword == False def make_definitions(): """ diff --git a/test/test_defined_names.py b/test/test_defined_names.py new file mode 100644 index 00000000..922fbe45 --- /dev/null +++ b/test/test_defined_names.py @@ -0,0 +1,75 @@ +""" +Tests for `api.defined_names`. +""" + +import textwrap + +from jedi import api +from .base import TestBase + + +class TestDefinedNames(TestBase): + + def assert_definition_names(self, definitions, names): + self.assertEqual([d.name for d in definitions], names) + + def check_defined_names(self, source, names): + definitions = api.defined_names(textwrap.dedent(source)) + self.assert_definition_names(definitions, names) + return definitions + + def test_get_definitions_flat(self): + self.check_defined_names(""" + import module + class Class: + pass + def func(): + pass + data = None + """, ['module', 'Class', 'func', 'data']) + + def test_dotted_assignment(self): + self.check_defined_names(""" + x = Class() + x.y.z = None + """, ['x']) + + def test_multiple_assignment(self): + self.check_defined_names(""" + x = y = None + """, ['x', 'y']) + + def test_multiple_imports(self): + self.check_defined_names(""" + from module import a, b + from another_module import * + """, ['a', 'b']) + + def test_nested_definitions(self): + definitions = self.check_defined_names(""" + class Class: + def f(): + pass + def g(): + pass + """, ['Class']) + subdefinitions = definitions[0].defined_names() + self.assert_definition_names(subdefinitions, ['f', 'g']) + self.assertEqual([d.full_name for d in subdefinitions], + ['Class.f', 'Class.g']) + + def test_nested_class(self): + definitions = self.check_defined_names(""" + class L1: + class L2: + class L3: + def f(): pass + def f(): pass + def f(): pass + def f(): pass + """, ['L1', 'f']) + subdefs = definitions[0].defined_names() + subsubdefs = subdefs[0].defined_names() + self.assert_definition_names(subdefs, ['L2', 'f']) + self.assert_definition_names(subsubdefs, ['L3', 'f']) + self.assert_definition_names(subsubdefs[0].defined_names(), ['f']) diff --git a/test/test_full_name.py b/test/test_full_name.py new file mode 100644 index 00000000..f74c76d8 --- /dev/null +++ b/test/test_full_name.py @@ -0,0 +1,93 @@ +""" +Tests for :attr:`.BaseDefinition.full_name`. + +There are three kinds of test: + +#. Test classes derived from :class:`MixinTestFullName`. + Child class defines :meth:`.get_definitions` to alter how + the api definition instance is created. + +#. :class:`TestFullDefinedName` is to test combination of + :attr:`.full_name` and :func:`.defined_names`. + +#. Misc single-function tests. + +""" + +import textwrap + +import jedi +from jedi import api_classes +from .base import TestBase + + +class MixinTestFullName(object): + + def get_definitions(self, source): + """ + Get definition objects of the variable at the end of `source`. + """ + raise NotImplementedError + + def check(self, source, desired): + definitions = self.get_definitions(textwrap.dedent(source)) + self.assertEqual(definitions[0].full_name, desired) + + def test_os_path_join(self): + self.check('import os; os.path.join', 'os.path.join') + + def test_builtin(self): + self.check('type', 'type') + + def test_from_import(self): + self.check('from os import path', 'os.path') + + +class TestFullNameWithGotoDefinitions(MixinTestFullName, TestBase): + + get_definitions = TestBase.goto_definitions + + def test_tuple_mapping(self): + self.check(""" + import re + any_re = re.compile('.*') + any_re""", 're.RegexObject') + + +class TestFullNameWithCompletions(MixinTestFullName, TestBase): + get_definitions = TestBase.completions + + +class TestFullDefinedName(TestBase): + + """ + Test combination of :attr:`.full_name` and :func:`.defined_names`. + """ + + def check(self, source, desired): + definitions = jedi.defined_names(textwrap.dedent(source)) + full_names = [d.full_name for d in definitions] + self.assertEqual(full_names, desired) + + def test_local_names(self): + self.check(""" + def f(): pass + class C: pass + """, ['f', 'C']) + + def test_imports(self): + self.check(""" + import os + from os import path + from os.path import join + from os import path as opath + """, ['os', 'os.path', 'os.path.join', 'os.path']) + + +def test_keyword_full_name_should_be_none(): + """issue #94""" + # Using `from jedi.keywords import Keyword` here does NOT work + # in Python 3. This is due to the import hack jedi using. + Keyword = api_classes.keywords.Keyword + d = api_classes.Definition(Keyword('(', (0, 0))) + assert d.full_name is None diff --git a/test/test_regression.py b/test/test_regression.py index 31aa058a..3753d0bf 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -14,9 +14,8 @@ import textwrap from .base import TestBase, unittest, cwd_at import jedi -from jedi._compatibility import utf8, unicode +from jedi._compatibility import utf8, unicode, is_py33 from jedi import api, parsing, common -api_classes = api.api_classes #jedi.set_debug_function(jedi.debug.print_to_stdout) @@ -314,14 +313,29 @@ class TestRegression(TestBase): types = [o.type for o in objs] assert 'import' not in types and 'class' in types - def test_keyword_definition_doc(self): + def test_keyword(self): """ github jedi-vim issue #44 """ defs = self.goto_definitions("print") assert [d.doc for d in defs] defs = self.goto_definitions("import") - assert len(defs) == 1 - assert [d.doc for d in defs] + assert len(defs) == 1 and [1 for d in defs if d.doc] + # unrelated to #44 + defs = self.goto_assignments("import") + assert len(defs) == 0 + completions = self.completions("import", (1,1)) + assert len(completions) == 0 + with common.ignored(jedi.NotFoundError): # TODO shouldn't throw that. + defs = self.goto_definitions("assert") + assert len(defs) == 1 + + def test_goto_assignments_keyword(self): + """ + Bug: goto assignments on ``in`` used to raise AttributeError:: + + 'unicode' object has no attribute 'generate_call_path' + """ + self.goto_assignments('in') def test_goto_following_on_imports(self): s = "import multiprocessing.dummy; multiprocessing.dummy" @@ -377,10 +391,47 @@ class TestRegression(TestBase): # jedi issue #150 s = "x()\nx( )\nx( )\nx ( )" parser = parsing.Parser(s) - for i, s in enumerate(parser.scope.statements, 3): + for i, s in enumerate(parser.module.statements, 3): for c in s.get_commands(): self.assertEqual(c.execution.end_pos[1], i) + def check_definition_by_marker(self, source, after_cursor, names): + r""" + Find definitions specified by `after_cursor` and check what found + + For example, for the following configuration, you can pass + ``after_cursor = 'y)'``.:: + + function( + x, y) + \ + `- You want cursor to be here + """ + source = textwrap.dedent(source) + for (i, line) in enumerate(source.splitlines()): + if after_cursor in line: + break + column = len(line) - len(after_cursor) + defs = self.goto_definitions(source, (i + 1, column)) + self.assertEqual([d.name for d in defs], names) + + def test_backslash_continuation(self): + """ + Test that ModuleWithCursor.get_path_until_cursor handles continuation + """ + self.check_definition_by_marker(r""" + x = 0 + a = \ + [1, 2, 3, 4, 5, 6, 7, 8, 9, x] # <-- here + """, '] # <-- here', ['int']) + + def test_backslash_continuation_and_bracket(self): + self.check_definition_by_marker(r""" + x = 0 + a = \ + [1, 2, 3, 4, 5, 6, 7, 8, 9, (x)] # <-- here + """, '(x)] # <-- here', [None]) + class TestDocstring(TestBase): @@ -410,28 +461,6 @@ class TestDocstring(TestBase): class TestFeature(TestBase): - def test_full_name(self): - """ feature request #61""" - assert self.completions('import os; os.path.join')[0].full_name \ - == 'os.path.join' - - def test_keyword_full_name_should_be_none(self): - """issue #94""" - # Using `from jedi.keywords import Keyword` here does NOT work - # in Python 3. This is due to the import hack jedi using. - Keyword = api_classes.keywords.Keyword - d = api_classes.Definition(Keyword('(', (0, 0))) - assert d.full_name is None - - def test_full_name_builtin(self): - self.assertEqual(self.completions('type')[0].full_name, 'type') - - def test_full_name_tuple_mapping(self): - s = """ - import re - any_re = re.compile('.*') - any_re""" - self.assertEqual(self.goto_definitions(s)[0].full_name, 're.RegexObject') def test_preload_modules(self): def check_loaded(*modules): @@ -453,81 +482,6 @@ class TestFeature(TestBase): cache.parser_cache = temp_cache - def test_quick_completion(self): - sources = [ - ('import json; json.l', (1, 19)), - ('import json; json.l ', (1, 19)), - ('import json\njson.l', (2, 6)), - ('import json\njson.l ', (2, 6)), - ('import json\njson.l\n\n', (2, 6)), - ('import json\njson.l \n\n', (2, 6)), - ('import json\njson.l \n \n\n', (2, 6)), - ] - for source, pos in sources: - # Run quick_complete - quick_completions = api._quick_complete(source) - # Run real completion - script = jedi.Script(source, pos[0], pos[1], '') - real_completions = script.completions() - # Compare results - quick_values = [(c.full_name, c.line, c.column) for c in quick_completions] - real_values = [(c.full_name, c.line, c.column) for c in real_completions] - self.assertEqual(quick_values, real_values) - - -class TestGetDefinitions(TestBase): - - def test_get_definitions_flat(self): - definitions = api.defined_names(""" - import module - class Class: - pass - def func(): - pass - data = None - """) - self.assertEqual([d.name for d in definitions], - ['module', 'Class', 'func', 'data']) - - def test_dotted_assignment(self): - definitions = api.defined_names(""" - x = Class() - x.y.z = None - """) - self.assertEqual([d.name for d in definitions], - ['x']) - - def test_multiple_assignment(self): - definitions = api.defined_names(""" - x = y = None - """) - self.assertEqual([d.name for d in definitions], - ['x', 'y']) - - def test_multiple_imports(self): - definitions = api.defined_names(""" - from module import a, b - from another_module import * - """) - self.assertEqual([d.name for d in definitions], - ['a', 'b']) - - def test_nested_definitions(self): - definitions = api.defined_names(""" - class Class: - def f(): - pass - def g(): - pass - """) - self.assertEqual([d.name for d in definitions], - ['Class']) - subdefinitions = definitions[0].defined_names() - self.assertEqual([d.name for d in subdefinitions], - ['f', 'g']) - self.assertEqual([d.full_name for d in subdefinitions], - ['Class.f', 'Class.g']) - class TestSpeed(TestBase): def _check_speed(time_per_run, number=4, run_warm=True): @@ -562,6 +516,44 @@ class TestSpeed(TestBase): #print(jedi.imports.imports_processed) +class TestInterpreterAPI(unittest.TestCase): + + def check_interpreter_complete(self, source, namespace, completions, + **kwds): + script = api.Interpreter(source, [namespace], **kwds) + cs = script.complete() + actual = [c.word for c in cs] + self.assertEqual(sorted(actual), sorted(completions)) + + def test_complete_raw_function(self): + from os.path import join + self.check_interpreter_complete('join().up', + locals(), + ['upper']) + + def test_complete_raw_function_different_name(self): + from os.path import join as pjoin + self.check_interpreter_complete('pjoin().up', + locals(), + ['upper']) + + def test_complete_raw_module(self): + import os + self.check_interpreter_complete('os.path.join().up', + locals(), + ['upper']) + + def test_complete_raw_instance(self): + import datetime + dt = datetime.datetime(2013, 1, 1) + completions = ['time', 'timetz', 'timetuple'] + if is_py33: + completions += ['timestamp'] + self.check_interpreter_complete('(dt - dt).ti', + locals(), + completions) + + def test_settings_module(): """ jedi.settings and jedi.cache.settings must be the same module. diff --git a/tox.ini b/tox.ini index c4d86518..8eb4e27c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,3 +16,8 @@ deps = commands = coverage run --source jedi -m py.test coverage report +[testenv:sith] +deps = + argparse +commands = + {envpython} sith.py --record {envtmpdir}/record.json random {posargs:jedi}