Bugfix for datetime objects with tzinfo from zoneinfo library

See https://github.com/collective/icalendar/issues/333 for details

Includes test code and changelog entry
pull/339/head
Tobias Brox 2021-11-17 14:36:13 +01:00
rodzic 90c0593506
commit d6de71b828
4 zmienionych plików z 136 dodań i 10 usunięć

Wyświetl plik

@ -15,7 +15,10 @@ New features:
Bug fixes:
- *add item here*
- proper handling of datetime objects with `tzinfo` generated through zoneinfo.ZoneInfo.
Ref: #334
Fixes: #333
[tobixen]
4.0.9 (2021-10-16)

Wyświetl plik

@ -56,6 +56,7 @@ icalendar contributors
- Clive Stevens <clivest2@gmail.com>
- Dalton Durst <github@daltondur.st>
- Kamil Mańkowski <kam193@wp.pl>
- Tobias Brox <tobias@redpill-linpro.com>
Find out who contributed::

Wyświetl plik

@ -53,15 +53,14 @@ def tzid_from_dt(dt):
tzid = None
if hasattr(dt.tzinfo, 'zone'):
tzid = dt.tzinfo.zone # pytz implementation
elif hasattr(dt.tzinfo, 'key'):
tzid = dt.tzinfo.key # ZoneInfo implementation
elif hasattr(dt.tzinfo, 'tzname'):
try:
tzid = dt.tzinfo.tzname(dt) # dateutil implementation
except AttributeError:
# No tzid available
pass
# dateutil implementation, but this is broken
# See https://github.com/collective/icalendar/issues/333 for details
tzid = dt.tzinfo.tzname(dt)
return tzid
def foldline(line, limit=75, fold_sep='\r\n '):
"""Make a string folded as defined in RFC5545
Lines of text SHOULD NOT be longer than 75 octets, excluding the line

Wyświetl plik

@ -8,11 +8,50 @@ import dateutil.parser
import icalendar
import os
import pytz
try:
import zoneinfo
except:
try:
from backports import zoneinfo
except:
zoneinfo = None
class TestTimezoned(unittest.TestCase):
def test_create_from_ical(self):
def test_create_from_ical_zoneinfo(self):
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
data = fp.read()
cal = icalendar.Calendar.from_ical(data)
self.assertEqual(
cal['prodid'].to_ical(),
b"-//Plone.org//NONSGML plone.app.event//EN"
)
timezones = cal.walk('VTIMEZONE')
self.assertEqual(len(timezones), 1)
tz = timezones[0]
self.assertEqual(tz['tzid'].to_ical(), b"Europe/Vienna")
std = tz.walk('STANDARD')[0]
self.assertEqual(
std.decoded('TZOFFSETFROM'),
datetime.timedelta(0, 7200)
)
ev1 = cal.walk('VEVENT')[0]
self.assertEqual(
ev1.decoded('DTSTART'),
datetime.datetime(2012, 2, 13, 10, 0, 0, tzinfo=zoneinfo.ZoneInfo('Europe/Vienna'))
)
self.assertEqual(
ev1.decoded('DTSTAMP'),
datetime.datetime(2010, 10, 10, 9, 10, 10, tzinfo=zoneinfo.ZoneInfo('UTC'))
)
def test_create_from_ical_pytz(self):
directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
data = fp.read()
@ -49,7 +88,7 @@ class TestTimezoned(unittest.TestCase):
)
)
def test_create_to_ical(self):
def test_create_to_ical_pytz(self):
cal = icalendar.Calendar()
cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN")
@ -132,6 +171,90 @@ class TestTimezoned(unittest.TestCase):
self.assertTrue("DTSTAMP;VALUE=DATE-TIME:20101010T081010Z" in test_out)
self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T081010Z" in test_out)
def test_create_to_ical_zoneinfo(self):
cal = icalendar.Calendar()
cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN")
cal.add('version', "2.0")
cal.add('x-wr-calname', "test create calendar")
cal.add('x-wr-caldesc', "icalendar tests")
cal.add('x-wr-relcalid', "12345")
cal.add('x-wr-timezone', "Europe/Vienna")
tzc = icalendar.Timezone()
tzc.add('tzid', 'Europe/Vienna')
tzc.add('x-lic-location', 'Europe/Vienna')
tzs = icalendar.TimezoneStandard()
tzs.add('tzname', 'CET')
tzs.add('dtstart', datetime.datetime(1970, 10, 25, 3, 0, 0))
tzs.add('rrule', {'freq': 'yearly', 'bymonth': 10, 'byday': '-1su'})
tzs.add('TZOFFSETFROM', datetime.timedelta(hours=2))
tzs.add('TZOFFSETTO', datetime.timedelta(hours=1))
tzd = icalendar.TimezoneDaylight()
tzd.add('tzname', 'CEST')
tzd.add('dtstart', datetime.datetime(1970, 3, 29, 2, 0, 0))
tzs.add('rrule', {'freq': 'yearly', 'bymonth': 3, 'byday': '-1su'})
tzd.add('TZOFFSETFROM', datetime.timedelta(hours=1))
tzd.add('TZOFFSETTO', datetime.timedelta(hours=2))
tzc.add_component(tzs)
tzc.add_component(tzd)
cal.add_component(tzc)
event = icalendar.Event()
tz = zoneinfo.ZoneInfo("Europe/Vienna")
event.add(
'dtstart',
datetime.datetime(2012, 2, 13, 10, 00, 00, tzinfo=tz))
event.add(
'dtend',
datetime.datetime(2012, 2, 17, 18, 00, 00, tzinfo=tz))
event.add(
'dtstamp',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add(
'created',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add('uid', '123456')
event.add(
'last-modified',
datetime.datetime(2010, 10, 10, 10, 10, 10, tzinfo=tz))
event.add('summary', 'artsprint 2012')
# event.add('rrule', 'FREQ=YEARLY;INTERVAL=1;COUNT=10')
event.add('description', 'sprinting at the artsprint')
event.add('location', 'aka bild, wien')
event.add('categories', 'first subject')
event.add('categories', 'second subject')
event.add('attendee', 'häns')
event.add('attendee', 'franz')
event.add('attendee', 'sepp')
event.add('contact', 'Max Mustermann, 1010 Wien')
event.add('url', 'http://plone.org')
cal.add_component(event)
test_out = b'|'.join(cal.to_ical().splitlines())
test_out = test_out.decode('utf-8')
vtimezone_lines = "BEGIN:VTIMEZONE|TZID:Europe/Vienna|X-LIC-LOCATION:"
"Europe/Vienna|BEGIN:STANDARD|DTSTART;VALUE=DATE-TIME:19701025T03"
"0000|RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10|RRULE:FREQ=YEARLY;B"
"YDAY=-1SU;BYMONTH=3|TZNAME:CET|TZOFFSETFROM:+0200|TZOFFSETTO:+01"
"00|END:STANDARD|BEGIN:DAYLIGHT|DTSTART;VALUE=DATE-TIME:19700329T"
"020000|TZNAME:CEST|TZOFFSETFROM:+0100|TZOFFSETTO:+0200|END:DAYLI"
"GHT|END:VTIMEZONE"
self.assertTrue(vtimezone_lines in test_out)
test_str = "DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20120213T100000"
self.assertTrue(test_str in test_out)
self.assertTrue("ATTENDEE:sepp" in test_out)
# ical standard expects DTSTAMP and CREATED in UTC
self.assertTrue("DTSTAMP;VALUE=DATE-TIME:20101010T081010Z" in test_out)
self.assertTrue("CREATED;VALUE=DATE-TIME:20101010T081010Z" in test_out)
def test_tzinfo_dateutil(self):
# Test for issues #77, #63
# references: #73,7430b66862346fe3a6a100ab25e35a8711446717