1
0
mirror of https://github.com/nvbn/thefuck.git synced 2025-01-18 20:11:17 +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,7 +184,27 @@ def for_app(*app_names, **kwargs):
return decorator(_for_app) return decorator(_for_app)
def get_cache_dir(): class Cache(object):
"""Lazy read cache and save changes at exit."""
def __init__(self):
self._db = None
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") default_xdg_cache_dir = os.path.expanduser("~/.cache")
cache_dir = os.getenv("XDG_CACHE_HOME", default_xdg_cache_dir) cache_dir = os.getenv("XDG_CACHE_HOME", default_xdg_cache_dir)
@ -197,6 +218,37 @@ def get_cache_dir():
return cache_dir 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):
"""Caches function result in temporary file. """Caches function result in temporary file.
@ -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:
return '0'
@decorator
def _cache(fn, *args, **kwargs):
if cache.disabled: if cache.disabled:
return fn(*args, **kwargs) return fn(*args, **kwargs)
# A bit obscure, but simplest way to generate unique key for
# 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: else:
value = fn(*args, **kwargs) return _cache.get_value(fn, depends_on, 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: return wrapper
value = fn(*args, **kwargs)
db[key] = {'etag': etag, 'value': value}
return value
return _cache return cache_decorator
cache.disabled = False cache.disabled = False