kopia lustrzana https://github.com/collective/icalendar
Merge branch 'main' into ruff-formmat
commit
cbb9cc630c
10
CHANGES.rst
10
CHANGES.rst
|
@ -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)
|
||||
------------------
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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('')
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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"]
|
||||
|
|
Ładowanie…
Reference in New Issue