diff --git a/CHANGES.rst b/CHANGES.rst index 8c7f5d2..254da50 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,14 +1,14 @@ Changelog ========= -5.0.8 (unreleased) ------------------- +5.0.11 (unreleased) +------------------- Minor changes: -- Update build configuration to build readthedocs. #538 -- No longer run the ``plone.app.event`` tests. -- Move pip caching into Python setup action. +- The cli utility now displays start and end datetimes in the user's local timezone. + Ref: #561 + [vimpostor] Breaking changes: @@ -20,7 +20,45 @@ New features: Bug fixes: -- ... +- Multivalue FREEBUSY property is now parsed properly + Ref: #27 + [jacadzaca] + +5.0.10 (unreleased) +------------------- + +Bug fixes: + +- Component._encode stops ignoring parameters argument on native values, now merges them + Fixes: #557 + [zocker1999net] + +5.0.9 (2023-09-24) +------------------ + +Bug fixes: + +- PERIOD values now set the timezone of their start and end. #556 + +5.0.8 (2023-09-18) +------------------ + +Minor changes: + +- Update build configuration to build readthedocs. #538 +- No longer run the ``plone.app.event`` tests. +- Add documentation on how to parse ``.ics`` files. #152 +- Move pip caching into Python setup action. +- Check that issue #165 can be closed. +- Updated about.rst for issue #527 +- Avoid ``vText.__repr__`` BytesWarning. + +Bug fixes: + +- Calendar components are now properly compared + Ref: #550 + Fixes: #526 + [jacadzaca] 5.0.7 (2023-05-29) ------------------ diff --git a/README.rst b/README.rst index dc39793..0ae54fd 100644 --- a/README.rst +++ b/README.rst @@ -42,6 +42,27 @@ files. .. _`pytz`: https://pypi.org/project/pytz/ .. _`BSD`: https://github.com/collective/icalendar/issues/2 +Quick Guide +----------- + +To **install** the package, run:: + + pip install icalendar + +You can open an ``.ics`` file and see all the events:: + + >>> import icalendar + >>> path_to_ics_file = "src/icalendar/tests/calendars/example.ics" + >>> with open(path_to_ics_file) as f: + ... calendar = icalendar.Calendar.from_ical(f.read()) + >>> for event in calendar.walk('VEVENT'): + ... print(event.get("SUMMARY")) + New Year's Day + Orthodox Christmas + International Women's Day + +Using this package, you can also create calendars from scratch or edit existing ones. + Versions and Compatibility -------------------------- diff --git a/docs/about.rst b/docs/about.rst index bfc3b97..7a1b082 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -1,15 +1,13 @@ About ===== -`Max M`_ had often needed to parse and generate iCalendar files. Finally he got +`Max M`_ had often needed to parse and generate iCalendar files. Finally, he got tired of writing ad-hoc tools. This package is his attempt at making an iCalendar package for Python. The inspiration has come from the email package in the standard lib, which he thinks is pretty simple, yet efficient and powerful. -At the time of writing this, last version was released more then 2 years ago. -Since then many things have changes. For one, `RFC 2445`_ was updated by `RFC -5545`_ which makes this package. So in some sense this package became outdated. +The icalendar package is an RFC 5545-compatible parser/generator for iCalendar files. .. _`Max M`: http://www.mxm.dk .. _`RFC 2445`: https://tools.ietf.org/html/rfc2445 diff --git a/docs/credits.rst b/docs/credits.rst index 9b91839..eedcc24 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -43,6 +43,7 @@ icalendar contributors - Thomas Bruederli - Thomas Weißschuh - Victor Varvaryuk +- Ville Skyttä - Wichert Akkerman - cillianderoiste - fitnr @@ -69,6 +70,7 @@ icalendar contributors - `Natasha Mattson `_ - Matt Lewis +- Felix Stupp Find out who contributed:: diff --git a/docs/install.rst b/docs/install.rst index 70b1324..6dccd5a 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -112,7 +112,7 @@ Try it out: Type "help", "copyright", "credits" or "license" for more information. >>> import icalendar >>> icalendar.__version__ - '5.0.7' + '5.0.10' Building the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/usage.rst b/docs/usage.rst index 2ae83c3..7d7aa0c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -342,5 +342,5 @@ Print out the calendar:: More documentation ================== -Have a look at the tests of this package to get more examples. +Have a look at the `tests `__ of this package to get more examples. All modules and classes docstrings, which document how they work. diff --git a/src/icalendar/__init__.py b/src/icalendar/__init__.py index a02e7d7..94597b0 100644 --- a/src/icalendar/__init__.py +++ b/src/icalendar/__init__.py @@ -1,4 +1,4 @@ -__version__ = '5.0.7' +__version__ = '5.0.10' from icalendar.cal import ( Calendar, diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 35a288b..0fd8ec2 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -113,7 +113,8 @@ class Component(CaselessDict): ############################# # handling of property values - def _encode(self, name, value, parameters=None, encode=1): + @staticmethod + def _encode(name, value, parameters=None, encode=1): """Encode values to icalendar property values. :param name: Name of the property. @@ -138,17 +139,19 @@ class Component(CaselessDict): return value if isinstance(value, types_factory.all_types): # Don't encode already encoded values. - return value - klass = types_factory.for_property(name) - obj = klass(value) + obj = value + else: + 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 - assert isinstance(parameters, Parameters) - obj.params = parameters + if not hasattr(obj, "params"): + obj.params = Parameters() + for key, item in parameters.items(): + if item is None: + if key in obj.params: + del obj.params[key] + else: + obj.params[key] = item return obj def add(self, name, value, parameters=None, encode=1): @@ -372,19 +375,26 @@ class Component(CaselessDict): if not component: raise ValueError(f'Property "{name}" does not have a parent component.') datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE', - 'FREEBUSY', 'RDATE', 'EXDATE') + 'RDATE', 'EXDATE') try: - if name in datetime_names and 'TZID' in params: - vals = factory(factory.from_ical(vals, params['TZID'])) + if name == 'FREEBUSY': + vals = vals.split(',') + if 'TZID' in params: + parsed_components = [factory(factory.from_ical(val, params['TZID'])) for val in vals] + else: + parsed_components = [factory(factory.from_ical(val)) for val in vals] + elif name in datetime_names and 'TZID' in params: + parsed_components = [factory(factory.from_ical(vals, params['TZID']))] else: - vals = factory(factory.from_ical(vals)) + parsed_components = [factory(factory.from_ical(vals))] except ValueError as e: if not component.ignore_exceptions: raise component.errors.append((uname, str(e))) else: - vals.params = params - component.add(name, vals, encode=0) + for parsed_component in parsed_components: + parsed_component.params = params + component.add(name, parsed_component, encode=0) if multiple: return comps @@ -436,6 +446,25 @@ class Component(CaselessDict): subs = ', '.join(str(it) for it in self.subcomponents) return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})" + def __eq__(self, other): + if not len(self.subcomponents) == len(other.subcomponents): + return False + + properties_equal = super().__eq__(other) + if not properties_equal: + return False + + # The subcomponents might not be in the same order, + # neither there's a natural key we can sort the subcomponents by nor + # are the subcomponent types hashable, so we cant put them in a set to + # check for set equivalence. We have to iterate over the subcomponents + # and look for each of them in the list. + for subcomponent in self.subcomponents: + if subcomponent not in other.subcomponents: + return False + + return True + ####################################### # components defined in RFC 5545 diff --git a/src/icalendar/cli.py b/src/icalendar/cli.py index 80ea561..ac53963 100755 --- a/src/icalendar/cli.py +++ b/src/icalendar/cli.py @@ -52,10 +52,10 @@ def view(event): end = event.decoded('dtend', default=start) duration = event.decoded('duration', default=end - start) if isinstance(start, datetime): - start = start.astimezone(start.tzinfo) + start = start.astimezone() start = start.strftime('%c') if isinstance(end, datetime): - end = end.astimezone(end.tzinfo) + end = end.astimezone() end = end.strftime('%c') return f""" Organizer: {organizer} diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index f322f65..b69e1a4 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -331,7 +331,7 @@ class vDDDTypes: if u.startswith(('P', '-P', '+P')): return vDuration.from_ical(ical) if '/' in u: - return vPeriod.from_ical(ical) + return vPeriod.from_ical(ical, timezone=timezone) if len(ical) in (15, 16): return vDatetime.from_ical(ical, timezone=timezone) @@ -533,6 +533,11 @@ class vPeriod: f'Cannot compare vPeriod with {other!r}') return cmp((self.start, self.end), (other.start, other.end)) + def __eq__(self, other): + if not isinstance(other, vPeriod): + return False + return (self.start, self.end) == (other.start, other.end) + def overlaps(self, other): if self.start > other.start: return other.overlaps(self) @@ -548,11 +553,11 @@ class vPeriod: + vDatetime(self.end).to_ical()) @staticmethod - def from_ical(ical): + def from_ical(ical, timezone=None): 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=timezone) + end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone) return (start, end_or_duration) except Exception: raise ValueError(f'Expected period format, got: {ical}') @@ -719,7 +724,7 @@ class vText(str): return self def __repr__(self): - return f"vText('{self.to_ical()}')" + return f"vText('{self.to_ical()!r}')" def to_ical(self): return escape_char(self).encode(self.encoding) diff --git a/src/icalendar/tests/calendars/example.ics b/src/icalendar/tests/calendars/example.ics new file mode 100644 index 0000000..a01c8f4 --- /dev/null +++ b/src/icalendar/tests/calendars/example.ics @@ -0,0 +1,40 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:collective/icalendar +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Holidays +X-WR-TIMEZONE:Etc/GMT +BEGIN:VEVENT +SUMMARY:New Year's Day +DTSTART:20220101 +DTEND:20220101 +DESCRIPTION:Happy New Year! +UID:636a0cc1dbd5a1667894465@icalendar +DTSTAMP:20221108T080105Z +STATUS:CONFIRMED +TRANSP:TRANSPARENT +SEQUENCE:0 +END:VEVENT +BEGIN:VEVENT +SUMMARY:Orthodox Christmas +DTSTART:20220107 +DTEND:20220107 +LOCATION:Russia +DESCRIPTION:It is Christmas again! +UID:636a0cc1dbfd91667894465@icalendar +STATUS:CONFIRMED +TRANSP:TRANSPARENT +SEQUENCE:0 +END:VEVENT +BEGIN:VEVENT +SUMMARY:International Women's Day +DTSTART:20220308 +DTEND:20220308 +DESCRIPTION:May the feminine be honoured! +UID:636a0cc1dc0f11667894465@icalendar +STATUS:CONFIRMED +TRANSP:TRANSPARENT +SEQUENCE:0 +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/src/icalendar/tests/calendars/issue_165_missing_event.ics b/src/icalendar/tests/calendars/issue_165_missing_event.ics new file mode 100644 index 0000000..b9675cf --- /dev/null +++ b/src/icalendar/tests/calendars/issue_165_missing_event.ics @@ -0,0 +1,28 @@ +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:Microsoft CDO for Microsoft Exchange +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:GMT +0100 (Standard) / GMT +0200 (Daylight) +BEGIN:STANDARD +DTSTART:16010101T030000 +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=10;BYDAY=-1SU +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:16010101T020000 +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20150703T071009Z +DTSTART;TZID="GMT +0100 (Standard) / GMT +0200 (Daylight)":20150703T100000 +SUMMARY:Sprint 25 Daily Standup +DTEND;TZID="GMT +0100 (Standard) / GMT +0200 (Daylight)":20150703T103000 +RRULE:FREQ=DAILY;UNTIL=20150722T080000Z;INTERVAL=1;BYDAY=MO, TU, WE, TH, FR + ;WKST=SU +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_27_multiple_periods_in_freebusy_multiple_freebusies.ics b/src/icalendar/tests/calendars/issue_27_multiple_periods_in_freebusy_multiple_freebusies.ics new file mode 100644 index 0000000..5986b9f --- /dev/null +++ b/src/icalendar/tests/calendars/issue_27_multiple_periods_in_freebusy_multiple_freebusies.ics @@ -0,0 +1,21 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN +METHOD:REPLY +BEGIN:VFREEBUSY +DTSTAMP:20120131T123000Z +ORGANIZER:MAILTO:organizer@domain.tld +DTSTART:20120101T000000Z +DTEND:20120201T000000Z +UID:null +ATTENDEE:MAILTO:attendee@domain.tld +FREEBUSY;FBTYPE=BUSY:20120103T091500Z/20120103T101500Z +FREEBUSY;FBTYPE=BUSY:20120113T130000Z/20120113T150000Z +FREEBUSY;FBTYPE=BUSY:20120116T130000Z/20120116T150000Z +FREEBUSY;FBTYPE=BUSY:20120117T091500Z/20120117T101500Z +FREEBUSY;FBTYPE=BUSY:20120118T160000Z/20120118T163000Z +FREEBUSY;FBTYPE=BUSY:20120124T083000Z/20120124T093000Z +FREEBUSY;FBTYPE=BUSY:20120124T123000Z/20120124T143000Z +FREEBUSY;FBTYPE=BUSY:20120131T091500Z/20120131T101500Z +END:VFREEBUSY +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_27_multiple_periods.ics b/src/icalendar/tests/calendars/issue_27_multiple_periods_in_freebusy_one_freebusy.ics similarity index 100% rename from src/icalendar/tests/calendars/issue_27_multiple_periods.ics rename to src/icalendar/tests/calendars/issue_27_multiple_periods_in_freebusy_one_freebusy.ics diff --git a/src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics new file mode 100644 index 0000000..67b32f5 --- /dev/null +++ b/src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:icalendar-2023 +BEGIN:VEVENT +UID:ical-jacadzaca-3 +SUMMARY: Some very different event ':' +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +END:VEVENT +BEGIN:VEVENT +UID:ical-jacadzaca-4 +SUMMARY: Some very different other event +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 +DTSTAMP:20211004T150245Z +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics new file mode 100644 index 0000000..4624f2f --- /dev/null +++ b/src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:icalendar-2023 +BEGIN:VEVENT +UID:1 +SUMMARY: Some event ':' +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics new file mode 100644 index 0000000..2406497 --- /dev/null +++ b/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:icalendar-2023 +BEGIN:VEVENT +UID:1 +SUMMARY: Some event ':' +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +END:VEVENT +BEGIN:VEVENT +UID:2 +SUMMARY: Some other event +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 +DTSTAMP:20211004T150245Z +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics new file mode 100644 index 0000000..6833bda --- /dev/null +++ b/src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics @@ -0,0 +1,18 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:icalendar-2023 +BEGIN:VEVENT +UID:2 +SUMMARY: Some other event +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000 +DTSTAMP:20211004T150245Z +END:VEVENT +BEGIN:VEVENT +UID:1 +SUMMARY: Some event ':' +DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000 +DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000 +DTSTAMP:20211004T150245Z +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/calendars/period_with_timezone.ics b/src/icalendar/tests/calendars/period_with_timezone.ics new file mode 100644 index 0000000..e82c10f --- /dev/null +++ b/src/icalendar/tests/calendars/period_with_timezone.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +X-WR-CALNAME;VALUE=TEXT:Test RDATE +BEGIN:VTIMEZONE +TZID:America/Vancouver +BEGIN:STANDARD +DTSTART:20221106T020000 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +RDATE:20231105T020000 +TZNAME:PST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:20230312T020000 +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +RDATE:20240310T020000 +TZNAME:PDT +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:1 +DESCRIPTION:Test RDATE +DTSTART;TZID=America/Vancouver:20230920T120000 +DTEND;TZID=America/Vancouver:20230920T140000 +EXDATE;TZID=America/Vancouver:20231220T120000 +RDATE;VALUE=PERIOD;TZID=America/Vancouver:20231213T120000/20231213T150000 +RRULE:FREQ=MONTHLY;COUNT=9;INTERVAL=1;BYDAY=+3WE;BYMONTH=1,2,3,4,5,9,10,11, + 12;WKST=MO +SUMMARY:Test RDATE +END:VEVENT +END:VCALENDAR diff --git a/src/icalendar/tests/test_cli_tool.py b/src/icalendar/tests/test_cli_tool.py index 20ba870..0ae9b5a 100644 --- a/src/icalendar/tests/test_cli_tool.py +++ b/src/icalendar/tests/test_cli_tool.py @@ -1,6 +1,11 @@ import unittest +from datetime import tzinfo, datetime from icalendar import Calendar, cli +try: + import zoneinfo +except ModuleNotFoundError: + from backports import zoneinfo INPUT = ''' BEGIN:VCALENDAR @@ -21,7 +26,7 @@ BEGIN:VEVENT ORGANIZER:organizer@test.test ATTENDEE:attendee1@example.com ATTENDEE:attendee2@test.test -SUMMARY:Test summury +SUMMARY:Test summary DTSTART;TZID=Europe/Warsaw:20220820T200000 DTEND;TZID=Europe/Warsaw:20220820T203000 LOCATION:New Amsterdam, 1010 Test Street @@ -36,13 +41,22 @@ END:VEVENT END:VCALENDAR ''' -PROPER_OUTPUT = ''' Organizer: organizer +def local_datetime(dt): + return datetime.strptime(dt, "%Y%m%dT%H%M%S").replace(tzinfo=zoneinfo.ZoneInfo("Europe/Warsaw")).astimezone().strftime('%c') + +# datetimes are displayed in the local timezone, so we cannot just hardcode them +firststart = local_datetime('20220820T103400') +firstend = local_datetime('20220820T113400') +secondstart = local_datetime('20220820T200000') +secondend = local_datetime('20220820T203000') + +PROPER_OUTPUT = f""" Organizer: organizer Attendees: attendee1 attendee2 Summary : Test Summary - Starts : Sat Aug 20 10:34:00 2022 - End : Sat Aug 20 11:34:00 2022 + Starts : {firststart} + End : {firstend} Duration : 1:00:00 Location : New Amsterdam, 1000 Sunrise Test Street Comment : Comment @@ -53,9 +67,9 @@ PROPER_OUTPUT = ''' Organizer: organizer Attendees: attendee1 attendee2 - Summary : Test summury - Starts : Sat Aug 20 20:00:00 2022 - End : Sat Aug 20 20:30:00 2022 + Summary : Test summary + Starts : {secondstart} + End : {secondend} Duration : 0:30:00 Location : New Amsterdam, 1010 Test Street Comment : @@ -75,7 +89,7 @@ PROPER_OUTPUT = ''' Organizer: organizer Description: -''' +""" class CLIToolTest(unittest.TestCase): def test_output_is_proper(self): diff --git a/src/icalendar/tests/test_issue_165_missing_event.py b/src/icalendar/tests/test_issue_165_missing_event.py new file mode 100644 index 0000000..4f927fb --- /dev/null +++ b/src/icalendar/tests/test_issue_165_missing_event.py @@ -0,0 +1,9 @@ +'''Issue #165 - Problem parsing a file with event recurring on weekdays + + https://github.com/collective/icalendar/issues/165 +''' +from icalendar import Calendar + +def test_issue_165_missing_event(calendars): + events = list(calendars.issue_165_missing_event.walk('VEVENT')) + assert len(events) == 1, "There was an event missing from the parsed events' list." diff --git a/src/icalendar/tests/test_issue_27_period.py b/src/icalendar/tests/test_issue_27_period.py index 84adc2c..6d92e5f 100644 --- a/src/icalendar/tests/test_issue_27_period.py +++ b/src/icalendar/tests/test_issue_27_period.py @@ -5,7 +5,10 @@ from icalendar import Calendar def test_issue_27_multiple_periods(calendars): - free_busy = list(calendars.issue_27_multiple_periods.walk('VFREEBUSY')) - assert len(free_busy) == 1 - - + free_busy = list(calendars.issue_27_multiple_periods_in_freebusy_multiple_freebusies.walk('VFREEBUSY'))[0] + free_busy_period = free_busy['freebusy'] + print(free_busy['freebusy']) + equivalent_way_of_defining_free_busy = list(calendars.issue_27_multiple_periods_in_freebusy_one_freebusy.walk('VFREEBUSY'))[0] + free_busy_period_equivalent = equivalent_way_of_defining_free_busy['freebusy'] + assert free_busy_period == free_busy_period_equivalent + diff --git a/src/icalendar/tests/test_issue_557_encode_native_parameters.py b/src/icalendar/tests/test_issue_557_encode_native_parameters.py new file mode 100644 index 0000000..99d9f05 --- /dev/null +++ b/src/icalendar/tests/test_issue_557_encode_native_parameters.py @@ -0,0 +1,152 @@ +"""These are tests for Issue #557 + +TL;DR: Component._encode lost given parameters +if the object to encode was already of native type, +making its behavior unexpected. + +see https://github.com/collective/icalendar/issues/557""" + + +import unittest + +from icalendar.cal import Component + + +class TestComponentEncode(unittest.TestCase): + def test_encode_non_native_parameters(self): + """Test _encode to add parameters to non-natives""" + self.__assert_native_content(self.summary) + self.__assert_native_kept_parameters(self.summary) + + def test_encode_native_keep_params_None(self): + """_encode should keep parameters on natives + if parameters=None + """ + new_sum = self.__add_params( + self.summary, + parameters=None, + ) + self.__assert_native_content(new_sum) + self.__assert_native_kept_parameters(new_sum) + + def test_encode_native_keep_params_empty(self): + """_encode should keep paramters on natives + if parameters={} + """ + new_sum = self.__add_params( + self.summary, + parameters={}, + ) + self.__assert_native_content(new_sum) + self.__assert_native_kept_parameters(new_sum) + + def test_encode_native_append_params(self): + """_encode should append paramters on natives + keeping old parameters + """ + new_sum = self.__add_params( + self.summary, + parameters={"X-PARAM": "Test123"}, + ) + self.__assert_native_content(new_sum) + self.__assert_native_kept_parameters(new_sum) + self.assertParameter(new_sum, "X-PARAM", "Test123") + + def test_encode_native_overwrite_params(self): + """_encode should overwrite single parameters + if they have the same name as old ones""" + new_sum = self.__add_params( + self.summary, + parameters={"LANGUAGE": "de"}, + ) + self.__assert_native_content(new_sum) + self.assertParameter(new_sum, "LANGUAGE", "de") + + def test_encode_native_remove_params(self): + """_encode should remove single parameters + if they are explicitly set to None""" + new_sum = self.__add_params( + self.summary, + parameters={"LANGUAGE": None}, + ) + self.__assert_native_content(new_sum) + self.assertParameterMissing(new_sum, "LANGUAGE") + + def test_encode_native_remove_already_missing(self): + """_encode should ignore removing a parameter + that was already missing""" + self.assertParameterMissing(self.summary, "X-MISSING") + new_sum = self.__add_params( + self.summary, + parameters={"X-MISSING": None}, + ) + self.__assert_native_content(new_sum) + self.__assert_native_kept_parameters(new_sum) + self.assertParameterMissing(self.summary, "X-MISSING") + + def test_encode_native_full_test(self): + """full test case with keeping, overwriting & removing properties""" + # preperation + orig_sum = self.__add_params( + self.summary, + parameters={ + "X-OVERWRITE": "overwrite me!", + "X-REMOVE": "remove me!", + "X-MISSING": None, + }, + ) + # preperation check + self.__assert_native_content(orig_sum) + self.__assert_native_kept_parameters(orig_sum) + self.assertParameter(orig_sum, "X-OVERWRITE", "overwrite me!") + self.assertParameter(orig_sum, "X-REMOVE", "remove me!") + self.assertParameterMissing(orig_sum, "X-MISSING") + # modification + new_sum = self.__add_params( + orig_sum, + parameters={ + "X-OVERWRITE": "overwritten", + "X-REMOVE": None, + "X-MISSING": None, + }, + ) + # final asserts + self.__assert_native_content(new_sum) + self.__assert_native_kept_parameters(new_sum) + self.assertParameter(new_sum, "X-OVERWRITE", "overwritten") + self.assertParameterMissing(new_sum, "X-REMOVE") + self.assertParameterMissing(new_sum, "X-MISSING") + + def setUp(self): + self.summary = self.__gen_native() + + def __assert_native_kept_parameters(self, obj): + self.assertParameter(obj, "LANGUAGE", "en") + + def __assert_native_content(self, obj): + self.assertEqual(obj, "English Summary") + + def __add_params(self, obj, parameters): + return Component._encode( + "SUMMARY", + obj, + parameters=parameters, + encode=True, + ) + + def __gen_native(self): + return Component._encode( + "SUMMARY", + "English Summary", + parameters={ + "LANGUAGE": "en", + }, + encode=True, + ) + + def assertParameterMissing(self, obj, name): + self.assertNotIn(name, obj.params) + + def assertParameter(self, obj, name, val): + self.assertIn(name, obj.params) + self.assertEqual(obj.params[name], val) diff --git a/src/icalendar/tests/test_period.py b/src/icalendar/tests/test_period.py index ff64a78..03df873 100644 --- a/src/icalendar/tests/test_period.py +++ b/src/icalendar/tests/test_period.py @@ -7,6 +7,7 @@ See import pytest import pytz from icalendar.prop import vDDDTypes +import datetime @pytest.mark.parametrize("calname,tzname,index,period_string", [ @@ -31,7 +32,7 @@ def test_issue_156_period_list_in_rdate(calendars, calname, tzname, index, perio calendar = calendars[calname] rdate = calendar.walk("vevent")[0]["rdate"] period = rdate.dts[index] - assert period.dt == vDDDTypes.from_ical(period_string, timezone=pytz.timezone(tzname)) + assert period.dt == vDDDTypes.from_ical(period_string, timezone=tzname) def test_duration_properly_parsed(events): @@ -46,3 +47,17 @@ def test_duration_properly_parsed(events): assert period[1].days == 0 assert period[1].seconds == (5 * 60 + 30) * 60 assert period[1] == duration + + +def test_tzid_is_part_of_the_parameters(calendars): + """The TZID should be mentioned in the parameters.""" + event = list(calendars.period_with_timezone.walk("VEVENT"))[0] + assert event["RDATE"].params["TZID"] == "America/Vancouver" + + +def test_tzid_is_part_of_the_period_values(calendars): + """The TZID should be set in the datetime.""" + event = list(calendars.period_with_timezone.walk("VEVENT"))[0] + start, end = event["RDATE"].dts[0].dt + assert start == pytz.timezone("America/Vancouver").localize(datetime.datetime(2023, 12, 13, 12)) + assert end == pytz.timezone("America/Vancouver").localize(datetime.datetime(2023, 12, 13, 15)) diff --git a/src/icalendar/tests/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index 3d09f04..410abce 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -1,7 +1,10 @@ +import itertools from datetime import datetime from datetime import timedelta import unittest +import pytest + import icalendar import pytz import re @@ -455,3 +458,33 @@ class TestCal(unittest.TestCase): icalendar.vUTCOffset.ignore_exceptions = True self.assertEqual(icalendar.Calendar.from_ical(cal_str).to_ical(), cal_str) icalendar.vUTCOffset.ignore_exceptions = False + + +@pytest.mark.parametrize( + 'calendar, other_calendar', + itertools.product([ + 'issue_156_RDATE_with_PERIOD_TZID_khal', + 'issue_156_RDATE_with_PERIOD_TZID_khal_2', + 'issue_178_custom_component_contains_other', + 'issue_178_custom_component_inside_other', + 'issue_526_calendar_with_events', + 'issue_526_calendar_with_different_events', + 'issue_526_calendar_with_event_subset', + ], repeat=2) +) +def test_comparing_calendars(calendars, calendar, other_calendar): + are_calendars_equal = calendars[calendar] == calendars[other_calendar] + are_calendars_actually_equal = calendar == other_calendar + assert are_calendars_equal == are_calendars_actually_equal + + +@pytest.mark.parametrize('calendar, shuffeled_calendar', [ + ( + 'issue_526_calendar_with_events', + 'issue_526_calendar_with_shuffeled_events', + ), +]) +def test_calendars_with_same_subcomponents_in_different_order_are_equal(calendars, calendar, shuffeled_calendar): + assert not calendars[calendar].subcomponents == calendars[shuffeled_calendar].subcomponents + assert calendars[calendar] == calendars[shuffeled_calendar] +