Skip to content

Commit a8e8764

Browse files
committed
Support foreign timezone objects
1 parent 6fbab8a commit a8e8764

File tree

6 files changed

+289
-1
lines changed

6 files changed

+289
-1
lines changed

graalpython/com.oracle.graal.python.test/src/tests/test_interop.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,39 @@ def test_foreign_datetime_behavior(self):
300300
self.assertIsNone(dt.dst())
301301
self.assertIsNone(dt.tzname())
302302

303+
def test_foreign_timezone_behavior(self):
304+
import datetime
305+
import java
306+
307+
ZoneId = java.type("java.time.ZoneId")
308+
309+
utc = ZoneId.of("UTC")
310+
self.assertIsInstance(utc, datetime.tzinfo)
311+
self.assertEqual(str(utc), "UTC")
312+
self.assertEqual(utc.tzname(None), "UTC")
313+
self.assertEqual(utc.utcoffset(None), datetime.timedelta())
314+
self.assertIsNone(utc.dst(None))
315+
316+
aware = datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=utc)
317+
self.assertIs(aware.tzinfo, utc)
318+
self.assertEqual(aware.utcoffset(), datetime.timedelta())
319+
self.assertEqual(aware.tzname(), "UTC")
320+
self.assertEqual(aware.isoformat(), "2025-03-23T07:08:09+00:00")
321+
322+
berlin = ZoneId.of("Europe/Berlin")
323+
self.assertIsInstance(berlin, datetime.tzinfo)
324+
self.assertIsNone(berlin.utcoffset(None))
325+
self.assertIsNone(berlin.dst(None))
326+
self.assertIsNone(berlin.tzname(None))
327+
328+
local = datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin)
329+
self.assertIs(local.tzinfo, berlin)
330+
self.assertEqual(local.utcoffset(), datetime.timedelta(hours=1))
331+
self.assertEqual(local.dst(), datetime.timedelta())
332+
self.assertEqual(local.tzname(), "CET")
333+
self.assertEqual(berlin.fromutc(datetime.datetime(2025, 3, 23, 6, 8, 9, tzinfo=berlin)),
334+
datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin))
335+
303336
def test_read(self):
304337
o = CustomObject()
305338
assert polyglot.__read__(o, "field") == o.field

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/Python3Core.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@
271271
import com.oracle.graal.python.builtins.objects.foreign.ForeignNumberBuiltins;
272272
import com.oracle.graal.python.builtins.objects.foreign.ForeignObjectBuiltins;
273273
import com.oracle.graal.python.builtins.objects.foreign.ForeignTimeBuiltins;
274+
import com.oracle.graal.python.builtins.objects.foreign.ForeignTimeZoneBuiltins;
274275
import com.oracle.graal.python.builtins.objects.frame.FrameBuiltins;
275276
import com.oracle.graal.python.builtins.objects.function.AbstractFunctionBuiltins;
276277
import com.oracle.graal.python.builtins.objects.function.BuiltinFunctionBuiltins;
@@ -506,6 +507,7 @@ private static PythonBuiltins[] initializeBuiltins(TruffleLanguage.Env env) {
506507
new ForeignDateBuiltins(),
507508
new ForeignDateTimeBuiltins(),
508509
new ForeignTimeBuiltins(),
510+
new ForeignTimeZoneBuiltins(),
509511
new ForeignAbstractClassBuiltins(),
510512
new ForeignExecutableBuiltins(),
511513
new ForeignInstantiableBuiltins(),

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
import com.oracle.graal.python.builtins.objects.foreign.ForeignNumberBuiltins;
190190
import com.oracle.graal.python.builtins.objects.foreign.ForeignObjectBuiltins;
191191
import com.oracle.graal.python.builtins.objects.foreign.ForeignTimeBuiltins;
192+
import com.oracle.graal.python.builtins.objects.foreign.ForeignTimeZoneBuiltins;
192193
import com.oracle.graal.python.builtins.objects.frame.FrameBuiltins;
193194
import com.oracle.graal.python.builtins.objects.function.AbstractFunctionBuiltins;
194195
import com.oracle.graal.python.builtins.objects.function.FunctionBuiltins;
@@ -848,7 +849,7 @@ It can be called either on the class (e.g. C.f()) or on an instance
848849
ForeignDate("ForeignDate", PDate, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation().slots(ForeignDateBuiltins.SLOTS)),
849850
ForeignTime("ForeignTime", PTime, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation().slots(ForeignTimeBuiltins.SLOTS)),
850851
ForeignDateTime("ForeignDateTime", PDateTime, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation().slots(ForeignDateTimeBuiltins.SLOTS)),
851-
ForeignTimeZone("ForeignTimeZone", PTzInfo, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation()),
852+
ForeignTimeZone("ForeignTimeZone", PTzInfo, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation().slots(ForeignTimeZoneBuiltins.SLOTS)),
852853

853854
// bz2
854855
BZ2Compressor("BZ2Compressor", PythonObject, newBuilder().publishInModule("_bz2").basetype().slots(BZ2CompressorBuiltins.SLOTS)),

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/datetime/TemporalNodes.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ public static Object toPythonTzInfo(Object tzInfo, ZoneId zoneId, Node inliningT
160160
if (tzInfo != null) {
161161
return tzInfo;
162162
}
163+
if (zoneId == null) {
164+
return null;
165+
}
166+
return zoneId;
167+
}
168+
169+
public static Object toFixedOffsetTimeZone(ZoneId zoneId, Node inliningTarget) {
163170
if (zoneId == null) {
164171
return null;
165172
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/datetime/TzInfoNodes.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,15 @@
4343
import com.oracle.graal.python.builtins.PythonBuiltinClassType;
4444
import com.oracle.graal.python.builtins.objects.cext.PythonAbstractNativeObject;
4545
import com.oracle.graal.python.nodes.object.BuiltinClassProfiles;
46+
import com.oracle.graal.python.nodes.object.IsForeignObjectNode;
4647
import com.oracle.truffle.api.dsl.Cached;
4748
import com.oracle.truffle.api.dsl.Fallback;
4849
import com.oracle.truffle.api.dsl.GenerateCached;
4950
import com.oracle.truffle.api.dsl.GenerateInline;
5051
import com.oracle.truffle.api.dsl.GenerateUncached;
5152
import com.oracle.truffle.api.dsl.Specialization;
53+
import com.oracle.truffle.api.interop.InteropLibrary;
54+
import com.oracle.truffle.api.library.CachedLibrary;
5255
import com.oracle.truffle.api.nodes.Node;
5356

5457
public class TzInfoNodes {
@@ -73,6 +76,13 @@ static boolean doNative(Node inliningTarget, PythonAbstractNativeObject value,
7376
return profile.profileObject(inliningTarget, value, PythonBuiltinClassType.PTzInfo);
7477
}
7578

79+
@Specialization(guards = "isForeignObjectNode.execute(inliningTarget, value)", limit = "1")
80+
static boolean doForeign(Node inliningTarget, Object value,
81+
@Cached IsForeignObjectNode isForeignObjectNode,
82+
@CachedLibrary("value") InteropLibrary interop) {
83+
return interop.isTimeZone(value);
84+
}
85+
7686
@Fallback
7787
static boolean doOther(@SuppressWarnings("unused") Object value) {
7888
return false;
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
package com.oracle.graal.python.builtins.objects.foreign;
42+
43+
import static com.oracle.graal.python.nodes.SpecialMethodNames.J___REDUCE__;
44+
import static com.oracle.graal.python.util.PythonUtils.TS_ENCODING;
45+
46+
import java.time.LocalDateTime;
47+
import java.time.ZoneId;
48+
import java.time.ZonedDateTime;
49+
import java.time.format.DateTimeFormatter;
50+
import java.util.List;
51+
import java.util.Locale;
52+
53+
import com.oracle.graal.python.annotations.Builtin;
54+
import com.oracle.graal.python.annotations.Slot;
55+
import com.oracle.graal.python.annotations.Slot.SlotKind;
56+
import com.oracle.graal.python.builtins.CoreFunctions;
57+
import com.oracle.graal.python.builtins.PythonBuiltinClassType;
58+
import com.oracle.graal.python.builtins.PythonBuiltins;
59+
import com.oracle.graal.python.builtins.modules.datetime.DateTimeNodes;
60+
import com.oracle.graal.python.builtins.modules.datetime.DatetimeModuleBuiltins;
61+
import com.oracle.graal.python.builtins.modules.datetime.PDateTime;
62+
import com.oracle.graal.python.builtins.modules.datetime.TemporalNodes;
63+
import com.oracle.graal.python.builtins.modules.datetime.TimeDeltaNodes;
64+
import com.oracle.graal.python.builtins.objects.PNone;
65+
import com.oracle.graal.python.builtins.objects.type.TpSlots;
66+
import com.oracle.graal.python.nodes.ErrorMessages;
67+
import com.oracle.graal.python.nodes.PRaiseNode;
68+
import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode;
69+
import com.oracle.graal.python.nodes.function.builtins.PythonBinaryBuiltinNode;
70+
import com.oracle.graal.python.nodes.function.builtins.PythonUnaryBuiltinNode;
71+
import com.oracle.graal.python.nodes.object.GetClassNode;
72+
import com.oracle.truffle.api.CompilerDirectives;
73+
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
74+
import com.oracle.truffle.api.dsl.Bind;
75+
import com.oracle.truffle.api.dsl.Cached;
76+
import com.oracle.truffle.api.dsl.GenerateNodeFactory;
77+
import com.oracle.truffle.api.dsl.NodeFactory;
78+
import com.oracle.truffle.api.dsl.Specialization;
79+
import com.oracle.truffle.api.interop.InteropLibrary;
80+
import com.oracle.truffle.api.interop.UnsupportedMessageException;
81+
import com.oracle.truffle.api.library.CachedLibrary;
82+
import com.oracle.truffle.api.nodes.Node;
83+
import com.oracle.truffle.api.strings.TruffleString;
84+
85+
@CoreFunctions(extendClasses = PythonBuiltinClassType.ForeignTimeZone)
86+
public final class ForeignTimeZoneBuiltins extends PythonBuiltins {
87+
public static final TpSlots SLOTS = ForeignTimeZoneBuiltinsSlotsGen.SLOTS;
88+
89+
@Override
90+
protected List<? extends NodeFactory<? extends PythonBuiltinBaseNode>> getNodeFactories() {
91+
return ForeignTimeZoneBuiltinsFactory.getFactories();
92+
}
93+
94+
@Slot(value = SlotKind.tp_repr, isComplex = true)
95+
@GenerateNodeFactory
96+
abstract static class ReprNode extends PythonUnaryBuiltinNode {
97+
@Specialization(limit = "1")
98+
@TruffleBoundary
99+
static TruffleString repr(Object self,
100+
@Bind Node inliningTarget,
101+
@CachedLibrary("self") InteropLibrary interop) {
102+
ZoneId zoneId = asZoneId(self, interop);
103+
TruffleString typeName = com.oracle.graal.python.builtins.objects.type.TypeNodes.GetTpNameNode.executeUncached(GetClassNode.executeUncached(self));
104+
String value = String.format("%s('%s')", typeName, zoneId.getId());
105+
return TruffleString.FromJavaStringNode.getUncached().execute(value, TS_ENCODING);
106+
}
107+
}
108+
109+
@Slot(value = SlotKind.tp_str, isComplex = true)
110+
@GenerateNodeFactory
111+
abstract static class StrNode extends PythonUnaryBuiltinNode {
112+
@Specialization(limit = "1")
113+
static TruffleString str(Object self,
114+
@CachedLibrary("self") InteropLibrary interop) {
115+
return TruffleString.FromJavaStringNode.getUncached().execute(asZoneId(self, interop).getId(), TS_ENCODING);
116+
}
117+
}
118+
119+
@Builtin(name = "utcoffset", minNumOfPositionalArgs = 1, parameterNames = {"$self", "dt"})
120+
@GenerateNodeFactory
121+
abstract static class UtcOffsetNode extends PythonBinaryBuiltinNode {
122+
@Specialization(limit = "1")
123+
@TruffleBoundary
124+
static Object utcoffset(Object self, Object dateTime,
125+
@Bind Node inliningTarget,
126+
@CachedLibrary("self") InteropLibrary interop,
127+
@Cached TemporalNodes.DateTimeLikeCheckNode dateTimeLikeCheckNode,
128+
@Cached TemporalNodes.ReadDateTimeValueNode readDateTimeValueNode) {
129+
ZoneId zoneId = asZoneId(self, interop);
130+
if (dateTime == PNone.NONE) {
131+
Object fixed = TemporalNodes.toFixedOffsetTimeZone(zoneId, inliningTarget);
132+
if (fixed == null) {
133+
return PNone.NONE;
134+
}
135+
return DatetimeModuleBuiltins.callUtcOffset(fixed, PNone.NONE, inliningTarget);
136+
}
137+
if (!dateTimeLikeCheckNode.execute(inliningTarget, dateTime)) {
138+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.FROMUTC_ARGUMENT_MUST_BE_A_DATETIME);
139+
}
140+
LocalDateTime localDateTime = readDateTimeValueNode.execute(inliningTarget, dateTime).toLocalDateTime();
141+
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
142+
return TimeDeltaNodes.NewNode.getUncached().execute(inliningTarget, PythonBuiltinClassType.PTimeDelta, 0, zonedDateTime.getOffset().getTotalSeconds(), 0, 0, 0, 0, 0);
143+
}
144+
}
145+
146+
@Builtin(name = "dst", minNumOfPositionalArgs = 1, parameterNames = {"$self", "dt"})
147+
@GenerateNodeFactory
148+
abstract static class DstNode extends PythonBinaryBuiltinNode {
149+
@Specialization(limit = "1")
150+
@TruffleBoundary
151+
static Object dst(Object self, Object dateTime,
152+
@Bind Node inliningTarget,
153+
@CachedLibrary("self") InteropLibrary interop,
154+
@Cached TemporalNodes.DateTimeLikeCheckNode dateTimeLikeCheckNode,
155+
@Cached TemporalNodes.ReadDateTimeValueNode readDateTimeValueNode) {
156+
ZoneId zoneId = asZoneId(self, interop);
157+
if (dateTime == PNone.NONE) {
158+
return PNone.NONE;
159+
}
160+
if (!dateTimeLikeCheckNode.execute(inliningTarget, dateTime)) {
161+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.FROMUTC_ARGUMENT_MUST_BE_A_DATETIME);
162+
}
163+
LocalDateTime localDateTime = readDateTimeValueNode.execute(inliningTarget, dateTime).toLocalDateTime();
164+
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
165+
int dstSeconds = (int) zoneId.getRules().getDaylightSavings(zonedDateTime.toInstant()).getSeconds();
166+
return TimeDeltaNodes.NewNode.getUncached().execute(inliningTarget, PythonBuiltinClassType.PTimeDelta, 0, dstSeconds, 0, 0, 0, 0, 0);
167+
}
168+
}
169+
170+
@Builtin(name = "tzname", minNumOfPositionalArgs = 1, parameterNames = {"$self", "dt"})
171+
@GenerateNodeFactory
172+
abstract static class TzNameNode extends PythonBinaryBuiltinNode {
173+
@Specialization(limit = "1")
174+
@TruffleBoundary
175+
static Object tzname(Object self, Object dateTime,
176+
@Bind Node inliningTarget,
177+
@CachedLibrary("self") InteropLibrary interop,
178+
@Cached TemporalNodes.DateTimeLikeCheckNode dateTimeLikeCheckNode,
179+
@Cached TemporalNodes.ReadDateTimeValueNode readDateTimeValueNode) {
180+
ZoneId zoneId = asZoneId(self, interop);
181+
if (dateTime == PNone.NONE) {
182+
return zoneId.getRules().isFixedOffset() ? TruffleString.FromJavaStringNode.getUncached().execute(zoneId.getId(), TS_ENCODING) : PNone.NONE;
183+
}
184+
if (!dateTimeLikeCheckNode.execute(inliningTarget, dateTime)) {
185+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.FROMUTC_ARGUMENT_MUST_BE_A_DATETIME);
186+
}
187+
LocalDateTime localDateTime = readDateTimeValueNode.execute(inliningTarget, dateTime).toLocalDateTime();
188+
String name = DateTimeFormatter.ofPattern("z", Locale.ENGLISH).format(localDateTime.atZone(zoneId));
189+
return TruffleString.FromJavaStringNode.getUncached().execute(name, TS_ENCODING);
190+
}
191+
}
192+
193+
@Builtin(name = "fromutc", minNumOfPositionalArgs = 1, parameterNames = {"$self", "dt"})
194+
@GenerateNodeFactory
195+
abstract static class FromUtcNode extends PythonBinaryBuiltinNode {
196+
@Specialization(limit = "1")
197+
@TruffleBoundary
198+
static Object fromutc(Object self, Object dateTime,
199+
@Bind Node inliningTarget,
200+
@CachedLibrary("self") InteropLibrary interop,
201+
@Cached TemporalNodes.DateTimeLikeCheckNode dateTimeLikeCheckNode,
202+
@Cached TemporalNodes.ReadDateTimeValueNode readDateTimeValueNode) {
203+
if (!dateTimeLikeCheckNode.execute(inliningTarget, dateTime)) {
204+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.FROMUTC_ARGUMENT_MUST_BE_A_DATETIME);
205+
}
206+
PDateTime asDateTime = (PDateTime) DateTimeNodes.AsManagedDateTimeNode.executeUncached(dateTime);
207+
if (asDateTime.tzInfo != self) {
208+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.ValueError, ErrorMessages.FROMUTC_DT_TZINFO_IS_NOT_SELF);
209+
}
210+
ZoneId zoneId = asZoneId(self, interop);
211+
LocalDateTime utcDateTime = readDateTimeValueNode.execute(inliningTarget, dateTime).toLocalDateTime();
212+
ZonedDateTime zonedDateTime = utcDateTime.atOffset(java.time.ZoneOffset.UTC).atZoneSameInstant(zoneId);
213+
return DateTimeNodes.NewUnsafeNode.getUncached().execute(inliningTarget, PythonBuiltinClassType.PDateTime, zonedDateTime.getYear(), zonedDateTime.getMonthValue(),
214+
zonedDateTime.getDayOfMonth(), zonedDateTime.getHour(), zonedDateTime.getMinute(), zonedDateTime.getSecond(), zonedDateTime.getNano() / 1_000, self, 0);
215+
}
216+
}
217+
218+
@Builtin(name = J___REDUCE__, minNumOfPositionalArgs = 1)
219+
@GenerateNodeFactory
220+
abstract static class ReduceNode extends PythonUnaryBuiltinNode {
221+
@Specialization(limit = "1")
222+
static Object reduce(Object self,
223+
@CachedLibrary("self") InteropLibrary interop) {
224+
return TruffleString.FromJavaStringNode.getUncached().execute(asZoneId(self, interop).getId(), TS_ENCODING);
225+
}
226+
}
227+
228+
private static ZoneId asZoneId(Object self, InteropLibrary interop) {
229+
try {
230+
return interop.asTimeZone(self);
231+
} catch (UnsupportedMessageException e) {
232+
throw CompilerDirectives.shouldNotReachHere(e);
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)