Merge pull request #365 from untitaker/fix-358-again

Refactor metadata None values
This commit is contained in:
Markus Unterwaditzer 2016-03-07 14:35:10 +01:00
commit acac746d2d
10 changed files with 61 additions and 55 deletions

View file

@ -3,6 +3,8 @@
Test suite for vdirsyncer. Test suite for vdirsyncer.
''' '''
import hypothesis.strategies as st
from vdirsyncer.utils.vobject import normalize_item from vdirsyncer.utils.vobject import normalize_item
@ -66,3 +68,9 @@ UID:{uid}
X-SOMETHING:{r} X-SOMETHING:{r}
HAHA:YES HAHA:YES
END:FOO''' END:FOO'''
printable_characters_strategy = st.text(
st.characters(blacklist_categories=(
'Cc', 'Cs'
))
)

View file

@ -12,7 +12,7 @@ from vdirsyncer.utils.vobject import Item
uid_strategy = st.text(st.characters(blacklist_categories=( uid_strategy = st.text(st.characters(blacklist_categories=(
'Zs', 'Zl', 'Zp', 'Zs', 'Zl', 'Zp',
'Cc' 'Cc', 'Cs'
))) )))

View file

@ -2,14 +2,17 @@
import random import random
from hypothesis import given
import hypothesis.strategies as st
import pytest import pytest
import vdirsyncer.exceptions as exceptions import vdirsyncer.exceptions as exceptions
from vdirsyncer.storage.base import Item from vdirsyncer.storage.base import Item, normalize_meta_value
from vdirsyncer.utils.compat import iteritems, text_type, urlquote, urlunquote from vdirsyncer.utils.compat import iteritems, text_type, urlquote, urlunquote
from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \ from .. import EVENT_TEMPLATE, TASK_TEMPLATE, VCARD_TEMPLATE, \
assert_item_equals assert_item_equals, printable_characters_strategy
def get_server_mixin(server_name): def get_server_mixin(server_name):
@ -269,12 +272,12 @@ class StorageTests(object):
pytest.skip('ownCloud is fundamentally broken.') pytest.skip('ownCloud is fundamentally broken.')
if not getattr(self, 'dav_server', ''): if not getattr(self, 'dav_server', ''):
assert s.get_meta('color') is None assert not s.get_meta('color')
assert s.get_meta('displayname') is None assert not s.get_meta('displayname')
try: try:
s.set_meta('color', None) s.set_meta('color', None)
assert s.get_meta('color') is None assert not s.get_meta('color')
s.set_meta('color', u'#ff0000') s.set_meta('color', u'#ff0000')
assert s.get_meta('color') == u'#ff0000' assert s.get_meta('color') == u'#ff0000'
except exceptions.UnsupportedMetadataError: except exceptions.UnsupportedMetadataError:
@ -285,3 +288,17 @@ class StorageTests(object):
rv = s.get_meta('displayname') rv = s.get_meta('displayname')
assert rv == x assert rv == x
assert isinstance(rv, text_type) assert isinstance(rv, text_type)
@given(value=st.one_of(
st.none(),
printable_characters_strategy.filter(lambda x: x.strip() != x)
))
def test_metadata_normalization(self, requires_metadata, s, value):
x = s.get_meta('displayname')
assert x == normalize_meta_value(x)
if not getattr(self, 'dav_server', None):
# ownCloud replaces "" with "unnamed"
# Also https://github.com/owncloud/core/issues/18409
s.set_meta('displayname', value)
assert s.get_meta('displayname') == normalize_meta_value(value)

View file

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from hypothesis import given
import hypothesis.strategies as st
import pytest import pytest
from vdirsyncer.metasync import MetaSyncConflict, metasync from vdirsyncer.metasync import MetaSyncConflict, metasync
@ -42,7 +39,7 @@ def test_basic(monkeypatch):
b.set_meta('foo', None) b.set_meta('foo', None)
metasync(a, b, status, keys=['foo']) metasync(a, b, status, keys=['foo'])
assert a.get_meta('foo') is b.get_meta('foo') is None assert not a.get_meta('foo') and not b.get_meta('foo')
def test_conflict(): def test_conflict():
@ -85,17 +82,3 @@ def test_conflict_x_wins(wins):
assert a.get_meta('foo') == b.get_meta('foo') == status['foo'] == ( assert a.get_meta('foo') == b.get_meta('foo') == status['foo'] == (
'bar' if wins == 'a' else 'baz' 'bar' if wins == 'a' else 'baz'
) )
@given(s=st.text().filter(lambda x: x.strip() == x), key=st.text())
def test_trailing_newline(s, key):
a = MemoryStorage()
b = MemoryStorage()
status = {}
a.set_meta(key, s + u'\n')
b.set_meta(key, s)
a.set_meta = b.set_meta = blow_up
metasync(a, b, status, keys=[key])
assert status[key] == s

View file

@ -1,5 +1,5 @@
from . import exceptions, log from . import exceptions, log
from .utils.compat import text_type from .storage.base import normalize_meta_value
logger = log.get(__name__) logger = log.get(__name__)
@ -34,9 +34,9 @@ def metasync(storage_a, storage_b, status, keys, conflict_resolution=None):
_b_to_a() _b_to_a()
for key in keys: for key in keys:
a = _normalize_value(storage_a.get_meta(key)) a = storage_a.get_meta(key)
b = _normalize_value(storage_b.get_meta(key)) b = storage_b.get_meta(key)
s = status.get(key) s = normalize_meta_value(status.get(key))
logger.debug(u'Key: {}'.format(key)) logger.debug(u'Key: {}'.format(key))
logger.debug(u'A: {}'.format(a)) logger.debug(u'A: {}'.format(a))
logger.debug(u'B: {}'.format(b)) logger.debug(u'B: {}'.format(b))
@ -53,11 +53,3 @@ def metasync(storage_a, storage_b, status, keys, conflict_resolution=None):
for key in set(status) - set(keys): for key in set(status) - set(keys):
del status[key] del status[key]
def _normalize_value(value):
if value is None:
return value
else:
assert isinstance(value, (bytes, text_type))
return value.strip()

View file

@ -5,7 +5,7 @@ import functools
from .. import exceptions, sync from .. import exceptions, sync
from ..utils import uniq from ..utils import uniq
from ..utils.compat import to_native, with_metaclass from ..utils.compat import to_native, to_unicode, with_metaclass
from ..utils.vobject import Item # noqa from ..utils.vobject import Item # noqa
@ -238,3 +238,7 @@ class Storage(with_metaclass(StorageMeta)):
''' '''
raise NotImplementedError('This storage does not support metadata.') raise NotImplementedError('This storage does not support metadata.')
def normalize_meta_value(value):
return to_unicode(value or u'').strip()

View file

@ -7,11 +7,11 @@ from lxml import etree
import requests import requests
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from .base import Item, Storage from .base import Item, Storage, normalize_meta_value
from .http import HTTP_STORAGE_PARAMETERS, USERAGENT, prepare_auth, \ from .http import HTTP_STORAGE_PARAMETERS, USERAGENT, prepare_auth, \
prepare_client_cert, prepare_verify prepare_client_cert, prepare_verify
from .. import exceptions, log, utils from .. import exceptions, log, utils
from ..utils.compat import text_type, to_native, to_unicode from ..utils.compat import text_type, to_native
dav_logger = log.get(__name__) dav_logger = log.get(__name__)
@ -597,9 +597,10 @@ class DavStorage(Storage):
root = _parse_xml(response.content) root = _parse_xml(response.content)
for prop in root.findall('.//' + lxml_selector): for prop in root.findall('.//' + lxml_selector):
text = getattr(prop, 'text', None) text = normalize_meta_value(getattr(prop, 'text', None))
if text: if text:
return to_unicode(text) return text
return u''
def set_meta(self, key, value): def set_meta(self, key, value):
try: try:
@ -609,7 +610,7 @@ class DavStorage(Storage):
lxml_selector = '{%s}%s' % (namespace, tagname) lxml_selector = '{%s}%s' % (namespace, tagname)
element = etree.Element(lxml_selector) element = etree.Element(lxml_selector)
element.text = value element.text = normalize_meta_value(value)
data = '''<?xml version="1.0" encoding="utf-8" ?> data = '''<?xml version="1.0" encoding="utf-8" ?>
<D:propertyupdate xmlns:D="DAV:"> <D:propertyupdate xmlns:D="DAV:">

View file

@ -6,7 +6,7 @@ import subprocess
from atomicwrites import atomic_write from atomicwrites import atomic_write
from .base import Item, Storage from .base import Item, Storage, normalize_meta_value
from .. import exceptions, log from .. import exceptions, log
from ..utils import checkdir, expand_path, generate_href, get_etag_from_file, \ from ..utils import checkdir, expand_path, generate_href, get_etag_from_file, \
get_etag_from_fileobject get_etag_from_fileobject
@ -183,16 +183,16 @@ class FilesystemStorage(Storage):
fpath = os.path.join(self.path, key) fpath = os.path.join(self.path, key)
try: try:
with open(fpath, 'rb') as f: with open(fpath, 'rb') as f:
return f.read().decode(self.encoding) or None return normalize_meta_value(f.read().decode(self.encoding))
except IOError as e: except IOError as e:
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
return None return u''
else: else:
raise raise
def set_meta(self, key, value): def set_meta(self, key, value):
value = value or u'' value = normalize_meta_value(value)
assert isinstance(value, text_type)
fpath = os.path.join(self.path, key) fpath = os.path.join(self.path, key)
with atomic_write(fpath, mode='wb', overwrite=True) as f: with atomic_write(fpath, mode='wb', overwrite=True) as f:
f.write(value.encode(self.encoding)) f.write(value.encode(self.encoding))

View file

@ -2,8 +2,9 @@
import random import random
import vdirsyncer.exceptions as exceptions from .base import Storage, normalize_meta_value
from vdirsyncer.storage.base import Storage
from .. import exceptions
def _random_string(): def _random_string():
@ -66,7 +67,7 @@ class MemoryStorage(Storage):
del self.items[href] del self.items[href]
def get_meta(self, key): def get_meta(self, key):
return self.metadata.get(key) return normalize_meta_value(self.metadata.get(key))
def set_meta(self, key, value): def set_meta(self, key, value):
self.metadata[key] = value self.metadata[key] = normalize_meta_value(value)

View file

@ -19,7 +19,7 @@ We also use a custom ``data``-URI for the redirect in OAuth:
import click import click
from .base import Item, Storage from .base import Item, Storage, normalize_meta_value
from .http import HTTP_STORAGE_PARAMETERS, prepare_client_cert, \ from .http import HTTP_STORAGE_PARAMETERS, prepare_client_cert, \
prepare_verify prepare_verify
from .. import exceptions, log, utils from .. import exceptions, log, utils
@ -225,15 +225,15 @@ class RemoteStorage(Storage):
def get_meta(self, key): def get_meta(self, key):
try: try:
return self.session.request('GET', key).text or None return normalize_meta_value(self.session.request('GET', key).text)
except exceptions.NotFoundError: except exceptions.NotFoundError:
pass return u''
def set_meta(self, key, value): def set_meta(self, key, value):
self.session.request( self.session.request(
'PUT', 'PUT',
key, key,
data=(value or u'').encode('utf-8'), data=normalize_meta_value(value).encode('utf-8'),
headers={'Content-Type': 'text/plain; charset=utf-8'} headers={'Content-Type': 'text/plain; charset=utf-8'}
) )