diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..c48bafc2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,18 @@ +[run] +omit = + jedi/_compatibility.py + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: diff --git a/.gitignore b/.gitignore index ad9b77e4..4755f455 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ *~ *.swp *.swo +*.pyc .ropeproject .tox -*.pyc +.coveralls.yml +.coverage /build/ /docs/_build/ /dist/ diff --git a/.travis.yml b/.travis.yml index e1529542..16d38fab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,17 @@ env: - TOXENV=py26 - TOXENV=py27 - TOXENV=py32 + - TOXENV=py33 + - TOXENV=cov +matrix: + allow_failures: + - env: TOXENV=cov install: - pip install --quiet --use-mirrors tox script: - tox +after_script: + - if [ $TOXENV == "cov" ]; then + pip install --quiet --use-mirrors coveralls; + coveralls; + fi diff --git a/README.rst b/README.rst index dc32f115..9ed17efe 100644 --- a/README.rst +++ b/README.rst @@ -6,6 +6,11 @@ Jedi - an awesome autocompletion library for Python :target: http://travis-ci.org/davidhalter/jedi :alt: Travis-CI build status +.. image:: https://coveralls.io/repos/davidhalter/jedi/badge.png?branch=master + :target: https://coveralls.io/r/davidhalter/jedi + :alt: Coverage Status + + 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 elements including many builtin functions. @@ -86,3 +91,25 @@ API for IDEs It's very easy to create an editor plugin that uses Jedi. See https://jedi.readthedocs.org/en/latest/docs/plugin-api.html for more information. + + +Testing +======= + +The test suite depends on ``tox`` and ``pytest``:: + + pip install tox pytest + +To run the tests for all supported Python versions:: + + PIP_INSECURE=t tox + +If you want to test only a specific Python version (e.g. Python 2.7), it's as +easy as :: + + tox -e py27 + +The ``PIP_INSECURE=t`` env variable is only needed for the ``py25`` target. + +Tests are also run automatically on `Travis CI +`_. diff --git a/docs/docs/history.rst b/docs/docs/history.rst index df2ecc69..227cd47d 100644 --- a/docs/docs/history.rst +++ b/docs/docs/history.rst @@ -5,7 +5,7 @@ A little history The Star Wars Jedi are awesome. My Jedi software tries to imitate a little bit of the precognition the Jedi have. There's even an awesome `scene -`_ of Monty Python Jedi's :-). +`_ of Monty Python Jedis :-). But actually the name hasn't so much to do with Star Wars. It's part of my second name. @@ -13,13 +13,13 @@ second name. After I explained Guido van Rossum, how some parts of my auto-completion work, he said (we drank a beer or two): - *Oh, that worries me* + *"Oh, that worries me..."* When it's finished, I hope he'll like it :-) -I actually started Jedi, because there were no good solutions available for -VIM. Most auto-completions just didn't work well. The only good solution was -PyCharm. I just like my good old VIM. Rope was never really intended to be an +I actually started Jedi, because there were no good solutions available for VIM. +Most auto-completions just didn't work well. The only good solution was PyCharm. +But I like my good old VIM. Rope was never really intended to be an auto-completion (and also I really hate project folders for my Python scripts). It's more of a refactoring suite. So I decided to do my own version of a completion, which would execute non-dangerous code. But I soon realized, that diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 9a704304..29aef154 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -7,11 +7,69 @@ Most of the code here is necessary to support Python 2.5. Once this dependency will be dropped, we'll get rid of most code. """ import sys +import imp +import os +try: + import importlib +except: + pass is_py3k = sys.hexversion >= 0x03000000 - +is_py33 = sys.hexversion >= 0x03030000 is_py25 = sys.hexversion < 0x02060000 +def find_module_py33(string, path=None): + mod_info = (None, None, None) + loader = None + if path is not None: + # Check for the module in the specidied path + loader = importlib.machinery.PathFinder.find_module(string, path) + else: + # Check for the module in sys.path + loader = importlib.machinery.PathFinder.find_module(string, sys.path) + if loader is None: + # Fallback to find builtins + loader = importlib.find_loader(string) + + if loader is None: + raise ImportError + + try: + if (loader.is_package(string)): + mod_info = (None, os.path.dirname(loader.path), True) + else: + filename = loader.get_filename(string) + if filename and os.path.exists(filename): + mod_info = (open(filename, 'U'), filename, False) + else: + mod_info = (None, filename, False) + except AttributeError: + mod_info = (None, loader.load_module(string).__name__, False) + + return mod_info + +def find_module_pre_py33(string, path=None): + mod_info = None + if path is None: + mod_info = imp.find_module(string) + else: + mod_info = imp.find_module(string, path) + + return (mod_info[0], mod_info[1], mod_info[2][2] == imp.PKG_DIRECTORY) + +def find_module(string, path=None): + """Provides information about a module. + + This function isolates the differences in importing libraries introduced with + python 3.3 on; it gets a module name and optionally a path. It will return a + tuple containin an open file for the module (if not builtin), the filename + or the name of the module if it is a builtin one and a boolean indicating + if the module is contained in a package.""" + if is_py33: + return find_module_py33(string, path) + else: + return find_module_pre_py33(string, path) + # next was defined in python 2.6, in python 3 obj.next won't be possible # anymore try: diff --git a/jedi/api_classes.py b/jedi/api_classes.py index 842fe195..77d4a489 100644 --- a/jedi/api_classes.py +++ b/jedi/api_classes.py @@ -3,6 +3,7 @@ The :mod:`api_classes` module contains the return classes of the API. These classes are the much bigger part of the whole API, because they contain the interesting information about completion and goto operations. """ +from __future__ import with_statement import re import os @@ -11,6 +12,7 @@ import functools from jedi._compatibility import unicode, next from jedi import settings +from jedi import common from jedi import parsing_representation as pr from jedi import cache import keywords @@ -144,10 +146,8 @@ class BaseDefinition(object): if not isinstance(self.definition, keywords.Keyword): par = self.definition while par is not None: - try: + with common.ignored(AttributeError): path.insert(0, par.name) - except AttributeError: - pass par = par.parent return path @@ -295,10 +295,8 @@ class BaseDefinition(object): if not path: return None # for keywords the path is empty - try: + with common.ignored(KeyError): path[0] = self._mapping[path[0]] - except KeyError: - pass for key, repl in self._tuple_mapping.items(): if tuple(path[:len(key)]) == key: path = [repl] + path[len(key):] diff --git a/jedi/cache.py b/jedi/cache.py index 2efc764d..e2c0c236 100644 --- a/jedi/cache.py +++ b/jedi/cache.py @@ -21,10 +21,16 @@ from __future__ import with_statement import time import os import sys -import pickle +import hashlib +try: + import cPickle as pickle +except: + import pickle +import shutil from jedi._compatibility import json from jedi import settings +from jedi import common from jedi import debug # memoize caches will be deleted after every action @@ -143,12 +149,10 @@ def cache_function_definition(stmt): def cache_star_import(func): def wrapper(scope, *args, **kwargs): - try: + with common.ignored(KeyError): mods = star_import_cache[scope] if mods[0] + settings.star_import_cache_validity > time.time(): return mods[1] - except KeyError: - pass # cache is too old and therefore invalid or not available invalidate_star_import_cache(scope) mods = func(scope, *args, **kwargs) @@ -160,15 +164,13 @@ def cache_star_import(func): def invalidate_star_import_cache(module, only_main=False): """ Important if some new modules are being reparsed """ - try: + with common.ignored(KeyError): t, mods = star_import_cache[module] del star_import_cache[module] for m in mods: invalidate_star_import_cache(m, only_main=True) - except KeyError: - pass if not only_main: # We need a list here because otherwise the list is being changed @@ -216,13 +218,36 @@ def save_module(path, name, parser, pickling=True): class _ModulePickling(object): + + version = 2 + """ + Version number (integer) for file system cache. + + Increment this number when there are any incompatible changes in + parser representation classes. For example, the following changes + are regarded as incompatible. + + - Class name is changed. + - Class is moved to another module. + - Defined slot of the class is changed. + """ + def __init__(self): self.__index = None - self.py_version = '%s.%s' % sys.version_info[:2] + self.py_tag = 'cpython-%s%s' % sys.version_info[:2] + """ + Short name for distinguish Python implementations and versions. + + It's like `sys.implementation.cache_tag` but for Python < 3.3 + we generate something similar. See: + http://docs.python.org/3/library/sys.html#sys.implementation + + .. todo:: Detect interpreter (e.g., PyPy). + """ def load_module(self, path, original_changed_time): try: - pickle_changed_time = self._index[self.py_version][path] + pickle_changed_time = self._index[path] except KeyError: return None if original_changed_time is not None \ @@ -240,10 +265,10 @@ class _ModulePickling(object): def save_module(self, path, parser_cache_item): self.__index = None try: - files = self._index[self.py_version] + files = self._index except KeyError: files = {} - self._index[self.py_version] = files + self._index = files with open(self._get_hashed_path(path), 'wb') as f: pickle.dump(parser_cache_item, f, pickle.HIGHEST_PROTOCOL) @@ -256,9 +281,16 @@ class _ModulePickling(object): if self.__index is None: try: with open(self._get_path('index.json')) as f: - self.__index = json.load(f) + data = json.load(f) except IOError: self.__index = {} + else: + # 0 means version is not defined (= always delete cache): + if data.get('version', 0) != self.version: + self.delete_cache() + self.__index = {} + else: + self.__index = data['index'] return self.__index def _remove_old_modules(self): @@ -269,18 +301,25 @@ class _ModulePickling(object): self._index # reload index def _flush_index(self): + data = {'version': self.version, 'index': self._index} with open(self._get_path('index.json'), 'w') as f: - json.dump(self._index, f) + json.dump(data, f) self.__index = None + def delete_cache(self): + shutil.rmtree(self._cache_directory()) + def _get_hashed_path(self, path): - return self._get_path('%s_%s.pkl' % (self.py_version, hash(path))) + return self._get_path('%s.pkl' % hashlib.md5(path.encode("utf-8")).hexdigest()) def _get_path(self, file): - dir = settings.cache_directory + dir = self._cache_directory() if not os.path.exists(dir): os.makedirs(dir) - return dir + os.path.sep + file + return os.path.join(dir, file) + + def _cache_directory(self): + return os.path.join(settings.cache_directory, self.py_tag) # is a singleton diff --git a/jedi/common.py b/jedi/common.py index 08eadd6b..ebd1f25d 100644 --- a/jedi/common.py +++ b/jedi/common.py @@ -163,3 +163,13 @@ def indent_block(text, indention=' '): text = text[:-1] lines = text.split('\n') return '\n'.join(map(lambda s: indention + s, lines)) + temp + + +@contextlib.contextmanager +def ignored(*exceptions): + """Context manager that ignores all of the specified exceptions. This will + be in the standard library starting with Python 3.4.""" + try: + yield + except exceptions: + pass diff --git a/jedi/dynamic.py b/jedi/dynamic.py index 8cc0b3eb..bd87eeb2 100644 --- a/jedi/dynamic.py +++ b/jedi/dynamic.py @@ -59,6 +59,7 @@ from jedi import cache from jedi import parsing_representation as pr from jedi import modules from jedi import settings +from jedi import common from jedi import debug from jedi import fast_parser import api_classes @@ -487,10 +488,8 @@ def related_name_add_import_modules(definitions, search_name): for d in definitions: if isinstance(d.parent, pr.Import): s = imports.ImportPath(d.parent, direct_resolve=True) - try: + with common.ignored(IndexError): new.add(s.follow(is_goto=True)[0]) - except IndexError: - pass return set(definitions) | new diff --git a/jedi/evaluate.py b/jedi/evaluate.py index c76bbb27..87d59afa 100644 --- a/jedi/evaluate.py +++ b/jedi/evaluate.py @@ -68,6 +68,7 @@ backtracking algorithm. .. todo:: nonlocal statement, needed or can be ignored? (py3k) """ +from __future__ import with_statement import sys import itertools @@ -429,11 +430,9 @@ def find_name(scope, name_str, position=None, search_global=False, if isinstance(scope, (er.Instance, er.Class)) \ and hasattr(r, 'get_descriptor_return'): # handle descriptors - try: + with common.ignored(KeyError): res_new += r.get_descriptor_return(scope) continue - except KeyError: - pass res_new.append(r) return res_new @@ -462,19 +461,15 @@ def check_getattr(inst, name_str): # str is important to lose the NamePart! module = builtin.Builtin.scope name = pr.Call(module, str(name_str), pr.Call.STRING, (0, 0), inst) - try: + with common.ignored(KeyError): result = inst.execute_subscope_by_name('__getattr__', [name]) - except KeyError: - pass if not result: # this is a little bit special. `__getattribute__` is executed # before anything else. But: I know no use case, where this # could be practical and the jedi would return wrong types. If # you ever have something, let me know! - try: + with common.ignored(KeyError): result = inst.execute_subscope_by_name('__getattribute__', [name]) - except KeyError: - pass return result @@ -536,10 +531,8 @@ def assign_tuples(tup, results, seek_name): debug.warning("invalid tuple lookup %s of result %s in %s" % (tup, results, seek_name)) else: - try: + with common.ignored(IndexError): types += func(index) - except IndexError: - pass return types result = [] @@ -648,11 +641,9 @@ def follow_call_list(call_list, follow_array=False): call = next(calls_iterator) except StopIteration: break - try: + with common.ignored(AttributeError): if str(call.name) == 'else': break - except AttributeError: - pass continue result += follow_call(call) elif call == '*': diff --git a/jedi/evaluate_representation.py b/jedi/evaluate_representation.py index f5c6e991..d5402e4b 100644 --- a/jedi/evaluate_representation.py +++ b/jedi/evaluate_representation.py @@ -9,6 +9,8 @@ instantiated. This class represents these cases. So, why is there also a ``Class`` class here? Well, there are decorators and they change classes in Python 3. """ +from __future__ import with_statement + import copy import itertools @@ -61,10 +63,8 @@ class Instance(use_metaclass(cache.CachedMetaClass, Executable)): else: # need to execute the __init__ function, because the dynamic param # searching needs it. - try: + with common.ignored(KeyError): self.execute_subscope_by_name('__init__', self.var_args) - except KeyError: - pass # Generated instances are classes that are just generated by self # (No var_args) used. self.is_generated = False @@ -769,9 +769,13 @@ class Generator(use_metaclass(cache.CachedMetaClass, pr.Base)): debug.warning('Tried to get array access on a generator', self) return [] - @property - def parent(self): - return self.func.parent + def __getattr__(self, name): + if name not in ['start_pos', 'end_pos', 'parent', 'get_imports', + 'asserts', 'doc', 'docstr', 'get_parent_until', 'get_code', + 'subscopes']: + raise AttributeError("Accessing %s of %s is not allowed." + % (self, name)) + return getattr(self.func, name) def __repr__(self): return "<%s of %s>" % (type(self).__name__, self.func) @@ -800,10 +804,8 @@ class Array(use_metaclass(cache.CachedMetaClass, pr.Base)): if isinstance(index, Instance) \ and str(index.name) in ['int', 'str'] \ and len(index.var_args) == 1: - try: + with common.ignored(KeyError, IndexError): return self.get_exact_index_types(index.var_args[0]) - except (KeyError, IndexError): - pass result = list(self._follow_values(self._array.values)) result += dynamic.check_array_additions(self) diff --git a/jedi/helpers.py b/jedi/helpers.py index c4532961..a61027c5 100644 --- a/jedi/helpers.py +++ b/jedi/helpers.py @@ -1,5 +1,8 @@ +from __future__ import with_statement + import copy +from jedi import common from jedi import parsing_representation as pr @@ -21,13 +24,11 @@ def fast_parent_copy(obj): before = () for cls in new_obj.__class__.__mro__: - try: + with common.ignored(AttributeError): if before == cls.__slots__: continue before = cls.__slots__ items += [(n, getattr(new_obj, n)) for n in before] - except AttributeError: - pass for key, value in items: # replace parent (first try _parent and then parent) @@ -35,10 +36,8 @@ def fast_parent_copy(obj): if key == 'parent' and '_parent' in items: # parent can be a property continue - try: + with common.ignored(KeyError): setattr(new_obj, key, new_elements[value]) - except KeyError: - pass elif key in ['parent_function', 'use_as_parent', '_sub_module']: continue elif isinstance(value, list): diff --git a/jedi/imports.py b/jedi/imports.py index 72a3d0f6..d6081386 100644 --- a/jedi/imports.py +++ b/jedi/imports.py @@ -5,23 +5,22 @@ any actual importing done. This module is about finding modules in the filesystem. This can be quite tricky sometimes, because Python imports are not always that simple. -Currently the import process uses ``imp`` to find modules. In the future, it's -a goal to use ``importlib`` for this purpose. There's a `pull request -`_ for that. +This module uses imp for python up to 3.2 and importlib for python 3.3 on; the +correct implementation is delegated to _compatibility. This module also supports import autocompletion, which means to complete statements like ``from datetim`` (curser at the end would return ``datetime``). """ - from __future__ import with_statement import os import pkgutil -import imp import sys import itertools +from jedi._compatibility import find_module from jedi import modules +from jedi import common from jedi import debug from jedi import parsing_representation as pr from jedi import cache @@ -123,11 +122,9 @@ class ImportPath(pr.Base): if self.import_stmt.relative_count: rel_path = self.get_relative_path() + '/__init__.py' - try: + with common.ignored(IOError): m = modules.Module(rel_path) names += m.parser.module.get_defined_names() - except IOError: - pass else: if on_import_stmt and isinstance(scope, pr.Module) \ and scope.path.endswith('__init__.py'): @@ -238,20 +235,22 @@ class ImportPath(pr.Base): global imports_processed imports_processed += 1 + importing = None if path is not None: - return imp.find_module(string, [path]) + importing = find_module(string, [path]) else: debug.dbg('search_module', string, self.file_path) # Override the sys.path. It works only good that way. # Injecting the path directly into `find_module` did not work. sys.path, temp = sys_path_mod, sys.path try: - i = imp.find_module(string) + importing = find_module(string) except ImportError: sys.path = temp raise sys.path = temp - return i + + return importing if self.file_path: sys_path_mod = list(self.sys_path_with_modifications()) @@ -259,6 +258,9 @@ class ImportPath(pr.Base): else: sys_path_mod = list(modules.get_sys_path()) + def module_not_found(): + raise ModuleNotFound('The module you searched has not been found') + current_namespace = (None, None, None) # now execute those paths rest = [] @@ -270,19 +272,19 @@ class ImportPath(pr.Base): and len(self.import_path) == 1: # follow `from . import some_variable` rel_path = self.get_relative_path() - try: + with common.ignored(ImportError): current_namespace = follow_str(rel_path, '__init__') - except ImportError: - pass if current_namespace[1]: rest = self.import_path[i:] else: - raise ModuleNotFound( - 'The module you searched has not been found') + module_not_found() + + if current_namespace == (None, None, False): + module_not_found() sys_path_mod.pop(0) # TODO why is this here? path = current_namespace[1] - is_package_directory = current_namespace[2][2] == imp.PKG_DIRECTORY + is_package_directory = current_namespace[2] f = None if is_package_directory or current_namespace[0]: diff --git a/jedi/keywords.py b/jedi/keywords.py index 5495ae32..cd591aa6 100644 --- a/jedi/keywords.py +++ b/jedi/keywords.py @@ -1,7 +1,10 @@ +from __future__ import with_statement + import pydoc import keyword from jedi._compatibility import is_py3k +from jedi import common import builtin try: @@ -63,12 +66,10 @@ def imitate_pydoc(string): # with unicode strings) string = str(string) h = pydoc.help - try: + with common.ignored(KeyError): # try to access symbols string = h.symbols[string] string, _, related = string.partition(' ') - except KeyError: - pass get_target = lambda s: h.topics.get(s, h.keywords.get(s)) while isinstance(string, str): diff --git a/jedi/modules.py b/jedi/modules.py index 9277e742..e67cc2c3 100644 --- a/jedi/modules.py +++ b/jedi/modules.py @@ -27,6 +27,7 @@ from jedi import parsing_representation as pr from jedi import fast_parser from jedi import debug from jedi import settings +from jedi import common class CachedModule(object): @@ -107,11 +108,9 @@ class ModuleWithCursor(Module): def parser(self): """ get the parser lazy """ if not self._parser: - try: + with common.ignored(KeyError): parser = cache.parser_cache[self.path].parser cache.invalidate_star_import_cache(parser.module) - except KeyError: - pass # 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. @@ -348,10 +347,8 @@ def sys_path_with_modifications(module): return [] # support for modules without a path is intentionally bad. curdir = os.path.abspath(os.curdir) - try: + with common.ignored(OSError): os.chdir(os.path.dirname(module.path)) - except OSError: - pass result = check_module(module) result += detect_django_path(module.path) @@ -372,12 +369,10 @@ def detect_django_path(module_path): else: module_path = new - try: + with common.ignored(IOError): with open(module_path + os.path.sep + 'manage.py'): debug.dbg('Found django path: %s' % module_path) result.append(module_path) - except IOError: - pass return result diff --git a/jedi/parsing.py b/jedi/parsing.py index 69607fec..e5279f71 100644 --- a/jedi/parsing.py +++ b/jedi/parsing.py @@ -15,6 +15,7 @@ within the statement. This lowers memory usage and cpu time and reduces the complexity of the ``Parser`` (there's another parser sitting inside ``Statement``, which produces ``Array`` and ``Call``). """ +from __future__ import with_statement import tokenize import keyword @@ -396,7 +397,7 @@ class Parser(object): self._check_user_stmt(stmt) # Attribute docstring (PEP 257) support - try: + with common.ignored(IndexError, AttributeError): # If string literal is being parsed first_tok = stmt.token_list[0] if (not stmt.set_vars and @@ -405,8 +406,6 @@ class Parser(object): first_tok[0] == tokenize.STRING): # ... then set it as a docstring self.scope.statements[-1].add_docstr(first_tok[1]) - except (IndexError, AttributeError): - pass if tok in always_break + not_first_break: self._gen.push_last_back() diff --git a/jedi/parsing_representation.py b/jedi/parsing_representation.py index 65210124..d4b8380c 100644 --- a/jedi/parsing_representation.py +++ b/jedi/parsing_representation.py @@ -34,6 +34,7 @@ statements in this scope. Check this out: See also :attr:`Scope.subscopes` and :attr:`Scope.statements`. """ +from __future__ import with_statement import os import re @@ -560,8 +561,10 @@ class Flow(Scope): @parent.setter def parent(self, value): self._parent = value - if self.next: + try: self.next.parent = value + except AttributeError: + return def get_code(self, first_indent=False, indention=' '): stmts = [] @@ -1278,11 +1281,8 @@ class Array(Call): inner = [] for i, stmt in enumerate(self.values): s = '' - try: + with common.ignored(IndexError): key = self.keys[i] - except IndexError: - pass - else: s += key.get_code(new_line=False) + ': ' s += stmt.get_code(new_line=False) inner.append(s) diff --git a/jedi/refactoring.py b/jedi/refactoring.py index a724ec16..ced1a732 100644 --- a/jedi/refactoring.py +++ b/jedi/refactoring.py @@ -12,11 +12,11 @@ following functions (sometimes bug-prone): - extract variable - inline variable """ - from __future__ import with_statement import difflib +from jedi import common from jedi import modules from jedi import helpers from jedi import parsing_representation as pr @@ -168,7 +168,7 @@ def inline(script): dct = {} definitions = script.goto() - try: + with common.ignored(AssertionError): assert len(definitions) == 1 stmt = definitions[0].definition related_names = script.related_names() @@ -202,7 +202,4 @@ def inline(script): else: new_lines.pop(index) - except AssertionError: - pass - return Refactoring(dct) diff --git a/setup.py b/setup.py index bc9c1fb1..bb0ee4e9 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,13 @@ setup(name='jedi', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 'Operating System :: OS Independent', - 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Text Editors :: Integrated Development Environments (IDE)', 'Topic :: Utilities', diff --git a/test/completion/std.py b/test/completion/std.py index 0eec6367..c4ed8c5a 100644 --- a/test/completion/std.py +++ b/test/completion/std.py @@ -89,12 +89,3 @@ def huhu(db): """ #? sqlite3.Connection() db - -# ----------------- -# various regression tests -# ----------------- - -#62 -import threading -#? ['_Verbose', '_VERBOSE'] -threading._Verbose diff --git a/test/conftest.py b/test/conftest.py index c5a00879..12a01b13 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -65,6 +65,18 @@ def pytest_generate_tests(metafunc): refactor.collect_dir_tests(base_dir, test_files)) +@pytest.fixture() +def isolated_jedi_cache(monkeypatch, tmpdir): + """ + Set `jedi.settings.cache_directory` to a temporary directory during test. + + Same as `clean_jedi_cache`, but create the temporary directory for + each test case (scope='function'). + """ + settings = base.jedi.settings + monkeypatch.setattr(settings, 'cache_directory', str(tmpdir)) + + @pytest.fixture(scope='session') def clean_jedi_cache(request): """ diff --git a/test/test_api_classes.py b/test/test_api_classes.py index bcb7a722..2da3e13e 100644 --- a/test/test_api_classes.py +++ b/test/test_api_classes.py @@ -32,7 +32,7 @@ def make_definitions(): definitions += api.defined_names(source) source += textwrap.dedent(""" - variable = sys or C or x or f or g or h""") + variable = sys or C or x or f or g or g() or h""") lines = source.splitlines() script = api.Script(source, len(lines), len('variable'), None) definitions += script.definition() @@ -50,4 +50,4 @@ def make_definitions(): @pytest.mark.parametrize('definition', make_definitions()) def test_basedefinition_type(definition): assert definition.type in ('module', 'class', 'instance', 'function', - 'statement', 'import', 'param') + 'generator', 'statement', 'import', 'param') diff --git a/test/test_cache.py b/test/test_cache.py index 9f845fc2..c27dc705 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -1,3 +1,5 @@ +import pytest + from jedi import settings from jedi.cache import ParserCacheItem, _ModulePickling @@ -21,10 +23,32 @@ def test_modulepickling_change_cache_dir(monkeypatch, tmpdir): monkeypatch.setattr(settings, 'cache_directory', dir_1) ModulePickling.save_module(path_1, item_1) - cached = ModulePickling.load_module(path_1, item_1.change_time - 1) + cached = load_stored_item(ModulePickling, path_1, item_1) assert cached == item_1.parser monkeypatch.setattr(settings, 'cache_directory', dir_2) ModulePickling.save_module(path_2, item_2) - cached = ModulePickling.load_module(path_1, item_1.change_time - 1) + cached = load_stored_item(ModulePickling, path_1, item_1) assert cached is None + + +def load_stored_item(cache, path, item): + """Load `item` stored at `path` in `cache`.""" + return cache.load_module(path, item.change_time - 1) + + +@pytest.mark.usefixtures("isolated_jedi_cache") +def test_modulepickling_delete_incompatible_cache(): + item = ParserCacheItem('fake parser') + path = 'fake path' + + cache1 = _ModulePickling() + cache1.version = 1 + cache1.save_module(path, item) + cached1 = load_stored_item(cache1, path, item) + assert cached1 == item.parser + + cache2 = _ModulePickling() + cache2.version = 2 + cached2 = load_stored_item(cache2, path, item) + assert cached2 is None diff --git a/tox.ini b/tox.ini index 6dc16930..205c8594 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py25, py26, py27, py32 +envlist = py25, py26, py27, py32, py33 [testenv] deps = pytest @@ -14,3 +14,9 @@ deps = deps = unittest2 {[testenv]deps} +[testenv:cov] +deps = + pytest-cov + {[testenv]deps} +commands = + py.test --cov jedi []