forked from VimPlug/jedi
Merge branch 'dev' of git://github.com/davidhalter/jedi
This commit is contained in:
@@ -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:
|
||||
|
||||
22
README.rst
22
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 <https://github.com/davidhalter/jedi-vim>`_
|
||||
- `Emacs-Plugin <https://github.com/tkf/emacs-jedi>`_
|
||||
- `Sublime-Plugin <https://github.com/svaiter/SublimeJEDI>`_
|
||||
- `wdb (web debugger) <https://github.com/Kozea/wdb>`_
|
||||
- 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
|
||||
<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
BIN
docs/_static/logo.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 25 KiB |
12
docs/conf.py
12
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,
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ Parser Representation (parser_representation.py)
|
||||
|
||||
.. automodule:: parsing_representation
|
||||
|
||||
Class inheritance diagram:
|
||||
|
||||
.. inheritance-diagram::
|
||||
SubModule
|
||||
Class
|
||||
|
||||
@@ -30,8 +30,15 @@ System-wide installation via a package manager
|
||||
Arch Linux
|
||||
~~~~~~~~~~
|
||||
|
||||
You can install jedi directly from AUR: `python-jedi at AUR
|
||||
<https://aur.archlinux.org/packages/python-jedi/>`__.
|
||||
You can install |jedi| directly from official AUR packages:
|
||||
|
||||
- `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
|
||||
<https://aur.archlinux.org/packages/vim-jedi/>`__.)
|
||||
@@ -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
|
||||
<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
|
||||
and type::
|
||||
|
||||
8
docs/docs/repl.rst
Normal file
8
docs/docs/repl.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
.. include:: ../global.rst
|
||||
|
||||
How to use Jedi from Python interpreter
|
||||
=======================================
|
||||
|
||||
.. automodule:: jedi.replstartup
|
||||
|
||||
.. autofunction:: jedi.utils.setup_readline
|
||||
@@ -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 <http://github.com/davidhalter/jedi-vim>`_
|
||||
- `Emacs <https://github.com/tkf/emacs-jedi>`_
|
||||
- `Sublime Text 2 <https://github.com/svaiter/SublimeJEDI>`_
|
||||
Vim:
|
||||
|
||||
- `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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
2
jedi/__main__.py
Normal file
2
jedi/__main__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from os import path
|
||||
print(path.join(path.dirname(path.abspath(__file__)), 'replstartup.py'))
|
||||
83
jedi/api.py
83
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
|
||||
[<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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
171
jedi/interpret.py
Normal file
171
jedi/interpret.py
Normal 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))
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
<SubModule: example.py@1-1>
|
||||
|
||||
@@ -248,14 +248,14 @@ class Scope(Simple, IsScope):
|
||||
... b = y
|
||||
... b.c = z
|
||||
... ''')
|
||||
>>> parser.scope.get_defined_names()
|
||||
>>> parser.module.get_defined_names()
|
||||
[<Name: a@2,0>, <Name: b@3,0>]
|
||||
|
||||
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()
|
||||
[<Name: a@2,0>, <Name: b@3,0>, <Name: b.c@4,0>]
|
||||
|
||||
"""
|
||||
|
||||
25
jedi/replstartup.py
Normal file
25
jedi/replstartup.py
Normal 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.
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
# ----------------
|
||||
|
||||
110
jedi/utils.py
Normal file
110
jedi/utils.py
Normal 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
311
sith.py
Executable 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()
|
||||
@@ -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
|
||||
# -----------------
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
75
test/test_defined_names.py
Normal file
75
test/test_defined_names.py
Normal 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
93
test/test_full_name.py
Normal 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
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user