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: 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) 4.0.9 (2021-10-16)

Wyświetl plik

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

Wyświetl plik

@ -53,15 +53,14 @@ def tzid_from_dt(dt):
tzid = None tzid = None
if hasattr(dt.tzinfo, 'zone'): if hasattr(dt.tzinfo, 'zone'):
tzid = dt.tzinfo.zone # pytz implementation tzid = dt.tzinfo.zone # pytz implementation
elif hasattr(dt.tzinfo, 'key'):
tzid = dt.tzinfo.key # ZoneInfo implementation
elif hasattr(dt.tzinfo, 'tzname'): elif hasattr(dt.tzinfo, 'tzname'):
try: # dateutil implementation, but this is broken
tzid = dt.tzinfo.tzname(dt) # dateutil implementation # See https://github.com/collective/icalendar/issues/333 for details
except AttributeError: tzid = dt.tzinfo.tzname(dt)
# No tzid available
pass
return tzid return tzid
def foldline(line, limit=75, fold_sep='\r\n '): def foldline(line, limit=75, fold_sep='\r\n '):
"""Make a string folded as defined in RFC5545 """Make a string folded as defined in RFC5545
Lines of text SHOULD NOT be longer than 75 octets, excluding the line 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 icalendar
import os import os
import pytz import pytz
try:
import zoneinfo
except:
try:
from backports import zoneinfo
except:
zoneinfo = None
class TestTimezoned(unittest.TestCase): 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__) directory = os.path.dirname(__file__)
with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp: with open(os.path.join(directory, 'timezoned.ics'), 'rb') as fp:
data = fp.read() 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 = icalendar.Calendar()
cal.add('prodid', "-//Plone.org//NONSGML plone.app.event//EN") 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("DTSTAMP;VALUE=DATE-TIME:20101010T081010Z" in test_out)
self.assertTrue("CREATED;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): def test_tzinfo_dateutil(self):
# Test for issues #77, #63 # Test for issues #77, #63
# references: #73,7430b66862346fe3a6a100ab25e35a8711446717 # references: #73,7430b66862346fe3a6a100ab25e35a8711446717