Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ae2c4e0
Add foreign temporal trait plumbing
timfel Mar 23, 2026
d1c4bc0
Add shared temporal projection helpers
timfel Mar 23, 2026
f3bc2ad
Implement ForeignDate methods
timfel Mar 23, 2026
81ddbfe
Implement ForeignTime methods
timfel Mar 23, 2026
6fbab8a
[GR-62450] Implement ForeignDateTime methods
timfel Mar 23, 2026
a8e8764
Support foreign timezone objects
timfel Mar 23, 2026
a88184b
[GR-62450] Add lib temporal check nodes
timfel Mar 23, 2026
b82def4
[GR-62450] Move temporal check helpers to lib
timfel Mar 23, 2026
fcd20b1
Fix compilation
timfel Mar 25, 2026
7897e0d
Get rid of AsManagedTimeNode for intermediate results
timfel Mar 25, 2026
fcc0544
Get rid of AsManagedDateNode for intermediate results
timfel Mar 25, 2026
75a3e55
Refactor all other trivial temp conversions to managed datetime objects
timfel Mar 26, 2026
02aaafe
Get rid of more PTimeDelta intermediate objects
timfel Mar 26, 2026
fa5e116
Introduce PyDeltaCheckNode
timfel Mar 26, 2026
d55e6ef
Get rid of AsManaged*Nodes for datetime classes as nodes
timfel Mar 26, 2026
964c637
Rename AsManaged*Nodes for datetime classes now they are just from-na…
timfel Mar 27, 2026
cf33067
Rename nodes to get intermediate time values
timfel Mar 27, 2026
427a136
declare method as static
timfel Mar 27, 2026
2726f2d
Add missing truffle boundaries
timfel Mar 27, 2026
dafa222
Rename TemporalNodes to TemporalValueNodes
timfel Mar 27, 2026
a230299
Final cleanup pass over new foreign datetime builtins
timfel Mar 27, 2026
ba0946d
One more fix around tzinfo and foreign objects
timfel Mar 27, 2026
c63f2f0
Fix regression around native datetime objects now flowing into more p…
timfel Mar 27, 2026
cf45486
Add missing truffle boundary
timfel Mar 27, 2026
e9a8669
Remove trivially redundant foreign date and datetime builtins
timfel Mar 30, 2026
ff608c1
Remove ForeignDateBuiltins
timfel Mar 30, 2026
51c1982
Remove ForeignTimeBuiltins and make foreign objects work with TimeBui…
timfel Mar 30, 2026
a7c39a7
Remove ForeignDateTimeBuiltins
timfel Mar 30, 2026
b7517a7
Patch our old asv to work on CPython 3.12, too (for now)
timfel Mar 30, 2026
857957c
Update changelog
timfel Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ language runtime. The main focus is on user-observable behavior of the engine.
* 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.
* 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.
* 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.
* 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.

## Version 25.0.1
* 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`.
Expand Down
169 changes: 168 additions & 1 deletion graalpython/com.oracle.graal.python.test/src/tests/test_interop.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ def test_single_trait_classes(self):
polyglot.ForeignObject,
polyglot.ForeignList,
polyglot.ForeignBoolean,
polyglot.ForeignDate,
polyglot.ForeignDateTime,
polyglot.ForeignException,
polyglot.ForeignExecutable,
polyglot.ForeignDict,
Expand All @@ -124,14 +126,25 @@ def test_single_trait_classes(self):
polyglot.ForeignNone,
polyglot.ForeignNumber,
polyglot.ForeignString,
polyglot.ForeignTime,
polyglot.ForeignTimeZone,
]

for c in classes:
self.assertIsInstance(c, type)
if c is polyglot.ForeignBoolean:
self.assertIs(c.__base__, polyglot.ForeignNumber)
elif c is not polyglot.ForeignObject:
self.assertIs(c.__base__, polyglot.ForeignObject)
if c is polyglot.ForeignDate:
self.assertIs(c.__base__, __import__("datetime").date)
elif c is polyglot.ForeignTime:
self.assertIs(c.__base__, __import__("datetime").time)
elif c is polyglot.ForeignDateTime:
self.assertIs(c.__base__, __import__("datetime").datetime)
elif c is polyglot.ForeignTimeZone:
self.assertIs(c.__base__, __import__("datetime").tzinfo)
else:
self.assertIs(c.__base__, polyglot.ForeignObject)

def test_get_class(self):
def wrap(obj):
Expand All @@ -155,6 +168,7 @@ def t(obj):
self.assertEqual(t("abc"), polyglot.ForeignString)

from java.lang import Object, Boolean, Integer, Throwable, Thread, Number, String
from java.time import LocalDate, LocalDateTime, LocalTime, ZoneId
from java.util import ArrayList, HashMap, ArrayDeque
from java.math import BigInteger
null = Integer.getInteger("something_that_does_not_exists")
Expand All @@ -172,6 +186,19 @@ def t(obj):
self.assertEqual(type(null), polyglot.ForeignNone)
self.assertEqual(type(BigInteger.valueOf(42)), polyglot.ForeignNumber)
self.assertEqual(type(wrap(String("abc"))), polyglot.ForeignString)
local_date = LocalDate.of(2025, 3, 23)
self.assertIsInstance(local_date, polyglot.ForeignDate)
self.assertIsInstance(local_date, __import__("datetime").date)

local_time = LocalTime.of(7, 8, 9)
self.assertIsInstance(local_time, polyglot.ForeignTime)
self.assertIsInstance(local_time, __import__("datetime").time)

local_date_time = LocalDateTime.of(2025, 3, 23, 7, 8, 9)
self.assertIsInstance(local_date_time, polyglot.ForeignDateTime)
self.assertIsInstance(local_date_time, __import__("datetime").datetime)

self.assertEqual(type(ZoneId.of("UTC")), polyglot.ForeignTimeZone)

def test_import(self):
def some_function():
Expand All @@ -186,6 +213,146 @@ def some_function():
assert imported_fun1 is some_function
assert imported_fun1() == "hello, polyglot world!"

def test_foreign_date_behavior(self):
import datetime
import java

LocalDate = java.type("java.time.LocalDate")

d = LocalDate.of(2025, 3, 23)
self.assertEqual(d.year, 2025)
self.assertEqual(d.month, 3)
self.assertEqual(d.day, 23)
self.assertEqual(str(d), "2025-03-23")
self.assertEqual(d.isoformat(), "2025-03-23")
self.assertEqual(d.ctime(), datetime.date(2025, 3, 23).ctime())
self.assertEqual(d.strftime("%Y-%m-%d"), "2025-03-23")
self.assertEqual(format(d, "%Y-%m-%d"), "2025-03-23")
self.assertEqual(d.toordinal(), datetime.date(2025, 3, 23).toordinal())
self.assertEqual(d.weekday(), datetime.date(2025, 3, 23).weekday())
self.assertEqual(d.isoweekday(), datetime.date(2025, 3, 23).isoweekday())
self.assertEqual(d.isocalendar(), datetime.date(2025, 3, 23).isocalendar())
self.assertEqual(d.timetuple(), datetime.date(2025, 3, 23).timetuple())
self.assertEqual(hash(d), hash(datetime.date(2025, 3, 23)))
self.assertEqual(d, datetime.date(2025, 3, 23))
self.assertEqual(d, LocalDate.of(2025, 3, 23))
self.assertEqual(d.replace(day=24), datetime.date(2025, 3, 24))
self.assertEqual(d + datetime.timedelta(days=1), datetime.date(2025, 3, 24))
self.assertEqual(d - datetime.timedelta(days=1), datetime.date(2025, 3, 22))
self.assertEqual(d - datetime.date(2025, 3, 20), datetime.timedelta(days=3))
self.assertEqual(d - LocalDate.of(2025, 3, 20), datetime.timedelta(days=3))

def test_foreign_time_behavior(self):
import datetime
import java

LocalTime = java.type("java.time.LocalTime")

t = LocalTime.of(7, 8, 9)
self.assertEqual(t.hour, 7)
self.assertEqual(t.minute, 8)
self.assertEqual(t.second, 9)
self.assertEqual(t.microsecond, 0)
self.assertEqual(str(t), "07:08:09")
self.assertEqual(t.isoformat(), "07:08:09")
self.assertEqual(t.strftime("%H:%M:%S"), "07:08:09")
self.assertEqual(format(t, "%H:%M:%S"), "07:08:09")
self.assertEqual(hash(t), hash(datetime.time(7, 8, 9)))
self.assertEqual(t, datetime.time(7, 8, 9))
self.assertEqual(t, LocalTime.of(7, 8, 9))
self.assertEqual(t.replace(second=10), datetime.time(7, 8, 10))
self.assertLess(t, datetime.time(7, 8, 10))
self.assertIsNone(t.tzinfo)
self.assertIsNone(t.utcoffset())
self.assertIsNone(t.dst())
self.assertIsNone(t.tzname())

def test_foreign_datetime_behavior(self):
import datetime
import java

LocalDateTime = java.type("java.time.LocalDateTime")
ZonedDateTime = java.type("java.time.ZonedDateTime")
ZoneId = java.type("java.time.ZoneId")

dt = LocalDateTime.of(2025, 3, 23, 7, 8, 9)
self.assertEqual(dt.year, 2025)
self.assertEqual(dt.month, 3)
self.assertEqual(dt.day, 23)
self.assertEqual(dt.hour, 7)
self.assertEqual(dt.minute, 8)
self.assertEqual(dt.second, 9)
self.assertEqual(dt.microsecond, 0)
self.assertEqual(str(dt), "2025-03-23 07:08:09")
self.assertEqual(dt.isoformat(), "2025-03-23T07:08:09")
self.assertEqual(dt.date(), datetime.date(2025, 3, 23))
self.assertEqual(dt.time(), datetime.time(7, 8, 9))
self.assertEqual(dt.timetz(), datetime.time(7, 8, 9))
self.assertEqual(dt.timetuple(), datetime.datetime(2025, 3, 23, 7, 8, 9).timetuple())
self.assertEqual(hash(dt), hash(datetime.datetime(2025, 3, 23, 7, 8, 9)))
self.assertEqual(dt, datetime.datetime(2025, 3, 23, 7, 8, 9))
self.assertEqual(dt, LocalDateTime.of(2025, 3, 23, 7, 8, 9))
self.assertEqual(dt.replace(minute=9), datetime.datetime(2025, 3, 23, 7, 9, 9))
self.assertEqual(dt + datetime.timedelta(days=1), datetime.datetime(2025, 3, 24, 7, 8, 9))
self.assertEqual(dt - datetime.timedelta(days=1), datetime.datetime(2025, 3, 22, 7, 8, 9))
self.assertEqual(dt - datetime.datetime(2025, 3, 20, 7, 8, 9), datetime.timedelta(days=3))
self.assertLess(dt, datetime.datetime(2025, 3, 23, 7, 8, 10))
self.assertIsNone(dt.tzinfo)
self.assertIsNone(dt.utcoffset())
self.assertIsNone(dt.dst())
self.assertIsNone(dt.tzname())

berlin = ZoneId.of("Europe/Berlin")
zoned_dt = ZonedDateTime.of(2025, 3, 23, 7, 8, 9, 0, berlin)
self.assertIsInstance(zoned_dt.tzinfo, datetime.tzinfo)
self.assertEqual(zoned_dt.utcoffset(), datetime.timedelta(hours=1))
self.assertEqual(zoned_dt.dst(), datetime.timedelta())
self.assertEqual(zoned_dt.tzname(), "CET")
self.assertEqual(zoned_dt.isoformat(), "2025-03-23T07:08:09+01:00")

def test_foreign_timezone_behavior(self):
import datetime
import java

ZoneId = java.type("java.time.ZoneId")
ZonedDateTime = java.type("java.time.ZonedDateTime")

utc = ZoneId.of("UTC")
self.assertIsInstance(utc, datetime.tzinfo)
self.assertEqual(str(utc), "UTC")
self.assertEqual(utc.tzname(None), "UTC")
self.assertEqual(utc.utcoffset(None), datetime.timedelta())
self.assertIsNone(utc.dst(None))

aware = datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=utc)
self.assertIs(aware.tzinfo, utc)
self.assertEqual(aware.utcoffset(), datetime.timedelta())
self.assertEqual(aware.tzname(), "UTC")
self.assertEqual(aware.isoformat(), "2025-03-23T07:08:09+00:00")

berlin = ZoneId.of("Europe/Berlin")
self.assertIsInstance(berlin, datetime.tzinfo)
self.assertIsNone(berlin.utcoffset(None))
self.assertIsNone(berlin.dst(None))
self.assertIsNone(berlin.tzname(None))

local = datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin)
self.assertIs(local.tzinfo, berlin)
self.assertEqual(local.utcoffset(), datetime.timedelta(hours=1))
self.assertEqual(local.dst(), datetime.timedelta())
self.assertEqual(local.tzname(), "CET")
self.assertEqual(berlin.fromutc(datetime.datetime(2025, 3, 23, 6, 8, 9, tzinfo=berlin)),
datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin))

foreign_aware = ZonedDateTime.of(2025, 3, 23, 6, 8, 9, 0, berlin)
self.assertEqual(berlin.fromutc(foreign_aware),
datetime.datetime(2025, 3, 23, 7, 8, 9, tzinfo=berlin))

overlap = berlin.fromutc(datetime.datetime(2025, 10, 26, 1, 30, tzinfo=berlin))
self.assertEqual(overlap, datetime.datetime(2025, 10, 26, 2, 30, tzinfo=berlin, fold=1))
self.assertEqual(overlap.fold, 1)
self.assertEqual(overlap.utcoffset(), datetime.timedelta(hours=1))

def test_read(self):
o = CustomObject()
assert polyglot.__read__(o, "field") == o.field
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@
import com.oracle.graal.python.builtins.objects.foreign.ForeignIterableBuiltins;
import com.oracle.graal.python.builtins.objects.foreign.ForeignNumberBuiltins;
import com.oracle.graal.python.builtins.objects.foreign.ForeignObjectBuiltins;
import com.oracle.graal.python.builtins.objects.foreign.ForeignTimeZoneBuiltins;
import com.oracle.graal.python.builtins.objects.frame.FrameBuiltins;
import com.oracle.graal.python.builtins.objects.function.AbstractFunctionBuiltins;
import com.oracle.graal.python.builtins.objects.function.BuiltinFunctionBuiltins;
Expand Down Expand Up @@ -500,6 +501,7 @@ private static PythonBuiltins[] initializeBuiltins(TruffleLanguage.Env env) {
new ForeignObjectBuiltins(),
new ForeignNumberBuiltins(),
new ForeignBooleanBuiltins(),
new ForeignTimeZoneBuiltins(),
new ForeignAbstractClassBuiltins(),
new ForeignExecutableBuiltins(),
new ForeignInstantiableBuiltins(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@
import com.oracle.graal.python.builtins.objects.foreign.ForeignIterableBuiltins;
import com.oracle.graal.python.builtins.objects.foreign.ForeignNumberBuiltins;
import com.oracle.graal.python.builtins.objects.foreign.ForeignObjectBuiltins;
import com.oracle.graal.python.builtins.objects.foreign.ForeignTimeZoneBuiltins;
import com.oracle.graal.python.builtins.objects.frame.FrameBuiltins;
import com.oracle.graal.python.builtins.objects.function.AbstractFunctionBuiltins;
import com.oracle.graal.python.builtins.objects.function.FunctionBuiltins;
Expand Down Expand Up @@ -1230,6 +1231,12 @@ def takewhile(predicate, iterable):
PTzInfo,
newBuilder().moduleName("datetime").publishInModule("_datetime").slots(TimeZoneBuiltins.SLOTS).doc("Fixed offset from UTC implementation of tzinfo.")),

// foreign datetime
ForeignDate("ForeignDate", PDate, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation()),
ForeignTime("ForeignTime", PTime, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation()),
ForeignDateTime("ForeignDateTime", PDateTime, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation()),
ForeignTimeZone("ForeignTimeZone", PTzInfo, newBuilder().publishInModule(J_POLYGLOT).basetype().addDict().disallowInstantiation().slots(ForeignTimeZoneBuiltins.SLOTS)),

// re
PPattern(
"Pattern",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public void postInitialize(Python3Core core) {
}
}

private PythonObject createUCDCompatibilityObject(Python3Core core, PythonModule self) {
private static PythonObject createUCDCompatibilityObject(Python3Core core, PythonModule self) {
TruffleString t_ucd = toTruffleStringUncached("UCD");
PythonClass clazz = PFactory.createPythonClassAndFixupSlots(null, core.getLanguage(), t_ucd, PythonBuiltinClassType.PythonObject,
new PythonAbstractClass[]{core.lookupType(PythonBuiltinClassType.PythonObject)});
Expand Down
Loading
Loading