From b22557150d458b65b39284930de67069010bcd43 Mon Sep 17 00:00:00 2001 From: Stanislav Laznicka Date: Wed, 13 Apr 2016 10:06:14 +0200 Subject: [PATCH 1/7] Introducing strong types to python-icalendar https://github.com/collective/icalendar/issues/187 --- CHANGES.rst | 18 +++ src/icalendar/cal.py | 38 +++-- src/icalendar/prop.py | 178 +++++++++++++++++------ src/icalendar/tests/decoding.ics | 77 ++++++++++ src/icalendar/tests/decoding2.ics | 75 ++++++++++ src/icalendar/tests/test_decoding.py | 32 ++++ src/icalendar/tests/test_fixed_issues.py | 62 +++++++- src/icalendar/tests/test_unit_cal.py | 2 +- 8 files changed, 422 insertions(+), 60 deletions(-) create mode 100644 src/icalendar/tests/decoding.ics create mode 100644 src/icalendar/tests/decoding2.ics create mode 100644 src/icalendar/tests/test_decoding.py diff --git a/CHANGES.rst b/CHANGES.rst index 2ea61aa3..9bf219c5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,24 @@ Changelog ========= +5.0 (unreleased) +---------------- + +Breaking changes: + +- 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* + +Bug fixes: + +- *add item here* + 4.0.9 (unreleased) ------------------ diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 2c7dcd7b..0fd22027 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,8 @@ def add(self, name, value, parameters=None, encode=1): # encode value if encode and isinstance(value, list) \ - and name.lower() not in ['rdate', 'exdate', 'categories']: + and not types_factory.is_list_property(name)\ + and name.lower() not in ('categories',): # 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 +223,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 +235,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 +376,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,10 +383,18 @@ 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')) + if types_factory.is_list_property(name): + 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 diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index 48a346b1..50f5d25c 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -239,13 +239,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'] @@ -259,12 +268,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 class vCategory(object): @@ -273,6 +286,7 @@ def __init__(self, c_list): if not hasattr(c_list, '__iter__'): c_list = [c_list] self.cats = [vText(c) for c in c_list] + self.params = Parameters() def to_ical(self): return b",".join([c.to_ical() for c in self.cats]) @@ -302,8 +316,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)): @@ -363,9 +376,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 @@ -373,7 +391,7 @@ def from_ical(ical): ) return date(*timetuple) except Exception: - raise ValueError('Wrong date format %s' % ical) + raise ValueError("Wrong date format '%s'" % ical) class vDatetime(object): @@ -389,7 +407,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 @@ -409,8 +435,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: @@ -422,6 +450,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 @@ -434,12 +466,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 Exception: - raise ValueError('Wrong datetime format: %s' % ical) + raise ValueError("Wrong datetime format '%s'" % ical) class vDuration(object): @@ -479,8 +511,10 @@ def to_ical(self): compat.unicode_type(abs(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() @@ -552,12 +586,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 Exception: raise ValueError('Expected period format, got: %s' % ical) @@ -746,6 +782,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") @@ -754,10 +798,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 Exception: - raise ValueError('Expected time, got: %s' % ical) + raise ValueError("Expected time, got: '%s'" % ical) class vUri(compat.unicode_type): @@ -921,9 +967,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 @@ -950,7 +996,7 @@ def __init__(self, *args, **kwargs): 'prodid': 'text', 'version': 'text', # Descriptive Component Properties - 'attach': 'uri', + 'attach': ('uri', 'binary'), 'categories': 'categories', 'class': 'text', 'comment': 'text', @@ -964,9 +1010,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', @@ -980,23 +1026,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', @@ -1020,14 +1066,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 @@ -1036,10 +1121,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..75a6bb18 --- /dev/null +++ b/src/icalendar/tests/test_decoding.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import icalendar +import unittest +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 772a7d5f..828b1433 100644 --- a/src/icalendar/tests/test_fixed_issues.py +++ b/src/icalendar/tests/test_fixed_issues.py @@ -277,7 +277,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 @@ -405,14 +405,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): @@ -478,3 +478,59 @@ def test_issue_237(self): expected_tzname = 'Brasília standard'.encode('ascii', 'replace') self.assertEqual(dtstart.tzinfo.zone, expected_zone) self.assertEqual(dtstart.tzname(), expected_tzname) + + 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'")]) + + # 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'")]) + + # -------- 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'")]) + + 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'")]) diff --git a/src/icalendar/tests/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index 9ad901ca..881834bc 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -433,7 +433,7 @@ 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 ''")]] ) def test_cal_strict_parsing(self): From 97c2ce33004bff2f86fadcf43a18a18120ae6431 Mon Sep 17 00:00:00 2001 From: Stanislav Laznicka Date: Fri, 12 Aug 2016 14:50:36 +0200 Subject: [PATCH 2/7] Improvement to error handling Previously, if error occured during the creation of inner type instance from iCalendar string, the error with the name of the bogus property would be stored in the appropriate component's errors attribute along with the error string but the property's value would be removed from the parsed representation. This patch keeps the value even with its parameters at that certain property, the value's type is changed to vText. Should allow implementation of https://github.com/collective/icalendar/issues/158 Improves https://github.com/collective/icalendar/issues/174 --- CHANGES.rst | 5 +++++ src/icalendar/cal.py | 21 ++++++++++++++++----- src/icalendar/tests/test_fixed_issues.py | 11 +++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9bf219c5..9d46c44d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,11 @@ Breaking changes: Refs #187 [stlaz] +- Improved error handling. The value and parameters of a property should no longer + be lost upon error. + Refs #158 #174 + [stlaz] + New features: - *add item here* diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 0fd22027..f8ef3071 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -385,7 +385,17 @@ def from_ical(cls, st, multiple=False): try: factory = types_factory.for_property(name, params.get('VALUE')) - if types_factory.is_list_property(name): + 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)) @@ -399,10 +409,11 @@ def from_ical(cls, st, multiple=False): 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/tests/test_fixed_issues.py b/src/icalendar/tests/test_fixed_issues.py index 828b1433..b1fc74e3 100644 --- a/src/icalendar/tests/test_fixed_issues.py +++ b/src/icalendar/tests/test_fixed_issues.py @@ -512,6 +512,8 @@ def test_issue_187(self): 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[:] @@ -519,6 +521,8 @@ def test_issue_187(self): 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[:] @@ -527,6 +531,8 @@ def test_issue_187(self): 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=') @@ -534,3 +540,8 @@ def test_issue_187(self): 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'}) From 039a7e686f1b1004a6e561906643127808d5ac65 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 12 Oct 2021 21:37:10 +0200 Subject: [PATCH 3/7] Fix vDDDLists check to only accept values of the same type. --- src/icalendar/prop.py | 2 +- src/icalendar/tests/test_unit_prop.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index 50f5d25c..c545ec57 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -251,7 +251,7 @@ def __init__(self, dt_list, type_class=None): ltype = type(dt_list[0]) for dt in dt_list: # raise ValueError if type of the input values differs - if not isinstance(dt, ltype): + if not type(dt) == ltype: raise ValueError("Trying to insert '%s' value into a list " "of '%s'".format(type(dt), ltype)) dt = type_class(dt) diff --git a/src/icalendar/tests/test_unit_prop.py b/src/icalendar/tests/test_unit_prop.py index e35bcccb..27d652a4 100644 --- a/src/icalendar/tests/test_unit_prop.py +++ b/src/icalendar/tests/test_unit_prop.py @@ -97,6 +97,13 @@ def test_prop_vDDDLists(self): dt_list = vDDDLists([datetime(2000, 1, 1), datetime(2000, 11, 11)]) self.assertEqual(dt_list.to_ical(), b'20000101T000000,20001111T000000') + def test_prop_vDDDLists_same_types(self): + """vDDDLists should raise an error when initialized with different date/time types. + """ + from ..prop import vDDDLists + with self.assertRaises(ValueError): + vDDDLists([date.today(), datetime.now()]) + def test_prop_vDDDTypes(self): from ..prop import vDDDTypes From 6c48e58febb7f28f1642249e1c4110210ee853bc Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 12 Oct 2021 22:52:25 +0200 Subject: [PATCH 4/7] Fix adding a "dtstart" entry was failing with date. --- src/icalendar/cal.py | 5 +- src/icalendar/prop.py | 50 +++++++++++------- src/icalendar/tests/test_fixed_issues.py | 11 ++++ src/icalendar/tests/test_unit_prop.py | 66 ++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 20 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index f8ef3071..d840e5a0 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -148,7 +148,10 @@ def _encode(self, name, value, parameters=None, encode=1): params[key] = item parameters = params klass = types_factory.for_property( - name, parameters.get('VALUE') if parameters else None) + name, + valuetype=parameters.get('VALUE') if parameters else None, + nativetype=type(value) + ) if types_factory.is_list_property(name): obj = vDDDLists(value, klass) else: diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index c545ec57..12b4055b 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -1070,6 +1070,11 @@ def __init__(self, *args, **kwargs): 'value': 'text', }) + native_type_map = { + datetime: 'date-time', + date: 'date', + } + list_properties = ('exdate', 'rdate') def is_list_property(self, name): @@ -1077,20 +1082,28 @@ def is_list_property(self, name): return True return False - def for_property(self, name, valuetype=None): + def for_property(self, name, valuetype=None, nativetype=None): """Returns inner representation type for a property @param valuetype: the value of the VALUE parameter if set """ res_type = self.types_map.get(name) + _nativetype = self.native_type_map.get(nativetype) + if res_type is None: - # unknown property - if valuetype is not None\ - and valuetype.upper() in list(self.keys()): + # Unknown property + + if valuetype and valuetype.upper() in list(self.keys()): return self[valuetype] - else: - return self['text'] + + if _nativetype: + return self[_nativetype] + + return self['text'] # Default fallback + if isinstance(res_type, tuple): - if valuetype is not None: + # List of values should have the same type + + if valuetype: # VALUE was set valuetype = valuetype.lower() if valuetype not in res_type: @@ -1098,21 +1111,20 @@ def for_property(self, name, valuetype=None): "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: + # The type in VALUE can be used + return self[valuetype] + + if _nativetype: + return self[_nativetype] + + return self[res_type[0]] # Fallback, use first type of tuple. + + elif valuetype 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] + .format(name=name, type=valuetype.upper())) - return res_type + return self[res_type] def to_ical(self, name, value): """Encodes a named value from a primitive python type to an icalendar diff --git a/src/icalendar/tests/test_fixed_issues.py b/src/icalendar/tests/test_fixed_issues.py index b1fc74e3..24271588 100644 --- a/src/icalendar/tests/test_fixed_issues.py +++ b/src/icalendar/tests/test_fixed_issues.py @@ -545,3 +545,14 @@ def test_issue_187(self): self.assertEqual(event['RDATE'].params, {'VALUE': 'BINARY', 'ENCODING': 'BASE64', 'FMTTYPE': 'text/plain'}) + + def test_pr_196__1(self): + """Test case from comment + https://github.com/collective/icalendar/pull/196#issuecomment-317485774 + """ + event = icalendar.Event() + event.add('DTSTART', datetime.date(2021, 10, 12)) + self.assertEqual( + event.to_ical(), + b'BEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20211012\r\nEND:VEVENT\r\n' + ) diff --git a/src/icalendar/tests/test_unit_prop.py b/src/icalendar/tests/test_unit_prop.py index 27d652a4..753b3fc7 100644 --- a/src/icalendar/tests/test_unit_prop.py +++ b/src/icalendar/tests/test_unit_prop.py @@ -506,6 +506,72 @@ def test_prop_TypesFactory(self): 'Rasmussen, Max M\xf8ller' ) + def test_prop_TypesFactory_for_property(self): + """Test all the different supported cases of for_property + """ + from icalendar import prop + factory = prop.TypesFactory() + + today = date.today() + now = datetime.now() + + self.assertEqual( + factory.for_property("version"), + prop.vText + ) + + self.assertEqual( + factory.for_property("rrule"), + prop.vRecur + ) + + # dtstart can be date-time or date + # Default + self.assertEqual( + factory.for_property("dtstart"), + prop.vDatetime + ) + # Specifying the valuetype + self.assertEqual( + factory.for_property("dtstart", valuetype="date"), + prop.vDate + ) + # Passing a native type + self.assertEqual( + factory.for_property("dtstart", nativetype=type(today)), + prop.vDate + ) + # valuetype takes precedence + self.assertEqual( + factory.for_property("dtstart", valuetype="date", nativetype=type(now)), + prop.vDate + ) + + # Asking for not existent types raises ValueError + with self.assertRaises(ValueError): + factory.for_property("dtstart", valuetype="does-not-exist") + with self.assertRaises(ValueError): + factory.for_property("version", valuetype="does-not-exist") + + # Unsresolved name fallbacks + self.assertEqual( + factory.for_property("does-not-exist"), + prop.vText + ) + self.assertEqual( + factory.for_property("does-not-exist", valuetype="date"), + prop.vDate + ) + self.assertEqual( + factory.for_property("does-not-exist", nativetype=type(today)), + prop.vDate + ) + # valuetype takes precedence + self.assertEqual( + factory.for_property("does-not-exist", valuetype="date", nativetype=type(now)), + prop.vDate + ) + class TestPropertyValues(unittest.TestCase): From 75098b7fb122589b6579e45baeddcfccccb708fd Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 13 Oct 2021 11:49:38 +0200 Subject: [PATCH 5/7] Fix backwards compatibility: Support DURATION.dt. --- src/icalendar/prop.py | 6 ++++++ src/icalendar/tests/test_fixed_issues.py | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index 12b4055b..a3001b59 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -485,6 +485,12 @@ def __init__(self, td): self.td = td self.params = Parameters() + @property + def dt(self): + # BBB: Backwards compatibility. + # Might be removed in future version. + return self.td + def to_ical(self): sign = "" td = self.td diff --git a/src/icalendar/tests/test_fixed_issues.py b/src/icalendar/tests/test_fixed_issues.py index 24271588..6b8d42a0 100644 --- a/src/icalendar/tests/test_fixed_issues.py +++ b/src/icalendar/tests/test_fixed_issues.py @@ -556,3 +556,12 @@ def test_pr_196__1(self): event.to_ical(), b'BEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20211012\r\nEND:VEVENT\r\n' ) + + def test_pr_196__2(self): + """Test case from comment + https://github.com/collective/icalendar/pull/196#issuecomment-318034052 + """ + event = icalendar.Event() + event.add('DURATION', datetime.timedelta(hours=2)) + self.assertEqual(event["DURATION"].td, datetime.timedelta(seconds=7200)) # Official API + self.assertEqual(event["DURATION"].dt, datetime.timedelta(seconds=7200)) # Backwards compatibility From bbab45752c47e52921f0a500c9e676203006a8b6 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 12 Oct 2021 21:50:18 +0200 Subject: [PATCH 6/7] Change isinstance tests for datetime more unambogious, even if no functionality change. --- src/icalendar/cal.py | 6 +++--- src/icalendar/prop.py | 38 ++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index d840e5a0..ea6c5581 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -182,7 +182,7 @@ def add(self, name, value, parameters=None, encode=1): :returns: None """ - if isinstance(value, datetime) and\ + if type(value) is datetime and\ name.lower() in ('dtstamp', 'created', 'last-modified'): # RFC expects UTC for those... force value conversion. if getattr(value, 'tzinfo', False) and value.tzinfo is not None: @@ -624,9 +624,9 @@ def to_tz(self): dst = {} tznames = set() for component in self.walk(): - if type(component) == Timezone: + if type(component) is Timezone: continue - assert isinstance(component['DTSTART'].dt, datetime), ( + assert type(component['DTSTART'].dt) is datetime, ( "VTIMEZONEs sub-components' DTSTART must be of type datetime, not date" ) try: diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index a3001b59..a70eabf6 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -304,19 +304,19 @@ class vDDDTypes(object): So this is practical. """ def __init__(self, dt): - if not isinstance(dt, (datetime, date, timedelta, time, tuple)): + if type(dt) not in (datetime, date, timedelta, time, tuple): raise ValueError('You must use datetime, date, timedelta, ' 'time or tuple (for periods)') - if isinstance(dt, datetime): + if type(dt) is datetime: self.params = Parameters({'value': 'DATE-TIME'}) - elif isinstance(dt, date): + elif type(dt) is date: self.params = Parameters({'value': 'DATE'}) - elif isinstance(dt, time): + elif type(dt) is time: self.params = Parameters({'value': 'TIME'}) - elif isinstance(dt, tuple): + elif type(dt) is tuple: self.params = Parameters({'value': 'PERIOD'}) - if isinstance(dt, (datetime, time)) and hasattr(dt, 'tzinfo'): + if type(dt) in (datetime, time) and hasattr(dt, 'tzinfo'): tzinfo = dt.tzinfo if tzinfo is not pytz.utc and\ (tzutc is None or not isinstance(tzinfo, tzutc)): @@ -328,15 +328,15 @@ def __init__(self, dt): def to_ical(self): dt = self.dt - if isinstance(dt, datetime): + if type(dt) is datetime: return vDatetime(dt).to_ical() - elif isinstance(dt, date): + elif type(dt) is date: return vDate(dt).to_ical() - elif isinstance(dt, timedelta): + elif type(dt) is timedelta: return vDuration(dt).to_ical() - elif isinstance(dt, time): + elif type(dt) is time: return vTime(dt).to_ical() - elif isinstance(dt, tuple) and len(dt) == 2: + elif type(dt) is tuple and len(dt) == 2: return vPeriod(dt).to_ical() else: raise ValueError('Unknown date type: {}'.format(type(dt))) @@ -367,7 +367,7 @@ class vDate(object): """Render and generates iCalendar date format. """ def __init__(self, dt): - if not isinstance(dt, date): + if type(dt) is not date: raise ValueError('Value MUST be a date instance') self.dt = dt self.params = Parameters({'value': 'DATE'}) @@ -480,7 +480,7 @@ class vDuration(object): """ def __init__(self, td): - if not isinstance(td, timedelta): + if type(td) is not timedelta: raise ValueError('Value MUST be a timedelta instance') self.td = td self.params = Parameters() @@ -543,15 +543,13 @@ class vPeriod(object): """ def __init__(self, per): start, end_or_duration = per - if not (isinstance(start, datetime) or isinstance(start, date)): + if type(start) not in (datetime, date): raise ValueError('Start value MUST be a datetime or date instance') - if not (isinstance(end_or_duration, datetime) or - isinstance(end_or_duration, date) or - isinstance(end_or_duration, timedelta)): + if type(end_or_duration) not in (datetime, date, timedelta): raise ValueError('end_or_duration MUST be a datetime, ' 'date or timedelta instance') by_duration = 0 - if isinstance(end_or_duration, timedelta): + if type(end_or_duration) is timedelta: by_duration = 1 duration = end_or_duration end = start + duration @@ -782,7 +780,7 @@ class vTime(object): def __init__(self, *args): if len(args) == 1: - if not isinstance(args[0], (time, datetime)): + if type(args[0]) not in (time, datetime): raise ValueError('Expected a datetime.time, got: %s' % args[0]) self.dt = args[0] else: @@ -871,7 +869,7 @@ class vUTCOffset(object): # propagate upwards def __init__(self, td): - if not isinstance(td, timedelta): + if not type(td) is timedelta: raise ValueError('Offset value MUST be a timedelta instance') self.td = td self.params = Parameters() From ee137f79133b8dc32c1bdafa0913612facdb90a1 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Tue, 12 Oct 2021 22:23:02 +0200 Subject: [PATCH 7/7] Cleanup. --- src/icalendar/cal.py | 31 +++++++++++++++---------------- src/icalendar/parser.py | 2 +- src/icalendar/prop.py | 30 ++++++++++++++++++++++-------- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index ea6c5581..3bced83d 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -115,7 +115,7 @@ def is_broken(self): ############################# # handling of property values - def _encode(self, name, value, parameters=None, encode=1): + def _encode(self, name, value, parameters=None, encode=True): """Encode values to icalendar property values. :param name: Name of the property. @@ -152,7 +152,7 @@ def _encode(self, name, value, parameters=None, encode=1): valuetype=parameters.get('VALUE') if parameters else None, nativetype=type(value) ) - if types_factory.is_list_property(name): + if types_factory.is_date_list_property(name): obj = vDDDLists(value, klass) else: obj = klass(value) @@ -161,7 +161,7 @@ def _encode(self, name, value, parameters=None, encode=1): obj.params = parameters return obj - def add(self, name, value, parameters=None, encode=1): + def add(self, name, value, parameters=None, encode=True): """Add a property. :param name: Name of the property. @@ -193,7 +193,7 @@ def add(self, name, value, parameters=None, encode=1): # encode value if encode and isinstance(value, list) \ - and not types_factory.is_list_property(name)\ + and not types_factory.is_date_list_property(name)\ and name.lower() not in ('categories',): # Individually convert each value to an ical type except rdate and # exdate, where lists of dates might be passed to vDDDLists. @@ -255,7 +255,7 @@ def decoded(self, name, default=_marker): # Inline values. A few properties have multiple values inlined in in one # property line. These methods are used for splitting and joining these. - def get_inline(self, name, decode=1): + def get_inline(self, name, decode=True): """Returns a list of values (split on comma). """ vals = [v.strip('" ') for v in q_split(self[name])] @@ -263,12 +263,12 @@ def get_inline(self, name, decode=1): return [self._decode(name, val) for val in vals] return vals - def set_inline(self, name, values, encode=1): + def set_inline(self, name, values, encode=True): """Converts a list of values into comma seperated string and sets value to that. """ if encode: - values = [self._encode(name, value, encode=1) for value in values] + values = [self._encode(name, value, encode=True) for value in values] self[name] = types_factory['inline'](q_join(values)) ######################### @@ -383,11 +383,10 @@ def from_ical(cls, st, multiple=False): if not component: raise ValueError('Property "{prop}" does not have ' 'a parent component.'.format(prop=name)) - datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE', - 'FREEBUSY', 'RDATE', 'EXDATE') + try: factory = types_factory.for_property(name, - params.get('VALUE')) + valuetype=params.get('VALUE')) except ValueError as e: if not component.ignore_exceptions: raise @@ -395,19 +394,19 @@ def from_ical(cls, st, multiple=False): # 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 + if (types_factory.is_date_list_property(name) and factory != vText): # TODO: list type currenty supports only datetime types vals = vDDDLists( vDDDLists.from_ical(vals, params.get('TZID'), factory)) + elif uname in types_factory.datetime_names and 'TZID' in params: + vals = factory(factory.from_ical(vals, params['TZID'])) else: - if name in datetime_names and 'TZID' in params: - vals = factory( - factory.from_ical(vals, params['TZID'])) - else: - vals = factory(factory.from_ical(vals)) + vals = factory(factory.from_ical(vals)) + except ValueError as e: if not component.ignore_exceptions: raise diff --git a/src/icalendar/parser.py b/src/icalendar/parser.py index 5344de89..59993065 100644 --- a/src/icalendar/parser.py +++ b/src/icalendar/parser.py @@ -196,7 +196,7 @@ def params(self): # TODO? # Later, when I get more time... need to finish this off now. The last major # thing missing. -# def _encode(self, name, value, cond=1): +# def _encode(self, name, value, cond=True): # # internal, for conditional convertion of values. # if cond: # klass = types_factory.for_property(name) diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index a70eabf6..ae6f4e73 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -483,7 +483,7 @@ def __init__(self, td): if type(td) is not timedelta: raise ValueError('Value MUST be a timedelta instance') self.td = td - self.params = Parameters() + self.params = Parameters({'value': 'DURATION'}) @property def dt(self): @@ -559,7 +559,7 @@ def __init__(self, per): if start > end: raise ValueError("Start time is greater than end time") - self.params = Parameters() + self.params = Parameters({'value': 'PERIOD'}) # set the timezone identifier # does not support different timezones for start and end tzid = tzid_from_dt(start) @@ -1079,10 +1079,25 @@ def __init__(self, *args, **kwargs): date: 'date', } - list_properties = ('exdate', 'rdate') - - def is_list_property(self, name): - if name.lower() in self.list_properties: + datetime_names = ( + 'COMPLETED', + 'CREATED', + 'DTEND', + 'DTSTAMP', + 'DTSTART', + 'DUE', + 'DURATION', + 'EXDATE' + 'FREEBUSY', + 'LAST-MODIFIED', + 'RDATE', + 'RECURRENCE-ID', + 'TRIGGER', + ) + date_list_properties = ('EXDATE', 'RDATE') + + def is_date_list_property(self, name): + if name.upper() in self.date_list_properties: return True return False @@ -1142,8 +1157,7 @@ def from_ical(self, name, value, valuetype=None): encoded string to a primitive python type. """ type_class = self.for_property(name, valuetype) - - if name.lower() in self.list_properties: + if name.upper() in self.date_list_properties: # this property is of list type decoded = vDDDLists.from_ical(value, unit_type=type_class) else: