From 842fbfce8aec6003387ef2b5bb2c159f5adb40ae Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Mon, 25 Feb 2013 21:55:20 +0100 Subject: [PATCH 01/97] Add api.Interpreter: Jedi API for Python REPLs --- jedi/api.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++--- jedi/modules.py | 6 +++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index e4f7f532..4c1af950 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -54,13 +54,13 @@ class Script(object): :type source_encoding: str """ def __init__(self, source, line, column, source_path, - source_encoding='utf-8'): + source_encoding='utf-8', fast=True): 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, fast=fast) self._source_path = source_path self.source_path = None if source_path is None \ else os.path.abspath(source_path) @@ -503,6 +503,59 @@ class Script(object): return sorted(d, key=lambda x: (x.module_path or '', x.start_pos)) +class Interpreter(Script): + + """ + Jedi API for Python REPLs. + + >>> from itertools import chain + >>> script = Interpreter('cha', [locals()]) + >>> script.complete() + [] + + """ + + def __init__(self, source, namespaces=[], 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 + super(Interpreter, self).__init__( + source, line, column, source_path, source_encoding, fast=False) + for ns in namespaces: + self._import_raw_namespace(ns) + + def _import_raw_namespace(self, raw_namespace): + for (variable, obj) in raw_namespace.items(): + try: + module = obj.__module__ + except: + continue + fakeimport = self._make_fakeimport(variable, module) + self._parser.scope.imports.append(fakeimport) + + def _make_fakeimport(self, variable, module): + submodule = self._parser.scope._sub_module + varname = pr.Name( + module=submodule, + names=[(variable, (0, 0))], + start_pos=(0, 0), + end_pos=(None, None)) + modname = pr.Name( + module=submodule, + names=[(module, (0, 0))], + start_pos=(0, 0), + end_pos=(None, None)) + fakeimport = pr.Import( + module=submodule, + namespace=varname, + from_ns=modname, + start_pos=(0, 0), + end_pos=(None, None)) + fakeimport.parent = submodule + return fakeimport + + def defined_names(source, source_path=None, source_encoding='utf-8'): """ Get all definitions in `source` sorted by its position. diff --git a/jedi/modules.py b/jedi/modules.py index 37e5bbac..035a894e 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -92,8 +92,10 @@ class ModuleWithCursor(Module): :param position: The position, the user is currently in. Only important \ for the main file. """ - def __init__(self, path, source, position): + def __init__(self, path, source, position, fast): super(ModuleWithCursor, self).__init__(path, source) + self._parserclass = fast_parser.FastParser if fast else \ + parsing.Parser self.position = position # this two are only used, because there is no nonlocal in Python 2 @@ -112,7 +114,7 @@ class ModuleWithCursor(Module): # Call the parser already here, because it will be used anyways. # Also, the position is here important (which will not be used by # default), therefore fill the cache here. - self._parser = fast_parser.FastParser(self.source, self.path, + self._parser = self._parserclass(self.source, self.path, self.position) # don't pickle that module, because it's changing fast cache.save_module(self.path, self.name, self._parser, From 3266aa2d00533753cf039098b53af79fe7de69bb Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 2 Mar 2013 19:56:12 +0100 Subject: [PATCH 02/97] Improve api.Interpreter docstring --- jedi/api.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index 4c1af950..2703f6e9 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -508,10 +508,16 @@ class Interpreter(Script): """ Jedi API for Python REPLs. - >>> from itertools import chain - >>> script = Interpreter('cha', [locals()]) - >>> script.complete() - [] + 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 """ @@ -529,7 +535,7 @@ class Interpreter(Script): for (variable, obj) in raw_namespace.items(): try: module = obj.__module__ - except: + except AttributeError: continue fakeimport = self._make_fakeimport(variable, module) self._parser.scope.imports.append(fakeimport) From fb197c42abe5e72dccd177ffe2795ac1660f7122 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 2 Mar 2013 20:40:36 +0100 Subject: [PATCH 03/97] Add tests for api.Interpreter (2/3 fail) --- test/test_regression.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/test_regression.py b/test/test_regression.py index d17c2627..41061676 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -539,6 +539,32 @@ class TestSpeed(TestBase): #print(jedi.imports.imports_processed) +class TestInterpreterAPI(unittest.TestCase): + + def check_interpreter_complete(self, source, namespace, completions, + **kwds): + cs = api.Interpreter(source, [namespace], **kwds).complete() + actual = [c.word for c in cs] + self.assertEqual(actual, completions) + + def test_complete_raw_function(self): + self.check_interpreter_complete('join().up', + {'join': os.path.join}, + ['upper']) + + def test_complete_raw_module(self): + self.check_interpreter_complete('os.path.join().up', + {'os': os}, + ['upper']) + + def test_complete_raw_instance(self): + import datetime + dt = datetime.datetime(2013, 1, 1) + self.check_interpreter_complete('dt.strftime("%Y").up', + {'dt': dt}, + ['upper']) + + def test_settings_module(): """ jedi.settings and jedi.cache.settings must be the same module. From 17770356e25e43acb1440ff8214f6a55f3eb186a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 2 Mar 2013 21:03:53 +0100 Subject: [PATCH 04/97] Implement the case where raw object is a module Previously failing test_complete_raw_module is passed. --- jedi/api.py | 48 +++++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index 2703f6e9..151c0ade 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -533,31 +533,45 @@ class Interpreter(Script): def _import_raw_namespace(self, raw_namespace): for (variable, obj) in raw_namespace.items(): - try: - module = obj.__module__ - except AttributeError: + module = getattr(obj, '__module__', None) + if module: + fakeimport = self._make_fakeimport(module, variable) + self._parser.scope.imports.append(fakeimport) continue - fakeimport = self._make_fakeimport(variable, module) - self._parser.scope.imports.append(fakeimport) - def _make_fakeimport(self, variable, module): + if getattr(obj, '__file__', None): + fakeimport = self._make_fakeimport(obj.__name__) + self._parser.scope.imports.append(fakeimport) + continue + + def _make_fakeimport(self, module, variable=None): submodule = self._parser.scope._sub_module - varname = pr.Name( - module=submodule, - names=[(variable, (0, 0))], - start_pos=(0, 0), - end_pos=(None, None)) + if variable: + varname = pr.Name( + module=submodule, + names=[(variable, (0, 0))], + start_pos=(0, 0), + end_pos=(None, None)) + else: + varname = None modname = pr.Name( module=submodule, names=[(module, (0, 0))], start_pos=(0, 0), end_pos=(None, None)) - fakeimport = pr.Import( - module=submodule, - namespace=varname, - from_ns=modname, - start_pos=(0, 0), - end_pos=(None, None)) + if varname: + fakeimport = pr.Import( + module=submodule, + namespace=varname, + from_ns=modname, + start_pos=(0, 0), + end_pos=(None, None)) + else: + fakeimport = pr.Import( + module=submodule, + namespace=modname, + start_pos=(0, 0), + end_pos=(None, None)) fakeimport.parent = submodule return fakeimport From cf48d1272d72b2f87781833cb0eb445afbe7d79b Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 04:42:39 +0100 Subject: [PATCH 05/97] Use proper interface to add fake Import object --- jedi/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index 151c0ade..612dfa3e 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -532,16 +532,17 @@ class Interpreter(Script): self._import_raw_namespace(ns) def _import_raw_namespace(self, raw_namespace): + scope = self._parser.scope for (variable, obj) in raw_namespace.items(): module = getattr(obj, '__module__', None) if module: fakeimport = self._make_fakeimport(module, variable) - self._parser.scope.imports.append(fakeimport) + scope.add_import(fakeimport) continue if getattr(obj, '__file__', None): fakeimport = self._make_fakeimport(obj.__name__) - self._parser.scope.imports.append(fakeimport) + scope.add_import(fakeimport) continue def _make_fakeimport(self, module, variable=None): @@ -572,7 +573,6 @@ class Interpreter(Script): namespace=modname, start_pos=(0, 0), end_pos=(None, None)) - fakeimport.parent = submodule return fakeimport From 11abd92d2b5d743aa4c6af25473caab7b37fb29e Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 04:49:53 +0100 Subject: [PATCH 06/97] Support alias in _make_fakeimport --- jedi/api.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/jedi/api.py b/jedi/api.py index 612dfa3e..6bbe52d7 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -545,7 +545,7 @@ class Interpreter(Script): scope.add_import(fakeimport) continue - def _make_fakeimport(self, module, variable=None): + def _make_fakeimport(self, module, variable=None, alias=None): submodule = self._parser.scope._sub_module if variable: varname = pr.Name( @@ -560,17 +560,27 @@ class Interpreter(Script): names=[(module, (0, 0))], start_pos=(0, 0), end_pos=(None, None)) + if alias: + aliasname = pr.Name( + module=submodule, + names=[(alias, (0, 0))], + start_pos=(0, 0), + end_pos=(None, None)) + else: + aliasname = None if varname: fakeimport = pr.Import( module=submodule, namespace=varname, from_ns=modname, + alias=aliasname, start_pos=(0, 0), end_pos=(None, None)) else: fakeimport = pr.Import( module=submodule, namespace=modname, + alias=aliasname, start_pos=(0, 0), end_pos=(None, None)) return fakeimport From 06c03cafa21d2204ece43b801a74d9f7c6946396 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 03:18:11 +0100 Subject: [PATCH 07/97] Implement the case where raw object is an instance There is still a bug in this implementation. The corresponding test fails. --- jedi/api.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/jedi/api.py b/jedi/api.py index 6bbe52d7..a5b8bebb 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -11,6 +11,8 @@ from __future__ import with_statement import re import os import warnings +import itertools +import tokenize from jedi import parsing from jedi import parsing_representation as pr @@ -528,6 +530,12 @@ class Interpreter(Script): column = len(lines[-1]) if column is None else column super(Interpreter, self).__init__( source, line, column, source_path, source_encoding, fast=False) + + count = itertools.count() + fmt = '*jedi-{0}*'.format + self._genname = lambda: fmt(next(count)) + """Generate unique variable names to avoid name collision.""" + for ns in namespaces: self._import_raw_namespace(ns) @@ -545,6 +553,17 @@ class Interpreter(Script): scope.add_import(fakeimport) continue + 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): submodule = self._parser.scope._sub_module if variable: @@ -585,6 +604,37 @@ class Interpreter(Script): end_pos=(None, None)) return fakeimport + def _make_fakestatement(self, lhs, rhs, call=False): + """ + Make a fake statement object that represents ``lhs = rhs``. + + :rtype: :class:`parsing_representation.Statement` + """ + submodule = self._parser.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)) + def defined_names(source, source_path=None, source_encoding='utf-8'): """ From 45cad4c83bf925c12c258886444fe7bc5b0390ce Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 04:34:14 +0100 Subject: [PATCH 08/97] Fake imports should locate before fake statements --- jedi/api.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index a5b8bebb..44be868f 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -569,21 +569,21 @@ class Interpreter(Script): if variable: varname = pr.Name( module=submodule, - names=[(variable, (0, 0))], - start_pos=(0, 0), + names=[(variable, (-1, 0))], + start_pos=(-1, 0), end_pos=(None, None)) else: varname = None modname = pr.Name( module=submodule, - names=[(module, (0, 0))], - start_pos=(0, 0), + names=[(module, (-1, 0))], + start_pos=(-1, 0), end_pos=(None, None)) if alias: aliasname = pr.Name( module=submodule, - names=[(alias, (0, 0))], - start_pos=(0, 0), + names=[(alias, (-1, 0))], + start_pos=(-1, 0), end_pos=(None, None)) else: aliasname = None @@ -593,14 +593,14 @@ class Interpreter(Script): namespace=varname, from_ns=modname, alias=aliasname, - start_pos=(0, 0), + start_pos=(-1, 0), end_pos=(None, None)) else: fakeimport = pr.Import( module=submodule, namespace=modname, alias=aliasname, - start_pos=(0, 0), + start_pos=(-1, 0), end_pos=(None, None)) return fakeimport From b4e3d1d65dfdd63bfb28d615001a1fc32efc7dc3 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 12:08:59 +0100 Subject: [PATCH 09/97] Fix test_complete_raw_instance example 'dt.strftime("%Y").up' does not work even if with `api.Script`. Change it to the one that can be handled by Jedi. --- test/test_regression.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/test_regression.py b/test/test_regression.py index 41061676..acbfc60e 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -543,9 +543,10 @@ class TestInterpreterAPI(unittest.TestCase): def check_interpreter_complete(self, source, namespace, completions, **kwds): - cs = api.Interpreter(source, [namespace], **kwds).complete() + script = api.Interpreter(source, [namespace], **kwds) + cs = script.complete() actual = [c.word for c in cs] - self.assertEqual(actual, completions) + self.assertEqual(sorted(actual), sorted(completions)) def test_complete_raw_function(self): self.check_interpreter_complete('join().up', @@ -560,9 +561,9 @@ class TestInterpreterAPI(unittest.TestCase): def test_complete_raw_instance(self): import datetime dt = datetime.datetime(2013, 1, 1) - self.check_interpreter_complete('dt.strftime("%Y").up', + self.check_interpreter_complete('(dt - dt).ti', {'dt': dt}, - ['upper']) + ['time', 'timetz', 'timetuple']) def test_settings_module(): From 12ac71b1fddece217d1146d4ac640d76cca50466 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 12:28:08 +0100 Subject: [PATCH 10/97] Document api.Interpreter --- jedi/api.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/jedi/api.py b/jedi/api.py index 44be868f..a1b67a64 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -525,6 +525,16 @@ class Interpreter(Script): def __init__(self, source, namespaces=[], line=None, column=None, source_path=None, source_encoding='utf-8'): + """ + Parse `source` and mixin interpreted Python objects from `namespaces`. + + :type source: str + :arg source: + :type namespaces: list of dict + :arg namespaces: + + Other optional arguments are same as the ones for :class:`Script`. + """ lines = source.splitlines() line = len(lines) if line is None else line column = len(lines[-1]) if column is None else column @@ -540,6 +550,12 @@ class Interpreter(Script): self._import_raw_namespace(ns) def _import_raw_namespace(self, raw_namespace): + """ + Import interpreted Python objects in a namespace. + + :type raw_namespace: dict + :arg raw_namespace: e.g., the dict given by `locals` + """ scope = self._parser.scope for (variable, obj) in raw_namespace.items(): module = getattr(obj, '__module__', None) @@ -565,6 +581,25 @@ class Interpreter(Script): 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` is given: ``import `` + - `module` and `variable` are given: ``from import `` + - all params are given: ``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._parser.scope._sub_module if variable: varname = pr.Name( @@ -608,6 +643,10 @@ class Interpreter(Script): """ 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._parser.scope._sub_module From 6fb42e5f4bbbabd542e7e54bc4986a4e94aafde9 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 12:37:00 +0100 Subject: [PATCH 11/97] Document _import_raw_namespace more --- jedi/api.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/jedi/api.py b/jedi/api.py index a1b67a64..28ba607c 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -553,22 +553,40 @@ class Interpreter(Script): """ 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._parser.scope for (variable, obj) in raw_namespace.items(): + # Import functions and classes module = getattr(obj, '__module__', None) if module: fakeimport = self._make_fakeimport(module, variable) scope.add_import(fakeimport) continue + # Import modules if getattr(obj, '__file__', None): fakeimport = self._make_fakeimport(obj.__name__) scope.add_import(fakeimport) continue + # Import instances objclass = getattr(obj, '__class__', None) module = getattr(objclass, '__module__', None) if objclass and module: From fc79f0e258d1c8df32d120a4cdb51523e1062672 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 12:47:03 +0100 Subject: [PATCH 12/97] Support case like `from os.path import join as pjoin` --- jedi/api.py | 3 ++- test/test_regression.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/jedi/api.py b/jedi/api.py index 28ba607c..6576a27c 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -576,7 +576,8 @@ class Interpreter(Script): # Import functions and classes module = getattr(obj, '__module__', None) if module: - fakeimport = self._make_fakeimport(module, variable) + fakeimport = self._make_fakeimport(module, obj.__name__, + variable) scope.add_import(fakeimport) continue diff --git a/test/test_regression.py b/test/test_regression.py index acbfc60e..87a89803 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -553,6 +553,11 @@ class TestInterpreterAPI(unittest.TestCase): {'join': os.path.join}, ['upper']) + def test_complete_raw_function_different_name(self): + self.check_interpreter_complete('pjoin().up', + {'pjoin': os.path.join}, + ['upper']) + def test_complete_raw_module(self): self.check_interpreter_complete('os.path.join().up', {'os': os}, From af108910966edaebff5c945110d8e6e4e75de53b Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 12:50:58 +0100 Subject: [PATCH 13/97] Document api.Interpreter._genname --- jedi/api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jedi/api.py b/jedi/api.py index 6576a27c..f8f98dab 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -544,7 +544,11 @@ class Interpreter(Script): count = itertools.count() fmt = '*jedi-{0}*'.format self._genname = lambda: fmt(next(count)) - """Generate unique variable names to avoid name collision.""" + """ + Generate unique variable names to avoid name collision. + To avoid name collision to already defined names, generated + names are invalid as Python identifier. + """ for ns in namespaces: self._import_raw_namespace(ns) From a93016db1bd66c0181d33525032b0db0211aca55 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 13:24:11 +0100 Subject: [PATCH 14/97] Python 2.5 compatibility fix --- jedi/api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index f8f98dab..cc9f165b 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -542,8 +542,7 @@ class Interpreter(Script): source, line, column, source_path, source_encoding, fast=False) count = itertools.count() - fmt = '*jedi-{0}*'.format - self._genname = lambda: fmt(next(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 From dc47d15de0c2956a367206d21a0886750df0ab88 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 3 Mar 2013 13:35:48 +0100 Subject: [PATCH 15/97] Use locals() for tests in TestInterpreterAPI It is better to check that Interpreter does not fail with the real name space. Previously the doctest using locals() failed because of the bug in _import_raw_namespace. --- jedi/api.py | 11 ++++++----- test/test_regression.py | 11 +++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index cc9f165b..a91e3e76 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -576,17 +576,18 @@ class Interpreter(Script): """ scope = self._parser.scope for (variable, obj) in raw_namespace.items(): + objname = getattr(obj, '__name__', None) + # Import functions and classes module = getattr(obj, '__module__', None) - if module: - fakeimport = self._make_fakeimport(module, obj.__name__, - variable) + if module and objname: + fakeimport = self._make_fakeimport(module, objname, variable) scope.add_import(fakeimport) continue # Import modules - if getattr(obj, '__file__', None): - fakeimport = self._make_fakeimport(obj.__name__) + if getattr(obj, '__file__', None) and objname: + fakeimport = self._make_fakeimport(objname) scope.add_import(fakeimport) continue diff --git a/test/test_regression.py b/test/test_regression.py index 87a89803..2d125fd5 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -549,25 +549,28 @@ class TestInterpreterAPI(unittest.TestCase): self.assertEqual(sorted(actual), sorted(completions)) def test_complete_raw_function(self): + from os.path import join self.check_interpreter_complete('join().up', - {'join': os.path.join}, + locals(), ['upper']) def test_complete_raw_function_different_name(self): + from os.path import join as pjoin self.check_interpreter_complete('pjoin().up', - {'pjoin': os.path.join}, + locals(), ['upper']) def test_complete_raw_module(self): + import os self.check_interpreter_complete('os.path.join().up', - {'os': os}, + locals(), ['upper']) def test_complete_raw_instance(self): import datetime dt = datetime.datetime(2013, 1, 1) self.check_interpreter_complete('(dt - dt).ti', - {'dt': dt}, + locals(), ['time', 'timetz', 'timetuple']) From c2e9ccda2bda4265c3cd953bc4a750fbd5f233cf Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 12 Mar 2013 14:25:53 +0100 Subject: [PATCH 16/97] Fix failing test: args for ModuleWithCursor --- jedi/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/modules.py b/jedi/modules.py index 035a894e..4a66d477 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -92,7 +92,7 @@ class ModuleWithCursor(Module): :param position: The position, the user is currently in. Only important \ for the main file. """ - def __init__(self, path, source, position, fast): + def __init__(self, path, source, position, fast=True): super(ModuleWithCursor, self).__init__(path, source) self._parserclass = fast_parser.FastParser if fast else \ parsing.Parser From 8fc396371c3213901acf853b93bbe6b4824f5459 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 12 Mar 2013 14:36:15 +0100 Subject: [PATCH 17/97] Respect settings.fast_parser --- jedi/api.py | 2 +- jedi/modules.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index a91e3e76..7b325933 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -56,7 +56,7 @@ class Script(object): :type source_encoding: str """ def __init__(self, source, line, column, source_path, - source_encoding='utf-8', fast=True): + source_encoding='utf-8', fast=None): api_classes._clear_caches() debug.reset_time() self.source = modules.source_to_unicode(source, source_encoding) diff --git a/jedi/modules.py b/jedi/modules.py index 4a66d477..462e6989 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -91,9 +91,13 @@ class ModuleWithCursor(Module): :param path: The module path of the file or None. :param position: The position, the user is currently in. Only important \ for the main file. + :param fast: Use `fast_parser.FastParser` or not. If None, respect + `settings.fast_parser`. """ - def __init__(self, path, source, position, fast=True): + def __init__(self, path, source, position, fast=None): super(ModuleWithCursor, self).__init__(path, source) + if fast is None: + fast = settings.fast_parser self._parserclass = fast_parser.FastParser if fast else \ parsing.Parser self.position = position From e8914e78569e6df70ebf359429313e9a288b145e Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 12 Mar 2013 16:14:03 +0100 Subject: [PATCH 18/97] Document api.Interpreter --- docs/conf.py | 1 + jedi/api.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 20bfc6a3..1dfb18c4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -267,6 +267,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/api.py b/jedi/api.py index 7b325933..96d88ceb 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -529,11 +529,14 @@ class Interpreter(Script): Parse `source` and mixin interpreted Python objects from `namespaces`. :type source: str - :arg source: + :arg source: Code to parse. :type namespaces: list of dict - :arg namespaces: + :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`. """ lines = source.splitlines() line = len(lines) if line is None else line From 3d0458bca9b8ca5fdd6085b3cd19dc2a903c4a66 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 4 May 2013 17:50:42 +0200 Subject: [PATCH 19/97] Export Interpreter class to top-level namespace --- jedi/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jedi/__init__.py b/jedi/__init__.py index 7d74b8c4..a97fa88c 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -40,7 +40,9 @@ 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, _quick_complete, +) from . import settings sys.path.pop(0) From 89edb739784863cbdb0884c63e3b1a4ef4e7cf5d Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 4 May 2013 17:58:02 +0200 Subject: [PATCH 20/97] Fix failure due to rebase --- jedi/modules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jedi/modules.py b/jedi/modules.py index 462e6989..bb3d4303 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -22,6 +22,8 @@ import os from ast import literal_eval from jedi._compatibility import exec_function, unicode +from jedi import settings +from jedi import parsing from jedi import cache from jedi import parsing_representation as pr from jedi import fast_parser From 17f5b9a79d03ed8b5ed37d527cb34d1d45ee5fe9 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 4 May 2013 18:08:25 +0200 Subject: [PATCH 21/97] Fix test failure in Python 3.3 There is a new method introduced in Python 3.3: http://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp --- test/test_regression.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/test_regression.py b/test/test_regression.py index 2d125fd5..783c4c9e 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 api_classes = api.api_classes @@ -569,9 +569,12 @@ class TestInterpreterAPI(unittest.TestCase): 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(), - ['time', 'timetz', 'timetuple']) + completions) def test_settings_module(): From 633f582184005439ff4c2bb2a3e99d0b604dd8da Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 11 May 2013 21:33:42 +0200 Subject: [PATCH 22/97] Add jedi.interpret.ObjectImporter and move scope manipulation functions in api.Interpreter to there. --- jedi/api.py | 159 +----------------------------------------- jedi/interpret.py | 171 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 156 deletions(-) create mode 100644 jedi/interpret.py diff --git a/jedi/api.py b/jedi/api.py index 96d88ceb..cfa2c738 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -11,8 +11,6 @@ from __future__ import with_statement import re import os import warnings -import itertools -import tokenize from jedi import parsing from jedi import parsing_representation as pr @@ -22,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 @@ -544,161 +543,9 @@ class Interpreter(Script): super(Interpreter, self).__init__( source, line, column, source_path, source_encoding, fast=False) - 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. - """ - + importer = interpret.ObjectImporter(self._parser.scope) for ns in namespaces: - self._import_raw_namespace(ns) - - 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._parser.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` is given: ``import `` - - `module` and `variable` are given: ``from import `` - - all params are given: ``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._parser.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._parser.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)) + importer.import_raw_namespace(ns) def defined_names(source, source_path=None, source_encoding='utf-8'): 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)) From 6af2a0677d583ab6b964477f79ccdb6f42653588 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 11 May 2013 21:36:43 +0200 Subject: [PATCH 23/97] Compute default line/column/source_path in api.Script --- jedi/api.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index cfa2c738..ef28e3d4 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -54,8 +54,12 @@ class Script(object): ``unicode`` object (default ``'utf-8'``). :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', fast=None): + 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) @@ -537,9 +541,6 @@ class Interpreter(Script): If `line` and `column` are None, they are assumed be at the end of `source`. """ - lines = source.splitlines() - line = len(lines) if line is None else line - column = len(lines[-1]) if column is None else column super(Interpreter, self).__init__( source, line, column, source_path, source_encoding, fast=False) From 08f8dcfae4d35e997cec9043f9f22d4a1e979829 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 11 May 2013 21:37:53 +0200 Subject: [PATCH 24/97] Do not repeat defaults in Interpreter.__init__ --- jedi/api.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index ef28e3d4..fb7f2d09 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -526,8 +526,7 @@ class Interpreter(Script): """ - def __init__(self, source, namespaces=[], line=None, column=None, - source_path=None, source_encoding='utf-8'): + def __init__(self, source, namespaces=[], **kwds): """ Parse `source` and mixin interpreted Python objects from `namespaces`. @@ -541,8 +540,7 @@ class Interpreter(Script): If `line` and `column` are None, they are assumed be at the end of `source`. """ - super(Interpreter, self).__init__( - source, line, column, source_path, source_encoding, fast=False) + super(Interpreter, self).__init__(source, fast=False, **kwds) importer = interpret.ObjectImporter(self._parser.scope) for ns in namespaces: From 2846fe980b468ac43b1c09461a192486787664aa Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 11 May 2013 21:39:53 +0200 Subject: [PATCH 25/97] Remove api._quick_complete --- jedi/__init__.py | 4 +--- jedi/api.py | 22 ---------------------- test/test_regression.py | 21 --------------------- 3 files changed, 1 insertion(+), 46 deletions(-) diff --git a/jedi/__init__.py b/jedi/__init__.py index a97fa88c..83b69efa 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -40,9 +40,7 @@ import sys # imports and circular imports... Just avoid it: sys.path.insert(0, __path__[0]) -from .api import ( - Script, Interpreter, NotFoundError, set_debug_function, _quick_complete, -) +from .api import Script, Interpreter, NotFoundError, set_debug_function from . import settings sys.path.pop(0) diff --git a/jedi/api.py b/jedi/api.py index fb7f2d09..dd4dd5e9 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -577,25 +577,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/test/test_regression.py b/test/test_regression.py index 783c4c9e..ca1b87b6 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -430,27 +430,6 @@ class TestFeature(TestBase): any_re""" self.assertEqual(self.goto_definitions(s)[0].full_name, 're.RegexObject') - 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): From a870fece0f9382661772eef04a53a986a0bc4e70 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 11 May 2013 21:47:55 +0200 Subject: [PATCH 26/97] Use fast parser in api.Interpreter --- jedi/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index dd4dd5e9..c4927540 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -540,9 +540,9 @@ class Interpreter(Script): If `line` and `column` are None, they are assumed be at the end of `source`. """ - super(Interpreter, self).__init__(source, fast=False, **kwds) + super(Interpreter, self).__init__(source, **kwds) - importer = interpret.ObjectImporter(self._parser.scope) + importer = interpret.ObjectImporter(self._parser.user_scope) for ns in namespaces: importer.import_raw_namespace(ns) From 19b3fef0c5ef6ecce738d4dc8c1cb09b3cac93ad Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 11 May 2013 21:59:39 +0200 Subject: [PATCH 27/97] Remove `fast` option from api.Script --- jedi/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index c4927540..e0243e46 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -55,7 +55,7 @@ class Script(object): :type source_encoding: str """ def __init__(self, source, line=None, column=None, source_path=None, - source_encoding='utf-8', fast=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 @@ -65,7 +65,7 @@ class Script(object): 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, fast=fast) + 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) From 52b3a326c4912f0bcc191a6e5eb59d66f0b28d2f Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 11 May 2013 22:01:48 +0200 Subject: [PATCH 28/97] Revert changes in jedi/modules.py in this branch --- jedi/modules.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/jedi/modules.py b/jedi/modules.py index bb3d4303..37e5bbac 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -22,8 +22,6 @@ import os from ast import literal_eval from jedi._compatibility import exec_function, unicode -from jedi import settings -from jedi import parsing from jedi import cache from jedi import parsing_representation as pr from jedi import fast_parser @@ -93,15 +91,9 @@ class ModuleWithCursor(Module): :param path: The module path of the file or None. :param position: The position, the user is currently in. Only important \ for the main file. - :param fast: Use `fast_parser.FastParser` or not. If None, respect - `settings.fast_parser`. """ - def __init__(self, path, source, position, fast=None): + def __init__(self, path, source, position): super(ModuleWithCursor, self).__init__(path, source) - if fast is None: - fast = settings.fast_parser - self._parserclass = fast_parser.FastParser if fast else \ - parsing.Parser self.position = position # this two are only used, because there is no nonlocal in Python 2 @@ -120,7 +112,7 @@ class ModuleWithCursor(Module): # Call the parser already here, because it will be used anyways. # Also, the position is here important (which will not be used by # default), therefore fill the cache here. - self._parser = self._parserclass(self.source, self.path, + self._parser = fast_parser.FastParser(self.source, self.path, self.position) # don't pickle that module, because it's changing fast cache.save_module(self.path, self.name, self._parser, From 3d6ef88795c0d3aae8fb1c1da519a6c003cdaa81 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 12 May 2013 08:15:48 +0200 Subject: [PATCH 29/97] Add simple PYTHONSTARTUP file --- jedi/__main__.py | 2 ++ jedi/replstartup.py | 25 +++++++++++++++++++++++++ jedi/utils.py | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 jedi/__main__.py create mode 100644 jedi/replstartup.py create mode 100644 jedi/utils.py 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/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`~!@#$%^&*-=+[{]}\\|;:\'",<>/?') From 6d1a33a3c76da488eddb7382d84f83002c875d24 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 15 May 2013 23:27:16 +0200 Subject: [PATCH 30/97] Added python2 AUR package --- docs/docs/installation.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/docs/installation.rst b/docs/docs/installation.rst index 35cc4bc3..77331322 100644 --- a/docs/docs/installation.rst +++ b/docs/docs/installation.rst @@ -30,8 +30,10 @@ System-wide installation via a package manager Arch Linux ~~~~~~~~~~ -You can install jedi directly from AUR: `python-jedi at AUR -`__. +You can install jedi directly from official AUR packages: + +- `python-jedi `__. +- `python2-jedi `__. (There is also a packaged version of the vim plugin available: `vim-jedi at AUR `__.) From 5c5f93d69eec37c00accf2f2f755b42faefa9294 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 15 May 2013 23:28:20 +0200 Subject: [PATCH 31/97] Typo in docs --- docs/docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/installation.rst b/docs/docs/installation.rst index 77331322..22eeab11 100644 --- a/docs/docs/installation.rst +++ b/docs/docs/installation.rst @@ -32,8 +32,8 @@ Arch Linux You can install jedi directly from official AUR packages: -- `python-jedi `__. -- `python2-jedi `__. +- `python-jedi `__ +- `python2-jedi `__ (There is also a packaged version of the vim plugin available: `vim-jedi at AUR `__.) From 16de02023fc9423fcf654c91b21326c6470398e3 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 15 May 2013 23:29:13 +0200 Subject: [PATCH 32/97] Use |jedi| macro in docs more consistently --- docs/docs/installation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/docs/installation.rst b/docs/docs/installation.rst index 22eeab11..f756702c 100644 --- a/docs/docs/installation.rst +++ b/docs/docs/installation.rst @@ -30,7 +30,7 @@ System-wide installation via a package manager Arch Linux ~~~~~~~~~~ -You can install jedi directly from official AUR packages: +You can install |jedi| directly from official AUR packages: - `python-jedi `__ - `python2-jedi `__ @@ -47,7 +47,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 @@ -55,7 +55,7 @@ Manual installation from a downloaded package If you prefer not to use an automated package installer, you can `download `__ a current copy of -*Jedi* and install it manually. +|jedi| and install it manually. To install it, navigate to the directory containing `setup.py` on your console and type:: From 6ed0e1580b9860faf6fab78d9d9a511f969f4b11 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 15 May 2013 23:31:46 +0200 Subject: [PATCH 33/97] Added remark about Python versions to installation docs --- docs/docs/installation.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/docs/installation.rst b/docs/docs/installation.rst index f756702c..49181e80 100644 --- a/docs/docs/installation.rst +++ b/docs/docs/installation.rst @@ -32,8 +32,13 @@ Arch Linux You can install |jedi| directly from official AUR packages: -- `python-jedi `__ -- `python2-jedi `__ +- `python-jedi `__ (Python 3) +- `python2-jedi `__ (Python 2) + +The specified Python version just refers to the *runtime environment* for +|jedi|. Use the Python 2 version if you're running vim (or whatever editor oyu +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 `__.) From e1afe2e80d0bb4e8d3ee481d6e0ca7229c2eaf1b Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 15 May 2013 23:33:14 +0200 Subject: [PATCH 34/97] Small docs changes --- docs/docs/development.rst | 2 ++ docs/docs/installation.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docs/development.rst b/docs/docs/development.rst index a6a8b3d1..9d40c0b0 100644 --- a/docs/docs/development.rst +++ b/docs/docs/development.rst @@ -64,6 +64,8 @@ Parser Representation (parser_representation.py) .. automodule:: parsing_representation +Class inheritance diagram: + .. inheritance-diagram:: SubModule Class diff --git a/docs/docs/installation.rst b/docs/docs/installation.rst index 49181e80..44cedfd6 100644 --- a/docs/docs/installation.rst +++ b/docs/docs/installation.rst @@ -36,7 +36,7 @@ You can install |jedi| directly from official AUR 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 oyu +|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*. From 7e2eb587e181a07da5b226b55e229ede275edd7b Mon Sep 17 00:00:00 2001 From: David Halter Date: Sat, 18 May 2013 23:56:12 +0430 Subject: [PATCH 35/97] is_keyword test --- test/test_api_classes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/test_api_classes.py b/test/test_api_classes.py index 65a9d0d2..714a11ef 100644 --- a/test/test_api_classes.py +++ b/test/test_api_classes.py @@ -3,7 +3,13 @@ import textwrap import pytest from jedi import api +import jedi +def test_is_keyword(): + results = jedi.Script('import ', 1, 1, None).goto_definitions() + assert len(results) == 1 and results[0].is_keyword == True + results = jedi.Script('str', 1, 1, None).goto_definitions() + assert len(results) == 1 and results[0].is_keyword == False def make_definitions(): """ From ee5f96f1195e59fbf23b9b4d35bdffd79f4aa613 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 18 May 2013 23:25:54 +0200 Subject: [PATCH 36/97] Fix test failure The bug was introduced when I merged branch 'interpreter-api' at 5f2477d5bf1b8601246e246adada21ab6c28c268. This patch redo (part of) 2846fe980b468ac43b1c09461a192486787664aa. --- test/test_regression.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/test/test_regression.py b/test/test_regression.py index 299e2357..5e111310 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -453,27 +453,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): From 7a8ea56b058a7fbb3ea2590b24e9dd679e34bc99 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 18 May 2013 23:01:21 +0200 Subject: [PATCH 37/97] Document jedi.replstartup usage --- docs/docs/repl.rst | 8 ++++++++ docs/index.rst | 1 + 2 files changed, 9 insertions(+) create mode 100644 docs/docs/repl.rst diff --git a/docs/docs/repl.rst b/docs/docs/repl.rst new file mode 100644 index 00000000..e911e46e --- /dev/null +++ b/docs/docs/repl.rst @@ -0,0 +1,8 @@ +.. include:: ../global.rst + +How to use Jedi from Python interpreter +======================================= + +.. automodule:: jedi.replstartup + +.. autofunction:: jedi.utils.setup_readline diff --git a/docs/index.rst b/docs/index.rst index c8e518ea..9f53f667 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,6 +22,7 @@ Docs docs/installation docs/features + docs/repl docs/recipes docs/plugin-api docs/history From 134dab174d0891ee0f4b5d71507dc78bc4e1153e Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 18 May 2013 23:05:15 +0200 Subject: [PATCH 38/97] Setup intersphinx module for linking Python doc For example, :mod:`readline` links to the corresponding page. --- docs/conf.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d7a2815b..3c46b089 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ sys.path.append(os.path.abspath('_themes')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.todo', - 'sphinx.ext.inheritance_diagram'] + 'sphinx.ext.intersphinx', 'sphinx.ext.inheritance_diagram'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -270,3 +270,10 @@ 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, +} From 27f8b342e1a4bea9c807b005d16f932880bb7136 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 18 May 2013 23:41:15 +0200 Subject: [PATCH 39/97] Document utils.setup_readline --- jedi/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/jedi/utils.py b/jedi/utils.py index eb617fe7..24e1e5cb 100644 --- a/jedi/utils.py +++ b/jedi/utils.py @@ -28,6 +28,14 @@ def readline_complete(text, state): 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 From 0c85642e6b22e3d2aabb6bda596f5df8f881898a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sat, 18 May 2013 23:49:48 +0200 Subject: [PATCH 40/97] Add utils.JediRLCompleter --- jedi/utils.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/jedi/utils.py b/jedi/utils.py index 24e1e5cb..2da56cf8 100644 --- a/jedi/utils.py +++ b/jedi/utils.py @@ -2,27 +2,20 @@ Utilities for end-users. """ -import sys +from rlcompleter import Completer from jedi import Interpreter -def readline_complete(text, state): - """ - Function to be passed to :func:`readline.set_completer`. +class JediRLCompleter(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 attr_matches(self, text): + if '(' in text or ')' in text: + completions = Interpreter(text, [self.namespace]).completions() + return [text + c.complete for c in completions] + else: + # NOTE: Completer is old type class + return Completer.attr_matches(self, text) def setup_readline(): @@ -42,7 +35,7 @@ def setup_readline(): except ImportError: print("Module readline not available.") else: - readline.set_completer(readline_complete) + readline.set_completer(JediRLCompleter().complete) readline.parse_and_bind("tab: complete") # Default delimiters minus "()": From 6a8a06fd0b514e0cc191f54aa0dffbc4924e851c Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 19 May 2013 00:24:49 +0200 Subject: [PATCH 41/97] Improve JediRLCompleter for minor cases --- jedi/utils.py | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/jedi/utils.py b/jedi/utils.py index 2da56cf8..41bc2da5 100644 --- a/jedi/utils.py +++ b/jedi/utils.py @@ -9,13 +9,51 @@ from jedi import Interpreter class JediRLCompleter(Completer): + 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='()'): + """ + 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): - if '(' in text or ')' in text: - completions = Interpreter(text, [self.namespace]).completions() - return [text + c.complete for c in completions] - else: - # NOTE: Completer is old type class - return Completer.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(): From 3b3310ee3ebcb41056870d5fc861619330a50894 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 19 May 2013 00:41:33 +0200 Subject: [PATCH 42/97] Determine readline delims programmatically --- jedi/utils.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/jedi/utils.py b/jedi/utils.py index 41bc2da5..e957dbd3 100644 --- a/jedi/utils.py +++ b/jedi/utils.py @@ -7,6 +7,23 @@ 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): def _jedi_matches(self, text): @@ -14,7 +31,7 @@ class JediRLCompleter(Completer): return [text + c.complete for c in completions] @staticmethod - def _split_for_default_matcher(text, delims='()'): + def _split_for_default_matcher(text, delims=_NON_DELIMS): """ Split `text` before passing it to :meth:`Completer.attr_matches` etc. @@ -75,6 +92,4 @@ def setup_readline(): else: readline.set_completer(JediRLCompleter().complete) readline.parse_and_bind("tab: complete") - - # Default delimiters minus "()": - readline.set_completer_delims(' \t\n`~!@#$%^&*-=+[{]}\\|;:\'",<>/?') + readline.set_completer_delims(_READLINE_JEDI_DELIMS) From 4bd03d0c74f0bb2afb5345ae3e7e792480526c26 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 19 May 2013 00:52:41 +0200 Subject: [PATCH 43/97] Document JediRLCompleter --- jedi/utils.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/jedi/utils.py b/jedi/utils.py index e957dbd3..37aa2225 100644 --- a/jedi/utils.py +++ b/jedi/utils.py @@ -26,6 +26,21 @@ else: 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] From 200d29713e048dcb6b79299bf070a85e39ddf164 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 19 May 2013 05:08:06 +0200 Subject: [PATCH 44/97] Make defined_names importable at top-level --- jedi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/__init__.py b/jedi/__init__.py index 0c29bca1..9c37f07c 100644 --- a/jedi/__init__.py +++ b/jedi/__init__.py @@ -43,7 +43,7 @@ import sys sys.path.insert(0, __path__[0]) from .api import Script, Interpreter, NotFoundError, set_debug_function, \ - preload_module + preload_module, defined_names from . import settings sys.path.pop(0) From 29f74c245d20990dd0e51dfd835285ce4a13d2d0 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 19 May 2013 04:42:01 +0200 Subject: [PATCH 45/97] Move tests for defined_names to test_defined_names.py --- test/test_defined_names.py | 60 ++++++++++++++++++++++++++++++++++++++ test/test_regression.py | 54 ---------------------------------- 2 files changed, 60 insertions(+), 54 deletions(-) create mode 100644 test/test_defined_names.py diff --git a/test/test_defined_names.py b/test/test_defined_names.py new file mode 100644 index 00000000..ee04d38e --- /dev/null +++ b/test/test_defined_names.py @@ -0,0 +1,60 @@ +""" +Tests for `api.defined_names`. +""" + +from jedi import api +from .base import TestBase + + +class TestDefinedNames(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']) diff --git a/test/test_regression.py b/test/test_regression.py index 5e111310..cc55773b 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -454,60 +454,6 @@ class TestFeature(TestBase): cache.parser_cache = temp_cache -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): """ Speed checks should typically be very tolerant. Some machines are From 6fadfb573caa7519429f784b6ccde05686cfe8e9 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 19 May 2013 04:46:27 +0200 Subject: [PATCH 46/97] Refactor TestDefinedNames --- test/test_defined_names.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/test/test_defined_names.py b/test/test_defined_names.py index ee04d38e..419252ed 100644 --- a/test/test_defined_names.py +++ b/test/test_defined_names.py @@ -8,51 +8,46 @@ from .base import TestBase class TestDefinedNames(TestBase): + def check_defined_names(self, source, names): + definitions = api.defined_names(source) + self.assertEqual([d.name for d in definitions], names) + return definitions + def test_get_definitions_flat(self): - definitions = api.defined_names(""" + self.check_defined_names(""" import module class Class: pass def func(): pass data = None - """) - self.assertEqual([d.name for d in definitions], - ['module', 'Class', 'func', 'data']) + """, ['module', 'Class', 'func', 'data']) def test_dotted_assignment(self): - definitions = api.defined_names(""" + self.check_defined_names(""" x = Class() x.y.z = None - """) - self.assertEqual([d.name for d in definitions], - ['x']) + """, ['x']) def test_multiple_assignment(self): - definitions = api.defined_names(""" + self.check_defined_names(""" x = y = None - """) - self.assertEqual([d.name for d in definitions], - ['x', 'y']) + """, ['x', 'y']) def test_multiple_imports(self): - definitions = api.defined_names(""" + self.check_defined_names(""" from module import a, b from another_module import * - """) - self.assertEqual([d.name for d in definitions], - ['a', 'b']) + """, ['a', 'b']) def test_nested_definitions(self): - definitions = api.defined_names(""" + definitions = self.check_defined_names(""" class Class: def f(): pass def g(): pass - """) - self.assertEqual([d.name for d in definitions], - ['Class']) + """, ['Class']) subdefinitions = definitions[0].defined_names() self.assertEqual([d.name for d in subdefinitions], ['f', 'g']) From e011683ea462e6ebc8fd7e08527b1cbf936ce508 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 19 May 2013 04:48:44 +0200 Subject: [PATCH 47/97] dedent source before passing it to defined_names test_nested_definitions fails now --- test/test_defined_names.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_defined_names.py b/test/test_defined_names.py index 419252ed..aeb13972 100644 --- a/test/test_defined_names.py +++ b/test/test_defined_names.py @@ -2,6 +2,8 @@ Tests for `api.defined_names`. """ +import textwrap + from jedi import api from .base import TestBase @@ -9,7 +11,7 @@ from .base import TestBase class TestDefinedNames(TestBase): def check_defined_names(self, source, names): - definitions = api.defined_names(source) + definitions = api.defined_names(textwrap.dedent(source)) self.assertEqual([d.name for d in definitions], names) return definitions From 6acf34efd3737cf72a5a44c5a49475d1a78a06c1 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Sun, 19 May 2013 05:07:23 +0200 Subject: [PATCH 48/97] Fix: api.defined_names was run against wrong scope when there is only one class is defined in the module. --- jedi/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jedi/api.py b/jedi/api.py index e148f350..5d2095e4 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -563,7 +563,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): From c5169b2d66ad359a41499770209fc6499cd27dfd Mon Sep 17 00:00:00 2001 From: David Halter Date: Sun, 19 May 2013 10:25:00 +0430 Subject: [PATCH 49/97] parsing.Parser.scope -> parsing.Parser._scope, fixes #224 --- jedi/parsing.py | 70 +++++++++++++++++----------------- jedi/parsing_representation.py | 8 ++-- test/test_regression.py | 2 +- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/jedi/parsing.py b/jedi/parsing.py index 5371f526..2772f679 100644 --- a/jedi/parsing.py +++ b/jedi/parsing.py @@ -56,7 +56,7 @@ class Parser(object): self.start_pos = self.end_pos = 1 + offset[0], offset[1] # initialize global Scope self.module = pr.SubModule(module_path, self.start_pos, top_module) - self.scope = self.module + self._scope = self.module self.current = (None, None) source = source + '\n' # end with \n, because the parser needs it @@ -390,7 +390,7 @@ class Parser(object): #print 'new_stat', set_vars, used_vars if self.freshscope and not self.no_docstr and len(tok_list) == 1 \ and self.last_token[0] == tokenize.STRING: - self.scope.add_docstr(self.last_token[1]) + self._scope.add_docstr(self.last_token[1]) return None, tok else: stmt = stmt_class(self.module, set_vars, used_vars, tok_list, @@ -408,7 +408,7 @@ class Parser(object): and len(stmt.token_list) == 1 and first_tok[0] == tokenize.STRING): # ... then set it as a docstring - self.scope.statements[-1].add_docstr(first_tok[1]) + self._scope.statements[-1].add_docstr(first_tok[1]) if tok in always_break + not_first_break: self._gen.push_last_back() @@ -429,7 +429,7 @@ class Parser(object): self.start_pos, self.end_pos = start_pos, end_pos except (StopIteration, common.MultiLevelStopIteration): # on finish, set end_pos correctly - s = self.scope + s = self._scope while s is not None: if isinstance(s, pr.Module) \ and not isinstance(s, pr.SubModule): @@ -443,8 +443,8 @@ class Parser(object): or self.user_scope is None and self.start_pos[0] >= self.user_position[0]): debug.dbg('user scope found [%s] = %s' % - (self.parserline.replace('\n', ''), repr(self.scope))) - self.user_scope = self.scope + (self.parserline.replace('\n', ''), repr(self._scope))) + self.user_scope = self._scope self.last_token = self.current self.current = (typ, tok) return self.current @@ -472,29 +472,29 @@ class Parser(object): #debug.dbg('main: tok=[%s] type=[%s] indent=[%s]'\ # % (tok, tokenize.tok_name[token_type], start_position[0])) - while token_type == tokenize.DEDENT and self.scope != self.module: + while token_type == tokenize.DEDENT and self._scope != self.module: token_type, tok = self.next() - if self.start_pos[1] <= self.scope.start_pos[1]: - self.scope.end_pos = self.start_pos - self.scope = self.scope.parent - if isinstance(self.scope, pr.Module) \ - and not isinstance(self.scope, pr.SubModule): - self.scope = self.module + if self.start_pos[1] <= self._scope.start_pos[1]: + self._scope.end_pos = self.start_pos + self._scope = self._scope.parent + if isinstance(self._scope, pr.Module) \ + and not isinstance(self._scope, pr.SubModule): + self._scope = self.module # check again for unindented stuff. this is true for syntax # errors. only check for names, because thats relevant here. If # some docstrings are not indented, I don't care. - while self.start_pos[1] <= self.scope.start_pos[1] \ + while self.start_pos[1] <= self._scope.start_pos[1] \ and (token_type == tokenize.NAME or tok in ['(', '['])\ - and self.scope != self.module: - self.scope.end_pos = self.start_pos - self.scope = self.scope.parent - if isinstance(self.scope, pr.Module) \ - and not isinstance(self.scope, pr.SubModule): - self.scope = self.module + and self._scope != self.module: + self._scope.end_pos = self.start_pos + self._scope = self._scope.parent + if isinstance(self._scope, pr.Module) \ + and not isinstance(self._scope, pr.SubModule): + self._scope = self.module - use_as_parent_scope = self.top_module if isinstance(self.scope, - pr.SubModule) else self.scope + use_as_parent_scope = self.top_module if isinstance(self._scope, + pr.SubModule) else self._scope first_pos = self.start_pos if tok == 'def': func = self._parse_function() @@ -503,7 +503,7 @@ class Parser(object): self.start_pos[0]) continue self.freshscope = True - self.scope = self.scope.add_scope(func, self._decorators) + self._scope = self._scope.add_scope(func, self._decorators) self._decorators = [] elif tok == 'class': cls = self._parse_class() @@ -511,7 +511,7 @@ class Parser(object): debug.warning("class: syntax error@%s" % self.start_pos[0]) continue self.freshscope = True - self.scope = self.scope.add_scope(cls, self._decorators) + self._scope = self._scope.add_scope(cls, self._decorators) self._decorators = [] # import stuff elif tok == 'import': @@ -522,7 +522,7 @@ class Parser(object): i = pr.Import(self.module, first_pos, end_pos, m, alias, defunct=defunct) self._check_user_stmt(i) - self.scope.add_import(i) + self._scope.add_import(i) if not imports: i = pr.Import(self.module, first_pos, self.end_pos, None, defunct=True) @@ -559,7 +559,7 @@ class Parser(object): alias, mod, star, relative_count, defunct=defunct or defunct2) self._check_user_stmt(i) - self.scope.add_import(i) + self._scope.add_import(i) self.freshscope = False #loops elif tok == 'for': @@ -569,7 +569,7 @@ class Parser(object): if tok == ':': s = [] if statement is None else [statement] f = pr.ForFlow(self.module, s, first_pos, set_stmt) - self.scope = self.scope.add_statement(f) + self._scope = self._scope.add_statement(f) else: debug.warning('syntax err, for flow started @%s', self.start_pos[0]) @@ -612,13 +612,13 @@ class Parser(object): # the flow statement, because a dedent releases the # main scope, so just take the last statement. try: - s = self.scope.statements[-1].set_next(f) + s = self._scope.statements[-1].set_next(f) except (AttributeError, IndexError): # If set_next doesn't exist, just add it. - s = self.scope.add_statement(f) + s = self._scope.add_statement(f) else: - s = self.scope.add_statement(f) - self.scope = s + s = self._scope.add_statement(f) + self._scope = s else: for i in inputs: i.parent = use_as_parent_scope @@ -629,7 +629,7 @@ class Parser(object): s = self.start_pos self.freshscope = False # add returns to the scope - func = self.scope.get_parent_until(pr.Function) + func = self._scope.get_parent_until(pr.Function) if tok == 'yield': func.is_generator = True @@ -646,7 +646,7 @@ class Parser(object): elif tok == 'global': stmt, tok = self._parse_statement(self.current) if stmt: - self.scope.add_statement(stmt) + self._scope.add_statement(stmt) for name in stmt.used_vars: # add the global to the top, because there it is # important. @@ -660,7 +660,7 @@ class Parser(object): elif tok == 'assert': stmt, tok = self._parse_statement() stmt.parent = use_as_parent_scope - self.scope.asserts.append(stmt) + self._scope.asserts.append(stmt) # default elif token_type in [tokenize.NAME, tokenize.STRING, tokenize.NUMBER] \ @@ -670,7 +670,7 @@ class Parser(object): # by the statement parser. stmt, tok = self._parse_statement(self.current) if stmt: - self.scope.add_statement(stmt) + self._scope.add_statement(stmt) self.freshscope = False else: if token_type not in [tokenize.COMMENT, tokenize.INDENT, diff --git a/jedi/parsing_representation.py b/jedi/parsing_representation.py index 52edd4c7..c7421d35 100644 --- a/jedi/parsing_representation.py +++ b/jedi/parsing_representation.py @@ -16,11 +16,11 @@ is the easiest way to write a parser. The same behaviour applies to ``Param``, which is being used in a function definition. The easiest way to play with this module is to use :class:`parsing.Parser`. -:attr:`parsing.Parser.scope` holds an instance of :class:`SubModule`: +:attr:`parsing.Parser.module` holds an instance of :class:`SubModule`: >>> from jedi.parsing import Parser >>> parser = Parser('import os', 'example.py') ->>> submodule = parser.scope +>>> submodule = parser.module >>> submodule @@ -247,14 +247,14 @@ class Scope(Simple, IsScope): ... b = y ... b.c = z ... ''') - >>> parser.scope.get_defined_names() + >>> parser.module.get_defined_names() [, ] Note that unlike :meth:`get_set_vars`, assignment to object attribute does not change the result because it does not change the defined names in this scope. - >>> parser.scope.get_set_vars() + >>> parser.module.get_set_vars() [, , ] """ diff --git a/test/test_regression.py b/test/test_regression.py index cc55773b..bf80dc8b 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -377,7 +377,7 @@ 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) From 384334ae064af6ca5a0a75c49b1f8b4cb3383b28 Mon Sep 17 00:00:00 2001 From: David Halter Date: Sun, 19 May 2013 20:40:54 +0430 Subject: [PATCH 50/97] goto on import statement (only import) raised an error --- jedi/api.py | 3 ++- test/test_regression.py | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index 5d2095e4..05eb60d9 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -342,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): diff --git a/test/test_regression.py b/test/test_regression.py index bf80dc8b..7ed276ac 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -314,14 +314,16 @@ 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 def test_goto_following_on_imports(self): s = "import multiprocessing.dummy; multiprocessing.dummy" From a9bf06987a1d9d00a653b5d0773fedad1ea16b0b Mon Sep 17 00:00:00 2001 From: David Halter Date: Sun, 19 May 2013 21:12:37 +0430 Subject: [PATCH 51/97] completion on empty import problem --- jedi/imports.py | 2 +- test/test_regression.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jedi/imports.py b/jedi/imports.py index 81669ba1..b0d6c5a7 100644 --- a/jedi/imports.py +++ b/jedi/imports.py @@ -162,9 +162,9 @@ class ImportPath(pr.Base): # If you edit e.g. gunicorn, there will be imports like this: # `from gunicorn import something`. But gunicorn is not in the # sys.path. Therefore look if gunicorn is a parent directory, #56. - parts = self.file_path.split(os.path.sep) in_path = [] if self.import_path: + parts = self.file_path.split(os.path.sep) for i, p in enumerate(parts): if p == self.import_path[0]: new = os.path.sep.join(parts[:i]) diff --git a/test/test_regression.py b/test/test_regression.py index 7ed276ac..04369bf0 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -324,6 +324,8 @@ class TestRegression(TestBase): # unrelated to #44 defs = self.goto_assignments("import") assert len(defs) == 0 + completions = self.completions("import", (1,1)) + assert len(completions) == 0 def test_goto_following_on_imports(self): s = "import multiprocessing.dummy; multiprocessing.dummy" From 5567a42334eb091f1ea0b13b232d87fb8f846261 Mon Sep 17 00:00:00 2001 From: David Halter Date: Tue, 21 May 2013 10:59:28 +0430 Subject: [PATCH 52/97] fix another keyword problem --- jedi/parsing.py | 3 ++- test/test_regression.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/jedi/parsing.py b/jedi/parsing.py index 2772f679..d402753e 100644 --- a/jedi/parsing.py +++ b/jedi/parsing.py @@ -659,7 +659,8 @@ class Parser(object): continue elif tok == 'assert': stmt, tok = self._parse_statement() - stmt.parent = use_as_parent_scope + 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, diff --git a/test/test_regression.py b/test/test_regression.py index 04369bf0..e72f0212 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -326,6 +326,9 @@ class TestRegression(TestBase): 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_following_on_imports(self): s = "import multiprocessing.dummy; multiprocessing.dummy" From dcf3f0dcdced9c44fea57899e8ac443f3946e852 Mon Sep 17 00:00:00 2001 From: David Halter Date: Tue, 21 May 2013 11:00:58 +0430 Subject: [PATCH 53/97] downloads badge, yay!!! --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 0abb4a2c..5f2beb9f 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,8 @@ Jedi - an awesome autocompletion library for Python :target: https://coveralls.io/r/davidhalter/jedi :alt: Coverage Status +.. image:: https://pypip.in/d/jedi/badge.png + :target: https://crate.io/packages/jedi/ Jedi is an autocompletion tool for Python that can be used in IDEs/editors. Jedi works. Jedi is fast. It understands all of the basic Python syntax From f4982606d87a4a110ac5da19d88064865193aa3d Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 21 May 2013 12:09:27 +0200 Subject: [PATCH 54/97] Refactor tests for full_name --- test/test_full_name.py | 45 +++++++++++++++++++++++++++++++++++++++++ test/test_regression.py | 23 --------------------- 2 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 test/test_full_name.py diff --git a/test/test_full_name.py b/test/test_full_name.py new file mode 100644 index 00000000..15a660c2 --- /dev/null +++ b/test/test_full_name.py @@ -0,0 +1,45 @@ +import textwrap + +import jedi +from jedi import api_classes +from .base import TestBase + + +class MixinTestFullName(object): + + 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 + + +def test_keyword_full_name_should_be_none(): + """issue #94""" + # Using `from jedi.keywords import Keyword` here does NOT work + # in Python 3. This is due to the import hack jedi using. + Keyword = api_classes.keywords.Keyword + d = api_classes.Definition(Keyword('(', (0, 0))) + assert d.full_name is None diff --git a/test/test_regression.py b/test/test_regression.py index bf80dc8b..5ccb1ff9 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -16,7 +16,6 @@ from .base import TestBase, unittest, cwd_at import jedi 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) @@ -410,28 +409,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): From 339ebbbf4eae486df288ad1ff2b8b79e0623287e Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 21 May 2013 12:30:21 +0200 Subject: [PATCH 55/97] Fix TestFullNameWithCompletions.test_from_import --- jedi/api_classes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jedi/api_classes.py b/jedi/api_classes.py index 04be77e7..6d4a9bdd 100644 --- a/jedi/api_classes.py +++ b/jedi/api_classes.py @@ -144,6 +144,8 @@ class BaseDefinition(object): path = [] if not isinstance(self._definition, keywords.Keyword): par = self._definition + with common.ignored(AttributeError): + path.append(par.alias) while par is not None: with common.ignored(AttributeError): path.insert(0, par.name) From 5d6719ed8c1a5c78a430dea8617e70c9db19096a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 21 May 2013 14:23:26 +0200 Subject: [PATCH 56/97] Add tests for defined_names + full_name --- test/test_full_name.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/test_full_name.py b/test/test_full_name.py index 15a660c2..3094f192 100644 --- a/test/test_full_name.py +++ b/test/test_full_name.py @@ -36,6 +36,28 @@ class TestFullNameWithCompletions(MixinTestFullName, TestBase): get_definitions = TestBase.completions +class TestFullDefinedName(TestBase): + + 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 From 851238386f90200f7de1a1ee638f70c35d9e7590 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 21 May 2013 13:49:23 +0200 Subject: [PATCH 57/97] Fix tests for defined_names + full_name --- jedi/api_classes.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/jedi/api_classes.py b/jedi/api_classes.py index 6d4a9bdd..d9ddbb29 100644 --- a/jedi/api_classes.py +++ b/jedi/api_classes.py @@ -142,11 +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 - with common.ignored(AttributeError): - path.append(par.alias) 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 From 91f3d524de8713b9d7007ef7a8b5bbc958ca3667 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 21 May 2013 16:57:40 +0200 Subject: [PATCH 58/97] Document test_full_name.py --- test/test_full_name.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/test_full_name.py b/test/test_full_name.py index 3094f192..f74c76d8 100644 --- a/test/test_full_name.py +++ b/test/test_full_name.py @@ -1,3 +1,19 @@ +""" +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 @@ -7,6 +23,12 @@ 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) @@ -38,6 +60,10 @@ class TestFullNameWithCompletions(MixinTestFullName, TestBase): 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] From 0ee2c1655178df1d030b7afc917a345c42591523 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Tue, 21 May 2013 20:16:44 +0200 Subject: [PATCH 59/97] Add TestDefinedNames.test_nested_class --- test/test_defined_names.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/test/test_defined_names.py b/test/test_defined_names.py index aeb13972..922fbe45 100644 --- a/test/test_defined_names.py +++ b/test/test_defined_names.py @@ -10,9 +10,12 @@ 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.assertEqual([d.name for d in definitions], names) + self.assert_definition_names(definitions, names) return definitions def test_get_definitions_flat(self): @@ -51,7 +54,22 @@ class TestDefinedNames(TestBase): pass """, ['Class']) subdefinitions = definitions[0].defined_names() - self.assertEqual([d.name for d in subdefinitions], - ['f', 'g']) + 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']) From 0fecb0a7800995a7ae28b6a7e2ee59624ed9a249 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 22 May 2013 22:33:50 +0200 Subject: [PATCH 60/97] Add sith.py, a script for random smoke test --- sith.py | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100755 sith.py diff --git a/sith.py b/sith.py new file mode 100755 index 00000000..c51552e0 --- /dev/null +++ b/sith.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python + +""" +Sith attacks (and helps debugging) Jedi. +""" + +import json +import os +import random +import sys +import traceback + +import jedi + + +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): + 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) + + +class RandomAtaccker(BaseAttacker): + + 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) + for (operation, args) in self.generate_attacks(maxtries, finder): + try: + self.attack(operation, *args) + except Exception: + self.add_record(sys.exc_info(), operation, args) + break + self.save_record(record) + + def add_arguments(self, parser): + super(RandomAtaccker, self).add_arguments(parser) + parser.add_argument( + '--maxtries', default=10000, type=int) + parser.add_argument( + 'rootpath', default='.', nargs='?', + help='root directory to look for Python files.') + + +class RedoAttacker(BaseAttacker): + + def do_run(self, record, recid): + self.load_record(record) + data = self.get_record(recid) + self.attack(data['operation'], *data['args']) + + def add_arguments(self, parser): + super(RedoAttacker, self).add_arguments(parser) + parser = parser.add_argument( + 'recid', default=0, type=int) + + +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, **kwds): + func(**kwds) + + def add_parser(self, attacker_class, *args, **kwds): + parser = self.subparsers.add_parser(*args, **kwds) + attacker = attacker_class() + attacker.add_arguments(parser) + + # Not required, just fore debugging: + self.parsers.append(parser) + self.attackers.append(attacker) + + def get_parser(self): + import argparse + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + '--record', default='record.json', + help='Exceptions are recorded in here.') + + self.subparsers = parser.add_subparsers() + self.add_parser(RandomAtaccker, 'random', help='Random attack') + self.add_parser(RedoAttacker, 'redo', help='Redo recorded attack') + + return parser + + +if __name__ == '__main__': + app = AttackApp() + app.run() From 311025258e872d24f412b9b5b4e9da4d82671c0a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 22 May 2013 22:51:34 +0200 Subject: [PATCH 61/97] Print exception --- sith.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sith.py b/sith.py index c51552e0..4d359fcb 100755 --- a/sith.py +++ b/sith.py @@ -4,6 +4,7 @@ Sith attacks (and helps debugging) Jedi. """ +from __future__ import print_function import json import os import random @@ -80,7 +81,20 @@ class BaseAttacker(object): parser.set_defaults(func=self.do_run) -class RandomAtaccker(BaseAttacker): +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 RandomAtaccker(MixinPrinter, BaseAttacker): operations = [ 'completions', 'goto_assignments', 'goto_definitions', 'usages', @@ -102,6 +116,7 @@ class RandomAtaccker(BaseAttacker): self.attack(operation, *args) except Exception: self.add_record(sys.exc_info(), operation, args) + self.print_record() break self.save_record(record) From 1e209aed37bb4cab8899b88d3e716a3a7161f946 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 22 May 2013 22:56:25 +0200 Subject: [PATCH 62/97] Add 'show' command --- sith.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/sith.py b/sith.py index 4d359fcb..e8259374 100755 --- a/sith.py +++ b/sith.py @@ -94,6 +94,17 @@ 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) + + def do_run(self, record, recid): + self.load_record(record) + + class RandomAtaccker(MixinPrinter, BaseAttacker): operations = [ @@ -129,17 +140,19 @@ class RandomAtaccker(MixinPrinter, BaseAttacker): help='root directory to look for Python files.') -class RedoAttacker(BaseAttacker): +class RedoAttacker(MixinLoader, BaseAttacker): def do_run(self, record, recid): - self.load_record(record) + super(RedoAttacker, self).do_run(record, recid) data = self.get_record(recid) self.attack(data['operation'], *data['args']) - def add_arguments(self, parser): - super(RedoAttacker, self).add_arguments(parser) - parser = parser.add_argument( - 'recid', default=0, type=int) + +class ShowRecord(MixinLoader, MixinPrinter, BaseAttacker): + + def do_run(self, record, recid): + super(ShowRecord, self).do_run(record, recid) + self.print_record() class AttackApp(object): @@ -174,6 +187,7 @@ class AttackApp(object): self.subparsers = parser.add_subparsers() self.add_parser(RandomAtaccker, 'random', help='Random attack') self.add_parser(RedoAttacker, 'redo', help='Redo recorded attack') + self.add_parser(ShowRecord, 'show', help='Show record') return parser From 6d026ec1af02712459aa013e74eee920cb6835cc Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 22 May 2013 23:11:50 +0200 Subject: [PATCH 63/97] Add --(i)pdb option --- sith.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/sith.py b/sith.py index e8259374..6ff37ca4 100755 --- a/sith.py +++ b/sith.py @@ -128,7 +128,7 @@ class RandomAtaccker(MixinPrinter, BaseAttacker): except Exception: self.add_record(sys.exc_info(), operation, args) self.print_record() - break + raise self.save_record(record) def add_arguments(self, parser): @@ -165,8 +165,17 @@ class AttackApp(object): parser = self.get_parser() self.do_run(**vars(parser.parse_args(args))) - def do_run(self, func, **kwds): - func(**kwds) + def do_run(self, func, debugger, **kwds): + try: + func(**kwds) + except: + exc_info = sys.exc_info() + if debugger == 'pdb': + import pdb + pdb.post_mortem(exc_info[2]) + elif debugger == 'ipdb': + import ipdb + ipdb.post_mortem(exc_info[2]) def add_parser(self, attacker_class, *args, **kwds): parser = self.subparsers.add_parser(*args, **kwds) @@ -183,6 +192,10 @@ class AttackApp(object): parser.add_argument( '--record', default='record.json', help='Exceptions are recorded in here.') + parser.add_argument( + '--pdb', dest='debugger', const='pdb', action='store_const') + parser.add_argument( + '--ipdb', dest='debugger', const='ipdb', action='store_const') self.subparsers = parser.add_subparsers() self.add_parser(RandomAtaccker, 'random', help='Random attack') From 2dee71ff4bbd82106ab27512d834ac0a5a352018 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 22 May 2013 23:14:32 +0200 Subject: [PATCH 64/97] Ignore jedi.NotFoundError --- sith.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sith.py b/sith.py index 6ff37ca4..e009bd32 100755 --- a/sith.py +++ b/sith.py @@ -125,6 +125,8 @@ class RandomAtaccker(MixinPrinter, BaseAttacker): for (operation, args) in self.generate_attacks(maxtries, finder): try: self.attack(operation, *args) + except jedi.NotFoundError: + pass except Exception: self.add_record(sys.exc_info(), operation, args) self.print_record() From 8bf5f9d539e74284f4e5957e47e2df0d1cf5423c Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 22 May 2013 23:31:42 +0200 Subject: [PATCH 65/97] Report attacking by "." --- sith.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/sith.py b/sith.py index e009bd32..6af7cabf 100755 --- a/sith.py +++ b/sith.py @@ -11,6 +11,11 @@ import random import sys import traceback +try: + from itertools import izip as zip +except ImportError: + pass + import jedi @@ -105,6 +110,31 @@ class MixinLoader(object): 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 RandomAtaccker(MixinPrinter, BaseAttacker): operations = [ @@ -122,13 +152,16 @@ class RandomAtaccker(MixinPrinter, BaseAttacker): 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 self.save_record(record) From cee0d4cf2fc5805bea8cc872a96414454dd8db3a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 22 May 2013 23:49:45 +0200 Subject: [PATCH 66/97] Generate better help / add more help --- sith.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/sith.py b/sith.py index 6af7cabf..971f07b8 100755 --- a/sith.py +++ b/sith.py @@ -2,6 +2,21 @@ """ Sith attacks (and helps debugging) Jedi. + +Randomly search Python files and run Jedi on it. Exception and used +arguments are recorded to ``./record.josn`` (specified by --record):: + + %(prog)s random + +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 @@ -85,6 +100,12 @@ class BaseAttacker(object): 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): @@ -104,7 +125,10 @@ class MixinLoader(object): def add_arguments(self, parser): super(MixinLoader, self).add_arguments(parser) parser = parser.add_argument( - 'recid', default=0, nargs='?', type=int) + '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) @@ -137,6 +161,10 @@ class AttackReporter(object): class RandomAtaccker(MixinPrinter, BaseAttacker): + """ + Randomly run Script().() against files under . + """ + operations = [ 'completions', 'goto_assignments', 'goto_definitions', 'usages', 'call_signatures'] @@ -177,6 +205,10 @@ class RandomAtaccker(MixinPrinter, BaseAttacker): 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) @@ -185,6 +217,10 @@ class RedoAttacker(MixinLoader, BaseAttacker): class ShowRecord(MixinLoader, MixinPrinter, BaseAttacker): + """ + Show recorded errors. + """ + def do_run(self, record, recid): super(ShowRecord, self).do_run(record, recid) self.print_record() @@ -213,8 +249,12 @@ class AttackApp(object): ipdb.post_mortem(exc_info[2]) def add_parser(self, attacker_class, *args, **kwds): - parser = self.subparsers.add_parser(*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: @@ -223,19 +263,23 @@ class AttackApp(object): def get_parser(self): import argparse - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) parser.add_argument( '--record', default='record.json', help='Exceptions are recorded in here.') parser.add_argument( - '--pdb', dest='debugger', const='pdb', action='store_const') + '--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') + '--ipdb', dest='debugger', const='ipdb', action='store_const', + help="Launch ipdb when error is raised.") self.subparsers = parser.add_subparsers() - self.add_parser(RandomAtaccker, 'random', help='Random attack') - self.add_parser(RedoAttacker, 'redo', help='Redo recorded attack') - self.add_parser(ShowRecord, 'show', help='Show record') + self.add_parser(RandomAtaccker, 'random') + self.add_parser(RedoAttacker, 'redo') + self.add_parser(ShowRecord, 'show') return parser From fd0ec772fbd8588c4fbdc8d4022b39f98e0eda17 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 00:20:11 +0200 Subject: [PATCH 67/97] Fix: record was not saved --- sith.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sith.py b/sith.py index 971f07b8..c40ff27a 100755 --- a/sith.py +++ b/sith.py @@ -192,7 +192,8 @@ class RandomAtaccker(MixinPrinter, BaseAttacker): reporter.error() self.print_record() raise - self.save_record(record) + finally: + self.save_record(record) def add_arguments(self, parser): super(RandomAtaccker, self).add_arguments(parser) From d246192df029a17491c48a17a8594bf106757359 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 00:49:20 +0200 Subject: [PATCH 68/97] Add short options --- sith.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sith.py b/sith.py index c40ff27a..df53c623 100755 --- a/sith.py +++ b/sith.py @@ -198,7 +198,7 @@ class RandomAtaccker(MixinPrinter, BaseAttacker): def add_arguments(self, parser): super(RandomAtaccker, self).add_arguments(parser) parser.add_argument( - '--maxtries', default=10000, type=int) + '--maxtries', '-l', default=10000, type=int) parser.add_argument( 'rootpath', default='.', nargs='?', help='root directory to look for Python files.') @@ -268,7 +268,7 @@ class AttackApp(object): formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__) parser.add_argument( - '--record', default='record.json', + '--record', '-R', default='record.json', help='Exceptions are recorded in here.') parser.add_argument( '--pdb', dest='debugger', const='pdb', action='store_const', From 286279f14a126272fc356cb5ca463292255cf274 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 00:53:45 +0200 Subject: [PATCH 69/97] Show default value for --record --- sith.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sith.py b/sith.py index df53c623..dbde8a84 100755 --- a/sith.py +++ b/sith.py @@ -269,7 +269,7 @@ class AttackApp(object): description=__doc__) parser.add_argument( '--record', '-R', default='record.json', - help='Exceptions are recorded in here.') + 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.") From ce92fd946b73f3af32fbc767874a5a1fab195926 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 01:11:58 +0200 Subject: [PATCH 70/97] Add failing test: test_goto_assignments_keyword --- test/test_regression.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_regression.py b/test/test_regression.py index 48e6b2ad..e44afb8d 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -329,6 +329,14 @@ class TestRegression(TestBase): 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" g = self.goto_assignments(s) From a4922774c0e1ab9fa171dc91ea69f3898684cada Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 01:15:30 +0200 Subject: [PATCH 71/97] Fix the previous error --- jedi/api.py | 4 +++- jedi/evaluate.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/jedi/api.py b/jedi/api.py index 05eb60d9..5eedd23c 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -391,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] diff --git a/jedi/evaluate.py b/jedi/evaluate.py index 38e0c86b..281b154d 100644 --- a/jedi/evaluate.py +++ b/jedi/evaluate.py @@ -775,7 +775,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 From 4d210af6bf5f910f0fbcd4b2ddbfaa299466ab31 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 23 May 2013 15:49:01 +0200 Subject: [PATCH 72/97] Fixed typo in sith docstring --- sith.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sith.py b/sith.py index dbde8a84..98bbe65b 100755 --- a/sith.py +++ b/sith.py @@ -4,7 +4,7 @@ Sith attacks (and helps debugging) Jedi. Randomly search Python files and run Jedi on it. Exception and used -arguments are recorded to ``./record.josn`` (specified by --record):: +arguments are recorded to ``./record.json`` (specified by --record):: %(prog)s random From 336ab323451d3e9a40167b631c95eab18ea3cd99 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 23 May 2013 16:23:08 +0200 Subject: [PATCH 73/97] Moved argparse import in sith.py to top (refs #231) --- sith.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sith.py b/sith.py index 98bbe65b..442c0f01 100755 --- a/sith.py +++ b/sith.py @@ -25,6 +25,7 @@ import os import random import sys import traceback +import argparse try: from itertools import izip as zip @@ -263,7 +264,6 @@ class AttackApp(object): self.attackers.append(attacker) def get_parser(self): - import argparse parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__) From 84ba8bbca11ab3e0bb6fd16ff794f8a3316e97d0 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 23 May 2013 16:24:39 +0200 Subject: [PATCH 74/97] Small doc improvement in sith.py --- sith.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sith.py b/sith.py index 442c0f01..bcaf49f4 100755 --- a/sith.py +++ b/sith.py @@ -6,7 +6,7 @@ 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 + %(prog)s random /path/to/sourcecode Redo recorded exception:: From dc5190abe21f0f19842f7c9e356800a6a59ee126 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 23 May 2013 16:27:25 +0200 Subject: [PATCH 75/97] Removed unused import from sith.py --- sith.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sith.py b/sith.py index bcaf49f4..91556bca 100755 --- a/sith.py +++ b/sith.py @@ -27,11 +27,6 @@ import sys import traceback import argparse -try: - from itertools import izip as zip -except ImportError: - pass - import jedi From 021940e46bc5948983338d45a3fba15be1bee638 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 23 May 2013 16:30:28 +0200 Subject: [PATCH 76/97] Fixed typo in sith.py classname --- sith.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sith.py b/sith.py index 91556bca..1f8e1558 100755 --- a/sith.py +++ b/sith.py @@ -19,7 +19,7 @@ Fallback to pdb when error is raised:: """ -from __future__ import print_function +from __future__ import print_function, division, unicode_literals import json import os import random @@ -155,7 +155,7 @@ class AttackReporter(object): .format(self.errors, self.tries)) -class RandomAtaccker(MixinPrinter, BaseAttacker): +class RandomAttacker(MixinPrinter, BaseAttacker): """ Randomly run Script().() against files under . @@ -192,7 +192,7 @@ class RandomAtaccker(MixinPrinter, BaseAttacker): self.save_record(record) def add_arguments(self, parser): - super(RandomAtaccker, self).add_arguments(parser) + super(RandomAttacker, self).add_arguments(parser) parser.add_argument( '--maxtries', '-l', default=10000, type=int) parser.add_argument( @@ -267,13 +267,13 @@ class AttackApp(object): 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.") + 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.") + help='Launch ipdb when error is raised.') self.subparsers = parser.add_subparsers() - self.add_parser(RandomAtaccker, 'random') + self.add_parser(RandomAttacker, 'random') self.add_parser(RedoAttacker, 'redo') self.add_parser(ShowRecord, 'show') From d9b2788ef8603613cffa0b7addef442c911bc1fb Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Thu, 23 May 2013 16:34:14 +0200 Subject: [PATCH 77/97] Fixed py26 ImportError in sith.py --- sith.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sith.py b/sith.py index 1f8e1558..bda0f663 100755 --- a/sith.py +++ b/sith.py @@ -25,7 +25,6 @@ import os import random import sys import traceback -import argparse import jedi @@ -281,5 +280,10 @@ class AttackApp(object): 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() From 346cb87830dd9c1c5829d5d9196f4ec10f55e30c Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 21:40:27 +0200 Subject: [PATCH 78/97] Print traceback in redo --- sith.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sith.py b/sith.py index bda0f663..34c70bea 100755 --- a/sith.py +++ b/sith.py @@ -208,7 +208,11 @@ class RedoAttacker(MixinLoader, BaseAttacker): def do_run(self, record, recid): super(RedoAttacker, self).do_run(record, recid) data = self.get_record(recid) - self.attack(data['operation'], *data['args']) + try: + self.attack(data['operation'], *data['args']) + except: + traceback.print_exc() + raise class ShowRecord(MixinLoader, MixinPrinter, BaseAttacker): From 3620633b2aeb0c24a2a5624fce46587f6baded1a Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 21:56:43 +0200 Subject: [PATCH 79/97] Turn off FS cache in sith.py by default --- sith.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/sith.py b/sith.py index 34c70bea..ca1e1804 100755 --- a/sith.py +++ b/sith.py @@ -28,6 +28,8 @@ import traceback import jedi +_unspecified = object() + class SourceCode(object): @@ -236,7 +238,12 @@ class AttackApp(object): parser = self.get_parser() self.do_run(**vars(parser.parse_args(args))) - def do_run(self, func, debugger, **kwds): + 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: @@ -274,6 +281,13 @@ class AttackApp(object): parser.add_argument( '--ipdb', dest='debugger', const='ipdb', action='store_const', help='Launch ipdb 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') From 636b10ab8e25224936115da4d33c97bcd770ad8c Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 22:41:36 +0200 Subject: [PATCH 80/97] Fix: 'NoneType' object has no attribute 'isinstance' --- jedi/evaluate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jedi/evaluate.py b/jedi/evaluate.py index 38e0c86b..67da3f04 100644 --- a/jedi/evaluate.py +++ b/jedi/evaluate.py @@ -318,7 +318,9 @@ def find_name(scope, name_str, position=None, search_global=False, exc = pr.Class, pr.Function until = lambda: par.parent.parent.get_parent_until(exc) - if par.isinstance(pr.Flow): + if par is None: + pass + elif par.isinstance(pr.Flow): if par.command == 'for': result += handle_for_loops(par) else: From ced1a83f5b8388d1ffbca763549c761c2292e957 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 23:08:28 +0200 Subject: [PATCH 81/97] Add a failing test: "a = b = 1"-style assignment --- test/completion/arrays.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/completion/arrays.py b/test/completion/arrays.py index 5ab96cae..ca020169 100644 --- a/test/completion/arrays.py +++ b/test/completion/arrays.py @@ -94,6 +94,16 @@ a4 b4 +# ----------------- +# multiple assignments +# ----------------- +a = b = 1 +#? int() +a +#? int() +b + + # ----------------- # unnessecary braces # ----------------- From b896f352adee99d84a6b4b58738bc94aae1d39d1 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 23:23:03 +0200 Subject: [PATCH 82/97] Support "a = b = 1"-style assignment --- jedi/evaluate.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/jedi/evaluate.py b/jedi/evaluate.py index 38e0c86b..b1e8bbe8 100644 --- a/jedi/evaluate.py +++ b/jedi/evaluate.py @@ -554,16 +554,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 +604,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) From 45bddec83a713712d2fd4395ef57586522e86204 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Thu, 23 May 2013 23:41:41 +0200 Subject: [PATCH 83/97] Add more complex assignment tests --- test/completion/arrays.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/completion/arrays.py b/test/completion/arrays.py index ca020169..3e22e933 100644 --- a/test/completion/arrays.py +++ b/test/completion/arrays.py @@ -103,6 +103,18 @@ a #? int() b +(a, b) = (c, (e, f)) = ('2', (3, 4)) +#? str() +a +#? tuple() +b +#? str() +c +#? int() +e +#? int() +f + # ----------------- # unnessecary braces From 91c605b7f0d9a509fe930ed01785c1930a14b493 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 18:58:34 +0200 Subject: [PATCH 84/97] Add --pudb option to sith.py --- sith.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sith.py b/sith.py index ca1e1804..659bbb3b 100755 --- a/sith.py +++ b/sith.py @@ -254,6 +254,9 @@ class AttackApp(object): elif debugger == 'ipdb': import ipdb ipdb.post_mortem(exc_info[2]) + elif debugger == 'pudb': + import pudb + pudb.post_mortem(exc_info) def add_parser(self, attacker_class, *args, **kwds): attacker = attacker_class() @@ -281,6 +284,9 @@ class AttackApp(object): 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=""" From 788eeb9bd5516c330a551d71664b45c69f846dce Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 19:10:19 +0200 Subject: [PATCH 85/97] Simplify how post_mortem is launched --- sith.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/sith.py b/sith.py index 659bbb3b..2f1eda18 100755 --- a/sith.py +++ b/sith.py @@ -247,16 +247,10 @@ class AttackApp(object): try: func(**kwds) except: - exc_info = sys.exc_info() - if debugger == 'pdb': - import pdb - pdb.post_mortem(exc_info[2]) - elif debugger == 'ipdb': - import ipdb - ipdb.post_mortem(exc_info[2]) - elif debugger == 'pudb': - import pudb - pudb.post_mortem(exc_info) + if debugger: + einfo = sys.exc_info() + pdb = __import__(debugger) + pdb.post_mortem(einfo if debugger == 'pudb' else einfo[2]) def add_parser(self, attacker_class, *args, **kwds): attacker = attacker_class() From 105bb2b1ca9d76d377ed24a10bc7ff0c05a219f3 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 19:43:23 +0200 Subject: [PATCH 86/97] ModuleWithCursor.get_path_until_cursor cannot handle "\" It raises: IndexError: string index out of range --- test/test_regression.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/test_regression.py b/test/test_regression.py index e44afb8d..1e72cadf 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -395,6 +395,22 @@ class TestRegression(TestBase): for c in s.get_commands(): self.assertEqual(c.execution.end_pos[1], i) + def test_backslash_continuation(self): + """ + Test that ModuleWithCursor.get_path_until_cursor handles continuation + """ + source = textwrap.dedent(r""" + x = 0 + a = \ + [1, 2, 3, 4, 5, 6, 7, 8, 9, x] # <-- here + """) + for (i, line) in enumerate(source.splitlines()): + if '<-- here' in line: + break + column = len(line) - len('] # <-- here') + defs = self.goto_definitions(source, (i + 1, column)) + self.assertEqual([d.name for d in defs], ['int']) + class TestDocstring(TestBase): From 05564c23d58e2c595604b5f48450d8986b5ecb92 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 21:06:48 +0200 Subject: [PATCH 87/97] Fix the error --- jedi/modules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jedi/modules.py b/jedi/modules.py index 37e5bbac..46195c80 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -141,6 +141,7 @@ class ModuleWithCursor(Module): last_line = self.get_line(self._line_temp) if last_line and last_line[-1] == '\\': line = last_line[:-1] + ' ' + line + self._line_length = len(last_line) else: break return line[::-1] From 71455f6b31ac36bbf276852a5a1b84adad133ce9 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 22:05:12 +0200 Subject: [PATCH 88/97] Add another failing case --- test/test_regression.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/test/test_regression.py b/test/test_regression.py index 1e72cadf..3753d0bf 100755 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -395,21 +395,42 @@ class TestRegression(TestBase): 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 """ - source = textwrap.dedent(r""" + self.check_definition_by_marker(r""" x = 0 a = \ [1, 2, 3, 4, 5, 6, 7, 8, 9, x] # <-- here - """) - for (i, line) in enumerate(source.splitlines()): - if '<-- here' in line: - break - column = len(line) - len('] # <-- here') - defs = self.goto_definitions(source, (i + 1, column)) - self.assertEqual([d.name for d in defs], ['int']) + """, '] # <-- 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): From 1f3c4700c9c581bf3f64cf570989788636f259d6 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 22:09:24 +0200 Subject: [PATCH 89/97] Fix get_path_until_cursor for empty path + continuation --- jedi/modules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jedi/modules.py b/jedi/modules.py index 46195c80..c583ec5f 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -188,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] From a94642b9c0f0762fe4079052fd7f53d9f4813319 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 23:00:43 +0200 Subject: [PATCH 90/97] Add testenv:sith in tox.ini --- sith.py | 4 ++++ tox.ini | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/sith.py b/sith.py index 2f1eda18..f903eb61 100755 --- a/sith.py +++ b/sith.py @@ -86,6 +86,9 @@ class BaseAttacker(object): return self.record['data'][recid] def save_record(self, path): + directory = os.path.dirname(path) + if not os.path.isdir(directory): + os.makedirs(directory) with open(path, 'w') as f: json.dump(self.record, f) @@ -251,6 +254,7 @@ class AttackApp(object): 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() diff --git a/tox.ini b/tox.ini index c4d86518..8eb4e27c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,3 +16,8 @@ deps = commands = coverage run --source jedi -m py.test coverage report +[testenv:sith] +deps = + argparse +commands = + {envpython} sith.py --record {envtmpdir}/record.json random {posargs:jedi} From 96ca86e9f7590f7fadba46521e9c9a7e35139852 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 23:01:35 +0200 Subject: [PATCH 91/97] Run random smoke test at Travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 29c03f1a..adf32985 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,11 @@ env: - TOXENV=py32 - TOXENV=py33 - TOXENV=cov + - TOXENV=sith matrix: allow_failures: - env: TOXENV=cov + - env: TOXENV=sith install: - pip install --quiet --use-mirrors tox script: From 868a4b5dd86df7aaf4751d9867ed4fc572479189 Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Fri, 24 May 2013 23:19:45 +0200 Subject: [PATCH 92/97] Fix sith.py for default --record --- sith.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sith.py b/sith.py index f903eb61..6f07d882 100755 --- a/sith.py +++ b/sith.py @@ -86,7 +86,7 @@ class BaseAttacker(object): return self.record['data'][recid] def save_record(self, path): - directory = os.path.dirname(path) + directory = os.path.dirname(os.path.abspath(path)) if not os.path.isdir(directory): os.makedirs(directory) with open(path, 'w') as f: From 06cd9752bda5247cf229d451899ef95f2daaea15 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Mon, 27 May 2013 10:33:20 +0200 Subject: [PATCH 93/97] Added YCM to plugin list --- README.rst | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 5f2beb9f..d9e11212 100644 --- a/README.rst +++ b/README.rst @@ -26,12 +26,15 @@ which uses Jedi's autocompletion. I encourage you to use Jedi in your IDEs. It's really easy. If there are any problems (also with licensing), just contact me. -Jedi can be used with the following plugins/software: +Jedi can be used with the following editors: -- `VIM-Plugin `_ -- `Emacs-Plugin `_ -- `Sublime-Plugin `_ -- `wdb (web debugger) `_ +- Vim (jedi-vim_, YouCompleteMe_) +- Emacs (emacs-jedi_) +- Sublime Text (SublimeJEDI_) + +And it powers the following projects: + +- wdb_ Here are some pictures: @@ -123,3 +126,10 @@ Tests are also run automatically on `Travis CI For more detailed information visit the `testing documentation `_ + + +.. _jedi-vim: https://github.com/davidhalter/jedi-vim +.. _youcompleteme: http://valloric.github.io/YouCompleteMe/ +.. _emacs-jedi: https://github.com/tkf/emacs-jedi +.. _sublimejedi: https://github.com/svaiter/SublimeJEDI +.. _wdb: https://github.com/Kozea/wdb From 97ef7b00db103e446e8fe4b4981bd986efa1f6e5 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Mon, 27 May 2013 10:38:43 +0200 Subject: [PATCH 94/97] Added YCM link to docs too --- docs/index.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 9f53f667..c6b03d35 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -45,9 +45,18 @@ Resources Editor Plugins -------------- -- `Vim `_ -- `Emacs `_ -- `Sublime Text 2 `_ +Vim: + +- `jedi-vim `_ +- `YouCompleteMe `_ + +Emacs: + +- `emacs-jedi `_ + +Sublime Text 2: + +- `SublimeJEDI `_ .. _other-software: From c5a2ba3d353937f6bbd6659d25100d91ccffa2a5 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Mon, 27 May 2013 10:40:43 +0200 Subject: [PATCH 95/97] s/emacs-jedi/Jedi.el/g --- README.rst | 4 ++-- docs/index.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d9e11212..096b3cd4 100644 --- a/README.rst +++ b/README.rst @@ -29,7 +29,7 @@ me. Jedi can be used with the following editors: - Vim (jedi-vim_, YouCompleteMe_) -- Emacs (emacs-jedi_) +- Emacs (Jedi.el_) - Sublime Text (SublimeJEDI_) And it powers the following projects: @@ -130,6 +130,6 @@ For more detailed information visit the `testing documentation .. _jedi-vim: https://github.com/davidhalter/jedi-vim .. _youcompleteme: http://valloric.github.io/YouCompleteMe/ -.. _emacs-jedi: https://github.com/tkf/emacs-jedi +.. _Jedi.el: https://github.com/tkf/emacs-jedi .. _sublimejedi: https://github.com/svaiter/SublimeJEDI .. _wdb: https://github.com/Kozea/wdb diff --git a/docs/index.rst b/docs/index.rst index c6b03d35..b6bb2755 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -52,7 +52,7 @@ Vim: Emacs: -- `emacs-jedi `_ +- `Jedi.el `_ Sublime Text 2: From 5de63873df761f00f28e0a116b9e3c20e0da748b Mon Sep 17 00:00:00 2001 From: Takafumi Arakaki Date: Wed, 19 Jun 2013 15:38:24 +0200 Subject: [PATCH 96/97] Fix and improve settings.cache_directory document fixes #244 --- jedi/settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jedi/settings.py b/jedi/settings.py index 666547c9..3f3598c8 100644 --- a/jedi/settings.py +++ b/jedi/settings.py @@ -127,8 +127,10 @@ cache_directory = os.path.expanduser(_cache_directory) """ The path where all the caches can be found. -On Linux, this defaults to ``~/.cache/jedi/``, on OS X to ``~/.jedi/`` and on -Windows to ``%APPDATA%\\Jedi\\Jedi\\``. +On Linux, this defaults to ``~/.cache/jedi/``, on OS X to +``~/Library/Caches/Jedi/`` and on Windows to ``%APPDATA%\\Jedi\\Jedi\\``. +On Linux, if environment variable ``$XDG_CACHE_HOME`` is set, +``$XDG_CACHE_HOME/jedi`` is used instead of the default one. """ # ---------------- From aa5bb539c13d708ab8ac49478511dced23cd2963 Mon Sep 17 00:00:00 2001 From: David Halter Date: Thu, 20 Jun 2013 11:25:33 +0200 Subject: [PATCH 97/97] added new logo, designed by @ganwell, refs #98 --- docs/_static/logo.png | Bin 21786 -> 25620 bytes docs/conf.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_static/logo.png b/docs/_static/logo.png index f56374e3ddf5d43870f985d1db32eeabd9054570..0c77f119e44333b08d04add593ac187f8adc71f0 100644 GIT binary patch literal 25620 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