diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 5d5b8e415f3cd2..65c034c054a17a 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -5661,6 +5661,37 @@ def test_astimezone_default_near_fold(self): s = t.astimezone() self.assertEqual(t.tzinfo, s.tzinfo) + @unittest.skipIf(sys.platform != "win32", "gh-148658 only affects Windows") + def test_astimezone_negative_timestamp_dst(self): + # gh-148658: astimezone() returned incorrect DST name for negative + # timestamps on Windows. Verify C layer returns correct tm_zone + # by checking astimezone() produces correct timezone names. + import time + winter_ts = -1 # 1969-12-31 + summer_ts = -15897600 # 1969-07-01 + + # Get expected timezone names from time.localtime + winter_local = time.localtime(winter_ts) + summer_local = time.localtime(summer_ts) + + dt_winter = self.theclass.fromtimestamp(winter_ts).astimezone() + dt_summer = self.theclass.fromtimestamp(summer_ts).astimezone() + + # Verify astimezone() returns correct timezone names + self.assertEqual(dt_winter.tzname(), winter_local.tm_zone) + self.assertEqual(dt_summer.tzname(), summer_local.tm_zone) + + # For DST timezones, winter and summer should have different names + # For non-DST timezones, names should be the same + if winter_local.tm_zone == summer_local.tm_zone: + # Non-DST timezone: both should be standard time + self.assertEqual(winter_local.tm_isdst, 0) + self.assertEqual(summer_local.tm_isdst, 0) + else: + # DST timezone: one should be standard, one DST + self.assertEqual(winter_local.tm_isdst, 0) + self.assertEqual(summer_local.tm_isdst, 1) + def test_aware_subtract(self): cls = self.theclass diff --git a/Misc/NEWS.d/next/Windows/2026-04-17-19-42-04.gh-issue-148658.yhgT1z.rst b/Misc/NEWS.d/next/Windows/2026-04-17-19-42-04.gh-issue-148658.yhgT1z.rst new file mode 100644 index 00000000000000..f2b37ef0e1ec93 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-04-17-19-42-04.gh-issue-148658.yhgT1z.rst @@ -0,0 +1 @@ +Fix DST timezone name for negative timestamps on Windows. diff --git a/Python/pytime.c b/Python/pytime.c index 399ff59ad01ab6..2bd20ac38f11d3 100644 --- a/Python/pytime.c +++ b/Python/pytime.c @@ -10,6 +10,7 @@ #endif #ifdef MS_WINDOWS # include // struct timeval +# include // time zone APIs #endif #if defined(__APPLE__) @@ -355,8 +356,43 @@ _PyTime_windows_filetime(time_t timer, struct tm *tm, int is_local) // `time.gmtime` and `time.localtime` will return `struct_time` containing this tm->tm_yday = _PyTime_calc_yday(&st_result); - /* DST flag: -1 (unknown) for local time on historical dates, 0 for UTC */ - tm->tm_isdst = is_local ? -1 : 0; + /* DST flag: -1 (unknown) for local time on historical dates, 0 for UTC. + * Detect DST by comparing standard time with local time (gh-148658). */ + if (is_local) { + TIME_ZONE_INFORMATION tzi; + DWORD tz_result = GetTimeZoneInformation(&tzi); + if (tz_result == TIME_ZONE_ID_INVALID || + tzi.DaylightDate.wMonth == 0) { + /* Timezone does not observe DST */ + tm->tm_isdst = 0; + } + else { + /* Calculate standard time from UTC and compare with local time. */ + ULONGLONG utc_ticks = ((ULONGLONG)timer + SECS_BETWEEN_EPOCHS) * + HUNDRED_NS_PER_SEC; + LONGLONG standard_offset_ticks = (LONGLONG)tzi.Bias * 60LL * + HUNDRED_NS_PER_SEC; + ULONGLONG standard_ticks = utc_ticks - standard_offset_ticks; + FILETIME ft_standard; + ft_standard.dwLowDateTime = (DWORD)(standard_ticks); + ft_standard.dwHighDateTime = (DWORD)(standard_ticks >> 32); + SYSTEMTIME st_standard; + if (!FileTimeToSystemTime(&ft_standard, &st_standard)) { + PyErr_SetFromWindowsErr(0); + return -1; + } + if (st_result.wHour == st_standard.wHour && + st_result.wMinute == st_standard.wMinute) { + tm->tm_isdst = 0; + } + else { + tm->tm_isdst = 1; + } + } + } + else { + tm->tm_isdst = 0; + } return 0; }