Skip to content

Commit b4968ce

Browse files
committed
[GR-72576][GR-72577] Fix regressions in pydantic-core
PullRequest: graalpython/4195
2 parents 8807e35 + 98f8216 commit b4968ce

30 files changed

+596
-494
lines changed

.github/workflows/downstream-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
platform: linux
2121
arch: amd64
2222
- id: macos-latest
23-
platform: darwin
23+
platform: macos
2424
arch: aarch64
2525

2626
runs-on: ${{ matrix.os.id }}
@@ -33,7 +33,7 @@ jobs:
3333
sudo apt-get install -y cmake
3434
3535
- name: Install CMake (Darwin)
36-
if: ${{ matrix.os.platform == 'darwin' && matrix.name == 'pybind11' }}
36+
if: ${{ matrix.os.platform == 'macos' && matrix.name == 'pybind11' }}
3737
run: |
3838
if brew list cmake >/dev/null 2>&1; then
3939
echo "cmake already installed"
@@ -58,7 +58,7 @@ jobs:
5858

5959
- name: Get GraalPy EA build
6060
run: |
61-
tarball="$(curl -sfL https://raw.githubusercontent.com/graalvm/graal-languages-ea-builds/refs/heads/main/graalpy/versions/latest-native-${{ matrix.os.platform }}-${{ matrix.os.arch}}.url)"
61+
tarball="$(curl -s "https://api.github.com/repos/graalvm/graalvm-ce-dev-builds/releases/latest" | jq -r --arg artifact "graalpy-community-dev-${{ matrix.os.platform }}-${{matrix.os.arch}}.tar.gz" '.assets[] | select(.name == $artifact) | .browser_download_url')"
6262
curl -sfL "$tarball" | tar xz
6363
6464
- name: Run downstream tests for ${{ matrix.name }}

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ repos:
3838
language: python
3939
types: [text]
4040
files: '\.(java|py|md|c|h|sh)$'
41-
exclude: '^graalpython/lib-python/.*'
41+
exclude: '^graalpython/lib-python/.*|graalpython/com.oracle.graal.python.pegparser/src/com/oracle/graal/python/pegparser/Parser.java'
4242
- id: check-toml
4343
exclude: '^graalpython/lib-python/.*'
4444
- id: check-added-large-files

graalpython/com.oracle.graal.python.test/src/tests/cpyext/test_datetime.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -67,6 +67,7 @@ def create_datetime_subclass(typename):
6767
NativeTimeSubclass = create_datetime_subclass("Time")
6868
NativeDateTimeSubclass = create_datetime_subclass("DateTime")
6969
NativeDeltaSubclass = create_datetime_subclass("Delta")
70+
NativeTZInfoSubclass = create_datetime_subclass("TZInfo")
7071

7172

7273
class ManagedNativeDateSubclass(NativeDateSubclass):
@@ -85,6 +86,17 @@ class ManagedNativeDeltaSubclass(NativeDeltaSubclass):
8586
pass
8687

8788

89+
class ManagedNativeTZInfoSubclass(NativeTZInfoSubclass):
90+
def utcoffset(self, dt):
91+
return datetime.timedelta(minutes=42)
92+
93+
def dst(self, dt):
94+
return datetime.timedelta(0)
95+
96+
def tzname(self, dt):
97+
return "ManagedNativeTZ"
98+
99+
88100
class TestPyDateTime(CPyExtTestCase):
89101

90102
test_PyDateTime_GET_YEAR = CPyExtFunction(
@@ -910,3 +922,21 @@ def test_timedelta(self):
910922
# Str/repr
911923
assert isinstance(str(x), str)
912924
assert isinstance(repr(x), str)
925+
926+
def test_tzinfo(self):
927+
# Unlike the other tests, the managed subclass implements the abstract methods so that we can test interactions with other objects
928+
cls = ManagedNativeTZInfoSubclass
929+
x = cls()
930+
assert is_native_object(x)
931+
assert type(x) is cls
932+
# Can pass as tzinfo argument
933+
dt = datetime.datetime(2024, 1, 8, 14, 31, tzinfo=x)
934+
assert dt.tzinfo is x
935+
dt = datetime.datetime.now(x)
936+
assert dt.tzinfo is x
937+
# __str__ and __repr__
938+
assert cls.__name__ in repr(x)
939+
assert cls.__name__ in str(x)
940+
# Pickle roundtrip
941+
unpickled = pickle.loads(pickle.dumps(x))
942+
assert type(unpickled) is cls

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

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -1248,6 +1248,139 @@ class A(datetime.timezone):
12481248

12491249

12501250
class TimeDeltaTest(unittest.TestCase):
1251+
1252+
def test_construction_types_field_overflow_and_rounding(self):
1253+
td = datetime.timedelta
1254+
1255+
# Accepts bool, int, float, subclasses and mixes
1256+
class IntSubclass(int): pass
1257+
class FloatSubclass(float): pass
1258+
1259+
self.assertEqual(td(days=True), td(days=1))
1260+
self.assertEqual(td(days=2.0), td(days=2))
1261+
self.assertEqual(td(days=IntSubclass(3)), td(days=3))
1262+
self.assertEqual(td(days=FloatSubclass(4.0)), td(days=4))
1263+
self.assertEqual(td(seconds=1.7), td(seconds=1, microseconds=700000))
1264+
self.assertEqual(td(microseconds=5.2), td(microseconds=5))
1265+
self.assertEqual(td(microseconds=1.8), td(microseconds=2))
1266+
1267+
# Overflow and normalization between fields
1268+
self.assertEqual(td(seconds=60), td(minutes=1))
1269+
self.assertEqual(td(minutes=60), td(hours=1))
1270+
self.assertEqual(td(hours=24), td(days=1))
1271+
self.assertEqual(td(milliseconds=1500), td(seconds=1, milliseconds=500))
1272+
self.assertEqual(td(microseconds=1000001), td(seconds=1, microseconds=1))
1273+
1274+
# Mix float/int/rounding
1275+
self.assertEqual(td(seconds=1.9, microseconds=1.7), td(seconds=1, microseconds=900002))
1276+
# Large overflow stacks
1277+
self.assertEqual(td(seconds=3600*25), td(days=1, hours=1))
1278+
self.assertEqual(td(milliseconds=24*60*60*1000), td(days=1))
1279+
1280+
# Rounding of fractional microseconds (half-to-even)
1281+
# .5 rounds to even
1282+
self.assertEqual(td(microseconds=0.5), td(microseconds=0))
1283+
self.assertEqual(td(microseconds=1.5), td(microseconds=2))
1284+
self.assertEqual(td(microseconds=2.5), td(microseconds=2))
1285+
self.assertEqual(td(microseconds=3.5), td(microseconds=4))
1286+
self.assertEqual(td(milliseconds=0.0005), td())
1287+
self.assertEqual(td(milliseconds=0.0015), td(microseconds=2))
1288+
# Negative
1289+
self.assertEqual(td(microseconds=-0.5), td(microseconds=0))
1290+
self.assertEqual(td(microseconds=-1.5), td(microseconds=-2))
1291+
self.assertEqual(td(microseconds=-2.5), td(microseconds=-2))
1292+
self.assertEqual(td(microseconds=-3.5), td(microseconds=-4))
1293+
1294+
# Rejects non-numeric types (already checked elsewhere, not repeated here)
1295+
1296+
def test_mul_div_divmod_overflow_and_rounding(self):
1297+
td = datetime.timedelta
1298+
1299+
# Multiplication overflow (try to exceed microsecond 32b int, expect OverflowError)
1300+
big = td(days=2**26) # very big, but not yet overflow
1301+
with self.assertRaises(OverflowError):
1302+
_ = big * (2**14)
1303+
1304+
with self.assertRaises(OverflowError):
1305+
_ = (2**14) * big
1306+
1307+
# Negative overflow
1308+
with self.assertRaises(OverflowError):
1309+
_ = -big * (2**14)
1310+
1311+
# Division overflow (should raise on absurdly small divisor)
1312+
huge = td(days=999999999)
1313+
with self.assertRaises(OverflowError):
1314+
_ = huge / 1e-20
1315+
1316+
# Divmod with float divisor is not allowed (CPython raises TypeError)
1317+
with self.assertRaises(TypeError):
1318+
divmod(huge, 1e-20)
1319+
1320+
# Multiplication without overflow
1321+
t1 = td(days=1, microseconds=1)
1322+
self.assertEqual(t1 * 2, td(days=2, microseconds=2))
1323+
self.assertEqual(2 * t1, td(days=2, microseconds=2))
1324+
self.assertEqual(t1 * 0, td())
1325+
1326+
# Division: round-half-to-even per CPython
1327+
# Case: exactly half
1328+
half_even = td(microseconds=3)
1329+
self.assertEqual(half_even / 2, td(microseconds=2)) # 1.5 rounds to 2 (even)
1330+
self.assertEqual((-half_even) / 2, td(microseconds=-2)) # -1.5 rounds to -2
1331+
1332+
# Closest not half
1333+
self.assertEqual(td(microseconds=5) / 2, td(microseconds=2)) # 2.5 rounds to 2 (even)
1334+
self.assertEqual(td(microseconds=7) / 2, td(microseconds=4)) # 3.5 rounds to 4 (even)
1335+
1336+
# .__truediv__ returns float if divisor is a timedelta (duration ratio)
1337+
self.assertEqual(td(days=3) / td(days=2), 1.5)
1338+
1339+
# Divmod by int is not supported in CPython (raises TypeError)
1340+
with self.assertRaises(TypeError):
1341+
divmod(td(seconds=5), 2)
1342+
with self.assertRaises(TypeError):
1343+
divmod(td(microseconds=7), 2)
1344+
1345+
# Division by zero raises
1346+
with self.assertRaises(ZeroDivisionError):
1347+
_ = t1 / 0
1348+
with self.assertRaises(ZeroDivisionError):
1349+
_ = t1 // 0
1350+
# divmod by zero (int) is not supported at all
1351+
with self.assertRaises(TypeError):
1352+
divmod(t1, 0)
1353+
# divmod by zero timedelta returns ZeroDivisionError
1354+
with self.assertRaises(ZeroDivisionError):
1355+
divmod(t1, td())
1356+
1357+
# Divmod with negative int is not supported (raises TypeError)
1358+
with self.assertRaises(TypeError):
1359+
divmod(td(seconds=3), -2)
1360+
1361+
# Division by timedelta returns float, test for correct rounding
1362+
self.assertEqual(td(seconds=5) / td(seconds=2), 2.5)
1363+
self.assertEqual(td(seconds=3) / td(seconds=2), 1.5)
1364+
self.assertEqual(td(seconds=3) // td(seconds=2), 1.0)
1365+
with self.assertRaises(ZeroDivisionError):
1366+
_ = td(days=1) / td()
1367+
1368+
# Edge: Divmod with timedelta as divisor, remainder sign
1369+
d, r = divmod(td(seconds=5), td(seconds=2))
1370+
self.assertEqual(d, 2.0)
1371+
self.assertEqual(r, td(seconds=1))
1372+
1373+
d, r = divmod(td(seconds=5), td(seconds=-2))
1374+
self.assertEqual(d, -3.0)
1375+
self.assertEqual(r, td(seconds=-1))
1376+
1377+
# Additional explicit float rounding half to even
1378+
self.assertEqual((td(microseconds=2) / 1.33333333333).microseconds, 2)
1379+
self.assertEqual((td(microseconds=3) / 1.5).microseconds, 2)
1380+
# result field normalization: negative microseconds yields +999998 with negative total_seconds
1381+
self.assertEqual((td(microseconds=-3) / 1.5), td(microseconds=-2))
1382+
self.assertAlmostEqual((td(microseconds=-3) / 1.5).total_seconds(), -2e-6)
1383+
12511384
def test_new(self):
12521385
# parameters type validation
12531386

@@ -1295,6 +1428,9 @@ def test_new(self):
12951428
with self.assertRaisesRegex(ValueError, "cannot convert float NaN to integer"):
12961429
datetime.timedelta(weeks = float("nan"))
12971430

1431+
def test_total_seconds(self):
1432+
self.assertAlmostEqual(datetime.timedelta(days=106751992).total_seconds(), 9223372108800.0)
1433+
12981434
class DateTimeTest(unittest.TestCase):
12991435

13001436
def test_strptime(self):

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

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -247,7 +247,7 @@ private static Object tryToDeserializeDateTime(Object cls, Object bytesObject, O
247247
}
248248

249249
if (naiveBytesCheck(bytes)) {
250-
if (tzInfo != PNone.NO_VALUE && !(tzInfo instanceof PTzInfo)) {
250+
if (tzInfo != PNone.NO_VALUE && !TzInfoNodes.TzInfoCheckNode.executeUncached(tzInfo)) {
251251
throw PRaiseNode.raiseStatic(inliningTarget, TypeError, ErrorMessages.BAD_TZINFO_STATE_ARG);
252252
}
253253

@@ -333,7 +333,25 @@ private static Object todayBoundary(Object cls, Node inliningTarget) {
333333
abstract static class NowNode extends PythonBinaryBuiltinNode {
334334

335335
@Specialization
336-
static Object nowInTimeZone(VirtualFrame frame, Object cls, PTzInfo tzInfo,
336+
@TruffleBoundary
337+
static Object nowNaive(Object cls, PNone tzInfo,
338+
@Bind Node inliningTarget) {
339+
var local = LocalDateTime.now();
340+
return DateTimeNodes.SubclassNewNode.getUncached().execute(inliningTarget,
341+
cls,
342+
local.getYear(),
343+
local.getMonthValue(),
344+
local.getDayOfMonth(),
345+
local.getHour(),
346+
local.getMinute(),
347+
local.getSecond(),
348+
local.getNano() / 1_000,
349+
PNone.NONE,
350+
0);
351+
}
352+
353+
@Fallback
354+
static Object nowInTimeZone(VirtualFrame frame, Object cls, Object tzInfo,
337355
@Bind Node inliningTarget,
338356
@Cached("createFor($node)") IndirectCallData.BoundaryCallData boundaryCallData) {
339357
Object saved = ExecutionContext.BoundaryCallContext.enter(frame, boundaryCallData);
@@ -347,7 +365,10 @@ static Object nowInTimeZone(VirtualFrame frame, Object cls, PTzInfo tzInfo,
347365
}
348366

349367
@TruffleBoundary
350-
private static Object nowInTimeZoneBoundary(Object cls, PTzInfo tzInfo, Node inliningTarget) {
368+
private static Object nowInTimeZoneBoundary(Object cls, Object tzInfo, Node inliningTarget) {
369+
if (!TzInfoNodes.TzInfoCheckNode.executeUncached(tzInfo)) {
370+
throw PRaiseNode.raiseStatic(inliningTarget, TypeError, ErrorMessages.TZINFO_ARGUMENT_MUST_BE_NONE_OR_OF_A_TZINFO_SUBCLASS_NOT_TYPE_P, tzInfo);
371+
}
351372
// convert current time in UTC to the given time zone with tzinfo.fromutc()
352373
LocalDateTime utc = LocalDateTime.now(ZoneOffset.UTC);
353374

@@ -361,36 +382,11 @@ private static Object nowInTimeZoneBoundary(Object cls, PTzInfo tzInfo, Node inl
361382
utc.getSecond(),
362383
utc.getNano() / 1_000,
363384
tzInfo, // set the final value beforehand - it's required by
364-
// #fromutc()
385+
// #fromutc()
365386
0);
366387

367388
return PyObjectCallMethodObjArgs.executeUncached(tzInfo, T_FROMUTC, self);
368389
}
369-
370-
@Specialization
371-
@TruffleBoundary
372-
static Object nowNaive(Object cls, PNone tzInfo,
373-
@Bind Node inliningTarget) {
374-
var local = LocalDateTime.now();
375-
return DateTimeNodes.SubclassNewNode.getUncached().execute(inliningTarget,
376-
cls,
377-
local.getYear(),
378-
local.getMonthValue(),
379-
local.getDayOfMonth(),
380-
local.getHour(),
381-
local.getMinute(),
382-
local.getSecond(),
383-
local.getNano() / 1_000,
384-
PNone.NONE,
385-
0);
386-
}
387-
388-
@Fallback
389-
static void doGeneric(Object cls, Object tzInfo,
390-
@Bind Node inliningTarget,
391-
@Cached PRaiseNode raiseNode) {
392-
throw raiseNode.raise(inliningTarget, TypeError, ErrorMessages.TZINFO_ARGUMENT_MUST_BE_NONE_OR_OF_A_TZINFO_SUBCLASS_NOT_TYPE_P, tzInfo);
393-
}
394390
}
395391

396392
@Slot(value = SlotKind.tp_str, isComplex = true)
@@ -978,7 +974,7 @@ private static Object fromTimestampBoundary(Object cls, Object timestampObject,
978974
if (tzInfoObject instanceof PNone) {
979975
tzInfo = null;
980976
} else {
981-
if (!(tzInfoObject instanceof PTzInfo)) {
977+
if (!TzInfoNodes.TzInfoCheckNode.executeUncached(tzInfoObject)) {
982978
throw PRaiseNode.raiseStatic(inliningTarget, TypeError, ErrorMessages.TZINFO_ARGUMENT_MUST_BE_NONE_OR_OF_A_TZINFO_SUBCLASS_NOT_TYPE_P, tzInfoObject);
983979
}
984980

@@ -1182,7 +1178,7 @@ static Object combine(Object cls, Object dateObject, Object timeObject, Object t
11821178
tzInfo = tzInfoObject;
11831179
}
11841180

1185-
if (tzInfo != null && !(tzInfo instanceof PTzInfo)) {
1181+
if (tzInfo != null && !TzInfoNodes.TzInfoCheckNode.executeUncached(tzInfo)) {
11861182
throw raiseNode.raise(inliningTarget,
11871183
TypeError,
11881184
ErrorMessages.TZINFO_ARGUMENT_MUST_BE_NONE_OR_OF_A_TZINFO_SUBCLASS_NOT_TYPE_P,
@@ -2703,7 +2699,7 @@ private static Object inTimeZoneBoundary(Object selfObj, Object tzInfo, Node inl
27032699
final Object targetTimeZone;
27042700
if (tzInfo instanceof PNone) {
27052701
targetTimeZone = getSystemTimeZoneAt(toLocalDateTime(self), self.fold, inliningTarget);
2706-
} else if (!(tzInfo instanceof PTzInfo)) {
2702+
} else if (!TzInfoNodes.TzInfoCheckNode.executeUncached(tzInfo)) {
27072703
throw PRaiseNode.raiseStatic(inliningTarget, TypeError, ErrorMessages.TZINFO_ARGUMENT_MUST_BE_NONE_OR_OF_A_TZINFO_SUBCLASS_NOT_TYPE_P, tzInfo);
27082704
} else {
27092705
targetTimeZone = tzInfo;

0 commit comments

Comments
 (0)