diff --git a/jedi/cache.py b/jedi/cache.py index e04b40e3..67fc84c5 100644 --- a/jedi/cache.py +++ b/jedi/cache.py @@ -25,6 +25,7 @@ try: import cPickle as pickle except: import pickle +import shutil from jedi._compatibility import json from jedi import settings @@ -219,6 +220,20 @@ def save_module(path, name, parser, pickling=True): class _ModulePickling(object): + + version = 1 + """ + 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] @@ -259,9 +274,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): @@ -272,10 +294,14 @@ 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(settings.cache_directory) + def _get_hashed_path(self, path): return self._get_path('%s_%s.pkl' % (self.py_version, hash(path))) 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_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