diff --git a/CHANGES.rst b/CHANGES.rst index 5119c874..1dc73e5d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,13 +1,23 @@ Changelog ========= -3.11.6 (unreleased) +4.0 (unreleased) ------------------- Breaking changes: - *add item here* +- Improved error handling. The value and parameters of a property should no longer + be lost upon error. + Refs #158 #174 + [stlaz] + +- Added strong typing of property values. Unknown properties with VALUE parameter + should now be represented as the appropriate type + Refs #187 + [stlaz] + New features: - *add item here* diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index e4dc350a..83cc7a18 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -141,14 +141,19 @@ def _encode(self, name, value, parameters=None, encode=1): if isinstance(value, types_factory.all_types): # Don't encode already encoded values. return value - klass = types_factory.for_property(name) - obj = klass(value) if parameters: if isinstance(parameters, dict): params = Parameters() for key, item in parameters.items(): params[key] = item parameters = params + klass = types_factory.for_property( + name, parameters.get('VALUE') if parameters else None) + if types_factory.is_list_property(name): + obj = vDDDLists(value, klass) + else: + obj = klass(value) + if parameters: assert isinstance(parameters, Parameters) obj.params = parameters return obj @@ -185,7 +190,7 @@ def add(self, name, value, parameters=None, encode=1): # encode value if encode and isinstance(value, list) \ - and name.lower() not in ['rdate', 'exdate']: + and not types_factory.is_list_property(name): # Individually convert each value to an ical type except rdate and # exdate, where lists of dates might be passed to vDDDLists. value = [self._encode(name, v, parameters, encode) for v in value] @@ -217,7 +222,11 @@ def _decode(self, name, value): if isinstance(value, vDDDLists): # TODO: Workaround unfinished decoding return value - decoded = types_factory.from_ical(name, value) + try: + valtype = value.params['VALUE'] + except (AttributeError, KeyError): + valtype = None + decoded = types_factory.from_ical(name, value, valtype) # TODO: remove when proper decoded is implemented in every prop.* class # Workaround to decode vText properly if isinstance(decoded, vText): @@ -225,11 +234,8 @@ def _decode(self, name, value): return decoded def decoded(self, name, default=_marker): - """Returns decoded value of property. + """Returns value of a property as a python native type. """ - # XXX: fail. what's this function supposed to do in the end? - # -rnix - if name in self: value = self[name] if isinstance(value, list): @@ -369,7 +375,6 @@ def from_ical(cls, st, multiple=False): _timezone_cache[component['TZID']] = component.to_tz() # we are adding properties to the current top of the stack else: - factory = types_factory.for_property(name) component = stack[-1] if stack else None if not component: raise ValueError('Property "{prop}" does not have ' @@ -377,18 +382,37 @@ def from_ical(cls, st, multiple=False): datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE', 'FREEBUSY', 'RDATE', 'EXDATE') try: - if name in datetime_names and 'TZID' in params: - vals = factory(factory.from_ical(vals, params['TZID'])) + factory = types_factory.for_property(name, + params.get('VALUE')) + except ValueError as e: + if not component.ignore_exceptions: + raise + else: + # add error message and fall back to vText value type + component.errors.append((uname, str(e))) + factory = types_factory['text'] + try: + if (types_factory.is_list_property(name) and + factory != vText): + # TODO: list type currenty supports only datetime types + vals = vDDDLists( + vDDDLists.from_ical(vals, params.get('TZID'), + factory)) else: - vals = factory(factory.from_ical(vals)) + if name in datetime_names and 'TZID' in params: + vals = factory( + factory.from_ical(vals, params['TZID'])) + else: + vals = factory(factory.from_ical(vals)) except ValueError as e: if not component.ignore_exceptions: raise component.errors.append((uname, unicode_type(e))) - component.add(name, None, encode=0) - else: - vals.params = params - component.add(name, vals, encode=0) + # fallback to vText and store the original value + vals = types_factory['text'](vals) + + vals.params = params + component.add(name, vals, encode=0) if multiple: return comps diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index f084e84e..35eafed4 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -238,13 +238,22 @@ def from_ical(cls, ical): class vDDDLists(object): """A list of vDDDTypes values. """ - def __init__(self, dt_list): + def __init__(self, dt_list, type_class=None): + if type_class is None: + type_class = vDDDTypes if not hasattr(dt_list, '__iter__'): dt_list = [dt_list] vDDD = [] tzid = None + if dt_list: + # we have some values, all should have the same type + ltype = type(dt_list[0]) for dt in dt_list: - dt = vDDDTypes(dt) + # raise ValueError if type of the input values differs + if not isinstance(dt, ltype): + raise ValueError("Trying to insert '%s' value into a list " + "of '%s'".format(type(dt), ltype)) + dt = type_class(dt) vDDD.append(dt) if 'TZID' in dt.params: tzid = dt.params['TZID'] @@ -258,12 +267,16 @@ def to_ical(self): dts_ical = (dt.to_ical() for dt in self.dts) return b",".join(dts_ical) - @staticmethod - def from_ical(ical, timezone=None): + @classmethod + def from_ical(cls, ical, timezone=None, unit_type=None): + if isinstance(ical, cls): + return ical.dts + if unit_type is None: + unit_type = vDDDTypes out = [] ical_dates = ical.split(",") for ical_dt in ical_dates: - out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone)) + out.append(unit_type.from_ical(ical_dt, timezone=timezone)) return out @@ -285,8 +298,7 @@ def __init__(self, dt): elif isinstance(dt, tuple): self.params = Parameters({'value': 'PERIOD'}) - if (isinstance(dt, datetime) or isinstance(dt, time))\ - and getattr(dt, 'tzinfo', False): + if isinstance(dt, (datetime, time)) and hasattr(dt, 'tzinfo'): tzinfo = dt.tzinfo if tzinfo is not pytz.utc and\ (tzutc is None or not isinstance(tzinfo, tzutc)): @@ -346,9 +358,14 @@ def to_ical(self): s = "%04d%02d%02d" % (self.dt.year, self.dt.month, self.dt.day) return s.encode('utf-8') - @staticmethod - def from_ical(ical): + @classmethod + def from_ical(cls, ical, timezone=None): + # timezone is a dummy in this method + if isinstance(ical, cls): + return ical.dt try: + if len(ical) != 8: # YYYYMMDD is 8 digits + raise ValueError timetuple = ( int(ical[:4]), # year int(ical[4:6]), # month @@ -356,7 +373,7 @@ def from_ical(ical): ) return date(*timetuple) except: - raise ValueError('Wrong date format %s' % ical) + raise ValueError("Wrong date format '%s'" % ical) class vDatetime(object): @@ -372,7 +389,15 @@ class vDatetime(object): """ def __init__(self, dt): self.dt = dt - self.params = Parameters() + self.params = Parameters({'value': 'DATE-TIME'}) + if hasattr(dt, 'tzinfo'): + tzinfo = dt.tzinfo + if tzinfo is not pytz.utc and\ + (tzutc is None or not isinstance(tzinfo, tzutc)): + # set the timezone as a parameter to the property + tzid = tzid_from_dt(dt) + if tzid: + self.params.update({'TZID': tzid}) def to_ical(self): dt = self.dt @@ -392,8 +417,10 @@ def to_ical(self): self.params.update({'TZID': tzid}) return s.encode('utf-8') - @staticmethod - def from_ical(ical, timezone=None): + @classmethod + def from_ical(cls, ical, timezone=None): + if isinstance(ical, cls): + return ical.dt tzinfo = None if timezone: try: @@ -402,6 +429,10 @@ def from_ical(ical, timezone=None): tzinfo = _timezone_cache.get(timezone, None) try: + if len(ical) not in (15, 16): + raise ValueError + if ical[8] != 'T': + raise ValueError timetuple = ( int(ical[:4]), # year int(ical[4:6]), # month @@ -414,12 +445,12 @@ def from_ical(ical, timezone=None): return tzinfo.localize(datetime(*timetuple)) elif not ical[15:]: return datetime(*timetuple) - elif ical[15:16] == 'Z': + elif ical[15] == 'Z': return pytz.utc.localize(datetime(*timetuple)) else: raise ValueError(ical) except: - raise ValueError('Wrong datetime format: %s' % ical) + raise ValueError("Wrong datetime format '%s'" % ical) class vDuration(object): @@ -458,8 +489,10 @@ def to_ical(self): compat.unicode_type(abs(self.td.days)).encode('utf-8') + b'D' + compat.unicode_type(timepart).encode('utf-8')) - @staticmethod - def from_ical(ical): + @classmethod + def from_ical(cls, ical): + if isinstance(ical, cls): + return ical.td try: match = DURATION_REGEX.match(ical) sign, weeks, days, hours, minutes, seconds = match.groups() @@ -531,12 +564,14 @@ def to_ical(self): return (vDatetime(self.start).to_ical() + b'/' + vDatetime(self.end).to_ical()) - @staticmethod - def from_ical(ical): + @classmethod + def from_ical(cls, ical, timezone=None): + if isinstance(ical, cls): + return (self.start, self.end) try: start, end_or_duration = ical.split('/') - start = vDDDTypes.from_ical(start) - end_or_duration = vDDDTypes.from_ical(end_or_duration) + start = vDDDTypes.from_ical(start, timezone) + end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone) return (start, end_or_duration) except: raise ValueError('Expected period format, got: %s' % ical) @@ -725,6 +760,14 @@ def __init__(self, *args): else: self.dt = time(*args) self.params = Parameters({'value': 'TIME'}) + if hasattr(self.dt, 'tzinfo'): + tzinfo = self.dt.tzinfo + if tzinfo is not pytz.utc and\ + (tzutc is None or not isinstance(tzinfo, tzutc)): + # set the timezone as a parameter to the property + tzid = tzid_from_dt(self.dt) + if tzid: + self.params.update({'TZID': tzid}) def to_ical(self): return self.dt.strftime("%H%M%S") @@ -733,10 +776,12 @@ def to_ical(self): def from_ical(ical): # TODO: timezone support try: + if len(ical) not in (6,7): + raise ValueError timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6])) return time(*timetuple) except: - raise ValueError('Expected time, got: %s' % ical) + raise ValueError("Expected time, got: '%s'" % ical) class vUri(compat.unicode_type): @@ -894,9 +939,9 @@ def __init__(self, *args, **kwargs): self['binary'] = vBinary self['boolean'] = vBoolean self['cal-address'] = vCalAddress - self['date'] = vDDDTypes - self['date-time'] = vDDDTypes - self['duration'] = vDDDTypes + self['date'] = vDate + self['date-time'] = vDatetime + self['duration'] = vDuration self['float'] = vFloat self['integer'] = vInt self['period'] = vPeriod @@ -922,7 +967,7 @@ def __init__(self, *args, **kwargs): 'prodid': 'text', 'version': 'text', # Descriptive Component Properties - 'attach': 'uri', + 'attach': ('uri', 'binary'), 'categories': 'text', 'class': 'text', 'comment': 'text', @@ -936,9 +981,9 @@ def __init__(self, *args, **kwargs): 'summary': 'text', # Date and Time Component Properties 'completed': 'date-time', - 'dtend': 'date-time', - 'due': 'date-time', - 'dtstart': 'date-time', + 'dtend': ('date-time', 'date'), + 'due': ('date-time', 'date'), + 'dtstart': ('date-time', 'date'), 'duration': 'duration', 'freebusy': 'period', 'transp': 'text', @@ -952,23 +997,23 @@ def __init__(self, *args, **kwargs): 'attendee': 'cal-address', 'contact': 'text', 'organizer': 'cal-address', - 'recurrence-id': 'date-time', + 'recurrence-id': ('date-time', 'date'), 'related-to': 'text', 'url': 'uri', 'uid': 'text', # Recurrence Component Properties - 'exdate': 'date-time-list', - 'exrule': 'recur', - 'rdate': 'date-time-list', + 'exdate': ('date-time', 'date'), # list + 'exrule': 'recur', # deprecated in RFC 5545 + 'rdate': ('date-time', 'date', 'period'), # list 'rrule': 'recur', # Alarm Component Properties 'action': 'text', 'repeat': 'integer', - 'trigger': 'duration', + 'trigger': ('duration', 'date-time'), # if datetime, must be UTC format # Change Management Component Properties - 'created': 'date-time', - 'dtstamp': 'date-time', - 'last-modified': 'date-time', + 'created': 'date-time', # must be in UTC time format + 'dtstamp': 'date-time', # must be in UTC time format + 'last-modified': 'date-time', # must be in UTC time format 'sequence': 'integer', # Miscellaneous Component Properties 'request-status': 'text', @@ -992,14 +1037,53 @@ def __init__(self, *args, **kwargs): 'role': 'text', 'rsvp': 'boolean', 'sent-by': 'cal-address', - 'tzid': 'text', + # 'tzid': 'text', would be an overlapping duplicate 'value': 'text', }) - def for_property(self, name): - """Returns a the default type for a property or parameter + list_properties = ('exdate', 'rdate') + + def is_list_property(self, name): + if name.lower() in self.list_properties: + return True + return False + + def for_property(self, name, valuetype=None): + """Returns inner representation type for a property + @param valuetype: the value of the VALUE parameter if set """ - return self[self.types_map.get(name, 'text')] + res_type = self.types_map.get(name) + if res_type is None: + # unknown property + if valuetype is not None\ + and valuetype.upper() in list(self.keys()): + return self[valuetype] + else: + return self['text'] + if isinstance(res_type, tuple): + if valuetype is not None: + # VALUE was set + valuetype = valuetype.lower() + if valuetype not in res_type: + raise ValueError("The VALUE parameter of {name} property " + "is not supported: '{type}'" + .format(name=name, type=valuetype.upper()) + ) + else: + # the type in VALUE can be used + res_type = self[valuetype] + else: + # VALUE was not set, use default type + res_type = self[res_type[0]] + elif valuetype is not None and valuetype.lower() != res_type: + raise ValueError("The VALUE parameter of {name} property is " + "not supported: '{type}'" + .format(name=name, type=valuetype.uppper())) + else: + # Only one type is allowed and if VALUE set, it corresponds to it + res_type = self[res_type] + + return res_type def to_ical(self, name, value): """Encodes a named value from a primitive python type to an icalendar @@ -1008,10 +1092,15 @@ def to_ical(self, name, value): type_class = self.for_property(name) return type_class(value).to_ical() - def from_ical(self, name, value): + def from_ical(self, name, value, valuetype=None): """Decodes a named property or parameter value from an icalendar encoded string to a primitive python type. """ - type_class = self.for_property(name) - decoded = type_class.from_ical(value) + type_class = self.for_property(name, valuetype) + + if name.lower() in self.list_properties: + # this property is of list type + decoded = vDDDLists.from_ical(value, unit_type=type_class) + else: + decoded = type_class.from_ical(value) return decoded diff --git a/src/icalendar/tests/decoding.ics b/src/icalendar/tests/decoding.ics new file mode 100644 index 00000000..bd9a410a --- /dev/null +++ b/src/icalendar/tests/decoding.ics @@ -0,0 +1,77 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Plone.org//NONSGML plone.app.event//EN +X-WR-TIMEZONE:Europe/Vienna +BEGIN:VTIMEZONE +TZID:Europe/Vienna +X-LIC-LOCATION:Europe/Vienna +BEGIN:DAYLIGHT +DTSTART;VALUE=DATE-TIME:20130331T030000 +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VEVENT +SUMMARY:e1 +DESCRIPTION:A basic event with many properties. +DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20130719T120000 +DTEND;TZID=Europe/Vienna;VALUE=DATE-TIME:20130720T130000 +DTSTAMP;VALUE=DATE-TIME:20130719T125936Z +UID:48f1a7ad64e847568d860cd092344970 +ATTENDEE;CN=attendee1;ROLE=REQ-PARTICIPANT:attendee1 +ATTENDEE;CN=attendee2;ROLE=REQ-PARTICIPANT:attendee2 +ATTENDEE;CN=attendee3;ROLE=REQ-PARTICIPANT:attendee3 +CONTACT:testcontactname\, 1234\, test@contact.email\, http://test.url +CREATED;VALUE=DATE-TIME:20130719T105931Z +LAST-MODIFIED;VALUE=DATE-TIME:20130719T105931Z +LOCATION:testlocation +URL:http://localhost:8080/Plone/testevent +END:VEVENT + +BEGIN:VEVENT +SUMMARY:e2 +DESCRIPTION:A recurring event with exdates +DTSTART:19960401T010000 +DTEND:19960401T020000 +RRULE:FREQ=DAILY;COUNT=100 +EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z +UID:48f1a7ad64e847568d860cd0923449702 +LAST-MODIFIED;VALUE=DATE-TIME:20130719T105931Z +END:VEVENT + +BEGIN:VEVENT +SUMMARY:e3 +DESCRIPTION:A Recurring event with multiple exdates, one per line. +DTSTART;TZID=Europe/Vienna:20120327T100000 +DTEND;TZID=Europe/Vienna:20120327T180000 +RRULE:FREQ=WEEKLY;UNTIL=20120703T080000Z;BYDAY=TU +EXDATE;TZID=Europe/Vienna:20120529T100000 +EXDATE;TZID=Europe/Vienna:20120403T100000 +EXDATE;TZID=Europe/Vienna:20120410T100000 +EXDATE;TZID=Europe/Vienna:20120501T100000 +EXDATE;TZID=Europe/Vienna:20120417T100000 +DTSTAMP:20130716T120638Z +UID:48f1a7ad64e847568d860cd0923449703 +LAST-MODIFIED;VALUE=DATE-TIME:20130719T105931Z +END:VEVENT + +BEGIN:VEVENT +SUMMARY:e4 +DESCRIPTION:Whole day event +DTSTART;VALUE=DATE:20130404 +DTEND;VALUE=DATE:20130404 +UID:48f1a7ad64e847568d860cd0923449704 +LAST-MODIFIED;VALUE=DATE-TIME:20130719T105931Z +END:VEVENT + +BEGIN:VEVENT +SUMMARY:e5 +DESCRIPTION:Open end event +DTSTART:20130402T120000 +UID:48f1a7ad64e847568d860cd0923449705 +LAST-MODIFIED;VALUE=DATE-TIME:20130719T105931Z +END:VEVENT + +END:VCALENDAR diff --git a/src/icalendar/tests/decoding2.ics b/src/icalendar/tests/decoding2.ics new file mode 100644 index 00000000..f6340959 --- /dev/null +++ b/src/icalendar/tests/decoding2.ics @@ -0,0 +1,75 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Plone.org//NONSGML plone.app.event//EN +X-WR-TIMEZONE:Europe/Vienna +BEGIN:VTIMEZONE +TZID:Europe/Vienna +X-LIC-LOCATION:Europe/Vienna +BEGIN:DAYLIGHT +DTSTART;VALUE=DATE-TIME:20130331T030000 +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +END:DAYLIGHT +END:VTIMEZONE + +BEGIN:VEVENT +SUMMARY:e1 with new title, more recent last modified +DESCRIPTION:A basic event with many properties, updated. +DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20130819T120000 +DTEND;TZID=Europe/Vienna;VALUE=DATE-TIME:20130820T130000 +DTSTAMP;VALUE=DATE-TIME:20130719T125936Z +UID:48f1a7ad64e847568d860cd092344970 +ATTENDEE;CN=attendee1;ROLE=REQ-PARTICIPANT:attendee1 +ATTENDEE;CN=attendee2;ROLE=REQ-PARTICIPANT:attendee2 +ATTENDEE;CN=attendee3;ROLE=REQ-PARTICIPANT:attendee3 +CONTACT:testcontactname\, 1234\, test@contact.email\, http://test.url +CREATED;VALUE=DATE-TIME:20130719T105931Z +LAST-MODIFIED;VALUE=DATE-TIME:20130819T105931Z +LOCATION:testlocation +URL:http://localhost:8080/Plone/testevent +END:VEVENT + +BEGIN:VEVENT +SUMMARY:e2, updated +DESCRIPTION:A recurring event with exdates, updated. +DTSTART:21000401T010000 +DTEND:21000401T020000 +UID:48f1a7ad64e847568d860cd0923449702 +LAST-MODIFIED;VALUE=DATE-TIME:20130719T105931Z +END:VEVENT + +BEGIN:VEVENT +SUMMARY:e3 (older version) +DESCRIPTION:A Recurring event with multiple exdates, one per line. +DTSTART;TZID=Europe/Vienna:20120327T100000 +DTEND;TZID=Europe/Vienna:20120327T180000 +RRULE:FREQ=WEEKLY;UNTIL=20120703T080000Z;BYDAY=TU +EXDATE;TZID=Europe/Vienna:20120529T100000 +EXDATE;TZID=Europe/Vienna:20120403T100000 +EXDATE;TZID=Europe/Vienna:20120410T100000 +EXDATE;TZID=Europe/Vienna:20120501T100000 +EXDATE;TZID=Europe/Vienna:20120417T100000 +DTSTAMP:20130716T120638Z +UID:48f1a7ad64e847568d860cd0923449703 +LAST-MODIFIED;VALUE=DATE-TIME:20130718T105931Z +END:VEVENT + +BEGIN:VEVENT +SUMMARY:e4 +DESCRIPTION:Whole day event +DTSTART;VALUE=DATE:20130404 +DTEND;VALUE=DATE:20130404 +UID:48f1a7ad64e847568d860cd0923449704 +LAST-MODIFIED;VALUE=DATE-TIME:20130719T105931Z +END:VEVENT + +BEGIN:VEVENT +SUMMARY:e5 +DESCRIPTION:Open end event +DTSTART:20130402T120000 +UID:48f1a7ad64e847568d860cd0923449705 +LAST-MODIFIED;VALUE=DATE-TIME:20130719T105931Z +END:VEVENT + +END:VCALENDAR diff --git a/src/icalendar/tests/test_decoding.py b/src/icalendar/tests/test_decoding.py new file mode 100644 index 00000000..60623bdd --- /dev/null +++ b/src/icalendar/tests/test_decoding.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from icalendar.tests import unittest + +import icalendar +import os + + +def _get_props(item): + ret = [] + for prop_name, _ in item.items(): + ret.append(item.decoded(prop_name)) + return ret + + +class DecodeIssues(unittest.TestCase): + + def test_icalendar_1(self): + directory = os.path.dirname(__file__) + ics = open(os.path.join(directory, 'decoding.ics'), 'rb') + cal = icalendar.Calendar.from_ical(ics.read()) + ics.close() + cal.to_ical() + for item in cal.walk('VEVENT'): + prop_list = _get_props(item) + + def test_icalendar_2(self): + directory = os.path.dirname(__file__) + ics = open(os.path.join(directory, 'decoding2.ics'), 'rb') + cal = icalendar.Calendar.from_ical(ics.read()) + ics.close() + cal.to_ical() + for item in cal.walk('VEVENT'): + prop_list = _get_props(item) diff --git a/src/icalendar/tests/test_fixed_issues.py b/src/icalendar/tests/test_fixed_issues.py index d1d2ade9..dfeb535b 100644 --- a/src/icalendar/tests/test_fixed_issues.py +++ b/src/icalendar/tests/test_fixed_issues.py @@ -275,7 +275,7 @@ def test_issue_116(self): b'BEGIN:VEVENT\r\nX-APPLE-STRUCTURED-LOCATION;VALUE=URI;' b'X-ADDRESS="367 George Street Sydney \r\n CBD NSW 2000";' b'X-APPLE-RADIUS=72;X-TITLE="367 George Street":' - b'geo:-33.868900\r\n \\,151.207000\r\nEND:VEVENT\r\n' + b'geo:-33.868900\r\n ,151.207000\r\nEND:VEVENT\r\n' ) # roundtrip @@ -403,14 +403,14 @@ def test_issue_178(self): 'DTSTAMP:20150121T080000', 'BEGIN:VEVENT', 'UID:12345', - 'DTSTART:20150122', + 'DTSTART;VALUE=DATE:20150122', 'END:VEVENT', 'END:MYCOMPTOO']) cal = icalendar.Calendar.from_ical(ical_str) self.assertEqual(cal.errors, []) self.assertEqual(cal.to_ical(), b'BEGIN:MYCOMPTOO\r\nDTSTAMP:20150121T080000\r\n' - b'BEGIN:VEVENT\r\nDTSTART:20150122\r\nUID:12345\r\n' + b'BEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20150122\r\nUID:12345\r\n' b'END:VEVENT\r\nEND:MYCOMPTOO\r\n') def test_issue_184(self): @@ -432,3 +432,70 @@ def test_issue_184(self): b'RDATE;VALUE=PERIOD:20150219T133000/PT10H\r\n' b'END:VEVENT\r\n' ) + + def test_issue_187(self): + """Issue 184: The property VALUE parameter is being ignored during + parsing. Also, the types are not strong as the RFC 5545 requires.""" + + orig_str = ['BEGIN:VEVENT', + 'DTSTAMP:20150217T095800', + 'DTSTART:20150408T120001', + 'RDATE:20150408T120001', + 'END:VEVENT' + ] + ical_str = orig_str[:] + # correct setup + event = icalendar.Event.from_ical('\r\n'.join(ical_str)) + self.assertEqual(event.to_ical(), + b'BEGIN:VEVENT\r\n' + b'DTSTART:20150408T120001\r\n' + b'DTSTAMP:20150217T095800\r\n' + b'RDATE:20150408T120001\r\n' + b'END:VEVENT\r\n') + self.assertEqual(event.errors, []) + # correct setup with an unknown property + ical_str.insert(4, 'MYPROP:200512,143022,064530') + event = icalendar.Event.from_ical('\r\n'.join(ical_str)) + self.assertTrue(isinstance(event['MYPROP'], icalendar.vText)) + # correct setup with unknown property - VALUE is set + ical_str[4] = 'MYPROP;VALUE=DATE-TIME:20050520T200505' + event = icalendar.Event.from_ical('\r\n'.join(ical_str)) + self.assertTrue(isinstance(event['MYPROP'], icalendar.vDatetime)) + # wrong setup with unknown property when VALUE is set + ical_str[4] = 'MYPROP;VALUE=TIME:20050520T200505' + event = icalendar.Event.from_ical('\r\n'.join(ical_str)) + self.assertEqual(event.errors, + [('MYPROP', "Expected time, got: '20050520T200505'")]) + self.assertEqual(event['MYPROP'], + icalendar.prop.vText('20050520T200505')) + + # Wrong default property value (DATE instead of DATE-TIME) + ical_str = orig_str[:] + ical_str[2] = 'DTSTART:20150408' + event = icalendar.Event.from_ical('\r\n'.join(ical_str)) + self.assertEqual(event.errors, + [('DTSTART', "Wrong datetime format '20150408'")]) + self.assertEqual(event['DTSTART'], + icalendar.prop.vText('20150408')) + + # -------- Wrong vDDDLists setups follow -------- + ical_str = orig_str[:] + # DATE-TIME value at EXDATE with VALUE:DATE + ical_str[3] = 'RDATE;VALUE=DATE:20150217T095800' + event = icalendar.Event.from_ical('\r\n'.join(ical_str)) + self.assertEqual(event.errors, + [('RDATE', "Wrong date format '20150217T095800'")]) + self.assertEqual(event['RDATE'], + icalendar.prop.vText('20150217T095800')) + + ical_str[3] = ('RDATE;FMTTYPE=text/plain;ENCODING=BASE64;VALUE=BINARY:' + 'c3RsYXo=') + event = icalendar.Event.from_ical('\r\n'.join(ical_str)) + self.assertEqual(event.errors, + [('RDATE', "The VALUE parameter of RDATE property " + "is not supported: 'BINARY'")]) + self.assertEqual(event['RDATE'], + icalendar.prop.vText('c3RsYXo=')) + self.assertEqual(event['RDATE'].params, + {'VALUE': 'BINARY', 'ENCODING': 'BASE64', + 'FMTTYPE': 'text/plain'}) diff --git a/src/icalendar/tests/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index 0cda1177..3786544a 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -428,5 +428,5 @@ def test_cal_Calendar(self): self.assertEqual( [e.errors for e in icalendar.cal.Calendar.from_ical(s).walk('VEVENT')], - [[], [('EXDATE', "Expected datetime, date, or time, got: ''")]] + [[], [('EXDATE', "Wrong date format ''")]] )