From 2e0388ec21103f9ffb047a95ed7c6654d0247716 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Aug 2023 16:53:34 +0100 Subject: [PATCH 01/25] Check that event is included in parsing This fixes https://github.com/collective/icalendar/issues/165 --- .../calendars/issue_165_missing_event.ics | 28 +++++++++++++++++++ .../tests/test_issue_165_missing_event.py | 9 ++++++ 2 files changed, 37 insertions(+) create mode 100644 src/icalendar/tests/calendars/issue_165_missing_event.ics create mode 100644 src/icalendar/tests/test_issue_165_missing_event.py 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 00000000..b9675cf0 --- /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/test_issue_165_missing_event.py b/src/icalendar/tests/test_issue_165_missing_event.py new file mode 100644 index 00000000..4f927fb2 --- /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." From 55879e3f9362786248456708b083084ccacec065 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Aug 2023 17:33:21 +0100 Subject: [PATCH 02/25] Add documentation about parsing see https://github.com/collective/icalendar/issues/152 --- README.rst | 21 ++++++++++++ docs/usage.rst | 2 +- src/icalendar/tests/calendars/example.ics | 40 +++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/icalendar/tests/calendars/example.ics diff --git a/README.rst b/README.rst index dc397939..0ae54fdf 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/usage.rst b/docs/usage.rst index 2ae83c30..7d7aa0c5 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/tests/calendars/example.ics b/src/icalendar/tests/calendars/example.ics new file mode 100644 index 00000000..a01c8f42 --- /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 From 078ae71502d9619ae76247655d5e365604ca631e Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Aug 2023 17:54:59 +0100 Subject: [PATCH 03/25] Add changelog entry --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8c7f5d2c..51b01192 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,7 @@ Minor changes: - Update build configuration to build readthedocs. #538 - No longer run the ``plone.app.event`` tests. - Move pip caching into Python setup action. +- Check that issue #165 can be closed. Breaking changes: From fe2e5df3bb82a2af5825262ce5ac7cf0b09267db Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Wed, 30 Aug 2023 17:56:23 +0100 Subject: [PATCH 04/25] Add changelog entry --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8c7f5d2c..b6d8ce4a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ 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. Breaking changes: From 088eea77e18fb893026468212110433de9724dc2 Mon Sep 17 00:00:00 2001 From: jaca Date: Fri, 1 Sep 2023 15:15:09 +0200 Subject: [PATCH 05/25] fix #526 --- CHANGES.rst | 5 ++- src/icalendar/cal.py | 16 +++++++++ ...sue_526_calendar_with_different_events.ics | 18 ++++++++++ .../issue_526_calendar_with_events.ics | 18 ++++++++++ ...sue_526_calendar_with_shuffeled_events.ics | 18 ++++++++++ src/icalendar/tests/test_unit_cal.py | 36 +++++++++++++++++++ 6 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/icalendar/tests/calendars/issue_526_calendar_with_different_events.ics create mode 100644 src/icalendar/tests/calendars/issue_526_calendar_with_events.ics create mode 100644 src/icalendar/tests/calendars/issue_526_calendar_with_shuffeled_events.ics diff --git a/CHANGES.rst b/CHANGES.rst index 861ff360..edc5cc05 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,7 +18,10 @@ New features: Bug fixes: -- ... +- Calendar components are now properly compared + Ref: #550 + Fixes: #526 + [jacadzaca] 5.0.7 (2023-05-29) ------------------ diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 35a288bf..b718765b 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -436,6 +436,22 @@ def __repr__(self): 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): + 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/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 00000000..67b32f54 --- /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_events.ics b/src/icalendar/tests/calendars/issue_526_calendar_with_events.ics new file mode 100644 index 00000000..2406497a --- /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 00000000..6833bdab --- /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/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index 3d09f046..9d9a1100 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -2,6 +2,8 @@ from datetime import timedelta import unittest +import pytest + import icalendar import pytz import re @@ -455,3 +457,37 @@ def test_cal_strict_parsing(self): 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', [ + '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', +]) +@pytest.mark.parametrize('other_calendar', [ + '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', +]) +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] + From 4f0028663e0ef8aa3bd420bb0e12f4940d631881 Mon Sep 17 00:00:00 2001 From: jaca Date: Fri, 1 Sep 2023 20:15:57 +0200 Subject: [PATCH 06/25] dont repeat parameter list in test_comparing_calendars --- src/icalendar/tests/test_unit_cal.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/icalendar/tests/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index 9d9a1100..e3a9ce3a 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -1,3 +1,4 @@ +import itertools from datetime import datetime from datetime import timedelta import unittest @@ -459,22 +460,17 @@ def test_cal_strict_parsing(self): icalendar.vUTCOffset.ignore_exceptions = False -@pytest.mark.parametrize('calendar', [ - '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', -]) -@pytest.mark.parametrize('other_calendar', [ - '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', -]) +@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', + ], 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 From 2dfd51cf59f7ab3a1f2572b04f2b76a198201a33 Mon Sep 17 00:00:00 2001 From: jaca Date: Fri, 1 Sep 2023 20:34:32 +0200 Subject: [PATCH 07/25] dont treat components with a subset of other a larger components' subcomponents as equal to the bigger component --- src/icalendar/cal.py | 3 +++ .../issue_526_calendar_with_event_subset.ics | 11 +++++++++++ src/icalendar/tests/test_unit_cal.py | 1 + 3 files changed, 15 insertions(+) create mode 100644 src/icalendar/tests/calendars/issue_526_calendar_with_event_subset.ics diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index b718765b..40889717 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -437,6 +437,9 @@ def __repr__(self): 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 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 00000000..4624f2f5 --- /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/test_unit_cal.py b/src/icalendar/tests/test_unit_cal.py index e3a9ce3a..410abce5 100644 --- a/src/icalendar/tests/test_unit_cal.py +++ b/src/icalendar/tests/test_unit_cal.py @@ -469,6 +469,7 @@ def test_cal_strict_parsing(self): '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): From c1527336df5505bfb42a264ae9d233a7241b4b55 Mon Sep 17 00:00:00 2001 From: Bob Florian Date: Fri, 15 Sep 2023 11:19:28 -0500 Subject: [PATCH 08/25] Update about.rst updated about.rst to include details that the repo is not obsolete and still compliant with RFC5545 --- docs/about.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/about.rst b/docs/about.rst index bfc3b97b..7a1b082e 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 From e8c6f8b04de4019f816f1216ee93820e1f338ab5 Mon Sep 17 00:00:00 2001 From: Bob Florian Date: Fri, 15 Sep 2023 11:21:36 -0500 Subject: [PATCH 09/25] Update CHANGES.rst --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index efea7c32..d6eaf63c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,7 @@ Minor changes: - 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 Breaking changes: From c13af2b5d99d41956951fbee5854f30c20da44ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 17 Sep 2023 09:38:05 +0300 Subject: [PATCH 10/25] Fix `vText.__repr__` `BytesWarning` Exposed by running in `python3 -b` mode. --- CHANGES.rst | 1 + docs/credits.rst | 1 + src/icalendar/prop.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d6eaf63c..955a11be 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,7 @@ Minor changes: - Move pip caching into Python setup action. - Check that issue #165 can be closed. - Updated about.rst for issue #527 +- Avoid ``vText.__repr__`` BytesWarning. Breaking changes: diff --git a/docs/credits.rst b/docs/credits.rst index 9b91839f..1343193c 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 diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index f322f65e..22314165 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -719,7 +719,7 @@ def __new__(cls, value, encoding=DEFAULT_ENCODING): 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) From a10268584fd107c5fee70c3350450b3124ab5089 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 18 Sep 2023 09:49:37 +0100 Subject: [PATCH 11/25] version 5.0.8 --- CHANGES.rst | 2 +- docs/install.rst | 2 +- src/icalendar/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 955a11be..9fa86921 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ Changelog ========= -5.0.8 (unreleased) +5.0.8 (2023-09-18) ------------------ Minor changes: diff --git a/docs/install.rst b/docs/install.rst index 70b13243..d6a36583 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.8' Building the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/icalendar/__init__.py b/src/icalendar/__init__.py index a02e7d7c..a0a68a19 100644 --- a/src/icalendar/__init__.py +++ b/src/icalendar/__init__.py @@ -1,4 +1,4 @@ -__version__ = '5.0.7' +__version__ = '5.0.8' from icalendar.cal import ( Calendar, From df769f88694745d3babb87457bc3080965aa62c4 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Mon, 18 Sep 2023 09:57:15 +0100 Subject: [PATCH 12/25] Add new CHANGELOG section for future release See https://icalendar.readthedocs.io/en/latest/maintenance.html#new-releases --- CHANGES.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9fa86921..0ba76a60 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,25 @@ Changelog ========= +5.0.9 (unreleased) +------------------ + +Minor changes: + +- ... + +Breaking changes: + +- ... + +New features: + +- ... + +Bug fixes: + +- ... + 5.0.8 (2023-09-18) ------------------ From 4c055a621310ad55b3ea52620d348a7a347c8464 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 19 Sep 2023 15:05:34 +0100 Subject: [PATCH 13/25] Add timezone to period value types The time zone was missing from PERIOD values. See https://github.com/niccokunzmann/python-recurring-ical-events/issues/113 --- CHANGES.rst | 2 +- src/icalendar/prop.py | 8 ++--- .../tests/calendars/period_with_timezone.ics | 32 +++++++++++++++++++ src/icalendar/tests/test_period.py | 17 +++++++++- 4 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 src/icalendar/tests/calendars/period_with_timezone.ics diff --git a/CHANGES.rst b/CHANGES.rst index 0ba76a60..8f74c244 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,7 +18,7 @@ New features: Bug fixes: -- ... +- PERIOD values now set the timezone of their start and end. #556 5.0.8 (2023-09-18) ------------------ diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index 22314165..b4448c6c 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -331,7 +331,7 @@ def from_ical(cls, ical, timezone=None): 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) @@ -548,11 +548,11 @@ def to_ical(self): + 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}') 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 00000000..e82c10f9 --- /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_period.py b/src/icalendar/tests/test_period.py index ff64a788..03df873f 100644 --- a/src/icalendar/tests/test_period.py +++ b/src/icalendar/tests/test_period.py @@ -7,6 +7,7 @@ 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)) From 98fe960ad31e306bee3b5a96b0160518f15b36cd Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sun, 24 Sep 2023 10:46:46 +0100 Subject: [PATCH 14/25] version 5.0.9 --- CHANGES.rst | 22 +--------------------- docs/install.rst | 2 +- src/icalendar/__init__.py | 2 +- 3 files changed, 3 insertions(+), 23 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8f74c244..fda597d6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,21 +1,9 @@ Changelog ========= -5.0.9 (unreleased) +5.0.9 (2023-09-24) ------------------ -Minor changes: - -- ... - -Breaking changes: - -- ... - -New features: - -- ... - Bug fixes: - PERIOD values now set the timezone of their start and end. #556 @@ -33,14 +21,6 @@ Minor changes: - Updated about.rst for issue #527 - Avoid ``vText.__repr__`` BytesWarning. -Breaking changes: - -- ... - -New features: - -- ... - Bug fixes: - Calendar components are now properly compared diff --git a/docs/install.rst b/docs/install.rst index d6a36583..fa66a0f8 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.8' + '5.0.9' Building the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/icalendar/__init__.py b/src/icalendar/__init__.py index a0a68a19..191cd823 100644 --- a/src/icalendar/__init__.py +++ b/src/icalendar/__init__.py @@ -1,4 +1,4 @@ -__version__ = '5.0.8' +__version__ = '5.0.9' from icalendar.cal import ( Calendar, From 4d6aacb1c9416165b5d3c38df00fa5414222700b Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Sun, 24 Sep 2023 10:59:08 +0100 Subject: [PATCH 15/25] Add new CHANGELOG section for future release See https://icalendar.readthedocs.io/en/latest/maintenance.html#new-releases --- CHANGES.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fda597d6..2d818302 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,25 @@ Changelog ========= +5.0.10 (unreleased) +------------------- + +Minor changes: + +- ... + +Breaking changes: + +- ... + +New features: + +- ... + +Bug fixes: + +- ... + 5.0.9 (2023-09-24) ------------------ From 0111be3292bc3b4689a7c7b44355e235915f61b3 Mon Sep 17 00:00:00 2001 From: Felix Stupp Date: Sun, 24 Sep 2023 14:16:06 +0200 Subject: [PATCH 16/25] Component._encode: convert to staticmethod - did not reference self at all - keeps compatibility with existing code - makes testing easier Signed-off-by: Felix Stupp --- src/icalendar/cal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 40889717..40483886 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -113,7 +113,8 @@ def is_broken(self): ############################# # 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. From 392deb69b731164e090347828303c212ea257e14 Mon Sep 17 00:00:00 2001 From: Felix Stupp Date: Sun, 24 Sep 2023 14:18:13 +0200 Subject: [PATCH 17/25] add test cases for Component._encoding concerning #557 Signed-off-by: Felix Stupp --- ...test_issue_557_encode_native_parameters.py | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/icalendar/tests/test_issue_557_encode_native_parameters.py 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 00000000..99d9f051 --- /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) From 61ed4d716a1601c2bcd10c8b355bff61c939e1af Mon Sep 17 00:00:00 2001 From: Felix Stupp Date: Sun, 24 Sep 2023 14:19:02 +0200 Subject: [PATCH 18/25] Component._encoding: only skip factory for natives Signed-off-by: Felix Stupp --- src/icalendar/cal.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 40483886..cfe7e314 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -139,9 +139,10 @@ def _encode(name, value, parameters=None, encode=1): 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() From 07f3c53953a9ea47edc881d3543de8e674681fa4 Mon Sep 17 00:00:00 2001 From: Felix Stupp Date: Sun, 24 Sep 2023 14:19:45 +0200 Subject: [PATCH 19/25] Component._encode: merge parameters Signed-off-by: Felix Stupp --- src/icalendar/cal.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index cfe7e314..2535680f 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -144,13 +144,14 @@ def _encode(name, value, parameters=None, encode=1): 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): From 4042ba8efc2dffc4ba9b527e63eef9dc7f922f43 Mon Sep 17 00:00:00 2001 From: Felix Stupp Date: Sun, 24 Sep 2023 14:33:14 +0200 Subject: [PATCH 20/25] add changelog entry & credits for fixing #557 --- CHANGES.rst | 3 +++ docs/credits.rst | 1 + 2 files changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 2d818302..0ae16733 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,6 +18,9 @@ New features: Bug fixes: +- Component._encode stops ignoring parameters argument on native values, now merges them + Fixes: #557 + [zocker1999net] - ... 5.0.9 (2023-09-24) diff --git a/docs/credits.rst b/docs/credits.rst index 1343193c..eedcc24b 100644 --- a/docs/credits.rst +++ b/docs/credits.rst @@ -70,6 +70,7 @@ icalendar contributors - `Natasha Mattson `_ - Matt Lewis +- Felix Stupp Find out who contributed:: From 48e2a681afc3c7dc9261d4a416ec02005ca2cab2 Mon Sep 17 00:00:00 2001 From: jaca Date: Tue, 26 Sep 2023 20:29:23 +0200 Subject: [PATCH 21/25] version 5.0.10 --- CHANGES.rst | 10 ++++++++-- docs/install.rst | 2 +- src/icalendar/__init__.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0ae16733..4df8d725 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ Changelog ========= -5.0.10 (unreleased) +5.0.11 (unreleased) ------------------- Minor changes: @@ -18,10 +18,16 @@ New features: Bug fixes: +- ... + +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) ------------------ diff --git a/docs/install.rst b/docs/install.rst index fa66a0f8..6dccd5ad 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.9' + '5.0.10' Building the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/icalendar/__init__.py b/src/icalendar/__init__.py index 191cd823..94597b0a 100644 --- a/src/icalendar/__init__.py +++ b/src/icalendar/__init__.py @@ -1,4 +1,4 @@ -__version__ = '5.0.9' +__version__ = '5.0.10' from icalendar.cal import ( Calendar, From 1e0c78ff8ea07631b679aeb6316cb7b4c9f4061f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Gro=C3=9F?= Date: Wed, 27 Sep 2023 22:05:30 +0200 Subject: [PATCH 22/25] cli: Display datetimes in local timezone Previously the start and end datetimes were always printed out in the timezone that they appear in the calendar entry. In 7a8d584b duration support was added and an attempt was already made to display the datetime in the local timezone. Unfortunately in that specific case the `start.astimezone(start.tzinfo)` is a no-op and does absolutely nothing. Unlike the name suggests, `astimezone()` adjusts the date and time data, such that they match the passed tzinfo, but since that is the same timezone data as before, nothing changes. [0] In order to properly convert to the user's local timezone, we need to call the method with no arguments. With this the timezone is always properly displayed, which makes up for a much nicer UX for users of the cli. The test has to be adapted to expect the datetime in the local timezone, hence we cannot hardcode the entire expected string anymore. [0] https://docs.python.org/3/library/datetime.html#datetime.datetime.astimezone --- CHANGES.rst | 4 +++- src/icalendar/cli.py | 4 ++-- src/icalendar/tests/test_cli_tool.py | 30 ++++++++++++++++++++-------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4df8d725..7d0b6da4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,9 @@ Changelog Minor changes: -- ... +- The cli utility now displays start and end datetimes in the user's local timezone. + Ref: #561 + [vimpostor] Breaking changes: diff --git a/src/icalendar/cli.py b/src/icalendar/cli.py index 80ea5619..ac53963c 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/tests/test_cli_tool.py b/src/icalendar/tests/test_cli_tool.py index 20ba8709..0ae9b5ab 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 @@ 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: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 @@ 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 @@ Description: -''' +""" class CLIToolTest(unittest.TestCase): def test_output_is_proper(self): From 1b44560e9f4b293c2a1ec679b328bada533204f2 Mon Sep 17 00:00:00 2001 From: jaca Date: Sat, 30 Sep 2023 15:24:23 +0200 Subject: [PATCH 23/25] improve test for issue #27 --- ...eriods_in_freebusy_multiple_freebusies.ics | 21 +++++++++++++++++++ ...iple_periods_in_freebusy_one_freebusy.ics} | 0 src/icalendar/tests/test_issue_27_period.py | 11 ++++++---- 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 src/icalendar/tests/calendars/issue_27_multiple_periods_in_freebusy_multiple_freebusies.ics rename src/icalendar/tests/calendars/{issue_27_multiple_periods.ics => issue_27_multiple_periods_in_freebusy_one_freebusy.ics} (100%) 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 00000000..5986b9f3 --- /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/test_issue_27_period.py b/src/icalendar/tests/test_issue_27_period.py index 84adc2cf..6d92e5fa 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 + From f3596823aa6c0e2f8414a99a12d38a57e4e68a2b Mon Sep 17 00:00:00 2001 From: jaca Date: Sat, 30 Sep 2023 15:25:24 +0200 Subject: [PATCH 24/25] add vPeriod,__eq__ --- src/icalendar/prop.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/icalendar/prop.py b/src/icalendar/prop.py index b4448c6c..b69e1a47 100644 --- a/src/icalendar/prop.py +++ b/src/icalendar/prop.py @@ -533,6 +533,11 @@ def __cmp__(self, other): 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) From 8fab650bfcd502a9ea7a881dda4a5e97b7cb3a32 Mon Sep 17 00:00:00 2001 From: jaca Date: Sat, 30 Sep 2023 15:26:46 +0200 Subject: [PATCH 25/25] multivalue FREEBUSY parses properly --- CHANGES.rst | 4 +++- src/icalendar/cal.py | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7d0b6da4..254da50e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,9 @@ New features: Bug fixes: -- ... +- Multivalue FREEBUSY property is now parsed properly + Ref: #27 + [jacadzaca] 5.0.10 (unreleased) ------------------- diff --git a/src/icalendar/cal.py b/src/icalendar/cal.py index 2535680f..0fd8ec2b 100644 --- a/src/icalendar/cal.py +++ b/src/icalendar/cal.py @@ -375,19 +375,26 @@ def from_ical(cls, st, multiple=False): 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