Merge branch 'main' into ruff-formmat

pull/789/head
Nicco Kunzmann 2025-03-28 10:31:28 +00:00 zatwierdzone przez GitHub
commit cbb9cc630c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
7 zmienionych plików z 179 dodań i 45 usunięć

Wyświetl plik

@ -1,7 +1,7 @@
Changelog
=========
6.1.3 (unreleased)
6.1.4 (unreleased)
------------------
Minor changes:
@ -20,6 +20,14 @@ Bug fixes:
- ...
6.1.3 (2025-03-19)
------------------
Bug fixes:
- Fix to permit TZID forward references to VTIMEZONEs
- Stabelize timezone id lookup, see `Issue 780 <https://github.com/collective/icalendar/issues/780>`_.
6.1.2 (2025-03-19)
------------------

Wyświetl plik

@ -1910,6 +1910,38 @@ class Calendar(Component):
"""Return the calendar example with the given name."""
return cls.from_ical(get_example("calendars", name))
@classmethod
def from_ical(cls, st, multiple=False):
comps = Component.from_ical(st, multiple=True)
all_timezones_so_far = True
for comp in comps:
for component in comp.subcomponents:
if component.name == 'VTIMEZONE':
if all_timezones_so_far:
pass
else:
# If a preceding component refers to a VTIMEZONE defined later in the source st
# (forward references are allowed by RFC 5545), then the earlier component may have
# the wrong timezone attached.
# However, during computation of comps, all VTIMEZONEs observed do end up in
# the timezone cache. So simply re-running from_ical will rely on the cache
# for those forward references to produce the correct result.
# See test_create_america_new_york_forward_reference.
return Component.from_ical(st, multiple)
else:
all_timezones_so_far = False
# No potentially forward VTIMEZONEs to worry about
if multiple:
return comps
if len(comps) > 1:
raise ValueError(cls._format_error(
'Found multiple components where only one is allowed', st))
if len(comps) < 1:
raise ValueError(cls._format_error(
'Found no components where exactly one is required', st))
return comps[0]
@property
def events(self) -> list[Event]:
"""All event components in the calendar.

Wyświetl plik

@ -0,0 +1,61 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
UID:noend123
DTSTART;TZID=custom_America/New_York_Forward_reference;VALUE=DATE-TIME:20140829T080000
DTSTART;TZID=custom_America/New_York_Forward_reference;VALUE=DATE-TIME:20140829T100000
SUMMARY:an event with a custom tz name
END:VEVENT
BEGIN:VTIMEZONE
TZID:custom_America/New_York_Forward_reference
LAST-MODIFIED:20050809T050000Z
BEGIN:DAYLIGHT
DTSTART:19670430T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19730429T070000Z
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:19671029T020000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=20061029T060000Z
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19740106T020000
RDATE:19750223T020000
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19760425T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=-1SU;UNTIL=19860427T070000Z
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19870405T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=20060402T070000Z
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
END:STANDARD
END:VTIMEZONE
END:VCALENDAR

Wyświetl plik

@ -434,4 +434,4 @@ def test_we_can_identify_dateutil_timezones(tzid):
But if we know their shortcodes, we should be able to identify them.
"""
tz = gettz(tzid)
assert tzid in tzids_from_tzinfo(tz)
assert tz is None or tzid in tzids_from_tzinfo(tz)

Wyświetl plik

@ -1,7 +1,8 @@
"""An example with multiple VCALENDAR components"""
"""Testing multiple VCALENDAR components and multiple VEVENT components"""
from icalendar.prop import vText
from icalendar.cal import Event
import pytest
def test_multiple(calendars):
"""Check opening multiple calendars."""
@ -14,3 +15,19 @@ def test_multiple(calendars):
assert cals[0]["prodid"] == vText(
"-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN"
)
def test_multiple_events():
"""Raises ValueError unless multiple=True"""
event_components="""
BEGIN:VEVENT
END:VEVENT
BEGIN:VEVENT
END:VEVENT
"""
with pytest.raises(ValueError) as exception:
Event.from_ical(event_components, multiple=False)
def test_missing_event():
"""Raises ValueError if no component found"""
with pytest.raises(ValueError) as exception:
Event.from_ical('')

Wyświetl plik

@ -138,6 +138,11 @@ def test_create_america_new_york(calendars, tzp):
dt = cal.events[0].start
assert tzid_from_dt(dt) in ("custom_America/New_York", "EDT")
def test_create_america_new_york_forward_reference(calendars, tzp):
"""testing America/New_York variant with VTIMEZONE as a forward reference"""
cal = calendars.america_new_york_forward_reference
dt = cal.walk('VEVENT')[0]['DTSTART'][0].dt
assert tzid_from_dt(dt) in ('custom_America/New_York_Forward_reference', "EDT")
def test_america_new_york_with_pytz(calendars, tzp, pytz_only):
"""Create a custom timezone with pytz and test the transition times."""

Wyświetl plik

@ -7,36 +7,56 @@ datetime.tzinfo object.
from __future__ import annotations
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING, Optional
import dateutil.tz.tz as tz
from dateutil.tz import tz
from icalendar.timezone import equivalent_timezone_ids_result
if TYPE_CHECKING:
from datetime import datetime, tzinfo
DATEUTIL_UTC = tz.gettz("UTC")
DATEUTIL_UTC_PATH : Optional[str] = getattr(DATEUTIL_UTC, "_filename", None)
DATEUTIL_ZONEINFO_PATH = (
None if DATEUTIL_UTC_PATH is None else Path(DATEUTIL_UTC_PATH).parent
)
def tzids_from_tzinfo(tzinfo: Optional[tzinfo]) -> tuple[str]:
"""Get several timezone ids if we can identify the timezone.
>>> import zoneinfo
>>> from icalendar.timezone.tzid import tzids_from_tzinfo
>>> tzids_from_tzinfo(zoneinfo.ZoneInfo("Africa/Accra"))
('Africa/Accra',)
>>> tzids_from_tzinfo(zoneinfo.ZoneInfo("Arctic/Longyearbyen"))
('Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Berlin', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
>>> from dateutil.tz import gettz
>>> tzids_from_tzinfo(gettz("Europe/Berlin"))
('Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Berlin', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
('Europe/Berlin', 'Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
""" # The example might need to change if you recreate the lookup tree
if tzinfo is None:
return ()
if hasattr(tzinfo, "zone"):
return (tzinfo.zone,) # pytz implementation
return get_equivalent_tzids(tzinfo.zone) # pytz implementation
if hasattr(tzinfo, "key"):
return (tzinfo.key,) # ZoneInfo implementation
if isinstance(tzinfo, tz._tzicalvtz):
return (tzinfo._tzid,)
return get_equivalent_tzids(tzinfo.key) # ZoneInfo implementation
if isinstance(tzinfo, tz._tzicalvtz): # noqa: SLF001
return get_equivalent_tzids(tzinfo._tzid) # noqa: SLF001
if isinstance(tzinfo, tz.tzstr):
return (tzinfo._s,)
return tuple(sorted(tzinfo2tzids(tzinfo)))
return get_equivalent_tzids(tzinfo._s) # noqa: SLF001
if hasattr(tzinfo, "_filename"): # dateutil.tz.tzfile # noqa: SIM102
if DATEUTIL_ZONEINFO_PATH is not None:
# tzfile('/usr/share/zoneinfo/Europe/Berlin')
path = tzinfo._filename # noqa: SLF001
if path.startswith(str(DATEUTIL_ZONEINFO_PATH)):
tzid = str(Path(path).relative_to(DATEUTIL_ZONEINFO_PATH))
return get_equivalent_tzids(tzid)
return get_equivalent_tzids(path)
if isinstance(tzinfo, tz.tzutc):
return get_equivalent_tzids("UTC")
return ()
def tzid_from_tzinfo(tzinfo: Optional[tzinfo]) -> Optional[str]:
@ -61,41 +81,32 @@ def tzid_from_dt(dt: datetime) -> Optional[str]:
return tzid
def tzinfo2tzids(tzinfo: Optional[tzinfo]) -> set[str]:
"""We return the tzids for a certain tzinfo object.
_EQUIVALENT_IDS : dict[str, set[str]] = defaultdict(set)
With different datetimes, we match
(tzinfo.utcoffset(dt), tzinfo.tzname(dt))
def _add_equivalent_ids(value:tuple|dict|set):
"""This adds equivalent ids/
If we could identify the timezone, you will receive a tuple
with at least one tzid. All tzids are equivalent which means
that they describe the same timezone.
As soon as one timezone implementation used claims their equivalence,
they are considered equivalent.
Have a look at icalendar.timezone.equivalent_timezone_ids.
"""
if isinstance(value, set):
for tzid in value:
_EQUIVALENT_IDS[tzid].update(value)
elif isinstance(value, tuple):
_add_equivalent_ids(value[1])
elif isinstance(value, dict):
for value in value.values():
_add_equivalent_ids(value)
else:
raise TypeError(f"Expected tuple, dict or set, not {value.__class__.__name__}: {value!r}")
You should get results with any timezone implementation if it is known.
This one is especially useful for dateutil.
_add_equivalent_ids(equivalent_timezone_ids_result.lookup)
In the following example, we can see that the timezone Africa/Accra
is equivalent to many others.
>>> import zoneinfo
>>> from icalendar.timezone.tzid import tzinfo2tzids
>>> "Europe/Berlin" in tzinfo2tzids(zoneinfo.ZoneInfo("Europe/Berlin"))
True
""" # The example might need to change if you recreate the lookup tree
if tzinfo is None:
return set()
from icalendar.timezone.equivalent_timezone_ids_result import lookup
while 1:
if isinstance(lookup, set):
return lookup
dt, offset2lookup = lookup
offset = tzinfo.utcoffset(dt)
lookup = offset2lookup.get(offset)
if lookup is None:
return set()
return set()
def get_equivalent_tzids(tzid: str) -> tuple[str]:
"""This returns the tzids which are equivalent to this one."""
ids = _EQUIVALENT_IDS.get(tzid, set())
return (tzid,) + tuple(sorted(ids - {tzid}))
__all__ = ["tzid_from_tzinfo", "tzid_from_dt", "tzids_from_tzinfo"]