diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 743c5fa..e4ecdaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,9 @@ Contributors - can submit pull requests - can work on branches inside the repository +Contributor changes should include focused tests where practical and should +update the changelog in `README.rst` unless the change is trivial. + ## Merge requirements - Passing tests diff --git a/README.rst b/README.rst index 257a0f7..217f1e5 100644 --- a/README.rst +++ b/README.rst @@ -243,6 +243,9 @@ Changelog - v3.0.0 + - Handle calendars that provide multiple ``X-WR-TIMEZONE`` values. + If all values match, use the repeated timezone; if values conflict, + ignore them because the target timezone is ambiguous. - Use ``icalendar.is_utc()`` instead of ``is_UTC()`` to identify UTC, catching aliases like ``Etc/GMT``. - Remove ``is_UTC()`` from ``CalendarWalker``. - Test ``pytz.timezone("UTC")`` and ``datetime.timezone.utc`` as arguments to ``to_standard()``. diff --git a/test/calendars/duplicate-conflicting-x-wr-timezone-values.in.ics b/test/calendars/duplicate-conflicting-x-wr-timezone-values.in.ics new file mode 100644 index 0000000..def2fd4 --- /dev/null +++ b/test/calendars/duplicate-conflicting-x-wr-timezone-values.in.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//x-wr-timezone test// +X-WR-TIMEZONE:Europe/Berlin +X-WR-TIMEZONE:America/New_York +BEGIN:VEVENT +UID:1 +DTSTART:20240101T120000Z +DTEND:20240101T130000Z +END:VEVENT +END:VCALENDAR diff --git a/test/calendars/duplicate-matching-x-wr-timezone-values.in.ics b/test/calendars/duplicate-matching-x-wr-timezone-values.in.ics new file mode 100644 index 0000000..5603b3b --- /dev/null +++ b/test/calendars/duplicate-matching-x-wr-timezone-values.in.ics @@ -0,0 +1,11 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//x-wr-timezone test// +X-WR-TIMEZONE:Europe/Berlin +X-WR-TIMEZONE:Europe/Berlin +BEGIN:VEVENT +UID:1 +DTSTART:20240101T120000Z +DTEND:20240101T130000Z +END:VEVENT +END:VCALENDAR diff --git a/test/test_convert_examples.py b/test/test_convert_examples.py index 6dfcec2..ac42791 100644 --- a/test/test_convert_examples.py +++ b/test/test_convert_examples.py @@ -122,3 +122,25 @@ def test_calendar_walker_is_utc_compatibility_hook(): assert walker.is_UTC(utc_dt) assert not walker.is_UTC(floating_dt) + + +def test_duplicate_matching_x_wr_timezone_values_are_used(calendars): + calendar = calendars["duplicate-matching-x-wr-timezone-values.in.ics"].as_icalendar() + output_bytes = x_wr_timezone.to_standard(calendar).to_ical() + + assert_has_line( + output_bytes, + ("DTSTART", "TZID=Europe/Berlin", "20240101T130000"), + "Duplicate matching time zones are used.", + ) + + +def test_duplicate_conflicting_x_wr_timezone_values_are_ignored(calendars): + calendar = calendars["duplicate-conflicting-x-wr-timezone-values.in.ics"].as_icalendar() + output_bytes = x_wr_timezone.to_standard(calendar).to_ical() + + assert_has_line( + output_bytes, + ("DTSTART", "20240101T120000Z"), + "Conflicting time zones are ignored.", + ) diff --git a/x_wr_timezone.py b/x_wr_timezone.py index dfd51e7..a9507f1 100644 --- a/x_wr_timezone.py +++ b/x_wr_timezone.py @@ -24,6 +24,16 @@ X_WR_TIMEZONE = "X-WR-TIMEZONE" +def _as_single_timezone(timezone): + """Return one time zone value from calendar metadata, or None if ambiguous.""" + if not isinstance(timezone, list): + return timezone + values = set(map(str, timezone)) + if len(values) == 1: + return timezone[0] + return None + + def _is_utc_fallback(dt): """Fallback UTC check for icalendar < 7.0.0.""" if dt.tzname() is None: @@ -193,6 +203,7 @@ def to_standard( """ if timezone is None: timezone = calendar.get(X_WR_TIMEZONE, None) + timezone = _as_single_timezone(timezone) if timezone is not None and not isinstance(timezone, datetime.tzinfo): timezone = zoneinfo.ZoneInfo(str(timezone)) result : icalendar.Calendar = calendar