From 77cdfd73bfd5d19aa80668507a61f69ff92ca638 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 8 Apr 2026 15:53:46 +0200 Subject: [PATCH 1/2] Add regression test for strftime timezone mismatch --- .../test/builtin/modules/TimeModuleTests.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/modules/TimeModuleTests.java diff --git a/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/modules/TimeModuleTests.java b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/modules/TimeModuleTests.java new file mode 100644 index 0000000000..4579a8ad7d --- /dev/null +++ b/graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/modules/TimeModuleTests.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.graal.python.test.builtin.modules; + +import static org.junit.Assert.assertEquals; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TimeZone; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Source; +import org.junit.Test; + +public class TimeModuleTests { + + @Test + public void strftimeTimezoneMatchesTzsetState() { + TimeZone previousDefault = TimeZone.getDefault(); + try { + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin")); + String result; + try (Context context = Context.newBuilder("python").allowAllAccess(true).build()) { + result = context.eval(Source.create("python", """ + import os + import time + + os.environ["TZ"] = "UTC" + time.tzset() + tt = time.localtime() + + "\\n".join(( + f"tm_zone={tt.tm_zone}", + f"tzname={time.tzname[tt.tm_isdst > 0]}", + f"strftime_tuple={time.strftime('%Z', tt)}", + f"strftime_now={time.strftime('%Z')}", + )) + """)).asString(); + } + + Map values = parseKeyValueLines(result); + String details = values.toString(); + assertEquals(details, values.get("tm_zone"), values.get("tzname")); + assertEquals(details, values.get("tm_zone"), values.get("strftime_tuple")); + assertEquals(details, values.get("tm_zone"), values.get("strftime_now")); + } finally { + TimeZone.setDefault(previousDefault); + } + } + + private static Map parseKeyValueLines(String output) { + Map values = new LinkedHashMap<>(); + for (String line : output.split("\\R")) { + int separator = line.indexOf('='); + values.put(line.substring(0, separator), line.substring(separator + 1)); + } + return values; + } +} From 8a2c8c950d544a49e8f4e91a37d4618eb98b6d05 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Wed, 8 Apr 2026 17:00:04 +0200 Subject: [PATCH 2/2] [GR-74747] Use tzset timezone for time.strftime --- .../builtins/modules/TimeModuleBuiltins.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/TimeModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/TimeModuleBuiltins.java index 33aa696318..a09bafc73a 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/TimeModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/TimeModuleBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. * Copyright (c) 2013, Regents of the University of California * * All rights reserved. @@ -739,15 +739,17 @@ private static String truncYear(int year) { return yearstr.substring(yearstr.length() - 2); } - private static GregorianCalendar getCalendar(int[] time) { + private static GregorianCalendar getCalendar(int[] time, TimeZone timeZone) { Month month = Month.of(time[1]); // GregorianCalendar expect months that starts from 0 - return new GregorianCalendar(time[0], month.ordinal(), time[2], time[3], time[4], time[5]); + GregorianCalendar calendar = new GregorianCalendar(timeZone); + calendar.set(time[0], month.ordinal(), time[2], time[3], time[4], time[5]); + return calendar; } // This taken from JPython + some switches were corrected to provide the // same result as CPython @TruffleBoundary - public static TruffleString format(String format, int[] date, TruffleString.FromJavaStringNode fromJavaStringNode) { + public static TruffleString format(String format, int[] date, TimeZone timeZone, TruffleString.FromJavaStringNode fromJavaStringNode) { String s = ""; int lastc = 0; int j; @@ -869,7 +871,7 @@ public static TruffleString format(String format, int[] date, TruffleString.From // TODO this is not correct, CPython counts the week of year // from day of year item [8] if (cal == null) { - cal = getCalendar(date); + cal = getCalendar(date, timeZone); } cal.setFirstDayOfWeek(Calendar.SUNDAY); @@ -900,7 +902,7 @@ public static TruffleString format(String format, int[] date, TruffleString.From // from day of year item [8] if (cal == null) { - cal = getCalendar(date); + cal = getCalendar(date, timeZone); } cal.setFirstDayOfWeek(Calendar.MONDAY); cal.setMinimalDaysInFirstWeek(7); @@ -945,7 +947,7 @@ public static TruffleString format(String format, int[] date, TruffleString.From case 'Z': // timezone name if (cal == null) { - cal = getCalendar(date); + cal = getCalendar(date, timeZone); } // If items[8] == 1, we're in daylight savings time. // -1 means the information was not available; treat this as if not in dst. @@ -964,6 +966,16 @@ public static TruffleString format(String format, int[] date, TruffleString.From return fromJavaStringNode.execute(s, TS_ENCODING); } + @TruffleBoundary + public static TruffleString format(String format, int[] date, TruffleString.FromJavaStringNode fromJavaStringNode) { + return format(format, date, TimeZone.getDefault(), fromJavaStringNode); + } + + @TruffleBoundary + private static TimeZone getTimeZone(ZoneId currentZoneId) { + return TimeZone.getTimeZone(currentZoneId); + } + @Specialization static TruffleString formatTime(PythonModule module, TruffleString format, @SuppressWarnings("unused") PNone time, @Bind Node inliningTarget, @@ -972,11 +984,11 @@ static TruffleString formatTime(PythonModule module, TruffleString format, @Supp @Shared("js2ts") @Cached TruffleString.FromJavaStringNode fromJavaStringNode, @Exclusive @Cached PRaiseNode raiseNode) { ModuleState moduleState = module.getModuleState(ModuleState.class); - return format(toJavaStringNode.execute(format), getIntLocalTimeStruct(moduleState.currentZoneId, (long) timeSeconds()), fromJavaStringNode); + return format(toJavaStringNode.execute(format), getIntLocalTimeStruct(moduleState.currentZoneId, (long) timeSeconds()), getTimeZone(moduleState.currentZoneId), fromJavaStringNode); } @Specialization - static TruffleString formatTime(VirtualFrame frame, @SuppressWarnings("unused") PythonModule module, TruffleString format, PTuple time, + static TruffleString formatTime(VirtualFrame frame, PythonModule module, TruffleString format, PTuple time, @Bind Node inliningTarget, @Cached SequenceStorageNodes.GetInternalObjectArrayNode getArray, @Cached PyNumberAsSizeNode asSizeNode, @@ -985,7 +997,7 @@ static TruffleString formatTime(VirtualFrame frame, @SuppressWarnings("unused") @Shared("js2ts") @Cached TruffleString.FromJavaStringNode fromJavaStringNode, @Exclusive @Cached PRaiseNode raiseNode) { int[] date = checkStructtime(frame, inliningTarget, time, getArray, asSizeNode, raiseNode); - return format(toJavaStringNode.execute(format), date, fromJavaStringNode); + return format(toJavaStringNode.execute(format), date, getTimeZone(module.getModuleState(ModuleState.class).currentZoneId), fromJavaStringNode); } @Specialization