kopia lustrzana https://github.com/collective/icalendar
Merge remote-tracking branch 'untitaker/preserve_order'
commit
a8e13b21d7
|
@ -12,6 +12,9 @@ Changelog
|
|||
- Remove incorrect use of __all__. We don't encourage using ``from package
|
||||
import *`` imports. Fixes #129.
|
||||
[eric-wieser]
|
||||
- Add optional ``sorted`` parameter to ``Component.to_ical``. Setting it to
|
||||
false allows the user to preserve the original property and parameter order.
|
||||
[untitaker]
|
||||
|
||||
|
||||
3.6.2 (2014-04-05)
|
||||
|
|
|
@ -35,3 +35,4 @@ icalendar contributors
|
|||
- Wichert Akkerman <wichert@wiggy.net>
|
||||
- spanktar <spanky@kapanka.com>
|
||||
- tgecho <tgecho@gmail.com>
|
||||
- Markus Unterwaditzer <markus@unterwaditzer.net>
|
||||
|
|
16
setup.py
16
setup.py
|
@ -11,9 +11,17 @@ longdesc += codecs.open('LICENSE.rst', encoding='utf-8').read()
|
|||
|
||||
|
||||
tests_require = []
|
||||
install_requires = [
|
||||
'setuptools',
|
||||
'python-dateutil',
|
||||
'pytz'
|
||||
]
|
||||
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
# Python unittest2 only needed for Python 2.6
|
||||
tests_require = ['unittest2']
|
||||
tests_require.append('unittest2')
|
||||
# OrderedDict was added in 2.7
|
||||
install_requires.append('ordereddict')
|
||||
|
||||
|
||||
setuptools.setup(
|
||||
|
@ -42,11 +50,7 @@ setuptools.setup(
|
|||
package_dir={'': 'src'},
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=[
|
||||
'setuptools',
|
||||
'python-dateutil',
|
||||
'pytz',
|
||||
],
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'test': tests_require
|
||||
}
|
||||
|
|
|
@ -267,13 +267,17 @@ class Component(CaselessDict):
|
|||
#####################
|
||||
# Generation
|
||||
|
||||
def property_items(self, recursive=True):
|
||||
def property_items(self, recursive=True, sorted=True):
|
||||
"""Returns properties in this component and subcomponents as:
|
||||
[(name, value), ...]
|
||||
"""
|
||||
vText = types_factory['text']
|
||||
properties = [('BEGIN', vText(self.name).to_ical())]
|
||||
property_names = self.sorted_keys()
|
||||
if sorted:
|
||||
property_names = self.sorted_keys()
|
||||
else:
|
||||
property_names = self.keys()
|
||||
|
||||
for name in property_names:
|
||||
values = self[name]
|
||||
if isinstance(values, list):
|
||||
|
@ -285,7 +289,7 @@ class Component(CaselessDict):
|
|||
if recursive:
|
||||
# recursion is fun!
|
||||
for subcomponent in self.subcomponents:
|
||||
properties += subcomponent.property_items()
|
||||
properties += subcomponent.property_items(sorted=sorted)
|
||||
properties.append(('END', vText(self.name).to_ical()))
|
||||
return properties
|
||||
|
||||
|
@ -353,24 +357,29 @@ class Component(CaselessDict):
|
|||
def __repr__(self):
|
||||
return '%s(%s)' % (self.name, data_encode(self))
|
||||
|
||||
def content_line(self, name, value):
|
||||
def content_line(self, name, value, sorted=True):
|
||||
"""Returns property as content line.
|
||||
"""
|
||||
params = getattr(value, 'params', Parameters())
|
||||
return Contentline.from_parts(name, params, value)
|
||||
return Contentline.from_parts(name, params, value, sorted=sorted)
|
||||
|
||||
def content_lines(self):
|
||||
def content_lines(self, sorted=True):
|
||||
"""Converts the Component and subcomponents into content lines.
|
||||
"""
|
||||
contentlines = Contentlines()
|
||||
for name, value in self.property_items():
|
||||
cl = self.content_line(name, value)
|
||||
for name, value in self.property_items(sorted=sorted):
|
||||
cl = self.content_line(name, value, sorted=sorted)
|
||||
contentlines.append(cl)
|
||||
contentlines.append('') # remember the empty string in the end
|
||||
return contentlines
|
||||
|
||||
def to_ical(self):
|
||||
content_lines = self.content_lines()
|
||||
def to_ical(self, sorted=True):
|
||||
'''
|
||||
:param sorted: Whether parameters and properties should be
|
||||
lexicographically sorted.
|
||||
'''
|
||||
|
||||
content_lines = self.content_lines(sorted=sorted)
|
||||
return content_lines.to_ical()
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,11 @@
|
|||
from icalendar.parser_tools import to_unicode
|
||||
from icalendar.parser_tools import data_encode
|
||||
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
except ImportError:
|
||||
from ordereddict import OrderedDict
|
||||
|
||||
|
||||
def canonsort_keys(keys, canonical_order=None):
|
||||
"""Sorts leading keys according to canonical_order. Keys not specified in
|
||||
|
@ -20,7 +25,7 @@ def canonsort_items(dict1, canonical_order=None):
|
|||
k in canonsort_keys(dict1.keys(), canonical_order)]
|
||||
|
||||
|
||||
class CaselessDict(dict):
|
||||
class CaselessDict(OrderedDict):
|
||||
"""A dictionary that isn't case sensitive, and only uses strings as keys.
|
||||
Values retain their case.
|
||||
"""
|
||||
|
@ -28,47 +33,47 @@ class CaselessDict(dict):
|
|||
def __init__(self, *args, **kwargs):
|
||||
"""Set keys to upper for initial dict.
|
||||
"""
|
||||
dict.__init__(self, *args, **kwargs)
|
||||
super(CaselessDict, self).__init__(*args, **kwargs)
|
||||
for key, value in self.items():
|
||||
key_upper = to_unicode(key).upper()
|
||||
if key != key_upper:
|
||||
dict.__delitem__(self, key)
|
||||
super(CaselessDict, self).__delitem__(key)
|
||||
self[key_upper] = value
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = to_unicode(key)
|
||||
return dict.__getitem__(self, key.upper())
|
||||
return super(CaselessDict, self).__getitem__(key.upper())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
key = to_unicode(key)
|
||||
dict.__setitem__(self, key.upper(), value)
|
||||
super(CaselessDict, self).__setitem__(key.upper(), value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
key = to_unicode(key)
|
||||
dict.__delitem__(self, key.upper())
|
||||
super(CaselessDict, self).__delitem__(key.upper())
|
||||
|
||||
def __contains__(self, key):
|
||||
key = to_unicode(key)
|
||||
return dict.__contains__(self, key.upper())
|
||||
return super(CaselessDict, self).__contains__(key.upper())
|
||||
|
||||
def get(self, key, default=None):
|
||||
key = to_unicode(key)
|
||||
return dict.get(self, key.upper(), default)
|
||||
return super(CaselessDict, self).get(key.upper(), default)
|
||||
|
||||
def setdefault(self, key, value=None):
|
||||
key = to_unicode(key)
|
||||
return dict.setdefault(self, key.upper(), value)
|
||||
return super(CaselessDict, self).setdefault(key.upper(), value)
|
||||
|
||||
def pop(self, key, default=None):
|
||||
key = to_unicode(key)
|
||||
return dict.pop(self, key.upper(), default)
|
||||
return super(CaselessDict, self).pop(key.upper(), default)
|
||||
|
||||
def popitem(self):
|
||||
return dict.popitem(self)
|
||||
return super(CaselessDict, self).popitem()
|
||||
|
||||
def has_key(self, key):
|
||||
key = to_unicode(key)
|
||||
return dict.__contains__(self, key.upper())
|
||||
return super(CaselessDict, self).__contains__(key.upper())
|
||||
|
||||
def update(self, indict):
|
||||
# Multiple keys where key1.upper() == key2.upper() will be lost.
|
||||
|
@ -76,11 +81,14 @@ class CaselessDict(dict):
|
|||
self[key] = value
|
||||
|
||||
def copy(self):
|
||||
return CaselessDict(dict.copy(self))
|
||||
return type(self)(super(CaselessDict, self).copy())
|
||||
|
||||
def __repr__(self):
|
||||
return 'CaselessDict(%s)' % data_encode(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self is other or dict(self.items()) == dict(other.items())
|
||||
|
||||
# A list of keys that must appear first in sorted_keys and sorted_items;
|
||||
# must be uppercase.
|
||||
canonical_order = None
|
||||
|
|
|
@ -194,10 +194,13 @@ class Parameters(CaselessDict):
|
|||
def __repr__(self):
|
||||
return 'Parameters(%s)' % data_encode(self)
|
||||
|
||||
def to_ical(self):
|
||||
def to_ical(self, sorted=True):
|
||||
result = []
|
||||
items = self.items()
|
||||
for key, value in sorted(items):
|
||||
items = list(self.items())
|
||||
if sorted:
|
||||
items.sort()
|
||||
|
||||
for key, value in items:
|
||||
value = param_value(value)
|
||||
if isinstance(value, compat.unicode_type):
|
||||
value = value.encode(DEFAULT_ENCODING)
|
||||
|
@ -270,7 +273,7 @@ class Contentline(compat.unicode_type):
|
|||
return self
|
||||
|
||||
@classmethod
|
||||
def from_parts(cls, name, params, values):
|
||||
def from_parts(cls, name, params, values, sorted=True):
|
||||
"""Turn a parts into a content line.
|
||||
"""
|
||||
assert isinstance(params, Parameters)
|
||||
|
@ -286,7 +289,7 @@ class Contentline(compat.unicode_type):
|
|||
name = to_unicode(name)
|
||||
values = to_unicode(values)
|
||||
if params:
|
||||
params = to_unicode(params.to_ical())
|
||||
params = to_unicode(params.to_ical(sorted=sorted))
|
||||
return cls(u'%s;%s:%s' % (name, params, values))
|
||||
return cls(u'%s:%s' % (name, values))
|
||||
|
||||
|
|
|
@ -275,6 +275,37 @@ class TestCalComponent(unittest.TestCase):
|
|||
self.assertEqual(component[property_name].dt.tzinfo,
|
||||
None)
|
||||
|
||||
def test_cal_Component_to_ical_property_order(self):
|
||||
Component = icalendar.cal.Component
|
||||
component_str = [b'BEGIN:VEVENT',
|
||||
b'DTSTART:19970714T170000Z',
|
||||
b'DTEND:19970715T035959Z',
|
||||
b'SUMMARY:Bastille Day Party',
|
||||
b'END:VEVENT']
|
||||
component = Component.from_ical(b'\r\n'.join(component_str))
|
||||
|
||||
sorted_str = component.to_ical().splitlines()
|
||||
assert sorted_str != component_str
|
||||
assert set(sorted_str) == set(component_str)
|
||||
|
||||
preserved_str = component.to_ical(sorted=False).splitlines()
|
||||
assert preserved_str == component_str
|
||||
|
||||
def test_cal_Component_to_ical_parameter_order(self):
|
||||
Component = icalendar.cal.Component
|
||||
component_str = [b'BEGIN:VEVENT',
|
||||
b'X-FOOBAR;C=one;A=two;B=three:helloworld.',
|
||||
b'END:VEVENT']
|
||||
component = Component.from_ical(b'\r\n'.join(component_str))
|
||||
|
||||
sorted_str = component.to_ical().splitlines()
|
||||
assert sorted_str[0] == component_str[0]
|
||||
assert sorted_str[1] == b'X-FOOBAR;A=two;B=three;C=one:helloworld.'
|
||||
assert sorted_str[2] == component_str[2]
|
||||
|
||||
preserved_str = component.to_ical(sorted=False).splitlines()
|
||||
assert preserved_str == component_str
|
||||
|
||||
|
||||
class TestCal(unittest.TestCase):
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue