From dd9ec39b65bb0e1014284d38e0bda67c8fe96dc2 Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Sun, 29 Mar 2026 13:56:17 -0400 Subject: [PATCH 01/12] make leap seconds thread safe --- erfa/leap_seconds.py | 30 +++++++++++++++++++----------- erfa/ufunc.c.templ | 7 ++++++- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/erfa/leap_seconds.py b/erfa/leap_seconds.py index 0c011fd..d6af92c 100644 --- a/erfa/leap_seconds.py +++ b/erfa/leap_seconds.py @@ -25,6 +25,7 @@ __all__ = ["get", "set", "update", "validate"] +import threading from datetime import datetime, timedelta from warnings import warn @@ -43,6 +44,10 @@ def __getattr__(name): return _expires_property() if name == "expired": return _expires_property() < datetime.now() + if name == "_expires": + return _expiration_data.expires + if name == "_expiration_days": + return _expiration_data.days raise AttributeError(f"module {__name__!r} has no attribute {name!r}") @@ -50,10 +55,15 @@ def __dir__(): return ["expired", "expires", "get", "set", "update", "validate"] -_expires = None -"""Explicit expiration date inferred from leap-second table.""" -_expiration_days = 180 -"""Number of days beyond last leap second at which table expires.""" +class _ExpirationData(threading.local): + def __init__(self): + # Explicit expiration date inferred from leap-second table. + self.expires = None + # Number of days beyond last leap second at which table expires. + self.days = 180 + + +_expiration_data = _ExpirationData() def get(): @@ -155,14 +165,13 @@ def set(table=None): If the leap seconds in the table are not on the 1st of January or July, or if the sorted TAI-UTC do not increase in increments of 1. """ - global _expires if table is None: expires = None else: table, expires = validate(table) set_leap_seconds(table) - _expires = expires + _expiration_data.expires = expires def _expires_property(): @@ -172,12 +181,12 @@ def _expires_property(): set the leap-second array, or a number of days beyond the last leap second. """ - if _expires is None: + if _expiration_data.expires is None: last = get()[-1] return (datetime(last["year"], last["month"], 1) + - timedelta(_expiration_days)) + timedelta(_expiration_data.days)) else: - return _expires + return _expiration_data.expires def update(table): @@ -207,7 +216,6 @@ def update(table): If the leap seconds in the table are not on the 1st of January or July, or if the sorted TAI-UTC do not increase in increments of 1. """ - global _expires table, expires = validate(table) # Get erfa table and check it is OK; if not, reset it. @@ -226,7 +234,7 @@ def update(table): # array is set, do not allow exceptions due to misformed expires). try: if expires is not None and expires > _expires_property(): - _expires = expires + _expiration_data.expires = expires except Exception as exc: warn("table 'expires' attribute ignored as comparing it " diff --git a/erfa/ufunc.c.templ b/erfa/ufunc.c.templ index 99aca2b..6fa3011 100644 --- a/erfa/ufunc.c.templ +++ b/erfa/ufunc.c.templ @@ -21,6 +21,7 @@ that do not support it (e.g., 3.13t) #include "numpy/ufuncobject.h" #include "erfa.h" #include "erfaextra.h" +#include "erfadatextra.h" // Backported NumPy 2 API (can be removed if numpy 2 is required) #if NPY_ABI_VERSION < 0x02000000 @@ -684,7 +685,7 @@ static PyObject * set_leap_seconds(PyObject *NPY_UNUSED(module), PyObject *args) { PyObject *leap_seconds = NULL; PyArrayObject *array; - static PyArrayObject *leap_second_array = NULL; + static ERFA_THREAD_LOCAL PyArrayObject *leap_second_array = NULL; if (!PyArg_ParseTuple(args, "|O:set_leap_seconds", &leap_seconds)) { return NULL; @@ -790,6 +791,10 @@ PyMODINIT_FUNC PyInit_ufunc(void) if (m == NULL) { return NULL; } +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + d = PyModule_GetDict(m); /* borrowed ref. */ if (d == NULL) { goto fail; From d9b12a53be4c32c939b3155121255562adba2054 Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Mon, 30 Mar 2026 06:52:48 -0400 Subject: [PATCH 02/12] add py314t test --- .github/workflows/ci_workflows.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index 74d147c..df4f643 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -49,6 +49,14 @@ jobs: toxenv: test-devdeps toxargs: -v + - name: Test with freethreaded python + os: ubuntu-latest + python: '3.14t' + toxenv: test-devdeps + toxargs: -v + + - name: Code style checks + os: ubuntu-latest - name: Code style checks os: ubuntu-latest python: 3.x From f7ce861018c8da237412f322d124c45482841c77 Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Mon, 30 Mar 2026 07:03:14 -0400 Subject: [PATCH 03/12] point to thread safe-er fork of erfa --- liberfa/erfa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liberfa/erfa b/liberfa/erfa index 1d9738b..f8ec5de 160000 --- a/liberfa/erfa +++ b/liberfa/erfa @@ -1 +1 @@ -Subproject commit 1d9738bed9954188722f976774d0903e5dae1857 +Subproject commit f8ec5dee38aba406ac4e57db0809833f5586e9d8 From 5c4bd1912aa4e4ebd62c0ff4b714b6056ce05a82 Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Mon, 30 Mar 2026 07:11:45 -0400 Subject: [PATCH 04/12] update submodule --- liberfa/erfa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liberfa/erfa b/liberfa/erfa index f8ec5de..c74dbc7 160000 --- a/liberfa/erfa +++ b/liberfa/erfa @@ -1 +1 @@ -Subproject commit f8ec5dee38aba406ac4e57db0809833f5586e9d8 +Subproject commit c74dbc76282c0e974e7df27079f7fa64ff9ffaee From 3fbc312fb78776a7bd28271f5701e58a01d6f660 Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Mon, 30 Mar 2026 13:46:38 -0400 Subject: [PATCH 05/12] update submodule --- liberfa/erfa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liberfa/erfa b/liberfa/erfa index c74dbc7..a599c86 160000 --- a/liberfa/erfa +++ b/liberfa/erfa @@ -1 +1 @@ -Subproject commit c74dbc76282c0e974e7df27079f7fa64ff9ffaee +Subproject commit a599c86f5c6653bb1259400eea0d53e03b489bb9 From 4405b0f353a27bd3aafca17e68594447732bf9bf Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Mon, 30 Mar 2026 14:00:32 -0400 Subject: [PATCH 06/12] add catch for missing threading support --- erfa/ufunc.c.templ | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erfa/ufunc.c.templ b/erfa/ufunc.c.templ index 6fa3011..88c00fc 100644 --- a/erfa/ufunc.c.templ +++ b/erfa/ufunc.c.templ @@ -685,7 +685,11 @@ static PyObject * set_leap_seconds(PyObject *NPY_UNUSED(module), PyObject *args) { PyObject *leap_seconds = NULL; PyArrayObject *array; - static ERFA_THREAD_LOCAL PyArrayObject *leap_second_array = NULL; + # ifdef ERFA_THREAD_LOCAL + static ERFA_THREAD_LOCAL PyArrayObject *leap_second_array = NULL; + # else + static PyArrayObject *leap_second_array = NULL; + # endif if (!PyArg_ParseTuple(args, "|O:set_leap_seconds", &leap_seconds)) { return NULL; @@ -791,8 +795,11 @@ PyMODINIT_FUNC PyInit_ufunc(void) if (m == NULL) { return NULL; } +// FIXME: also require ERFA_HAS_THREAD_LOCAL #ifdef Py_GIL_DISABLED + # ifdef ERFA_THREAD_LOCAL PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); + # endif #endif d = PyModule_GetDict(m); /* borrowed ref. */ From 8469befca6020380ef1a9ae0896830ec6e03dad1 Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Mon, 30 Mar 2026 14:00:45 -0400 Subject: [PATCH 07/12] update submodule --- liberfa/erfa | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liberfa/erfa b/liberfa/erfa index a599c86..618946e 160000 --- a/liberfa/erfa +++ b/liberfa/erfa @@ -1 +1 @@ -Subproject commit a599c86f5c6653bb1259400eea0d53e03b489bb9 +Subproject commit 618946ecce4299b5b5b68006b0ab05877de921ac From 76b1a9ca16228c32ef37364604aa12aef24bd087 Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Mon, 30 Mar 2026 14:10:06 -0400 Subject: [PATCH 08/12] fix merge error --- .github/workflows/ci_workflows.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index 6204a02..b8be3ac 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -55,12 +55,6 @@ jobs: toxenv: test-devdeps toxargs: -v - - name: Code style checks - os: ubuntu-latest - python: 3.x - toxenv: codestyle - toxargs: -v - - name: Documentation build os: ubuntu-latest python: 3.x From f5d605031afbfca7e062077ad8c25fc92f29cadb Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Tue, 31 Mar 2026 09:39:26 -0400 Subject: [PATCH 09/12] try adding a test environment for freethreaded python --- .github/workflows/ci_workflows.yml | 2 +- tox.ini | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index b8be3ac..f3a6990 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -52,7 +52,7 @@ jobs: - name: Test with freethreaded python os: ubuntu-latest python: '3.14t' - toxenv: test-devdeps + toxenv: test-freethreaded toxargs: -v - name: Documentation build diff --git a/tox.ini b/tox.ini index aab6e66..f0ed34a 100644 --- a/tox.ini +++ b/tox.ini @@ -32,11 +32,13 @@ description = run tests devdeps: with the latest developer version of key dependencies oldestdeps: with the oldest supported version of key dependencies + freethreaded: with multi-threaded tests and a free-threaded python build # The following provides some specific pinnings for key packages deps = oldestdeps: numpy==1.21.* devdeps: numpy>=0.0.dev0 + freethreaded: pytest-run-parallel # The following indicates which extras_require from setup.cfg will be installed extras = @@ -66,3 +68,4 @@ extras = docs commands = pip freeze sphinx-build -W -b linkcheck . _build/html + From 825e7895769cf9bef74d477c99a6eb84369d9fda Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Tue, 31 Mar 2026 09:40:37 -0400 Subject: [PATCH 10/12] remove extra line --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index f0ed34a..21f90cf 100644 --- a/tox.ini +++ b/tox.ini @@ -68,4 +68,3 @@ extras = docs commands = pip freeze sphinx-build -W -b linkcheck . _build/html - From 2451703f737929e29b6b37304925c456b1bf60ad Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Tue, 31 Mar 2026 09:57:39 -0400 Subject: [PATCH 11/12] set parallel threads for freethreaded test --- .github/workflows/ci_workflows.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index f3a6990..91fb334 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -54,6 +54,7 @@ jobs: python: '3.14t' toxenv: test-freethreaded toxargs: -v + toxposargs: --parallel-threads=100 - name: Documentation build os: ubuntu-latest From 8111a956f51615a4e3c34bfeffec5cba47565b0a Mon Sep 17 00:00:00 2001 From: Colm Talbot Date: Tue, 31 Mar 2026 10:15:12 -0400 Subject: [PATCH 12/12] make sure the gil is disabled for free threaded tests --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 21f90cf..ee25275 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CC,CI setenv = devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/scientific-python-nightly-wheels/simple + freethreaded: PYTHON_GIL = 0 # Run the tests in a temporary directory to make sure that we don't import # pyerfa from the source tree