diff --git a/tests/test_sync.py b/tests/test_sync.py index 73937ff..d5779a9 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -9,6 +9,7 @@ import pytest +import vdirsyncer.exceptions as exceptions from vdirsyncer.storage.base import Item from vdirsyncer.storage.memory import MemoryStorage from vdirsyncer.sync import BothReadOnly, StorageEmpty, SyncConflict, sync @@ -251,10 +252,14 @@ def test_both_readonly(): def test_readonly(): a = MemoryStorage() - b = MemoryStorage(read_only=True) + b = MemoryStorage() status = {} href_a, _ = a.upload(Item(u'UID:1')) href_b, _ = b.upload(Item(u'UID:2')) + b.read_only = True + with pytest.raises(exceptions.ReadOnlyError): + b.upload(Item(u'UID:3')) + sync(a, b, status) assert len(status) == 2 and a.has(href_a) and not b.has(href_a) sync(a, b, status) diff --git a/vdirsyncer/exceptions.py b/vdirsyncer/exceptions.py index e7fd14d..a77e77d 100644 --- a/vdirsyncer/exceptions.py +++ b/vdirsyncer/exceptions.py @@ -44,3 +44,7 @@ class AlreadyExistingError(PreconditionFailed): class WrongEtagError(PreconditionFailed): '''Wrong etag''' + + +class ReadOnlyError(Error): + '''Storage is read-only.''' diff --git a/vdirsyncer/storage/base.py b/vdirsyncer/storage/base.py index 48b0beb..0a3fd64 100644 --- a/vdirsyncer/storage/base.py +++ b/vdirsyncer/storage/base.py @@ -7,13 +7,31 @@ :license: MIT, see LICENSE for more details. ''' +import functools from .. import exceptions from ..utils import uniq +from ..utils.compat import with_metaclass from ..utils.vobject import Item # noqa -class Storage(object): +def mutating_storage_method(f): + @functools.wraps(f) + def inner(self, *args, **kwargs): + if self.read_only: + raise exceptions.ReadOnlyError('This storage is read-only.') + return f(self, *args, **kwargs) + return inner + + +class StorageMeta(type): + def __init__(cls, name, bases, d): + for method in ('update', 'upload', 'delete'): + setattr(cls, method, mutating_storage_method(getattr(cls, method))) + return super(StorageMeta, cls).__init__(name, bases, d) + + +class Storage(with_metaclass(StorageMeta)): '''Superclass of all storages, mainly useful to summarize the interface to implement. @@ -63,8 +81,9 @@ class Storage(object): if read_only is None: read_only = self.read_only if self.read_only and not read_only: - raise ValueError('This storage is read-only.') + raise ValueError('This storage can only be read-only.') self.read_only = bool(read_only) + if collection and instance_name: instance_name = '{}/{}'.format(instance_name, collection) self.instance_name = instance_name diff --git a/vdirsyncer/utils/compat.py b/vdirsyncer/utils/compat.py index 20bfd0b..0316c38 100644 --- a/vdirsyncer/utils/compat.py +++ b/vdirsyncer/utils/compat.py @@ -27,3 +27,11 @@ else: # pragma: no cover text_type = str iteritems = lambda x: x.items() itervalues = lambda x: x.values() + + +def with_metaclass(meta, *bases): + '''Original code from six, by Benjamin Peterson.''' + class metaclass(meta): + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {})