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:
 | 
					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)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Ładowanie…
	
		Reference in New Issue