-
Notifications
You must be signed in to change notification settings - Fork 209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix 1367: :khal crashes with zoneinfo attrib error adding new event #1374
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -32,14 +32,14 @@ | |||||
import icalendar.prop | ||||||
import pytz | ||||||
from click import style | ||||||
from pytz.tzinfo import StaticTzInfo | ||||||
from zoneinfo import ZoneInfo | ||||||
|
||||||
from ..custom_types import LocaleConfiguration | ||||||
from ..exceptions import FatalError | ||||||
from ..icalendar import cal_from_ics, delete_instance, invalid_timezone | ||||||
from ..parse_datetime import timedelta2str | ||||||
from ..plugins import FORMATTERS | ||||||
from ..utils import generate_random_uid, is_aware, to_naive_utc, to_unix_time | ||||||
from ..utils import generate_random_uid, is_aware, to_unix_time | ||||||
|
||||||
logger = logging.getLogger('khal') | ||||||
|
||||||
|
@@ -377,30 +377,44 @@ def _create_calendar() -> icalendar.Calendar: | |||||
) | ||||||
return calendar | ||||||
|
||||||
|
||||||
@property | ||||||
def raw(self) -> str: | ||||||
"""Creates a VCALENDAR containing VTIMEZONEs | ||||||
""" | ||||||
"""Creates a VCALENDAR containing VTIMEZONEs.""" | ||||||
calendar = self._create_calendar() | ||||||
tzs = [] | ||||||
|
||||||
# Collect timezones from the events | ||||||
for vevent in self._vevents.values(): | ||||||
# Handle DTSTART timezone | ||||||
if hasattr(vevent['DTSTART'].dt, 'tzinfo') and vevent['DTSTART'].dt.tzinfo is not None: | ||||||
tzs.append(vevent['DTSTART'].dt.tzinfo) | ||||||
if 'DTEND' in vevent and hasattr(vevent['DTEND'].dt, 'tzinfo') and \ | ||||||
vevent['DTEND'].dt.tzinfo is not None and \ | ||||||
vevent['DTEND'].dt.tzinfo not in tzs: | ||||||
# Handle DTEND timezone | ||||||
if ( | ||||||
'DTEND' in vevent | ||||||
and hasattr(vevent['DTEND'].dt, 'tzinfo') | ||||||
and vevent['DTEND'].dt.tzinfo is not None | ||||||
and vevent['DTEND'].dt.tzinfo not in tzs | ||||||
): | ||||||
tzs.append(vevent['DTEND'].dt.tzinfo) | ||||||
|
||||||
# Add VTIMEZONE components for collected timezones | ||||||
for tzinfo in tzs: | ||||||
if tzinfo == pytz.UTC: | ||||||
continue | ||||||
timezone = create_timezone(tzinfo, self.start) | ||||||
if tzinfo in [pytz.UTC, ZoneInfo("UTC")]: | ||||||
continue # Skip UTC as it doesn't need a VTIMEZONE | ||||||
# Determine start date for timezone creation | ||||||
start_date = getattr(self, 'start', None) | ||||||
timezone = create_timezone(tzinfo, start_date) # Ensure create_timezone supports both pytz and ZoneInfo | ||||||
calendar.add_component(timezone) | ||||||
|
||||||
# Add VEVENT components to the calendar | ||||||
for vevent in self._vevents.values(): | ||||||
calendar.add_component(vevent) | ||||||
|
||||||
# Return the VCALENDAR as a string | ||||||
return calendar.to_ical().decode('utf-8') | ||||||
|
||||||
|
||||||
def export_ics(self, path: str) -> None: | ||||||
"""export event as ICS | ||||||
""" | ||||||
|
@@ -919,110 +933,90 @@ def duration(self) -> dt.timedelta: | |||||
|
||||||
|
||||||
def create_timezone( | ||||||
tz: pytz.BaseTzInfo, | ||||||
first_date: Optional[dt.datetime]=None, | ||||||
last_date: Optional[dt.datetime]=None | ||||||
tz, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please leave type hints here:
Suggested change
|
||||||
first_date: Optional[dt.datetime] = None, | ||||||
last_date: Optional[dt.datetime] = None, | ||||||
) -> icalendar.Timezone: | ||||||
""" | ||||||
create an icalendar vtimezone from a pytz.tzinfo object | ||||||
|
||||||
:param tz: the timezone | ||||||
:param first_date: the very first datetime that needs to be included in the | ||||||
transition times, typically the DTSTART value of the (first recurring) | ||||||
event | ||||||
:param last_date: the last datetime that needs to included, typically the | ||||||
end of the (very last) event (of a recursion set) | ||||||
:returns: timezone information | ||||||
|
||||||
we currently have a problem here: | ||||||
|
||||||
pytz.timezones only carry the absolute dates of time zone transitions, | ||||||
not their RRULEs. This will a) make for rather bloated VTIMEZONE | ||||||
components, especially for long recurring events, b) we'll need to | ||||||
specify for which time range this VTIMEZONE should be generated and c) | ||||||
will not be valid for recurring events that go into eternity. | ||||||
|
||||||
Possible Solutions: | ||||||
|
||||||
As this information is not provided by pytz at all, there is no | ||||||
easy solution, we'd really need to ship another version of the OLSON DB. | ||||||
|
||||||
Create an icalendar vtimezone from a pytz.tzinfo or zoneinfo.ZoneInfo object. | ||||||
""" | ||||||
if isinstance(tz, StaticTzInfo): | ||||||
if isinstance(tz, pytz.tzinfo.StaticTzInfo): | ||||||
return _create_timezone_static(tz) | ||||||
|
||||||
# TODO last_date = None, recurring to infinity | ||||||
first_date = dt.datetime.today() if not first_date else first_date | ||||||
last_date = first_date + dt.timedelta(days=1) if not last_date else last_date | ||||||
Comment on lines
+946
to
+947
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not entirely familiar with this code. Why did you remove |
||||||
|
||||||
first_date = dt.datetime.today() if not first_date else to_naive_utc(first_date) | ||||||
last_date = first_date + dt.timedelta(days=1) if not last_date else to_naive_utc(last_date) | ||||||
timezone = icalendar.Timezone() | ||||||
timezone.add('TZID', tz) | ||||||
|
||||||
dst = { | ||||||
one[2]: 'DST' in two.__repr__() | ||||||
for one, two in iter(tz._tzinfos.items()) # type: ignore | ||||||
} | ||||||
bst = { | ||||||
one[2]: 'BST' in two.__repr__() | ||||||
for one, two in iter(tz._tzinfos.items()) # type: ignore | ||||||
} | ||||||
|
||||||
# looking for the first and last transition time we need to include | ||||||
first_num, last_num = 0, len(tz._utc_transition_times) - 1 # type: ignore | ||||||
first_tt = tz._utc_transition_times[0] # type: ignore | ||||||
last_tt = tz._utc_transition_times[-1] # type: ignore | ||||||
for num, transtime in enumerate(tz._utc_transition_times): # type: ignore | ||||||
if first_date > transtime > first_tt: | ||||||
first_num = num | ||||||
first_tt = transtime | ||||||
if last_tt > transtime > last_date: | ||||||
last_num = num | ||||||
last_tt = transtime | ||||||
|
||||||
timezones: Dict[str, icalendar.Component] = {} | ||||||
for num in range(first_num, last_num + 1): | ||||||
name = tz._transition_info[num][2] # type: ignore | ||||||
if name in timezones: | ||||||
ttime = tz.fromutc(tz._utc_transition_times[num]).replace(tzinfo=None) # type: ignore | ||||||
if 'RDATE' in timezones[name]: | ||||||
timezones[name]['RDATE'].dts.append( | ||||||
icalendar.prop.vDDDTypes(ttime)) | ||||||
else: | ||||||
timezones[name].add('RDATE', ttime) | ||||||
continue | ||||||
timezone.add("TZID", tz.key if isinstance(tz, ZoneInfo) else str(tz)) | ||||||
|
||||||
if dst[name] or bst[name]: | ||||||
subcomp = icalendar.TimezoneDaylight() | ||||||
else: | ||||||
subcomp = icalendar.TimezoneStandard() | ||||||
# Handle timezones based on their type | ||||||
transitions = [] | ||||||
|
||||||
subcomp.add('TZNAME', tz._transition_info[num][2]) # type: ignore | ||||||
subcomp.add( | ||||||
'DTSTART', | ||||||
tz.fromutc(tz._utc_transition_times[num]).replace(tzinfo=None)) # type: ignore | ||||||
subcomp.add('TZOFFSETTO', tz._transition_info[num][0]) # type: ignore | ||||||
subcomp.add('TZOFFSETFROM', tz._transition_info[num - 1][0]) # type: ignore | ||||||
timezones[name] = subcomp | ||||||
if isinstance(tz, ZoneInfo): | ||||||
# Handle ZoneInfo transitions manually since it doesn't have _transitions | ||||||
transitions = _get_zoneinfo_transitions(tz, first_date, last_date) | ||||||
|
||||||
for subcomp in timezones.values(): | ||||||
elif isinstance(tz, pytz.BaseTzInfo): | ||||||
# Use pytz's transitions | ||||||
transitions = tz._utc_transition_times # Avoid using internal attributes directly. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
for transition in transitions: | ||||||
start, end, offset = transition | ||||||
subcomp = ( | ||||||
icalendar.TimezoneDaylight() | ||||||
if isinstance(offset, pytz.tzinfo.DstTzInfo) and offset.dst.total_seconds() != 0 | ||||||
else icalendar.TimezoneStandard() | ||||||
) | ||||||
subcomp.add("DTSTART", start) | ||||||
|
||||||
# Check if offset is a timedelta (this happens in some cases, like if the transition is not timezone-aware) | ||||||
if isinstance(offset, dt.timedelta): | ||||||
subcomp.add("TZOFFSETFROM", offset) | ||||||
subcomp.add("TZOFFSETTO", offset) | ||||||
elif isinstance(offset, pytz.tzinfo.DstTzInfo): | ||||||
subcomp.add("TZOFFSETFROM", offset.utcoffset) | ||||||
subcomp.add("TZOFFSETTO", offset.dst if offset.dst else offset.utcoffset) | ||||||
|
||||||
subcomp.add("TZNAME", offset) | ||||||
timezone.add_component(subcomp) | ||||||
|
||||||
return timezone | ||||||
|
||||||
|
||||||
def _create_timezone_static(tz: StaticTzInfo) -> icalendar.Timezone: | ||||||
"""create an icalendar vtimezone from a StaticTzInfo | ||||||
|
||||||
:param tz: the timezone | ||||||
:returns: timezone information | ||||||
""" | ||||||
def _create_timezone_static(tz: pytz.tzinfo.StaticTzInfo) -> icalendar.Timezone: | ||||||
"""Create a static timezone VTIMEZONE component for pytz.""" | ||||||
timezone = icalendar.Timezone() | ||||||
timezone.add('TZID', tz) | ||||||
timezone.add("TZID", tz.zone) | ||||||
subcomp = icalendar.TimezoneStandard() | ||||||
subcomp.add('TZNAME', tz) | ||||||
subcomp.add('DTSTART', dt.datetime(1601, 1, 1)) | ||||||
subcomp.add('RDATE', dt.datetime(1601, 1, 1)) | ||||||
subcomp.add('TZOFFSETTO', tz._utcoffset) # type: ignore | ||||||
subcomp.add('TZOFFSETFROM', tz._utcoffset) # type: ignore | ||||||
subcomp.add("TZNAME", tz.zone) | ||||||
subcomp.add("DTSTART", dt.datetime(1601, 1, 1)) | ||||||
subcomp.add("RDATE", dt.datetime(1601, 1, 1)) | ||||||
subcomp.add("TZOFFSETTO", tz.utcoffset(dt.datetime.now())) | ||||||
subcomp.add("TZOFFSETFROM", tz.utcoffset(dt.datetime.now())) | ||||||
timezone.add_component(subcomp) | ||||||
return timezone | ||||||
|
||||||
|
||||||
def _get_zoneinfo_transitions(tz: ZoneInfo, first_date: dt.datetime, last_date: dt.datetime): | ||||||
""" | ||||||
This function simulates transition extraction for ZoneInfo. | ||||||
Since ZoneInfo does not expose transitions like pytz, we manually collect relevant | ||||||
transition data (if available). | ||||||
""" | ||||||
transitions = [] | ||||||
|
||||||
# For example, you could manually simulate transitions based on historical knowledge | ||||||
# of the time zone, or use a library like `backports.zoneinfo` to fetch transitions. | ||||||
# But for simplicity, this is left as a stub for now. | ||||||
|
||||||
# Here we simulate adding some transitions based on the ZoneInfo timezone. | ||||||
# You could insert actual logic here based on your needs. | ||||||
|
||||||
# Example: simulate a "standard time" to "daylight saving time" transition | ||||||
start = first_date | ||||||
end = last_date | ||||||
offset = dt.timedelta(hours=1) # Example offset | ||||||
transitions.append((start, end, offset)) | ||||||
|
||||||
return transitions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-blocking nitpick: I would use a set here, so we can just insert new entries without having any duplicates (inserting an existing element into a set is a no-op):