1
0
forked from VimPlug/jedi

Merge branch 'dev' of git://github.com/davidhalter/jedi

This commit is contained in:
Laurens Van Houtven
2013-06-23 22:50:00 +02:00
28 changed files with 1124 additions and 201 deletions

View File

@@ -5,9 +5,11 @@ env:
- TOXENV=py32 - TOXENV=py32
- TOXENV=py33 - TOXENV=py33
- TOXENV=cov - TOXENV=cov
- TOXENV=sith
matrix: matrix:
allow_failures: allow_failures:
- env: TOXENV=cov - env: TOXENV=cov
- env: TOXENV=sith
install: install:
- pip install --quiet --use-mirrors tox - pip install --quiet --use-mirrors tox
script: script:

View File

@@ -10,6 +10,8 @@ Jedi - an awesome autocompletion library for Python
:target: https://coveralls.io/r/davidhalter/jedi :target: https://coveralls.io/r/davidhalter/jedi
:alt: Coverage Status :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 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 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 It's really easy. If there are any problems (also with licensing), just contact
me. me.
Jedi can be used with the following plugins/software: Jedi can be used with the following editors:
- `VIM-Plugin <https://github.com/davidhalter/jedi-vim>`_ - Vim (jedi-vim_, YouCompleteMe_)
- `Emacs-Plugin <https://github.com/tkf/emacs-jedi>`_ - Emacs (Jedi.el_)
- `Sublime-Plugin <https://github.com/svaiter/SublimeJEDI>`_ - Sublime Text (SublimeJEDI_)
- `wdb (web debugger) <https://github.com/Kozea/wdb>`_
And it powers the following projects:
- wdb_
Here are some pictures: Here are some pictures:
@@ -121,3 +126,10 @@ Tests are also run automatically on `Travis CI
For more detailed information visit the `testing documentation For more detailed information visit the `testing documentation
<https://jedi.readthedocs.org/en/latest/docs/testing.html>`_ <https://jedi.readthedocs.org/en/latest/docs/testing.html>`_
.. _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

BIN
docs/_static/logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -28,7 +28,7 @@ sys.path.append(os.path.abspath('_themes'))
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.todo', 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. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@@ -141,7 +141,7 @@ html_static_path = ['_static']
# Custom sidebar templates, maps document names to template names. # Custom sidebar templates, maps document names to template names.
html_sidebars = { html_sidebars = {
'**': [ '**': [
# 'sidebarlogo.html', 'sidebarlogo.html',
'localtoc.html', 'localtoc.html',
# 'relations.html', # 'relations.html',
'ghbuttons.html', 'ghbuttons.html',
@@ -266,6 +266,14 @@ todo_include_todos = False
# -- Options for autodoc module ------------------------------------------------ # -- Options for autodoc module ------------------------------------------------
autoclass_content = 'both'
autodoc_member_order = 'bysource' autodoc_member_order = 'bysource'
autodoc_default_flags = [] autodoc_default_flags = []
#autodoc_default_flags = ['members', 'undoc-members'] #autodoc_default_flags = ['members', 'undoc-members']
# -- Options for intersphinx module --------------------------------------------
intersphinx_mapping = {
'http://docs.python.org/': None,
}

View File

@@ -64,6 +64,8 @@ Parser Representation (parser_representation.py)
.. automodule:: parsing_representation .. automodule:: parsing_representation
Class inheritance diagram:
.. inheritance-diagram:: .. inheritance-diagram::
SubModule SubModule
Class Class

View File

@@ -30,8 +30,15 @@ System-wide installation via a package manager
Arch Linux Arch Linux
~~~~~~~~~~ ~~~~~~~~~~
You can install jedi directly from AUR: `python-jedi at AUR You can install |jedi| directly from official AUR packages:
<https://aur.archlinux.org/packages/python-jedi/>`__.
- `python-jedi <https://aur.archlinux.org/packages/python-jedi/>`__ (Python 3)
- `python2-jedi <https://aur.archlinux.org/packages/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 (There is also a packaged version of the vim plugin available: `vim-jedi at AUR
<https://aur.archlinux.org/packages/vim-jedi/>`__.) <https://aur.archlinux.org/packages/vim-jedi/>`__.)
@@ -45,7 +52,7 @@ Debian packages are available as `experimental packages
Others 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 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 If you prefer not to use an automated package installer, you can `download
<https://github.com/davidhalter/jedi/archive/master.zip>`__ a current copy of <https://github.com/davidhalter/jedi/archive/master.zip>`__ 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 To install it, navigate to the directory containing `setup.py` on your console
and type:: and type::

8
docs/docs/repl.rst Normal file
View File

@@ -0,0 +1,8 @@
.. include:: ../global.rst
How to use Jedi from Python interpreter
=======================================
.. automodule:: jedi.replstartup
.. autofunction:: jedi.utils.setup_readline

View File

@@ -22,6 +22,7 @@ Docs
docs/installation docs/installation
docs/features docs/features
docs/repl
docs/recipes docs/recipes
docs/plugin-api docs/plugin-api
docs/history docs/history
@@ -44,9 +45,18 @@ Resources
Editor Plugins Editor Plugins
-------------- --------------
- `Vim <http://github.com/davidhalter/jedi-vim>`_ Vim:
- `Emacs <https://github.com/tkf/emacs-jedi>`_
- `Sublime Text 2 <https://github.com/svaiter/SublimeJEDI>`_ - `jedi-vim <http://github.com/davidhalter/jedi-vim>`_
- `YouCompleteMe <http://valloric.github.io/YouCompleteMe/>`_
Emacs:
- `Jedi.el <https://github.com/tkf/emacs-jedi>`_
Sublime Text 2:
- `SublimeJEDI <https://github.com/svaiter/SublimeJEDI>`_
.. _other-software: .. _other-software:

View File

@@ -42,8 +42,8 @@ import sys
# imports and circular imports... Just avoid it: # imports and circular imports... Just avoid it:
sys.path.insert(0, __path__[0]) sys.path.insert(0, __path__[0])
from .api import Script, NotFoundError, set_debug_function, _quick_complete, \ from .api import Script, Interpreter, NotFoundError, set_debug_function, \
preload_module preload_module, defined_names
from . import settings from . import settings
sys.path.pop(0) sys.path.pop(0)

2
jedi/__main__.py Normal file
View File

@@ -0,0 +1,2 @@
from os import path
print(path.join(path.dirname(path.abspath(__file__)), 'replstartup.py'))

View File

@@ -20,6 +20,7 @@ from jedi import helpers
from jedi import common from jedi import common
from jedi import cache from jedi import cache
from jedi import modules from jedi import modules
from jedi import interpret
from jedi._compatibility import next, unicode from jedi._compatibility import next, unicode
import evaluate import evaluate
import keywords import keywords
@@ -53,14 +54,18 @@ class Script(object):
``unicode`` object (default ``'utf-8'``). ``unicode`` object (default ``'utf-8'``).
:type source_encoding: str :type source_encoding: str
""" """
def __init__(self, source, line, column, source_path, def __init__(self, source, line=None, column=None, source_path=None,
source_encoding='utf-8'): 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() api_classes._clear_caches()
debug.reset_time() debug.reset_time()
self.source = modules.source_to_unicode(source, source_encoding) self.source = modules.source_to_unicode(source, source_encoding)
self.pos = line, column self.pos = line, column
self._module = modules.ModuleWithCursor(source_path, self._module = modules.ModuleWithCursor(
source=self.source, position=self.pos) source_path, source=self.source, position=self.pos)
self._source_path = source_path self._source_path = source_path
self.source_path = None if source_path is None \ self.source_path = None if source_path is None \
else os.path.abspath(source_path) else os.path.abspath(source_path)
@@ -337,7 +342,8 @@ class Script(object):
:rtype: list of :class:`api_classes.Definition` :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) return self._sorted_defs(d)
def _goto(self, add_import_name=False): def _goto(self, add_import_name=False):
@@ -385,7 +391,9 @@ class Script(object):
defs, search_name = evaluate.goto(stmt) defs, search_name = evaluate.goto(stmt)
definitions = follow_inexistent_imports(defs) definitions = follow_inexistent_imports(defs)
if isinstance(user_stmt, pr.Statement): 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 # The cursor must be after the start, otherwise the
# statement is just an assignee. # statement is just an assignee.
definitions = [user_stmt] definitions = [user_stmt]
@@ -503,6 +511,45 @@ class Script(object):
return sorted(d, key=lambda x: (x.module_path or '', x.start_pos)) 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'): def defined_names(source, source_path=None, source_encoding='utf-8'):
""" """
Get all definitions in `source` sorted by its position. 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), modules.source_to_unicode(source, source_encoding),
module_path=source_path, module_path=source_path,
) )
return api_classes._defined_names(parser.scope) return api_classes._defined_names(parser.module)
def preload_module(*modules): 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_warning = warnings
debug.enable_notice = notices debug.enable_notice = notices
debug.enable_speed = speed 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
[<Completion: date>, <Completion: datetime>, ...]
: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()

View File

@@ -142,9 +142,19 @@ class BaseDefinition(object):
def path(self): def path(self):
"""The module path.""" """The module path."""
path = [] path = []
def insert_nonnone(x):
if x:
path.insert(0, x)
if not isinstance(self._definition, keywords.Keyword): if not isinstance(self._definition, keywords.Keyword):
par = self._definition par = self._definition
while par is not None: 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): with common.ignored(AttributeError):
path.insert(0, par.name) path.insert(0, par.name)
par = par.parent par = par.parent

View File

@@ -318,7 +318,9 @@ def find_name(scope, name_str, position=None, search_global=False,
exc = pr.Class, pr.Function exc = pr.Class, pr.Function
until = lambda: par.parent.parent.get_parent_until(exc) 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': if par.command == 'for':
result += handle_for_loops(par) result += handle_for_loops(par)
else: else:
@@ -554,16 +556,33 @@ def assign_tuples(tup, results, seek_name):
else: else:
r = eval_results(i) r = eval_results(i)
# are there still tuples or is it just a Call. # LHS of tuples can be nested, so resolve it recursively
if isinstance(command, pr.Array): result += find_assignments(command, r, seek_name)
# These are "sub"-tuples.
result += assign_tuples(command, r, seek_name)
else:
if command.name.names[-1] == seek_name:
result += r
return result 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 @recursion.RecursionDecorator
@cache.memoize_default(default=()) @cache.memoize_default(default=())
def follow_statement(stmt, seek_name=None): 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: if len(stmt.get_set_vars()) > 1 and seek_name and stmt.assignment_details:
new_result = [] new_result = []
for ass_commands, op in stmt.assignment_details: 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 result = new_result
return set(result) return set(result)
@@ -775,6 +794,9 @@ def goto(stmt, call_path=None):
commands = stmt.get_commands() commands = stmt.get_commands()
assert len(commands) == 1 assert len(commands) == 1
call = commands[0] call = commands[0]
if isinstance(call, (str, unicode)):
call_path = [call]
else:
call_path = list(call.generate_call_path()) call_path = list(call.generate_call_path())
scope = stmt.get_parent_until(pr.IsScope) scope = stmt.get_parent_until(pr.IsScope)

View File

@@ -162,9 +162,9 @@ class ImportPath(pr.Base):
# If you edit e.g. gunicorn, there will be imports like this: # If you edit e.g. gunicorn, there will be imports like this:
# `from gunicorn import something`. But gunicorn is not in the # `from gunicorn import something`. But gunicorn is not in the
# sys.path. Therefore look if gunicorn is a parent directory, #56. # sys.path. Therefore look if gunicorn is a parent directory, #56.
parts = self.file_path.split(os.path.sep)
in_path = [] in_path = []
if self.import_path: if self.import_path:
parts = self.file_path.split(os.path.sep)
for i, p in enumerate(parts): for i, p in enumerate(parts):
if p == self.import_path[0]: if p == self.import_path[0]:
new = os.path.sep.join(parts[:i]) new = os.path.sep.join(parts[:i])

171
jedi/interpret.py Normal file
View File

@@ -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>``
- `module` and `variable`: ``from <module> import <variable>``
- all: ``from <module> import <variable> as <alias>``
:type module: str
:arg module: ``<module>`` part in ``from <module> import ...``
:type variable: str
:arg variable: ``<variable>`` part in ``from ... import <variable>``
:type alias: str
:arg alias: ``<alias>`` part in ``... import ... as <alias>``.
: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))

View File

@@ -141,6 +141,7 @@ class ModuleWithCursor(Module):
last_line = self.get_line(self._line_temp) last_line = self.get_line(self._line_temp)
if last_line and last_line[-1] == '\\': if last_line and last_line[-1] == '\\':
line = last_line[:-1] + ' ' + line line = last_line[:-1] + ' ' + line
self._line_length = len(last_line)
else: else:
break break
return line[::-1] return line[::-1]
@@ -187,6 +188,7 @@ class ModuleWithCursor(Module):
elif token_type == tokenize.NUMBER: elif token_type == tokenize.NUMBER:
pass pass
else: else:
self._column_temp = self._line_length - end[1]
break break
self._column_temp = self._line_length - end[1] self._column_temp = self._line_length - end[1]

View File

@@ -56,7 +56,7 @@ class Parser(object):
self.start_pos = self.end_pos = 1 + offset[0], offset[1] self.start_pos = self.end_pos = 1 + offset[0], offset[1]
# initialize global Scope # initialize global Scope
self.module = pr.SubModule(module_path, self.start_pos, top_module) self.module = pr.SubModule(module_path, self.start_pos, top_module)
self.scope = self.module self._scope = self.module
self.current = (None, None) self.current = (None, None)
source = source + '\n' # end with \n, because the parser needs it 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 #print 'new_stat', set_vars, used_vars
if self.freshscope and not self.no_docstr and len(tok_list) == 1 \ if self.freshscope and not self.no_docstr and len(tok_list) == 1 \
and self.last_token[0] == tokenize.STRING: 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 return None, tok
else: else:
stmt = stmt_class(self.module, set_vars, used_vars, tok_list, 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 len(stmt.token_list) == 1
and first_tok[0] == tokenize.STRING): and first_tok[0] == tokenize.STRING):
# ... then set it as a docstring # ... 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: if tok in always_break + not_first_break:
self._gen.push_last_back() self._gen.push_last_back()
@@ -429,7 +429,7 @@ class Parser(object):
self.start_pos, self.end_pos = start_pos, end_pos self.start_pos, self.end_pos = start_pos, end_pos
except (StopIteration, common.MultiLevelStopIteration): except (StopIteration, common.MultiLevelStopIteration):
# on finish, set end_pos correctly # on finish, set end_pos correctly
s = self.scope s = self._scope
while s is not None: while s is not None:
if isinstance(s, pr.Module) \ if isinstance(s, pr.Module) \
and not isinstance(s, pr.SubModule): and not isinstance(s, pr.SubModule):
@@ -443,8 +443,8 @@ class Parser(object):
or self.user_scope is None or self.user_scope is None
and self.start_pos[0] >= self.user_position[0]): and self.start_pos[0] >= self.user_position[0]):
debug.dbg('user scope found [%s] = %s' % debug.dbg('user scope found [%s] = %s' %
(self.parserline.replace('\n', ''), repr(self.scope))) (self.parserline.replace('\n', ''), repr(self._scope)))
self.user_scope = self.scope self.user_scope = self._scope
self.last_token = self.current self.last_token = self.current
self.current = (typ, tok) self.current = (typ, tok)
return self.current return self.current
@@ -472,29 +472,29 @@ class Parser(object):
#debug.dbg('main: tok=[%s] type=[%s] indent=[%s]'\ #debug.dbg('main: tok=[%s] type=[%s] indent=[%s]'\
# % (tok, tokenize.tok_name[token_type], start_position[0])) # % (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() token_type, tok = self.next()
if self.start_pos[1] <= self.scope.start_pos[1]: if self.start_pos[1] <= self._scope.start_pos[1]:
self.scope.end_pos = self.start_pos self._scope.end_pos = self.start_pos
self.scope = self.scope.parent self._scope = self._scope.parent
if isinstance(self.scope, pr.Module) \ if isinstance(self._scope, pr.Module) \
and not isinstance(self.scope, pr.SubModule): and not isinstance(self._scope, pr.SubModule):
self.scope = self.module self._scope = self.module
# check again for unindented stuff. this is true for syntax # check again for unindented stuff. this is true for syntax
# errors. only check for names, because thats relevant here. If # errors. only check for names, because thats relevant here. If
# some docstrings are not indented, I don't care. # 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 (token_type == tokenize.NAME or tok in ['(', '['])\
and self.scope != self.module: and self._scope != self.module:
self.scope.end_pos = self.start_pos self._scope.end_pos = self.start_pos
self.scope = self.scope.parent self._scope = self._scope.parent
if isinstance(self.scope, pr.Module) \ if isinstance(self._scope, pr.Module) \
and not isinstance(self.scope, pr.SubModule): and not isinstance(self._scope, pr.SubModule):
self.scope = self.module self._scope = self.module
use_as_parent_scope = self.top_module if isinstance(self.scope, use_as_parent_scope = self.top_module if isinstance(self._scope,
pr.SubModule) else self.scope pr.SubModule) else self._scope
first_pos = self.start_pos first_pos = self.start_pos
if tok == 'def': if tok == 'def':
func = self._parse_function() func = self._parse_function()
@@ -503,7 +503,7 @@ class Parser(object):
self.start_pos[0]) self.start_pos[0])
continue continue
self.freshscope = True self.freshscope = True
self.scope = self.scope.add_scope(func, self._decorators) self._scope = self._scope.add_scope(func, self._decorators)
self._decorators = [] self._decorators = []
elif tok == 'class': elif tok == 'class':
cls = self._parse_class() cls = self._parse_class()
@@ -511,7 +511,7 @@ class Parser(object):
debug.warning("class: syntax error@%s" % self.start_pos[0]) debug.warning("class: syntax error@%s" % self.start_pos[0])
continue continue
self.freshscope = True self.freshscope = True
self.scope = self.scope.add_scope(cls, self._decorators) self._scope = self._scope.add_scope(cls, self._decorators)
self._decorators = [] self._decorators = []
# import stuff # import stuff
elif tok == 'import': elif tok == 'import':
@@ -522,7 +522,7 @@ class Parser(object):
i = pr.Import(self.module, first_pos, end_pos, m, i = pr.Import(self.module, first_pos, end_pos, m,
alias, defunct=defunct) alias, defunct=defunct)
self._check_user_stmt(i) self._check_user_stmt(i)
self.scope.add_import(i) self._scope.add_import(i)
if not imports: if not imports:
i = pr.Import(self.module, first_pos, self.end_pos, None, i = pr.Import(self.module, first_pos, self.end_pos, None,
defunct=True) defunct=True)
@@ -559,7 +559,7 @@ class Parser(object):
alias, mod, star, relative_count, alias, mod, star, relative_count,
defunct=defunct or defunct2) defunct=defunct or defunct2)
self._check_user_stmt(i) self._check_user_stmt(i)
self.scope.add_import(i) self._scope.add_import(i)
self.freshscope = False self.freshscope = False
#loops #loops
elif tok == 'for': elif tok == 'for':
@@ -569,7 +569,7 @@ class Parser(object):
if tok == ':': if tok == ':':
s = [] if statement is None else [statement] s = [] if statement is None else [statement]
f = pr.ForFlow(self.module, s, first_pos, set_stmt) 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: else:
debug.warning('syntax err, for flow started @%s', debug.warning('syntax err, for flow started @%s',
self.start_pos[0]) self.start_pos[0])
@@ -612,13 +612,13 @@ class Parser(object):
# the flow statement, because a dedent releases the # the flow statement, because a dedent releases the
# main scope, so just take the last statement. # main scope, so just take the last statement.
try: try:
s = self.scope.statements[-1].set_next(f) s = self._scope.statements[-1].set_next(f)
except (AttributeError, IndexError): except (AttributeError, IndexError):
# If set_next doesn't exist, just add it. # If set_next doesn't exist, just add it.
s = self.scope.add_statement(f) s = self._scope.add_statement(f)
else: else:
s = self.scope.add_statement(f) s = self._scope.add_statement(f)
self.scope = s self._scope = s
else: else:
for i in inputs: for i in inputs:
i.parent = use_as_parent_scope i.parent = use_as_parent_scope
@@ -629,7 +629,7 @@ class Parser(object):
s = self.start_pos s = self.start_pos
self.freshscope = False self.freshscope = False
# add returns to the scope # add returns to the scope
func = self.scope.get_parent_until(pr.Function) func = self._scope.get_parent_until(pr.Function)
if tok == 'yield': if tok == 'yield':
func.is_generator = True func.is_generator = True
@@ -646,7 +646,7 @@ class Parser(object):
elif tok == 'global': elif tok == 'global':
stmt, tok = self._parse_statement(self.current) stmt, tok = self._parse_statement(self.current)
if stmt: if stmt:
self.scope.add_statement(stmt) self._scope.add_statement(stmt)
for name in stmt.used_vars: for name in stmt.used_vars:
# add the global to the top, because there it is # add the global to the top, because there it is
# important. # important.
@@ -659,8 +659,9 @@ class Parser(object):
continue continue
elif tok == 'assert': elif tok == 'assert':
stmt, tok = self._parse_statement() stmt, tok = self._parse_statement()
if stmt is not None:
stmt.parent = use_as_parent_scope stmt.parent = use_as_parent_scope
self.scope.asserts.append(stmt) self._scope.asserts.append(stmt)
# default # default
elif token_type in [tokenize.NAME, tokenize.STRING, elif token_type in [tokenize.NAME, tokenize.STRING,
tokenize.NUMBER] \ tokenize.NUMBER] \
@@ -670,7 +671,7 @@ class Parser(object):
# by the statement parser. # by the statement parser.
stmt, tok = self._parse_statement(self.current) stmt, tok = self._parse_statement(self.current)
if stmt: if stmt:
self.scope.add_statement(stmt) self._scope.add_statement(stmt)
self.freshscope = False self.freshscope = False
else: else:
if token_type not in [tokenize.COMMENT, tokenize.INDENT, if token_type not in [tokenize.COMMENT, tokenize.INDENT,

View File

@@ -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. which is being used in a function definition.
The easiest way to play with this module is to use :class:`parsing.Parser`. 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 >>> from jedi.parsing import Parser
>>> parser = Parser('import os', 'example.py') >>> parser = Parser('import os', 'example.py')
>>> submodule = parser.scope >>> submodule = parser.module
>>> submodule >>> submodule
<SubModule: example.py@1-1> <SubModule: example.py@1-1>
@@ -248,14 +248,14 @@ class Scope(Simple, IsScope):
... b = y ... b = y
... b.c = z ... b.c = z
... ''') ... ''')
>>> parser.scope.get_defined_names() >>> parser.module.get_defined_names()
[<Name: a@2,0>, <Name: b@3,0>] [<Name: a@2,0>, <Name: b@3,0>]
Note that unlike :meth:`get_set_vars`, assignment to object Note that unlike :meth:`get_set_vars`, assignment to object
attribute does not change the result because it does not change attribute does not change the result because it does not change
the defined names in this scope. the defined names in this scope.
>>> parser.scope.get_set_vars() >>> parser.module.get_set_vars()
[<Name: a@2,0>, <Name: b@3,0>, <Name: b.c@4,0>] [<Name: a@2,0>, <Name: b@3,0>, <Name: b.c@4,0>]
""" """

25
jedi/replstartup.py Normal file
View File

@@ -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<TAB> # 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.

View File

@@ -127,8 +127,10 @@ cache_directory = os.path.expanduser(_cache_directory)
""" """
The path where all the caches can be found. The path where all the caches can be found.
On Linux, this defaults to ``~/.cache/jedi/``, on OS X to ``~/.jedi/`` and on On Linux, this defaults to ``~/.cache/jedi/``, on OS X to
Windows to ``%APPDATA%\\Jedi\\Jedi\\``. ``~/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.
""" """
# ---------------- # ----------------

110
jedi/utils.py Normal file
View File

@@ -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)

311
sith.py Executable file
View File

@@ -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().<method>() against files under <rootpath>.
"""
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()

View File

@@ -94,6 +94,28 @@ a4
b4 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 # unnessecary braces
# ----------------- # -----------------

View File

@@ -3,7 +3,13 @@ import textwrap
import pytest import pytest
from jedi import api 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(): def make_definitions():
""" """

View File

@@ -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'])

93
test/test_full_name.py Normal file
View File

@@ -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

View File

@@ -14,9 +14,8 @@ import textwrap
from .base import TestBase, unittest, cwd_at from .base import TestBase, unittest, cwd_at
import jedi import jedi
from jedi._compatibility import utf8, unicode from jedi._compatibility import utf8, unicode, is_py33
from jedi import api, parsing, common from jedi import api, parsing, common
api_classes = api.api_classes
#jedi.set_debug_function(jedi.debug.print_to_stdout) #jedi.set_debug_function(jedi.debug.print_to_stdout)
@@ -314,14 +313,29 @@ class TestRegression(TestBase):
types = [o.type for o in objs] types = [o.type for o in objs]
assert 'import' not in types and 'class' in types assert 'import' not in types and 'class' in types
def test_keyword_definition_doc(self): def test_keyword(self):
""" github jedi-vim issue #44 """ """ github jedi-vim issue #44 """
defs = self.goto_definitions("print") defs = self.goto_definitions("print")
assert [d.doc for d in defs] assert [d.doc for d in defs]
defs = self.goto_definitions("import") defs = self.goto_definitions("import")
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 assert len(defs) == 1
assert [d.doc for d in defs]
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): def test_goto_following_on_imports(self):
s = "import multiprocessing.dummy; multiprocessing.dummy" s = "import multiprocessing.dummy; multiprocessing.dummy"
@@ -377,10 +391,47 @@ class TestRegression(TestBase):
# jedi issue #150 # jedi issue #150
s = "x()\nx( )\nx( )\nx ( )" s = "x()\nx( )\nx( )\nx ( )"
parser = parsing.Parser(s) 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(): for c in s.get_commands():
self.assertEqual(c.execution.end_pos[1], i) 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): class TestDocstring(TestBase):
@@ -410,28 +461,6 @@ class TestDocstring(TestBase):
class TestFeature(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 test_preload_modules(self):
def check_loaded(*modules): def check_loaded(*modules):
@@ -453,81 +482,6 @@ class TestFeature(TestBase):
cache.parser_cache = temp_cache 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): class TestSpeed(TestBase):
def _check_speed(time_per_run, number=4, run_warm=True): def _check_speed(time_per_run, number=4, run_warm=True):
@@ -562,6 +516,44 @@ class TestSpeed(TestBase):
#print(jedi.imports.imports_processed) #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(): def test_settings_module():
""" """
jedi.settings and jedi.cache.settings must be the same module. jedi.settings and jedi.cache.settings must be the same module.

View File

@@ -16,3 +16,8 @@ deps =
commands = commands =
coverage run --source jedi -m py.test coverage run --source jedi -m py.test
coverage report coverage report
[testenv:sith]
deps =
argparse
commands =
{envpython} sith.py --record {envtmpdir}/record.json random {posargs:jedi}