Skip to content

Commit ba0946d

Browse files
committed
One more fix around tzinfo and foreign objects
1 parent a230299 commit ba0946d

3 files changed

Lines changed: 72 additions & 9 deletions

File tree

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ def test_foreign_datetime_behavior(self):
272272
import java
273273

274274
LocalDateTime = java.type("java.time.LocalDateTime")
275+
ZonedDateTime = java.type("java.time.ZonedDateTime")
276+
ZoneId = java.type("java.time.ZoneId")
275277

276278
dt = LocalDateTime.of(2025, 3, 23, 7, 8, 9)
277279
self.assertEqual(dt.year, 2025)
@@ -300,11 +302,20 @@ def test_foreign_datetime_behavior(self):
300302
self.assertIsNone(dt.dst())
301303
self.assertIsNone(dt.tzname())
302304

305+
berlin = ZoneId.of("Europe/Berlin")
306+
zoned_dt = ZonedDateTime.of(2025, 3, 23, 7, 8, 9, 0, berlin)
307+
self.assertIsInstance(zoned_dt.tzinfo, datetime.tzinfo)
308+
self.assertEqual(zoned_dt.utcoffset(), datetime.timedelta(hours=1))
309+
self.assertEqual(zoned_dt.dst(), datetime.timedelta())
310+
self.assertEqual(zoned_dt.tzname(), "CET")
311+
self.assertEqual(zoned_dt.isoformat(), "2025-03-23T07:08:09+01:00")
312+
303313
def test_foreign_timezone_behavior(self):
304314
import datetime
305315
import java
306316

307317
ZoneId = java.type("java.time.ZoneId")
318+
ZonedDateTime = java.type("java.time.ZonedDateTime")
308319

309320
utc = ZoneId.of("UTC")
310321
self.assertIsInstance(utc, datetime.tzinfo)
@@ -333,6 +344,15 @@ def test_foreign_timezone_behavior(self):
333344
self.assertEqual(berlin.fromutc(datetime.datetime(2025, 3, 23, 6, 8, 9, tzinfo=berlin)),
334345
datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin))
335346

347+
foreign_aware = ZonedDateTime.of(2025, 3, 23, 6, 8, 9, 0, berlin)
348+
self.assertEqual(berlin.fromutc(foreign_aware),
349+
datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin))
350+
351+
overlap = berlin.fromutc(datetime.datetime(2025, 10, 26, 1, 30, tzinfo=berlin))
352+
self.assertEqual(overlap, datetime.datetime(2025, 10, 26, 2, 30, tzinfo=berlin, fold=1))
353+
self.assertEqual(overlap.fold, 1)
354+
self.assertEqual(overlap.utcoffset(), datetime.timedelta(hours=1))
355+
336356
def test_read(self):
337357
o = CustomObject()
338358
assert polyglot.__read__(o, "field") == o.field

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,11 @@ public static Object toPythonTzInfo(Object tzInfo, ZoneId zoneId, Node inliningT
232232
if (zoneId == null) {
233233
return null;
234234
}
235-
return zoneId;
235+
Object fixedOffsetTimeZone = toFixedOffsetTimeZone(zoneId, inliningTarget);
236+
if (fixedOffsetTimeZone != null) {
237+
return fixedOffsetTimeZone;
238+
}
239+
return PythonContext.get(inliningTarget).getEnv().asGuestValue(zoneId);
236240
}
237241

238242
public static Object toFixedOffsetTimeZone(ZoneId zoneId, Node inliningTarget) {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/foreign/ForeignTimeZoneBuiltins.java

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545

4646
import java.time.LocalDateTime;
4747
import java.time.ZoneId;
48+
import java.time.ZoneOffset;
4849
import java.time.ZonedDateTime;
4950
import java.time.format.DateTimeFormatter;
5051
import java.util.List;
@@ -133,8 +134,7 @@ static Object utcoffset(Object self, Object dateTime,
133134
if (!PyDateTimeCheckNode.executeUncached(dateTime)) {
134135
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.FROMUTC_ARGUMENT_MUST_BE_A_DATETIME);
135136
}
136-
LocalDateTime localDateTime = TemporalValueNodes.GetDateTimeValue.executeUncached(inliningTarget, dateTime).toLocalDateTime();
137-
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
137+
ZonedDateTime zonedDateTime = resolveDateTimeAtZone(TemporalValueNodes.GetDateTimeValue.executeUncached(inliningTarget, dateTime), zoneId);
138138
return TimeDeltaNodes.NewNode.getUncached().execute(inliningTarget, PythonBuiltinClassType.PTimeDelta, 0, zonedDateTime.getOffset().getTotalSeconds(), 0, 0, 0, 0, 0);
139139
}
140140
}
@@ -153,8 +153,7 @@ static Object dst(Object self, Object dateTime,
153153
if (!PyDateTimeCheckNode.executeUncached(dateTime)) {
154154
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.FROMUTC_ARGUMENT_MUST_BE_A_DATETIME);
155155
}
156-
LocalDateTime localDateTime = TemporalValueNodes.GetDateTimeValue.executeUncached(inliningTarget, dateTime).toLocalDateTime();
157-
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
156+
ZonedDateTime zonedDateTime = resolveDateTimeAtZone(TemporalValueNodes.GetDateTimeValue.executeUncached(inliningTarget, dateTime), zoneId);
158157
int dstSeconds = (int) zoneId.getRules().getDaylightSavings(zonedDateTime.toInstant()).getSeconds();
159158
return TimeDeltaNodes.NewNode.getUncached().execute(inliningTarget, PythonBuiltinClassType.PTimeDelta, 0, dstSeconds, 0, 0, 0, 0, 0);
160159
}
@@ -174,8 +173,7 @@ static Object tzname(Object self, Object dateTime,
174173
if (!PyDateTimeCheckNode.executeUncached(dateTime)) {
175174
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.FROMUTC_ARGUMENT_MUST_BE_A_DATETIME);
176175
}
177-
LocalDateTime localDateTime = TemporalValueNodes.GetDateTimeValue.executeUncached(inliningTarget, dateTime).toLocalDateTime();
178-
String name = DateTimeFormatter.ofPattern("z", Locale.ENGLISH).format(localDateTime.atZone(zoneId));
176+
String name = DateTimeFormatter.ofPattern("z", Locale.ENGLISH).format(resolveDateTimeAtZone(TemporalValueNodes.GetDateTimeValue.executeUncached(inliningTarget, dateTime), zoneId));
179177
return toTruffleStringUncached(name);
180178
}
181179
}
@@ -191,14 +189,15 @@ static Object fromutc(Object self, Object dateTime,
191189
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.FROMUTC_ARGUMENT_MUST_BE_A_DATETIME);
192190
}
193191
DateTimeValue asDateTime = TemporalValueNodes.GetDateTimeValue.executeUncached(inliningTarget, dateTime);
194-
if (asDateTime.tzInfo != self) {
192+
if (!hasMatchingTimeZone(asDateTime, self)) {
195193
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.ValueError, ErrorMessages.FROMUTC_DT_TZINFO_IS_NOT_SELF);
196194
}
197195
ZoneId zoneId = asZoneId(self);
198196
LocalDateTime utcDateTime = asDateTime.toLocalDateTime();
199197
ZonedDateTime zonedDateTime = utcDateTime.atOffset(java.time.ZoneOffset.UTC).atZoneSameInstant(zoneId);
198+
int fold = getFold(zonedDateTime);
200199
return DateTimeNodes.NewUnsafeNode.getUncached().execute(inliningTarget, PythonBuiltinClassType.PDateTime, zonedDateTime.getYear(), zonedDateTime.getMonthValue(),
201-
zonedDateTime.getDayOfMonth(), zonedDateTime.getHour(), zonedDateTime.getMinute(), zonedDateTime.getSecond(), zonedDateTime.getNano() / 1_000, self, 0);
200+
zonedDateTime.getDayOfMonth(), zonedDateTime.getHour(), zonedDateTime.getMinute(), zonedDateTime.getSecond(), zonedDateTime.getNano() / 1_000, self, fold);
202201
}
203202
}
204203

@@ -219,4 +218,44 @@ private static ZoneId asZoneId(Object self) {
219218
throw CompilerDirectives.shouldNotReachHere(e);
220219
}
221220
}
221+
222+
private static boolean hasMatchingTimeZone(DateTimeValue dateTime, Object self) {
223+
if (dateTime.tzInfo == self) {
224+
return true;
225+
}
226+
if (dateTime.zoneId != null) {
227+
return asZoneId(self).equals(dateTime.zoneId);
228+
}
229+
if (dateTime.tzInfo == null) {
230+
return false;
231+
}
232+
InteropLibrary interop = InteropLibrary.getUncached(dateTime.tzInfo);
233+
if (!interop.isTimeZone(dateTime.tzInfo)) {
234+
return false;
235+
}
236+
try {
237+
return asZoneId(self).equals(interop.asTimeZone(dateTime.tzInfo));
238+
} catch (UnsupportedMessageException e) {
239+
throw CompilerDirectives.shouldNotReachHere(e);
240+
}
241+
}
242+
243+
private static int getFold(ZonedDateTime zonedDateTime) {
244+
List<ZoneOffset> validOffsets = zonedDateTime.getZone().getRules().getValidOffsets(zonedDateTime.toLocalDateTime());
245+
if (validOffsets.size() < 2) {
246+
return 0;
247+
}
248+
return zonedDateTime.getOffset().equals(validOffsets.get(validOffsets.size() - 1)) ? 1 : 0;
249+
}
250+
251+
@TruffleBoundary
252+
private static ZonedDateTime resolveDateTimeAtZone(DateTimeValue dateTime, ZoneId zoneId) {
253+
LocalDateTime localDateTime = dateTime.toLocalDateTime();
254+
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
255+
List<ZoneOffset> validOffsets = zoneId.getRules().getValidOffsets(localDateTime);
256+
if (validOffsets.size() < 2) {
257+
return zonedDateTime;
258+
}
259+
return dateTime.fold == 1 ? zonedDateTime.withLaterOffsetAtOverlap() : zonedDateTime.withEarlierOffsetAtOverlap();
260+
}
222261
}

0 commit comments

Comments
 (0)