Skip to content

Commit 33f5e2a

Browse files
committed
[GR-62450] Support foreign temporal traits and methods
PullRequest: graalpython/4346
2 parents ba2aa32 + 857957c commit 33f5e2a

26 files changed

Lines changed: 1861 additions & 650 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ language runtime. The main focus is on user-observable behavior of the engine.
1717
* Added support for specifying generics on foreign classes, and inheriting from such classes. Especially when using Java classes that support generics, this allows expressing the generic types in Python type annotations as well.
1818
* Added a new `java` backend for the `pyexpat` module that uses a Java XML parser instead of the native `expat` library. It can be useful when running without native access or multiple-context scenarios. This backend is the default when embedding and can be switched back to native `expat` by setting `python.PyExpatModuleBackend` option to `native`. Standalone distribution still defaults to native expat backend.
1919
* Add a new context option `python.UnicodeCharacterDatabaseNativeFallback` to control whether the ICU database may fall back to the native unicode character database from CPython for features and characters not supported by ICU. This requires native access to be enabled and is disabled by default for embeddings.
20+
* Foreign temporal objects (dates, times, and timezones) are now given a Python class corresponding to their interop traits, i.e., `date`, `time`, `datetime`, or `tzinfo`. This allows any foreign objects with these traits to be used in place of the native Python types and Python methods available on these types work on the foreign types.
2021

2122
## Version 25.0.1
2223
* Allow users to keep going on unsupported JDK/OS/ARCH combinations at their own risk by opting out of early failure using `-Dtruffle.UseFallbackRuntime=true`, `-Dpolyglot.engine.userResourceCache=/set/to/a/writeable/dir`, `-Dpolyglot.engine.allowUnsupportedPlatform=true`, and `-Dpolyglot.python.UnsupportedPlatformEmulates=[linux|macos|windows]` and `-Dorg.graalvm.python.resources.exclude=native.files`.

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

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ def test_single_trait_classes(self):
114114
polyglot.ForeignObject,
115115
polyglot.ForeignList,
116116
polyglot.ForeignBoolean,
117+
polyglot.ForeignDate,
118+
polyglot.ForeignDateTime,
117119
polyglot.ForeignException,
118120
polyglot.ForeignExecutable,
119121
polyglot.ForeignDict,
@@ -124,14 +126,25 @@ def test_single_trait_classes(self):
124126
polyglot.ForeignNone,
125127
polyglot.ForeignNumber,
126128
polyglot.ForeignString,
129+
polyglot.ForeignTime,
130+
polyglot.ForeignTimeZone,
127131
]
128132

129133
for c in classes:
130134
self.assertIsInstance(c, type)
131135
if c is polyglot.ForeignBoolean:
132136
self.assertIs(c.__base__, polyglot.ForeignNumber)
133137
elif c is not polyglot.ForeignObject:
134-
self.assertIs(c.__base__, polyglot.ForeignObject)
138+
if c is polyglot.ForeignDate:
139+
self.assertIs(c.__base__, __import__("datetime").date)
140+
elif c is polyglot.ForeignTime:
141+
self.assertIs(c.__base__, __import__("datetime").time)
142+
elif c is polyglot.ForeignDateTime:
143+
self.assertIs(c.__base__, __import__("datetime").datetime)
144+
elif c is polyglot.ForeignTimeZone:
145+
self.assertIs(c.__base__, __import__("datetime").tzinfo)
146+
else:
147+
self.assertIs(c.__base__, polyglot.ForeignObject)
135148

136149
def test_get_class(self):
137150
def wrap(obj):
@@ -155,6 +168,7 @@ def t(obj):
155168
self.assertEqual(t("abc"), polyglot.ForeignString)
156169

157170
from java.lang import Object, Boolean, Integer, Throwable, Thread, Number, String
171+
from java.time import LocalDate, LocalDateTime, LocalTime, ZoneId
158172
from java.util import ArrayList, HashMap, ArrayDeque
159173
from java.math import BigInteger
160174
null = Integer.getInteger("something_that_does_not_exists")
@@ -172,6 +186,19 @@ def t(obj):
172186
self.assertEqual(type(null), polyglot.ForeignNone)
173187
self.assertEqual(type(BigInteger.valueOf(42)), polyglot.ForeignNumber)
174188
self.assertEqual(type(wrap(String("abc"))), polyglot.ForeignString)
189+
local_date = LocalDate.of(2025, 3, 23)
190+
self.assertIsInstance(local_date, polyglot.ForeignDate)
191+
self.assertIsInstance(local_date, __import__("datetime").date)
192+
193+
local_time = LocalTime.of(7, 8, 9)
194+
self.assertIsInstance(local_time, polyglot.ForeignTime)
195+
self.assertIsInstance(local_time, __import__("datetime").time)
196+
197+
local_date_time = LocalDateTime.of(2025, 3, 23, 7, 8, 9)
198+
self.assertIsInstance(local_date_time, polyglot.ForeignDateTime)
199+
self.assertIsInstance(local_date_time, __import__("datetime").datetime)
200+
201+
self.assertEqual(type(ZoneId.of("UTC")), polyglot.ForeignTimeZone)
175202

176203
def test_import(self):
177204
def some_function():
@@ -186,6 +213,146 @@ def some_function():
186213
assert imported_fun1 is some_function
187214
assert imported_fun1() == "hello, polyglot world!"
188215

216+
def test_foreign_date_behavior(self):
217+
import datetime
218+
import java
219+
220+
LocalDate = java.type("java.time.LocalDate")
221+
222+
d = LocalDate.of(2025, 3, 23)
223+
self.assertEqual(d.year, 2025)
224+
self.assertEqual(d.month, 3)
225+
self.assertEqual(d.day, 23)
226+
self.assertEqual(str(d), "2025-03-23")
227+
self.assertEqual(d.isoformat(), "2025-03-23")
228+
self.assertEqual(d.ctime(), datetime.date(2025, 3, 23).ctime())
229+
self.assertEqual(d.strftime("%Y-%m-%d"), "2025-03-23")
230+
self.assertEqual(format(d, "%Y-%m-%d"), "2025-03-23")
231+
self.assertEqual(d.toordinal(), datetime.date(2025, 3, 23).toordinal())
232+
self.assertEqual(d.weekday(), datetime.date(2025, 3, 23).weekday())
233+
self.assertEqual(d.isoweekday(), datetime.date(2025, 3, 23).isoweekday())
234+
self.assertEqual(d.isocalendar(), datetime.date(2025, 3, 23).isocalendar())
235+
self.assertEqual(d.timetuple(), datetime.date(2025, 3, 23).timetuple())
236+
self.assertEqual(hash(d), hash(datetime.date(2025, 3, 23)))
237+
self.assertEqual(d, datetime.date(2025, 3, 23))
238+
self.assertEqual(d, LocalDate.of(2025, 3, 23))
239+
self.assertEqual(d.replace(day=24), datetime.date(2025, 3, 24))
240+
self.assertEqual(d + datetime.timedelta(days=1), datetime.date(2025, 3, 24))
241+
self.assertEqual(d - datetime.timedelta(days=1), datetime.date(2025, 3, 22))
242+
self.assertEqual(d - datetime.date(2025, 3, 20), datetime.timedelta(days=3))
243+
self.assertEqual(d - LocalDate.of(2025, 3, 20), datetime.timedelta(days=3))
244+
245+
def test_foreign_time_behavior(self):
246+
import datetime
247+
import java
248+
249+
LocalTime = java.type("java.time.LocalTime")
250+
251+
t = LocalTime.of(7, 8, 9)
252+
self.assertEqual(t.hour, 7)
253+
self.assertEqual(t.minute, 8)
254+
self.assertEqual(t.second, 9)
255+
self.assertEqual(t.microsecond, 0)
256+
self.assertEqual(str(t), "07:08:09")
257+
self.assertEqual(t.isoformat(), "07:08:09")
258+
self.assertEqual(t.strftime("%H:%M:%S"), "07:08:09")
259+
self.assertEqual(format(t, "%H:%M:%S"), "07:08:09")
260+
self.assertEqual(hash(t), hash(datetime.time(7, 8, 9)))
261+
self.assertEqual(t, datetime.time(7, 8, 9))
262+
self.assertEqual(t, LocalTime.of(7, 8, 9))
263+
self.assertEqual(t.replace(second=10), datetime.time(7, 8, 10))
264+
self.assertLess(t, datetime.time(7, 8, 10))
265+
self.assertIsNone(t.tzinfo)
266+
self.assertIsNone(t.utcoffset())
267+
self.assertIsNone(t.dst())
268+
self.assertIsNone(t.tzname())
269+
270+
def test_foreign_datetime_behavior(self):
271+
import datetime
272+
import java
273+
274+
LocalDateTime = java.type("java.time.LocalDateTime")
275+
ZonedDateTime = java.type("java.time.ZonedDateTime")
276+
ZoneId = java.type("java.time.ZoneId")
277+
278+
dt = LocalDateTime.of(2025, 3, 23, 7, 8, 9)
279+
self.assertEqual(dt.year, 2025)
280+
self.assertEqual(dt.month, 3)
281+
self.assertEqual(dt.day, 23)
282+
self.assertEqual(dt.hour, 7)
283+
self.assertEqual(dt.minute, 8)
284+
self.assertEqual(dt.second, 9)
285+
self.assertEqual(dt.microsecond, 0)
286+
self.assertEqual(str(dt), "2025-03-23 07:08:09")
287+
self.assertEqual(dt.isoformat(), "2025-03-23T07:08:09")
288+
self.assertEqual(dt.date(), datetime.date(2025, 3, 23))
289+
self.assertEqual(dt.time(), datetime.time(7, 8, 9))
290+
self.assertEqual(dt.timetz(), datetime.time(7, 8, 9))
291+
self.assertEqual(dt.timetuple(), datetime.datetime(2025, 3, 23, 7, 8, 9).timetuple())
292+
self.assertEqual(hash(dt), hash(datetime.datetime(2025, 3, 23, 7, 8, 9)))
293+
self.assertEqual(dt, datetime.datetime(2025, 3, 23, 7, 8, 9))
294+
self.assertEqual(dt, LocalDateTime.of(2025, 3, 23, 7, 8, 9))
295+
self.assertEqual(dt.replace(minute=9), datetime.datetime(2025, 3, 23, 7, 9, 9))
296+
self.assertEqual(dt + datetime.timedelta(days=1), datetime.datetime(2025, 3, 24, 7, 8, 9))
297+
self.assertEqual(dt - datetime.timedelta(days=1), datetime.datetime(2025, 3, 22, 7, 8, 9))
298+
self.assertEqual(dt - datetime.datetime(2025, 3, 20, 7, 8, 9), datetime.timedelta(days=3))
299+
self.assertLess(dt, datetime.datetime(2025, 3, 23, 7, 8, 10))
300+
self.assertIsNone(dt.tzinfo)
301+
self.assertIsNone(dt.utcoffset())
302+
self.assertIsNone(dt.dst())
303+
self.assertIsNone(dt.tzname())
304+
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+
313+
def test_foreign_timezone_behavior(self):
314+
import datetime
315+
import java
316+
317+
ZoneId = java.type("java.time.ZoneId")
318+
ZonedDateTime = java.type("java.time.ZonedDateTime")
319+
320+
utc = ZoneId.of("UTC")
321+
self.assertIsInstance(utc, datetime.tzinfo)
322+
self.assertEqual(str(utc), "UTC")
323+
self.assertEqual(utc.tzname(None), "UTC")
324+
self.assertEqual(utc.utcoffset(None), datetime.timedelta())
325+
self.assertIsNone(utc.dst(None))
326+
327+
aware = datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=utc)
328+
self.assertIs(aware.tzinfo, utc)
329+
self.assertEqual(aware.utcoffset(), datetime.timedelta())
330+
self.assertEqual(aware.tzname(), "UTC")
331+
self.assertEqual(aware.isoformat(), "2025-03-23T07:08:09+00:00")
332+
333+
berlin = ZoneId.of("Europe/Berlin")
334+
self.assertIsInstance(berlin, datetime.tzinfo)
335+
self.assertIsNone(berlin.utcoffset(None))
336+
self.assertIsNone(berlin.dst(None))
337+
self.assertIsNone(berlin.tzname(None))
338+
339+
local = datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin)
340+
self.assertIs(local.tzinfo, berlin)
341+
self.assertEqual(local.utcoffset(), datetime.timedelta(hours=1))
342+
self.assertEqual(local.dst(), datetime.timedelta())
343+
self.assertEqual(local.tzname(), "CET")
344+
self.assertEqual(berlin.fromutc(datetime.datetime(2025, 3, 23, 6, 8, 9, tzinfo=berlin)),
345+
datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin))
346+
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+
189356
def test_read(self):
190357
o = CustomObject()
191358
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
@@ -268,6 +268,7 @@
268268
import com.oracle.graal.python.builtins.objects.foreign.ForeignIterableBuiltins;
269269
import com.oracle.graal.python.builtins.objects.foreign.ForeignNumberBuiltins;
270270
import com.oracle.graal.python.builtins.objects.foreign.ForeignObjectBuiltins;
271+
import com.oracle.graal.python.builtins.objects.foreign.ForeignTimeZoneBuiltins;
271272
import com.oracle.graal.python.builtins.objects.frame.FrameBuiltins;
272273
import com.oracle.graal.python.builtins.objects.function.AbstractFunctionBuiltins;
273274
import com.oracle.graal.python.builtins.objects.function.BuiltinFunctionBuiltins;
@@ -500,6 +501,7 @@ private static PythonBuiltins[] initializeBuiltins(TruffleLanguage.Env env) {
500501
new ForeignObjectBuiltins(),
501502
new ForeignNumberBuiltins(),
502503
new ForeignBooleanBuiltins(),
504+
new ForeignTimeZoneBuiltins(),
503505
new ForeignAbstractClassBuiltins(),
504506
new ForeignExecutableBuiltins(),
505507
new ForeignInstantiableBuiltins(),

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
import com.oracle.graal.python.builtins.objects.foreign.ForeignIterableBuiltins;
187187
import com.oracle.graal.python.builtins.objects.foreign.ForeignNumberBuiltins;
188188
import com.oracle.graal.python.builtins.objects.foreign.ForeignObjectBuiltins;
189+
import com.oracle.graal.python.builtins.objects.foreign.ForeignTimeZoneBuiltins;
189190
import com.oracle.graal.python.builtins.objects.frame.FrameBuiltins;
190191
import com.oracle.graal.python.builtins.objects.function.AbstractFunctionBuiltins;
191192
import com.oracle.graal.python.builtins.objects.function.FunctionBuiltins;
@@ -1236,6 +1237,12 @@ def takewhile(predicate, iterable):
12361237
PTzInfo,
12371238
newBuilder().moduleName("datetime").publishInModule("_datetime").slots(TimeZoneBuiltins.SLOTS).doc("Fixed offset from UTC implementation of tzinfo.")),
12381239

1240+
// foreign datetime
1241+
ForeignDate("ForeignDate", PDate, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation()),
1242+
ForeignTime("ForeignTime", PTime, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation()),
1243+
ForeignDateTime("ForeignDateTime", PDateTime, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation()),
1244+
ForeignTimeZone("ForeignTimeZone", PTzInfo, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation().slots(ForeignTimeZoneBuiltins.SLOTS)),
1245+
12391246
// re
12401247
PPattern(
12411248
"Pattern",

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/UnicodeDataModuleBuiltins.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public void postInitialize(Python3Core core) {
150150
}
151151
}
152152

153-
private PythonObject createUCDCompatibilityObject(Python3Core core, PythonModule self) {
153+
private static PythonObject createUCDCompatibilityObject(Python3Core core, PythonModule self) {
154154
TruffleString t_ucd = toTruffleStringUncached("UCD");
155155
PythonClass clazz = PFactory.createPythonClassAndFixupSlots(null, core.getLanguage(), t_ucd, PythonBuiltinClassType.PythonObject,
156156
new PythonAbstractClass[]{core.lookupType(PythonBuiltinClassType.PythonObject)});

0 commit comments

Comments
 (0)