diff --git a/docs/conf.py b/docs/conf.py index fe93a75e..d7a2815b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -266,6 +266,7 @@ todo_include_todos = False # -- Options for autodoc module ------------------------------------------------ +autoclass_content = 'both' autodoc_member_order = 'bysource' autodoc_default_flags = [] #autodoc_default_flags = ['members', 'undoc-members'] diff --git a/jedi/__init__.py b/jedi/__init__.py index feff0bba..0c29bca1 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -42,7 +42,7 @@ 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, \ +from .api import Script, Interpreter, NotFoundError, set_debug_function, \ preload_module from . import settings diff --git a/jedi/__main__.py b/jedi/__main__.py new file mode 100644 index 00000000..73cde226 --- /dev/null +++ b/jedi/__main__.py @@ -0,0 +1,2 @@ +from os import path +print(path.join(path.dirname(path.abspath(__file__)), 'replstartup.py')) diff --git a/jedi/api.py b/jedi/api.py index edae10ca..e148f350 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -20,6 +20,7 @@ from jedi import helpers from jedi import common from jedi import cache from jedi import modules +from jedi import interpret from jedi._compatibility import next, unicode import evaluate import keywords @@ -53,14 +54,18 @@ class Script(object): ``unicode`` object (default ``'utf-8'``). :type source_encoding: str """ - def __init__(self, source, line, column, source_path, - source_encoding='utf-8'): + def __init__(self, source, line=None, column=None, source_path=None, + source_encoding='utf-8'): + lines = source.splitlines() + line = len(lines) if line is None else line + column = len(lines[-1]) if column is None else column + api_classes._clear_caches() debug.reset_time() self.source = modules.source_to_unicode(source, source_encoding) self.pos = line, column - self._module = modules.ModuleWithCursor(source_path, - source=self.source, position=self.pos) + self._module = modules.ModuleWithCursor( + source_path, source=self.source, position=self.pos) self._source_path = source_path self.source_path = None if source_path is None \ else os.path.abspath(source_path) @@ -503,6 +508,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. @@ -545,25 +589,3 @@ def set_debug_function(func_cb=debug.print_to_stdout, warnings=True, debug.enable_warning = warnings debug.enable_notice = notices debug.enable_speed = speed - - -def _quick_complete(source): - """ - Convenience function to complete a source string at the end. - - Example: - - >>> _quick_complete(''' - ... import datetime - ... datetime.da''') #doctest: +ELLIPSIS - [, , ...] - - :param source: The source code to be completed. - :type source: string - :return: Completion objects as returned by :meth:`complete`. - :rtype: list of :class:`api_classes.Completion` - """ - lines = re.sub(r'[\n\r\s]*$', '', source).splitlines() - pos = len(lines), len(lines[-1]) - script = Script(source, pos[0], pos[1], '') - return script.completions() diff --git a/jedi/interpret.py b/jedi/interpret.py new file mode 100644 index 00000000..6792369c --- /dev/null +++ b/jedi/interpret.py @@ -0,0 +1,171 @@ +""" +Module to handle interpreted Python objects. +""" + +import itertools +import tokenize + +from jedi import parsing_representation as pr + + +class ObjectImporter(object): + + """ + Import objects in "raw" namespace such as :func:`locals`. + """ + + def __init__(self, scope): + self.scope = scope + + count = itertools.count() + self._genname = lambda: '*jedi-%s*' % next(count) + """ + Generate unique variable names to avoid name collision. + To avoid name collision to already defined names, generated + names are invalid as Python identifier. + """ + + def import_raw_namespace(self, raw_namespace): + """ + Import interpreted Python objects in a namespace. + + Three kinds of objects are treated here. + + 1. Functions and classes. The objects imported like this:: + + from os.path import join + + 2. Modules. The objects imported like this:: + + import os + + 3. Instances. The objects created like this:: + + from datetime import datetime + dt = datetime(2013, 1, 1) + + :type raw_namespace: dict + :arg raw_namespace: e.g., the dict given by `locals` + """ + scope = self.scope + for (variable, obj) in raw_namespace.items(): + objname = getattr(obj, '__name__', None) + + # Import functions and classes + module = getattr(obj, '__module__', None) + if module and objname: + fakeimport = self.make_fakeimport(module, objname, variable) + scope.add_import(fakeimport) + continue + + # Import modules + if getattr(obj, '__file__', None) and objname: + fakeimport = self.make_fakeimport(objname) + scope.add_import(fakeimport) + continue + + # Import instances + objclass = getattr(obj, '__class__', None) + module = getattr(objclass, '__module__', None) + if objclass and module: + alias = self._genname() + fakeimport = self.make_fakeimport(module, objclass.__name__, + alias) + fakestmt = self.make_fakestatement(variable, alias, call=True) + scope.add_import(fakeimport) + scope.add_statement(fakestmt) + continue + + def make_fakeimport(self, module, variable=None, alias=None): + """ + Make a fake import object. + + The following statements are created depending on what parameters + are given: + + - only `module`: ``import `` + - `module` and `variable`: ``from import `` + - all: ``from import as `` + + :type module: str + :arg module: ```` part in ``from import ...`` + :type variable: str + :arg variable: ```` part in ``from ... import `` + :type alias: str + :arg alias: ```` part in ``... import ... as ``. + + :rtype: :class:`parsing_representation.Import` + """ + submodule = self.scope._sub_module + if variable: + varname = pr.Name( + module=submodule, + names=[(variable, (-1, 0))], + start_pos=(-1, 0), + end_pos=(None, None)) + else: + varname = None + modname = pr.Name( + module=submodule, + names=[(module, (-1, 0))], + start_pos=(-1, 0), + end_pos=(None, None)) + if alias: + aliasname = pr.Name( + module=submodule, + names=[(alias, (-1, 0))], + start_pos=(-1, 0), + end_pos=(None, None)) + else: + aliasname = None + if varname: + fakeimport = pr.Import( + module=submodule, + namespace=varname, + from_ns=modname, + alias=aliasname, + start_pos=(-1, 0), + end_pos=(None, None)) + else: + fakeimport = pr.Import( + module=submodule, + namespace=modname, + alias=aliasname, + start_pos=(-1, 0), + end_pos=(None, None)) + return fakeimport + + def make_fakestatement(self, lhs, rhs, call=False): + """ + Make a fake statement object that represents ``lhs = rhs``. + + :type call: bool + :arg call: When `call` is true, make a fake statement that represents + ``lhs = rhs()``. + + :rtype: :class:`parsing_representation.Statement` + """ + submodule = self.scope._sub_module + lhsname = pr.Name( + module=submodule, + names=[(lhs, (0, 0))], + start_pos=(0, 0), + end_pos=(None, None)) + rhsname = pr.Name( + module=submodule, + names=[(rhs, (0, 0))], + start_pos=(0, 0), + end_pos=(None, None)) + token_list = [lhsname, (tokenize.OP, '=', (0, 0)), rhsname] + if call: + token_list.extend([ + (tokenize.OP, '(', (0, 0)), + (tokenize.OP, ')', (0, 0)), + ]) + return pr.Statement( + module=submodule, + set_vars=[lhsname], + used_vars=[rhsname], + token_list=token_list, + start_pos=(0, 0), + end_pos=(None, None)) diff --git a/jedi/replstartup.py b/jedi/replstartup.py new file mode 100644 index 00000000..40ee77da --- /dev/null +++ b/jedi/replstartup.py @@ -0,0 +1,25 @@ +""" +``PYTHONSTARTUP`` to use Jedi in your Python interpreter. + +To use Jedi completion in Python interpreter, add the following in your shell +setup (e.g., ``.bashrc``):: + + export PYTHONSTARTUP="$(python -m jedi)" + +Then you will be able to use Jedi completer in your Python interpreter:: + + $ python + Python 2.7.2+ (default, Jul 20 2012, 22:15:08) + [GCC 4.6.1] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> import os + >>> os.path.join().split().in # doctest: +SKIP + os.path.join().split().index os.path.join().split().insert + +""" + +import jedi.utils +jedi.utils.setup_readline() +del jedi +# Note: try not to do many things here, as it will contaminate global +# namespace of the interpreter. diff --git a/jedi/utils.py b/jedi/utils.py new file mode 100644 index 00000000..eb617fe7 --- /dev/null +++ b/jedi/utils.py @@ -0,0 +1,41 @@ +""" +Utilities for end-users. +""" + +import sys + +from jedi import Interpreter + + +def readline_complete(text, state): + """ + Function to be passed to :func:`readline.set_completer`. + + Usage:: + + import readline + readline.set_completer(readline_complete) + + """ + ns = vars(sys.modules['__main__']) + completions = Interpreter(text, [ns]).completions() + try: + return text + completions[state].complete + except IndexError: + return None + + +def setup_readline(): + """ + Install Jedi completer to :mod:`readline`. + """ + try: + import readline + except ImportError: + print("Module readline not available.") + else: + readline.set_completer(readline_complete) + readline.parse_and_bind("tab: complete") + + # Default delimiters minus "()": + readline.set_completer_delims(' \t\n`~!@#$%^&*-=+[{]}\\|;:\'",<>/?') diff --git a/test/test_regression.py b/test/test_regression.py index 31aa058a..299e2357 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -14,7 +14,7 @@ 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 @@ -562,6 +562,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.