mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	A new, and much hairier, implementation of astimezone(), building on
an idea from Guido. This restores that the datetime implementation never passes a datetime d to a tzinfo method unless d.tzinfo is the tzinfo instance whose method is being called. That in turn allows enormous simplifications in user-written tzinfo classes (see the Python sandbox US.py and EU.py for fully fleshed-out examples). d.astimezone(tz) also raises ValueError now if d lands in the one hour of the year that can't be expressed in tz (this can happen iff tz models both standard and daylight time). That it used to return a nonsense result always ate at me, and it turned out that it seemed impossible to force a consistent nonsense result under the new implementation (which doesn't know anything about how tzinfo classes implement their methods -- it can only infer properties indirectly). Guido doesn't like this -- expect it to change. New tests of conversion between adjacent DST-aware timezones don't pass yet, and are commented out. Running the datetime tests in a loop under a debug build leaks 9 references per test run, but I don't believe the datetime code is the cause (it didn't leak the last time I changed the C code, and the leak is the same if I disable all the tests that invoke the only function that changed here). I'll pursue that next.
This commit is contained in:
		
							parent
							
								
									ba2f875d90
								
							
						
					
					
						commit
						521fc15e62
					
				
					 2 changed files with 211 additions and 109 deletions
				
			
		|  | @ -2560,16 +2560,7 @@ def dst(self, dt): | ||||||
|             # An exception instead may be sensible here, in one or more of |             # An exception instead may be sensible here, in one or more of | ||||||
|             # the cases. |             # the cases. | ||||||
|             return ZERO |             return ZERO | ||||||
| 
 |         assert dt.tzinfo is self | ||||||
|         convert_endpoints_to_utc = False |  | ||||||
|         if dt.tzinfo is not self: |  | ||||||
|             # Convert dt to UTC. |  | ||||||
|             offset = dt.utcoffset() |  | ||||||
|             if offset is None: |  | ||||||
|                 # Again, an exception instead may be sensible. |  | ||||||
|                 return ZERO |  | ||||||
|             convert_endpoints_to_utc = True |  | ||||||
|             dt -= offset |  | ||||||
| 
 | 
 | ||||||
|         # Find first Sunday in April. |         # Find first Sunday in April. | ||||||
|         start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) |         start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) | ||||||
|  | @ -2579,10 +2570,6 @@ def dst(self, dt): | ||||||
|         end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) |         end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) | ||||||
|         assert end.weekday() == 6 and end.month == 10 and end.day >= 25 |         assert end.weekday() == 6 and end.month == 10 and end.day >= 25 | ||||||
| 
 | 
 | ||||||
|         if convert_endpoints_to_utc: |  | ||||||
|             start -= self.stdoffset    # start is in std time |  | ||||||
|             end -= self.stdoffset + HOUR # end is in DST time |  | ||||||
| 
 |  | ||||||
|         # Can't compare naive to aware objects, so strip the timezone from |         # Can't compare naive to aware objects, so strip the timezone from | ||||||
|         # dt first. |         # dt first. | ||||||
|         if start <= dt.astimezone(None) < end: |         if start <= dt.astimezone(None) < end: | ||||||
|  | @ -2591,6 +2578,8 @@ def dst(self, dt): | ||||||
|             return ZERO |             return ZERO | ||||||
| 
 | 
 | ||||||
| Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT") | Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT") | ||||||
|  | Central  = USTimeZone(-6, "Central",  "CST", "CDT") | ||||||
|  | Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") | ||||||
| Pacific  = USTimeZone(-8, "Pacific",  "PST", "PDT") | Pacific  = USTimeZone(-8, "Pacific",  "PST", "PDT") | ||||||
| utc_real = FixedOffset(0, "UTC", 0) | utc_real = FixedOffset(0, "UTC", 0) | ||||||
| # For better test coverage, we want another flavor of UTC that's west of | # For better test coverage, we want another flavor of UTC that's west of | ||||||
|  | @ -2602,25 +2591,17 @@ class TestTimezoneConversions(unittest.TestCase): | ||||||
|     dston = datetimetz(2002, 4, 7, 2) |     dston = datetimetz(2002, 4, 7, 2) | ||||||
|     dstoff = datetimetz(2002, 10, 27, 2) |     dstoff = datetimetz(2002, 10, 27, 2) | ||||||
| 
 | 
 | ||||||
|     def convert_between_tz_and_utc(self, tz, utc): |  | ||||||
|         dston = self.dston.replace(tzinfo=tz) |  | ||||||
|         dstoff = self.dstoff.replace(tzinfo=tz) |  | ||||||
|         for delta in (timedelta(weeks=13), |  | ||||||
|                       DAY, |  | ||||||
|                       HOUR, |  | ||||||
|                       timedelta(minutes=1), |  | ||||||
|                       timedelta(microseconds=1)): |  | ||||||
| 
 | 
 | ||||||
|             for during in dston, dston + delta, dstoff - delta: |     # Check a time that's inside DST. | ||||||
|                 self.assertEqual(during.dst(), HOUR) |     def checkinside(self, dt, tz, utc, dston, dstoff): | ||||||
|  |         self.assertEqual(dt.dst(), HOUR) | ||||||
| 
 | 
 | ||||||
|         # Conversion to our own timezone is always an identity. |         # Conversion to our own timezone is always an identity. | ||||||
|                 self.assertEqual(during.astimezone(tz), during) |         self.assertEqual(dt.astimezone(tz), dt) | ||||||
|         # Conversion to None is always the same as stripping tzinfo. |         # Conversion to None is always the same as stripping tzinfo. | ||||||
|                 self.assertEqual(during.astimezone(None), |         self.assertEqual(dt.astimezone(None), dt.replace(tzinfo=None)) | ||||||
|                                  during.replace(tzinfo=None)) |  | ||||||
| 
 | 
 | ||||||
|                 asutc = during.astimezone(utc) |         asutc = dt.astimezone(utc) | ||||||
|         there_and_back = asutc.astimezone(tz) |         there_and_back = asutc.astimezone(tz) | ||||||
| 
 | 
 | ||||||
|         # Conversion to UTC and back isn't always an identity here, |         # Conversion to UTC and back isn't always an identity here, | ||||||
|  | @ -2631,19 +2612,19 @@ def convert_between_tz_and_utc(self, tz, utc): | ||||||
|         # daylight time then (it's "after 2am"), really an alias |         # daylight time then (it's "after 2am"), really an alias | ||||||
|         # for 1:MM:SS standard time.  The latter form is what |         # for 1:MM:SS standard time.  The latter form is what | ||||||
|         # conversion back from UTC produces. |         # conversion back from UTC produces. | ||||||
|                 if during.date() == dston.date() and during.hour == 2: |         if dt.date() == dston.date() and dt.hour == 2: | ||||||
|             # We're in the redundant hour, and coming back from |             # We're in the redundant hour, and coming back from | ||||||
|             # UTC gives the 1:MM:SS standard-time spelling. |             # UTC gives the 1:MM:SS standard-time spelling. | ||||||
|                     self.assertEqual(there_and_back + HOUR, during) |             self.assertEqual(there_and_back + HOUR, dt) | ||||||
|             # Although during was considered to be in daylight |             # Although during was considered to be in daylight | ||||||
|             # time, there_and_back is not. |             # time, there_and_back is not. | ||||||
|             self.assertEqual(there_and_back.dst(), ZERO) |             self.assertEqual(there_and_back.dst(), ZERO) | ||||||
|             # They're the same times in UTC. |             # They're the same times in UTC. | ||||||
|             self.assertEqual(there_and_back.astimezone(utc), |             self.assertEqual(there_and_back.astimezone(utc), | ||||||
|                                      during.astimezone(utc)) |                              dt.astimezone(utc)) | ||||||
|         else: |         else: | ||||||
|             # We're not in the redundant hour. |             # We're not in the redundant hour. | ||||||
|                     self.assertEqual(during, there_and_back) |             self.assertEqual(dt, there_and_back) | ||||||
| 
 | 
 | ||||||
|         # Because we have a redundant spelling when DST begins, |         # Because we have a redundant spelling when DST begins, | ||||||
|         # there is (unforunately) an hour when DST ends that can't |         # there is (unforunately) an hour when DST ends that can't | ||||||
|  | @ -2654,8 +2635,7 @@ def convert_between_tz_and_utc(self, tz, utc): | ||||||
|         # standard time.  The hour 1:MM:SS standard time == |         # standard time.  The hour 1:MM:SS standard time == | ||||||
|         # 2:MM:SS daylight time can't be expressed in local time. |         # 2:MM:SS daylight time can't be expressed in local time. | ||||||
|         nexthour_utc = asutc + HOUR |         nexthour_utc = asutc + HOUR | ||||||
|                 nexthour_tz = nexthour_utc.astimezone(tz) |         if dt.date() == dstoff.date() and dt.hour == 1: | ||||||
|                 if during.date() == dstoff.date() and during.hour == 1: |  | ||||||
|             # We're in the hour before DST ends.  The hour after |             # We're in the hour before DST ends.  The hour after | ||||||
|             # is ineffable. |             # is ineffable. | ||||||
|             # For concreteness, picture Eastern.  during is of |             # For concreteness, picture Eastern.  during is of | ||||||
|  | @ -2668,20 +2648,37 @@ def convert_between_tz_and_utc(self, tz, utc): | ||||||
|             # That's correct, too, *if* 1:MM:SS were taken as |             # That's correct, too, *if* 1:MM:SS were taken as | ||||||
|             # being standard time.  But it's not -- on this day |             # being standard time.  But it's not -- on this day | ||||||
|             # it's taken as daylight time. |             # it's taken as daylight time. | ||||||
|                     self.assertEqual(during, nexthour_tz) |             self.assertRaises(ValueError, | ||||||
|  |                               nexthour_utc.astimezone, tz) | ||||||
|         else: |         else: | ||||||
|                     self.assertEqual(nexthour_tz - during, HOUR) |             nexthour_tz = nexthour_utc.astimezone(utc) | ||||||
|  |             self.assertEqual(nexthour_tz - dt, HOUR) | ||||||
| 
 | 
 | ||||||
|             for outside in dston - delta, dstoff, dstoff + delta: |     # Check a time that's outside DST. | ||||||
|                 self.assertEqual(outside.dst(), ZERO) |     def checkoutside(self, dt, tz, utc): | ||||||
|                 there_and_back = outside.astimezone(utc).astimezone(tz) |         self.assertEqual(dt.dst(), ZERO) | ||||||
|                 self.assertEqual(outside, there_and_back) |  | ||||||
| 
 | 
 | ||||||
|         # Conversion to our own timezone is always an identity. |         # Conversion to our own timezone is always an identity. | ||||||
|                 self.assertEqual(outside.astimezone(tz), outside) |         self.assertEqual(dt.astimezone(tz), dt) | ||||||
|         # Conversion to None is always the same as stripping tzinfo. |         # Conversion to None is always the same as stripping tzinfo. | ||||||
|                 self.assertEqual(outside.astimezone(None), |         self.assertEqual(dt.astimezone(None), dt.replace(tzinfo=None)) | ||||||
|                                  outside.replace(tzinfo=None)) | 
 | ||||||
|  |     def convert_between_tz_and_utc(self, tz, utc): | ||||||
|  |         dston = self.dston.replace(tzinfo=tz) | ||||||
|  |         dstoff = self.dstoff.replace(tzinfo=tz) | ||||||
|  |         for delta in (timedelta(weeks=13), | ||||||
|  |                       DAY, | ||||||
|  |                       HOUR, | ||||||
|  |                       timedelta(minutes=1), | ||||||
|  |                       timedelta(microseconds=1)): | ||||||
|  | 
 | ||||||
|  |             self.checkinside(dston, tz, utc, dston, dstoff) | ||||||
|  |             for during in dston + delta, dstoff - delta: | ||||||
|  |                 self.checkinside(during, tz, utc, dston, dstoff) | ||||||
|  | 
 | ||||||
|  |             self.checkoutside(dstoff, tz, utc) | ||||||
|  |             for outside in dston - delta, dstoff + delta: | ||||||
|  |                 self.checkoutside(outside, tz, utc) | ||||||
| 
 | 
 | ||||||
|     def test_easy(self): |     def test_easy(self): | ||||||
|         # Despite the name of this test, the endcases are excruciating. |         # Despite the name of this test, the endcases are excruciating. | ||||||
|  | @ -2694,6 +2691,9 @@ def test_easy(self): | ||||||
|         # hours" don't overlap. |         # hours" don't overlap. | ||||||
|         self.convert_between_tz_and_utc(Eastern, Pacific) |         self.convert_between_tz_and_utc(Eastern, Pacific) | ||||||
|         self.convert_between_tz_and_utc(Pacific, Eastern) |         self.convert_between_tz_and_utc(Pacific, Eastern) | ||||||
|  |         # XXX These fail! | ||||||
|  |         #self.convert_between_tz_and_utc(Eastern, Central) | ||||||
|  |         #self.convert_between_tz_and_utc(Central, Eastern) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_suite(): | def test_suite(): | ||||||
|  |  | ||||||
|  | @ -4751,6 +4751,11 @@ datetimetz_astimezone(PyDateTime_DateTimeTZ *self, PyObject *args, | ||||||
| 	int ss = DATE_GET_SECOND(self); | 	int ss = DATE_GET_SECOND(self); | ||||||
| 	int us = DATE_GET_MICROSECOND(self); | 	int us = DATE_GET_MICROSECOND(self); | ||||||
| 
 | 
 | ||||||
|  | 	PyObject *result; | ||||||
|  | 	PyObject *temp; | ||||||
|  | 	int myoff, otoff, newoff; | ||||||
|  | 	int none; | ||||||
|  | 
 | ||||||
| 	PyObject *tzinfo; | 	PyObject *tzinfo; | ||||||
| 	static char *keywords[] = {"tz", NULL}; | 	static char *keywords[] = {"tz", NULL}; | ||||||
| 
 | 
 | ||||||
|  | @ -4760,30 +4765,127 @@ datetimetz_astimezone(PyDateTime_DateTimeTZ *self, PyObject *args, | ||||||
| 	if (check_tzinfo_subclass(tzinfo) < 0) | 	if (check_tzinfo_subclass(tzinfo) < 0) | ||||||
| 		return NULL; | 		return NULL; | ||||||
| 
 | 
 | ||||||
| 	if (tzinfo != Py_None && self->tzinfo != Py_None) { |         /* Don't call utcoffset unless necessary. */ | ||||||
| 		int none; | 	result = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo); | ||||||
| 		int selfoffset; | 	if (result == NULL || | ||||||
| 		selfoffset = call_utcoffset(self->tzinfo, | 	    tzinfo == Py_None || | ||||||
| 					    (PyObject *)self, | 	    self->tzinfo == Py_None || | ||||||
| 					    &none); | 	    self->tzinfo == tzinfo) | ||||||
| 	        if (selfoffset == -1 && PyErr_Occurred()) | 		return result; | ||||||
| 	        	return NULL; | 
 | ||||||
| 	        if (! none) { |         /* Get the offsets.  If either object turns out to be naive, again
 | ||||||
| 			int tzoffset; |          * there's no conversion of date or time fields. | ||||||
| 	        	tzoffset = call_utcoffset(tzinfo, |          */ | ||||||
| 	        				  (PyObject *)self, | 	myoff = call_utcoffset(self->tzinfo, (PyObject *)self, &none); | ||||||
| 	        				  &none); | 	if (myoff == -1 && PyErr_Occurred()) | ||||||
| 	        	if (tzoffset == -1 && PyErr_Occurred()) | 		goto Fail; | ||||||
| 	        		return NULL; | 	if (none) | ||||||
| 	        	if (! none) { | 		return result; | ||||||
| 	        		mm -= selfoffset - tzoffset; | 
 | ||||||
| 	        		if (normalize_datetime(&y, &m, &d, | 	otoff = call_utcoffset(tzinfo, result, &none); | ||||||
| 	        				       &hh, &mm, &ss, &us) < 0) | 	if (otoff == -1 && PyErr_Occurred()) | ||||||
| 	        			return NULL; | 		goto Fail; | ||||||
|  | 	if (none) | ||||||
|  | 		return result; | ||||||
|  | 
 | ||||||
|  | 	/* Add otoff-myoff to result. */ | ||||||
|  | 	mm += otoff - myoff; | ||||||
|  | 	if (normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0) | ||||||
|  | 		goto Fail; | ||||||
|  | 	temp = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo); | ||||||
|  | 	if (temp == NULL) | ||||||
|  | 		goto Fail; | ||||||
|  | 	Py_DECREF(result); | ||||||
|  | 	result = temp; | ||||||
|  | 
 | ||||||
|  | 	/* If tz is a fixed-offset class, we're done, but we can't know
 | ||||||
|  | 	 * whether it is.  If it's a DST-aware class, and we're not near a | ||||||
|  | 	 * DST boundary, we're also done.  If we crossed a DST boundary, | ||||||
|  | 	 * the offset will be different now, and that's our only clue. | ||||||
|  | 	 * Unfortunately, we can be in trouble even if we didn't cross a | ||||||
|  | 	 * DST boundary, if we landed on one of the DST "problem hours". | ||||||
|  | 	 */ | ||||||
|  | 	newoff = call_utcoffset(tzinfo, result, &none); | ||||||
|  | 	if (newoff == -1 && PyErr_Occurred()) | ||||||
|  | 		goto Fail; | ||||||
|  | 	if (none) | ||||||
|  | 		goto Inconsistent; | ||||||
|  | 
 | ||||||
|  | 	if (newoff != otoff) { | ||||||
|  | 		/* We did cross a boundary.  Try to correct. */ | ||||||
|  | 		mm += newoff - otoff; | ||||||
|  | 		if (normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0) | ||||||
|  | 			goto Fail; | ||||||
|  | 		temp = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo); | ||||||
|  | 		if (temp == NULL) | ||||||
|  | 			goto Fail; | ||||||
|  | 		Py_DECREF(result); | ||||||
|  | 		result = temp; | ||||||
|  | 
 | ||||||
|  | 		otoff = call_utcoffset(tzinfo, result, &none); | ||||||
|  | 		if (otoff == -1 && PyErr_Occurred()) | ||||||
|  | 			goto Fail; | ||||||
|  | 		if (none) | ||||||
|  | 			goto Inconsistent; | ||||||
|         } |         } | ||||||
|  | 	/* If this is the first hour of DST, it may be a local time that
 | ||||||
|  | 	 * doesn't make sense on the local clock, in which case the naive | ||||||
|  | 	 * hour before it (in standard time) is equivalent and does make | ||||||
|  | 	 * sense on the local clock.  So force that. | ||||||
|  | 	 */ | ||||||
|  | 	hh -= 1; | ||||||
|  | 	if (normalize_datetime(&y, &m, &d, &hh, &mm, &ss, &us) < 0) | ||||||
|  | 		goto Fail; | ||||||
|  | 	temp = new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo); | ||||||
|  | 	if (temp == NULL) | ||||||
|  | 		goto Fail; | ||||||
|  | 	newoff = call_utcoffset(tzinfo, temp, &none); | ||||||
|  | 	if (newoff == -1 && PyErr_Occurred()) { | ||||||
|  | 		Py_DECREF(temp); | ||||||
|  | 		goto Fail; | ||||||
| 	} | 	} | ||||||
|  | 	if (none) { | ||||||
|  | 		Py_DECREF(temp); | ||||||
|  | 		goto Inconsistent; | ||||||
| 	} | 	} | ||||||
| 	return new_datetimetz(y, m, d, hh, mm, ss, us, tzinfo); | 	/* Are temp and result really the same time?  temp == result iff
 | ||||||
|  | 	 * temp - newoff == result - otoff, iff | ||||||
|  | 	 * (result - HOUR) - newoff = result - otoff, iff | ||||||
|  | 	 * otoff - newoff == HOUR | ||||||
|  | 	 */ | ||||||
|  | 	if (otoff - newoff == 60) { | ||||||
|  | 		/* use the local time that makes sense */ | ||||||
|  | 		Py_DECREF(result); | ||||||
|  | 		return temp; | ||||||
|  | 	} | ||||||
|  | 	Py_DECREF(temp); | ||||||
|  | 
 | ||||||
|  | 	/* There's still a problem with the unspellable (in local time)
 | ||||||
|  | 	 * hour after DST ends. | ||||||
|  | 	 */ | ||||||
|  | 	temp = datetime_richcompare((PyDateTime_DateTime *)self, | ||||||
|  | 				    result, Py_EQ); | ||||||
|  | 	if (temp == NULL) | ||||||
|  | 		goto Fail; | ||||||
|  | 	if (temp == Py_True) { | ||||||
|  | 		Py_DECREF(temp); | ||||||
|  | 		return result; | ||||||
|  | 	} | ||||||
|  | 	Py_DECREF(temp); | ||||||
|  |         /* Else there's no way to spell self in zone other.tz. */ | ||||||
|  |         PyErr_SetString(PyExc_ValueError, "astimezone(): the source " | ||||||
|  |         		"datetimetz can't be expressed in the target " | ||||||
|  |         		"timezone's local time"); | ||||||
|  |         goto Fail; | ||||||
|  | 
 | ||||||
|  | Inconsistent: | ||||||
|  | 	PyErr_SetString(PyExc_ValueError, "astimezone(): tz.utcoffset() " | ||||||
|  | 			"gave inconsistent results; cannot convert"); | ||||||
|  | 
 | ||||||
|  | 	/* fall thru to failure */ | ||||||
|  | Fail: | ||||||
|  | 	Py_DECREF(result); | ||||||
|  | 	return NULL; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject * | static PyObject * | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Tim Peters
						Tim Peters