kopia lustrzana https://github.com/collective/icalendar
Bugfix for datetime objects with tzinfo from zoneinfo library
See https://github.com/collective/icalendar/issues/333 for details Includes test code and changelog entrypull/339/head
rodzic
90c0593506
commit
d6de71b828
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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::
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue