diff --git a/ChangeLog b/ChangeLog index 5d60e1e..277b19d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,15 @@ +2012-12-02 Thomas Vander Stichele + + * morituri/common/cache.py (added): + * morituri/test/cache (added): + * morituri/test/cache/result (added): + * morituri/test/cache/result/fe105a11.pickle (added): + * morituri/test/test_common_cache.py (added): + * morituri/common/Makefile.am: + * morituri/common/program.py: + * morituri/test/Makefile.am: + Extract ResultCache object into separate file. + 2012-12-02 Thomas Vander Stichele * morituri/rip/drive.py: diff --git a/morituri/common/Makefile.am b/morituri/common/Makefile.am index b3304f6..3d64742 100644 --- a/morituri/common/Makefile.am +++ b/morituri/common/Makefile.am @@ -6,6 +6,7 @@ morituri_PYTHON = \ __init__.py \ accurip.py \ checksum.py \ + cache.py \ common.py \ config.py \ drive.py \ diff --git a/morituri/common/cache.py b/morituri/common/cache.py new file mode 100644 index 0000000..3e372f4 --- /dev/null +++ b/morituri/common/cache.py @@ -0,0 +1,177 @@ +# -*- Mode: Python; test-case-name: morituri.test.test_common_cache -*- +# vi:si:et:sw=4:sts=4:ts=4 + +# Morituri - for those about to RIP + +# Copyright (C) 2009 Thomas Vander Stichele + +# This file is part of morituri. +# +# morituri is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# morituri is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with morituri. If not, see . + +import os +import os.path +import tempfile +import shutil + +from morituri.result import result +from morituri.extern.log import log + + +class Persister(object): + """ + I wrap an optional pickle to persist an object to disk. + + Instantiate me with a path to automatically unpickle the object. + Call persist to store the object to disk; it will get stored if it + changed from the on-disk object. + + @ivar object: the persistent object + """ + + def __init__(self, path=None, default=None): + """ + If path is not given, the object will not be persisted. + This allows code to transparently deal with both persisted and + non-persisted objects, since the persist method will just end up + doing nothing. + """ + self._path = path + self.object = None + + self._unpickle(default) + + def persist(self, obj=None): + """ + Persist the given object, if we have a persistence path and the + object changed. + + If object is not given, re-persist our object, always. + If object is given, only persist if it was changed. + """ + # don't pickle if it's already ok + if obj and obj == self.object: + return + + # store the object on ourselves if not None + if obj is not None: + self.object = obj + + # don't pickle if there is no path + if not self._path: + return + + # default to pickling our object again + if obj is None: + obj = self.object + + # pickle + self.object = obj + (fd, path) = tempfile.mkstemp(suffix='.morituri.pickle') + handle = os.fdopen(fd, 'wb') + import pickle + pickle.dump(obj, handle, 2) + handle.close() + # do an atomic move + shutil.move(path, self._path) + + def _unpickle(self, default=None): + self.object = default + + if not self._path: + return None + + if not os.path.exists(self._path): + return None + + handle = open(self._path) + import pickle + + try: + self.object = pickle.load(handle) + except: + # can fail for various reasons; in that case, pretend we didn't + # load it + pass + + def delete(self): + self.object = None + os.unlink(self._path) + + +class PersistedCache(object): + """ + I wrap a directory of persisted objects. + """ + + path = None + + def __init__(self, path): + self.path = path + try: + os.makedirs(self.path) + except OSError, e: + if e.errno != 17: # FIXME + raise + + def _getPath(self, key): + return os.path.join(self.path, '%s.pickle' % key) + + def get(self, key): + """ + Returns the persister for the given key. + """ + persister = Persister(self._getPath(key)) + if persister.object: + if hasattr(persister.object, 'instanceVersion'): + o = persister.object + if o.instanceVersion < o.__class__.classVersion: + persister.delete() + + return persister + + +class ResultCache(log.Loggable): + + def __init__(self, path=None): + if not path: + path = self._getResultCachePath() + + self._path = path + self._pcache = PersistedCache(self._path) + + def _getResultCachePath(self): + path = os.path.join(os.path.expanduser('~'), '.morituri', 'cache', + 'result') + return path + + def getRipResult(self, cddbdiscid): + """ + Retrieve the persistable RipResult either from our cache (from a + previous, possibly aborted rip), or return a new one. + + @rtype: L{Persistable} for L{result.RipResult} + """ + presult = self._pcache.get(cddbdiscid) + + if not presult.object: + self.debug('result for cddbdiscid %r not in cache, creating', + cddbdiscid) + presult.object = result.RipResult() + presult.persist(self.result) + else: + self.debug('result for cddbdiscid %r found in cache, reusing', + cddbdiscid) + + return presult diff --git a/morituri/common/program.py b/morituri/common/program.py index 1c970f0..afdb5c9 100644 --- a/morituri/common/program.py +++ b/morituri/common/program.py @@ -27,8 +27,7 @@ Common functionality and class for all programs using morituri. import os import time -from morituri.common import common, log, musicbrainzngs -from morituri.result import result +from morituri.common import common, log, musicbrainzngs, cache from morituri.program import cdrdao, cdparanoia from morituri.image import image @@ -59,16 +58,13 @@ class Program(log.Loggable): @param record: whether to record results of API calls for playback. """ self._record = record + self._cache = cache.ResultCache() def _getTableCachePath(self): path = os.path.join(os.path.expanduser('~'), '.morituri', 'cache', 'table') return path - def _getResultCachePath(self): - path = os.path.join(os.path.expanduser('~'), '.morituri', 'cache', - 'result') - return path def loadDevice(self, device): """ @@ -129,22 +125,8 @@ class Program(log.Loggable): """ assert self.result is None - path = self._getResultCachePath() - - pcache = common.PersistedCache(path) - presult = pcache.get(cddbdiscid) - - if not presult.object: - self.debug('result for cddbdiscid %r not in cache, creating', - cddbdiscid) - presult.object = result.RipResult() - presult.persist(self.result) - else: - self.debug('result for cddbdiscid %r found in cache, reusing', - cddbdiscid) - - self.result = presult.object - self._presult = presult + self._presult = self._cache.getRipResult(cddbdiscid) + self.result = self._presult.object return self.result diff --git a/morituri/test/Makefile.am b/morituri/test/Makefile.am index f620a36..deb8b9a 100644 --- a/morituri/test/Makefile.am +++ b/morituri/test/Makefile.am @@ -4,6 +4,7 @@ EXTRA_DIST = \ __init__.py \ common.py \ test_common_accurip.py \ + test_common_cache.py \ test_common_checksum.py \ test_common_common.py \ test_common_config.py \ @@ -45,7 +46,8 @@ EXTRA_DIST = \ cdparanoia.progress.error \ cdrdao.readtoc.progress \ silentalarm.result.pickle \ - track.flac + track.flac \ + cache/result/fe105a11.pickle # re-generation of test files when needed @@ -57,4 +59,3 @@ regenerate: track.flac track.flac: gst-launch audiotestsrc num-buffers=10 samplesperbuffer=588 ! audioconvert ! audio/x-raw-int,channels=2,width=16,height=16,rate=44100 ! flacenc ! filesink location=track.flac - diff --git a/morituri/test/cache/result/fe105a11.pickle b/morituri/test/cache/result/fe105a11.pickle new file mode 100644 index 0000000..97c91cc Binary files /dev/null and b/morituri/test/cache/result/fe105a11.pickle differ diff --git a/morituri/test/test_common_cache.py b/morituri/test/test_common_cache.py new file mode 100644 index 0000000..6dfe87d --- /dev/null +++ b/morituri/test/test_common_cache.py @@ -0,0 +1,19 @@ +# -*- Mode: Python; test-case-name: morituri.test.test_common_cache -*- +# vi:si:et:sw=4:sts=4:ts=4 + +import os + +from morituri.common import cache + +from morituri.test import common as tcommon + + +class ResultCacheTestCase(tcommon.TestCase): + + def setUp(self): + self.cache = cache.ResultCache( + os.path.join(os.path.dirname(__file__), 'cache', 'result')) + + def testGet(self): + result = self.cache.getRipResult('fe105a11') + self.assertEquals(result.object.title, "The Writing's on the Wall")