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
	
	 Tobias Brox
						Tobias Brox