Add a native _zoneinfo module (#8915)

* Add a native _zoneinfo module

ZoneInfo reads TZif tuples from zoneinfo._common.load_data. The stdlib
module implements utcoffset, dst, tzname, fromutc, and the zone cache.
whats_left compares that class. Cache tests no longer expect failure.

Assisted-by: Grok:grok-4.7

* Fix ZoneInfo layout and keep its cache on the module

utcoffset, dst, and tzname take the object. Zone data is boxed so the
payload lines up with tzinfo on 32-bit. The strong, weak, and timedelta
caches live on each interpreter's _zoneinfo module.

Assisted-by: Grok:grok-4.7
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-10-01 11:32:32 +09:00
1 parent ab3d759e74
commit cb0f3b012b
6 files changed
+1479 -23

No files matched your search

-4
View File
@@ -1609,7 +1609,6 @@ class ZoneInfoCacheTest(TzPathUserMixin, ZoneInfoTestBase):
class CZoneInfoCacheTest(ZoneInfoCacheTest):
module = c_zoneinfo
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: RuntimeError not raised
def test_inconsistent_weak_cache_get(self):
class Cache:
def get(self, key, default=None):
@@ -1628,7 +1627,6 @@ class CZoneInfoCacheTest(ZoneInfoCacheTest):
"Unexpected instance of int in ZI weak cache for key 'America/Los_Angeles'"
)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: AttributeError not raised
def test_deleted_weak_cache(self):
class ZI(self.klass):
pass
@@ -1641,7 +1639,6 @@ class CZoneInfoCacheTest(ZoneInfoCacheTest):
with self.assertRaises(AttributeError):
ZI.clear_cache()
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'int' object has no attribute '_from_cache'
def test_inconsistent_weak_cache_setdefault(self):
class Cache:
def get(self, key, default=None):
@@ -2092,7 +2089,6 @@ class ExtensionBuiltTest(unittest.TestCase):
rely on these tests as an indication of stable properties of these classes.
"""
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: type object 'ZoneInfo' has unexpected attribute '_weak_cache'
def test_cache_location(self):
# The pure Python version stores caches on attributes, but the C
# extension stores them in C globals (at least for now)
+11 -3
View File
@@ -5,7 +5,7 @@
// cspell:ignore ymd ordinal isocalendar ISO fromisoformat
// cspell:ignore dnum zreplacement colonzreplacement zname freplacement somezreplacement
// cspell:ignore tzoffset tzmicrosecond tzsign dtstr nogo dtobj tzusec timet
pub(crate) use _datetime::module_def;
pub(crate) use _datetime::{PyTzInfo, datetime_type, module_def, timedelta_from_seconds};
#[pymodule]
mod _datetime {
@@ -940,10 +940,18 @@ mod _datetime {
PyDate::static_type()
}
fn datetime_type() -> &'static Py<PyType> {
pub(crate) fn datetime_type() -> &'static Py<PyType> {
PyDateTime::static_type()
}
pub(crate) fn timedelta_from_seconds(
seconds: i64,
vm: &VirtualMachine,
) -> PyResult<PyObjectRef> {
let delta = PyDelta::new_ex(0, seconds, 0, true, vm)?;
Ok(delta.into_ref(&vm.ctx).into())
}
fn time_type() -> &'static Py<PyType> {
PyTime::static_type()
}
@@ -2331,7 +2339,7 @@ mod _datetime {
#[pyattr]
#[pyclass(module = "datetime", name = "tzinfo")]
#[derive(Debug, Default, PyPayload)]
pub(super) struct PyTzInfo {}
pub(crate) struct PyTzInfo {}
impl Constructor for PyTzInfo {
type Args = FuncArgs;
File diff suppressed because it is too large. Load diff
+2
View File
@@ -57,6 +57,7 @@ mod mmap;
mod _datetime;
mod _heapq;
mod _queue;
mod _zoneinfo;
mod pickle;
mod pyexpat;
mod pystruct;
@@ -259,6 +260,7 @@ pub fn stdlib_module_defs(ctx: &Context) -> Vec<&'static builtins::PyModuleDef>
pyexpat::module_def(ctx),
pystruct::module_def(ctx),
_datetime::module_def(ctx),
_zoneinfo::module_def(ctx),
_heapq::module_def(ctx),
_queue::module_def(ctx),
random::module_def(ctx),
@@ -0,0 +1,37 @@
import tempfile
from pathlib import Path
from zoneinfo import ZoneInfo, reset_tzpath
import _interpreters
# Fixed-offset UTC TZif, so the cache check does not need system zone data.
UTC = b"TZif2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00UTC\x00TZif2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00UTC\x00\nUTC0\n"
with tempfile.TemporaryDirectory() as directory:
Path(directory, "UTC").write_bytes(UTC)
reset_tzpath([directory])
main = ZoneInfo("UTC")
assert main is ZoneInfo("UTC")
interp = _interpreters.create()
try:
result = _interpreters.run_string(
interp,
"""
from zoneinfo import ZoneInfo, reset_tzpath
reset_tzpath([tzpath])
first = ZoneInfo("UTC")
second = ZoneInfo("UTC")
assert first is second
assert id(first) != main_id
ZoneInfo.clear_cache()
""",
{"tzpath": directory, "main_id": id(main)},
)
finally:
_interpreters.destroy(interp)
if result is not None:
detail = getattr(result, "formatted", None) or repr(result)
raise AssertionError(detail)
assert main is ZoneInfo("UTC")
-16
View File
@@ -414,20 +414,6 @@ def _pure_decimal_baseline(wrapper):
return result
def _use_pure_zoneinfo(dir_result):
"""Replace the accelerated ZoneInfo with the pure-Python class."""
import zoneinfo._zoneinfo as pure_zoneinfo
cls = pure_zoneinfo.ZoneInfo
name = cls.__name__
for key in list(dir_result):
if key == name or key.startswith(f"{name}."):
del dir_result[key]
dir_result[name] = extra_info(cls)
for attr, info in own_attrs(cls):
dir_result[f"{name}.{attr}"] = info
def gen_modules():
# check name because modules listed have side effects on import,
# e.g. printing something or opening a webpage
@@ -465,8 +451,6 @@ def gen_modules():
):
for attr, info in own_attrs(item):
dir_result[f"{item_name}.{attr}"] = info
if mod_name == "zoneinfo":
_use_pure_zoneinfo(dir_result)
modules[mod_name] = dir_result
return modules