Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .github/workflows/ci_workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ jobs:
toxenv: test-devdeps
toxargs: -v

- name: Test with freethreaded python
os: ubuntu-latest
python: '3.14t'
toxenv: test-freethreaded
toxargs: -v
toxposargs: --parallel-threads=100

- name: Documentation build
os: ubuntu-latest
python: 3.x
Expand Down
30 changes: 19 additions & 11 deletions erfa/leap_seconds.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

__all__ = ["get", "set", "update", "validate"]

import threading
from datetime import datetime, timedelta
from warnings import warn

Expand All @@ -41,17 +42,26 @@ 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}")


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():
Expand Down Expand Up @@ -153,14 +163,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():
Expand All @@ -170,11 +179,11 @@ 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))
return _expires
timedelta(_expiration_data.days))
return _expiration_data.expires


def update(table):
Expand Down Expand Up @@ -204,7 +213,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.
Expand All @@ -223,7 +231,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 "
Expand Down
14 changes: 13 additions & 1 deletion erfa/ufunc.c.templ
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -684,7 +685,11 @@ static PyObject *
set_leap_seconds(PyObject *NPY_UNUSED(module), PyObject *args) {
PyObject *leap_seconds = NULL;
PyArrayObject *array;
static 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;
Expand Down Expand Up @@ -790,6 +795,13 @@ 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. */
if (d == NULL) {
goto fail;
Expand Down
2 changes: 1 addition & 1 deletion liberfa/erfa
3 changes: 3 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,11 +33,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 =
Expand Down
Loading