1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-01-31 10:11:14 +00:00

#707: Reimplement cache

This commit is contained in:
Vladimir Iakovlev 2017-10-10 08:30:26 +02:00
parent 7a04a1f4c5
commit d9fd5e8a6b
3 changed files with 85 additions and 62 deletions

View File

@ -3,11 +3,10 @@
import pytest import pytest
import warnings import warnings
from mock import Mock from mock import Mock
import six
from thefuck.utils import default_settings, \ from thefuck.utils import default_settings, \
memoize, get_closest, get_all_executables, replace_argument, \ memoize, get_closest, get_all_executables, replace_argument, \
get_all_matched_commands, is_app, for_app, cache, \ get_all_matched_commands, is_app, for_app, cache, \
get_valid_history_without_current get_valid_history_without_current, _cache
from thefuck.types import Command from thefuck.types import Command
@ -124,10 +123,6 @@ def test_for_app(script, names, result):
class TestCache(object): class TestCache(object):
@pytest.fixture(autouse=True)
def enable_cache(self, monkeypatch):
monkeypatch.setattr('thefuck.utils.cache.disabled', False)
@pytest.fixture @pytest.fixture
def shelve(self, mocker): def shelve(self, mocker):
value = {} value = {}
@ -151,9 +146,14 @@ class TestCache(object):
mocker.patch('thefuck.utils.shelve.open', new_callable=lambda: _Shelve) mocker.patch('thefuck.utils.shelve.open', new_callable=lambda: _Shelve)
return value return value
@pytest.fixture(autouse=True)
def enable_cache(self, monkeypatch, shelve):
monkeypatch.setattr('thefuck.utils.cache.disabled', False)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mtime(self, mocker): def mtime(self, mocker):
mocker.patch('thefuck.utils.os.path.getmtime', return_value=0) mocker.patch('thefuck.utils.os.path.getmtime', return_value=0)
_cache._init_db()
@pytest.fixture @pytest.fixture
def fn(self): def fn(self):
@ -164,11 +164,10 @@ class TestCache(object):
return fn return fn
@pytest.fixture @pytest.fixture
def key(self): def key(self, monkeypatch):
if six.PY2: monkeypatch.setattr('thefuck.utils.Cache._get_key',
return 'tests.test_utils.<function fn ' lambda *_: 'key')
else: return 'key'
return 'tests.test_utils.<function TestCache.fn.<locals>.fn '
def test_with_blank_cache(self, shelve, fn, key): def test_with_blank_cache(self, shelve, fn, key):
assert shelve == {} assert shelve == {}

View File

@ -5,7 +5,7 @@ import sys
import six import six
from .. import logs from .. import logs
from ..conf import settings from ..conf import settings
from ..utils import DEVNULL, memoize, cache from ..utils import DEVNULL, cache
from .generic import Generic from .generic import Generic
@ -35,7 +35,6 @@ class Fish(Generic):
' end\n' ' end\n'
'end').format(alias_name, alter_history) 'end').format(alias_name, alter_history)
@memoize
@cache('~/.config/fish/config.fish', '~/.config/fish/functions') @cache('~/.config/fish/config.fish', '~/.config/fish/functions')
def get_aliases(self): def get_aliases(self):
overridden = self._get_overridden_aliases() overridden = self._get_overridden_aliases()

View File

@ -1,9 +1,10 @@
import atexit
import json
import os import os
import pickle import pickle
import re import re
import shelve import shelve
import six import six
from contextlib import closing
from decorator import decorator from decorator import decorator
from difflib import get_close_matches from difflib import get_close_matches
from functools import wraps from functools import wraps
@ -183,19 +184,70 @@ def for_app(*app_names, **kwargs):
return decorator(_for_app) return decorator(_for_app)
def get_cache_dir(): class Cache(object):
default_xdg_cache_dir = os.path.expanduser("~/.cache") """Lazy read cache and save changes at exit."""
cache_dir = os.getenv("XDG_CACHE_HOME", default_xdg_cache_dir)
# Ensure the cache_path exists, Python 2 does not have the exist_ok def __init__(self):
# parameter self._db = None
try:
os.makedirs(cache_dir)
except OSError:
if not os.path.isdir(cache_dir):
raise
return cache_dir def _init_db(self):
cache_dir = self._get_cache_dir()
cache_path = Path(cache_dir).joinpath('thefuck').as_posix()
try:
self._db = shelve.open(cache_path)
except (shelve_open_error, ImportError):
# Caused when switching between Python versions
warn("Removing possibly out-dated cache")
os.remove(cache_path)
self._db = shelve.open(cache_path)
atexit.register(self._db.close)
def _get_cache_dir(self):
default_xdg_cache_dir = os.path.expanduser("~/.cache")
cache_dir = os.getenv("XDG_CACHE_HOME", default_xdg_cache_dir)
# Ensure the cache_path exists, Python 2 does not have the exist_ok
# parameter
try:
os.makedirs(cache_dir)
except OSError:
if not os.path.isdir(cache_dir):
raise
return cache_dir
def _get_mtime(self, path):
try:
return str(os.path.getmtime(path))
except OSError:
return '0'
def _get_key(self, fn, depends_on, args, kwargs):
parts = (fn.__module__, repr(fn).split('at')[0],
depends_on, args, kwargs)
return json.dumps(parts)
def get_value(self, fn, depends_on, args, kwargs):
if self._db is None:
self._init_db()
depends_on = [Path(name).expanduser().absolute().as_posix()
for name in depends_on]
# We can't use pickle here
key = self._get_key(fn, depends_on, args, kwargs)
etag = '.'.join(self._get_mtime(path) for path in depends_on)
if self._db.get(key, {}).get('etag') == etag:
return self._db[key]['value']
else:
value = fn(*args, **kwargs)
self._db[key] = {'etag': etag, 'value': value}
return value
_cache = Cache()
def cache(*depends_on): def cache(*depends_on):
@ -207,45 +259,18 @@ def cache(*depends_on):
Function wrapped in `cache` should be arguments agnostic. Function wrapped in `cache` should be arguments agnostic.
""" """
def _get_mtime(name): def cache_decorator(fn):
path = Path(name).expanduser().absolute().as_posix() @memoize
try: @wraps(fn)
return str(os.path.getmtime(path)) def wrapper(*args, **kwargs):
except OSError: if cache.disabled:
return '0' return fn(*args, **kwargs)
else:
return _cache.get_value(fn, depends_on, args, kwargs)
@decorator return wrapper
def _cache(fn, *args, **kwargs):
if cache.disabled:
return fn(*args, **kwargs)
# A bit obscure, but simplest way to generate unique key for return cache_decorator
# functions and methods in python 2 and 3:
key = '{}.{}'.format(fn.__module__, repr(fn).split('at')[0])
etag = '.'.join(_get_mtime(name) for name in depends_on)
cache_dir = get_cache_dir()
cache_path = Path(cache_dir).joinpath('thefuck').as_posix()
try:
with closing(shelve.open(cache_path)) as db:
if db.get(key, {}).get('etag') == etag:
return db[key]['value']
else:
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
except (shelve_open_error, ImportError):
# Caused when switching between Python versions
warn("Removing possibly out-dated cache")
os.remove(cache_path)
with closing(shelve.open(cache_path)) as db:
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
return _cache
cache.disabled = False cache.disabled = False