[Git][debian-gis-team/cftime][master] 4 commits: New upstream version 1.6.6
Bas Couwenberg (@sebastic)
gitlab at salsa.debian.org
Fri Sep 18 13:10:09 BST 2026
Bas Couwenberg pushed to branch master at Debian GIS Project / cftime
Commits:
1712bfca by Bas Couwenberg at 2026-09-18T05:03:19-07:00
New upstream version 1.6.6
- - - - -
3de01c5a by Bas Couwenberg at 2026-09-18T05:03:19-07:00
Update upstream source from tag 'upstream/1.6.6'
Update to upstream version '1.6.6'
with Debian dir 9a0a5ea264d7ac712c65ebfbb4c18e5c16b9323c
- - - - -
cfc94efa by Bas Couwenberg at 2026-09-18T05:03:40-07:00
New upstream release.
- - - - -
2169d617 by Bas Couwenberg at 2026-09-18T05:05:18-07:00
Set distribution to unstable.
- - - - -
13 changed files:
- .github/dependabot.yml
- .github/workflows/cibuildwheel.yml
- .github/workflows/deploy-docs.yml
- .github/workflows/tests_conda.yml
- .github/workflows/tests_latest.yml
- Changelog
- README.md
- debian/changelog
- pyproject.toml
- setup.py
- src/cftime/__init__.py
- src/cftime/_cftime.pyx
- test/test_cftime.py
Changes:
=====================================
.github/dependabot.yml
=====================================
@@ -11,3 +11,5 @@ updates:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly
+ cooldown:
+ default-days: 7
=====================================
.github/workflows/cibuildwheel.yml
=====================================
@@ -4,9 +4,9 @@ on:
pull_request:
branches:
- master
- push:
- tags:
- - "v*"
+ release:
+ types:
+ - published
jobs:
build_bdist:
@@ -17,114 +17,94 @@ jobs:
fail-fast: false
matrix:
include:
- - os: ubuntu-22.04
+ - os: ubuntu-24.04
arch: x86_64
- - os: ubuntu-22.04
+ - os: ubuntu-24.04-arm
arch: aarch64
- os: windows-2022
arch: AMD64
- os: macos-14
arch: arm64
- - os: macos-13
+ - os: macos-15-intel
arch: x86_64
+ - os: windows-11-arm
+ arch: ARM64
steps:
- - uses: actions/checkout at v5
+ - uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
-
- # For aarch64 support
- # https://cibuildwheel.pypa.io/en/stable/faq/#emulation
- - uses: docker/setup-qemu-action at v3
- with:
- platforms: all
- if: runner.os == 'Linux' && matrix.arch == 'aarch64'
+ persist-credentials: false
- name: Build just oldest and newest on PRs, all on tags
+ if: ${{ github.event_name == 'pull_request' }}
shell: bash
- # - Always omit musllinux_aarch64 because it's slow and niche
- # - Always skip free-threading wheels for now
# - On PPs, omit musllinux for speed
- # - On PRs, run just oldest and newest Python versions
+ # - On PRs, run just oldest and newest Python versions (3.11 is the oldest abi3 target)
run: |
- if [[ "${{ github.event_name }}" == "pull_request" ]]; then
- CIBW_SKIP="cp311-* cp312-* cp313-* cp314t-* *musllinux*"
- else
- CIBW_SKIP="cp314t-* *musllinux_aarch64"
- fi
+ CIBW_SKIP="cp310-win_arm64 *musllinux*"
echo "CIBW_SKIP=$CIBW_SKIP" >> $GITHUB_ENV
echo "Setting CIBW_SKIP=$CIBW_SKIP"
+ CIBW_TEST_SKIP="cp312-* cp313-*"
+ echo "CIBW_TEST_SKIP=$CIBW_TEST_SKIP" >> $GITHUB_ENV
+ echo "Setting CIBW_TEST_SKIP=$CIBW_TEST_SKIP"
+
- name: "Building ${{ matrix.os }} (${{ matrix.arch }}) wheels"
- uses: pypa/cibuildwheel at v3.2.1
+ uses: pypa/cibuildwheel at 1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0
env:
CIBW_SKIP: ${{ env.CIBW_SKIP }}
+ CIBW_TEST_SKIP: ${{ env.CIBW_TEST_SKIP }}
CIBW_ARCHS: ${{ matrix.arch }}
- CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014
- CIBW_MANYLINUX_AARCH64_IMAGE: manylinux2014
- # Emulated testing is slow, so trust that the Python 3.14 test is good enough on aarch64
- # (takes about 5 minutes per wheel to build, and 5 minutes to test)
- CIBW_TEST_SKIP: "cp3{10,11,12,13}-*_aarch64"
- CIBW_TEST_GROUPS: dev
- CIBW_TEST_COMMAND: >
- python -c "import cftime; print(f'cftime v{cftime.__version__}')" &&
- python -m pytest -vv {package}/test
- - uses: actions/upload-artifact at v4
+ - uses: actions/upload-artifact at 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pypi-artifacts-${{ matrix.os }}-${{ matrix.arch }}
path: ${{ github.workspace }}/wheelhouse/*.whl
-
+ permissions:
+ actions: write
build_sdist:
name: Build source distribution
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout at v5
+ - uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
+ persist-credentials: false
- name: Build sdist
run: >
pip install build
&& python -m build --sdist . --outdir dist
- - uses: actions/upload-artifact at v4
+ - uses: actions/upload-artifact at 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pypi-artifacts
path: ${{ github.workspace }}/dist/*.tar.gz
- show-artifacts:
- needs: [build_bdist, build_sdist]
- name: "Show artifacts"
- runs-on: ubuntu-22.04
- steps:
- - uses: actions/download-artifact at v5
- with:
- pattern: pypi-artifacts*
- path: ${{ github.workspace }}/dist
- merge-multiple: true
-
- - shell: bash
- run: |
- ls -l ${{ github.workspace }}/dist
-
+ permissions:
+ actions: write
publish-artifacts-pypi:
needs: [build_bdist, build_sdist]
name: "Publish to PyPI"
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
# upload to PyPI for every tag starting with 'v'
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
steps:
- - uses: actions/download-artifact at v5
+ - uses: actions/download-artifact at 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: pypi-artifacts*
path: ${{ github.workspace }}/dist
merge-multiple: true
- - uses: pypa/gh-action-pypi-publish at release/v1
+ # Move to Trusted Publishing
+ - uses: pypa/gh-action-pypi-publish at dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
- user: __token__
- password: ${{ secrets.PYPI_PASSWORD }}
print_hash: true
+ environment:
+ name: release
+ url: https://pypi.org/p/cftime
+ permissions:
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
=====================================
.github/workflows/deploy-docs.yml
=====================================
@@ -1,22 +1,32 @@
name: Build and Deploy docs
on:
+ pull_request:
push:
branches:
- master
+ release:
+ types:
+ - published
+
+# Deny all permissions by default
+permissions: {}
jobs:
build-docs:
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- name: checkout
- uses: actions/checkout at v5
+ uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
+ persist-credentials: false
- name: Setup Mamba
- uses: mamba-org/setup-micromamba at v2
+ uses: mamba-org/setup-micromamba at f457c30a868e4760d3a6fcea5f25dc655b8edf39 # v3.2.1
with:
environment-name: TEST
create-args: >-
@@ -43,7 +53,8 @@ jobs:
popd
- name: Deploy
- uses: peaceiris/actions-gh-pages at v4
+ if: success() && github.event_name == 'release'
+ uses: peaceiris/actions-gh-pages at 84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: docs/_build/html
=====================================
.github/workflows/tests_conda.yml
=====================================
@@ -5,6 +5,9 @@ on:
push:
branches: [master]
+# Deny all permissions by default
+permissions: {}
+
jobs:
run:
runs-on: ${{ matrix.os }}
@@ -25,10 +28,12 @@ jobs:
experimental: true
steps:
- - uses: actions/checkout at v5
+ - uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Setup micromamba Env
- uses: mamba-org/setup-micromamba at v2
+ uses: mamba-org/setup-micromamba at f457c30a868e4760d3a6fcea5f25dc655b8edf39 # v3.2.1
with:
environment-name: TEST
create-args: >-
=====================================
.github/workflows/tests_latest.yml
=====================================
@@ -1,18 +1,27 @@
name: Build and test with development python
-on: [push, pull_request]
+
+on:
+ pull_request:
+ push:
+
+# Deny all permissions by default
+permissions: {}
+
jobs:
build-linux:
name: Python (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.14.0b.1"]
+ python-version: ["3.14"]
steps:
- - uses: actions/checkout at v5
+ - uses: actions/checkout at 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python at v6
+ uses: actions/setup-python at 5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
=====================================
Changelog
=====================================
@@ -1,3 +1,8 @@
+version 1.6.6 (release tag v1.6.6rel)
+=====================================
+* added new CF calendar "tai", which is the same as proleptic_gregorian but is only
+ valid for dates after 1958-01-01.
+
version 1.6.5 (release tag v1.6.5rel)
=====================================
* python 3.14 wheels, 3.8/3.9 support dropped.
=====================================
README.md
=====================================
@@ -1,10 +1,9 @@
# cftime
Time-handling functionality from netcdf4-python
-[](https://github.com/Unidata/cftime/actions)
-[](http://python.org/pypi/cftime)
-[](https://coveralls.io/github/Unidata/cftime?branch=master)
-[](https://github.com/Unidata/cftime/tags)
+[](https://github.com/Unidata/cftime/actions/workflows/github-code-scanning/codeql)
+[](http://python.org/pypi/cftime)
+[](https://anaconda.org/conda-forge/cftime)
[](https://github.com/Unidata/cftime/releases)
[](https://github.com/UniData/cftime/commits/master)
[](https://zenodo.org/badge/latestdoi/73107250)
@@ -12,6 +11,8 @@ Time-handling functionality from netcdf4-python
## News
For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog).
+9/15/2026: Version 1.6.6 release. Limited API compliant wheels (should work with python 3.15). New cftime calendar "tai".
+
10/13/2025: Version 1.6.5 release. Minor bugfixes/optimizations, wheels for python 3.14, no more wheels for 3.8/3.9.
6/7/2024: Version 1.6.4 release. Wheels for muslinux and aarch64, numpy 2.0 compatibility.
=====================================
debian/changelog
=====================================
@@ -1,11 +1,12 @@
-cftime (1.6.5-2) UNRELEASED; urgency=medium
+cftime (1.6.6-1) unstable; urgency=medium
+ * New upstream release.
* Update Upstream-Contact email address.
* Use test-build-validate-cleanup instead of test-build-twice.
* Drop Priority: optional, default since dpkg 1.22.13.
* Bump Standards-Version to 4.7.4, changes: priority.
- -- Bas Couwenberg <sebastic at debian.org> Tue, 14 Oct 2025 05:39:22 +0200
+ -- Bas Couwenberg <sebastic at debian.org> Fri, 18 Sep 2026 05:05:09 -0700
cftime (1.6.5-1) unstable; urgency=medium
=====================================
pyproject.toml
=====================================
@@ -1,6 +1,6 @@
[project]
name = "cftime"
-version = "1.6.5"
+version = "1.6.6"
description = "Time-handling functionality from netcdf4-python"
readme = "README.md"
authors = [
@@ -20,10 +20,12 @@ classifiers = [
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
+ 'Programming Language :: Python :: 3.15',
'Topic :: Scientific/Engineering',
]
dependencies = [
- "numpy>=1.21.2",
+ "numpy>=2.3.0; platform_system == 'Windows' and platform_machine == 'ARM64'",
+ "numpy>=1.21.2; platform_system != 'Windows' or platform_machine != 'ARM64'",
]
requires-python = ">=3.10"
@@ -35,7 +37,6 @@ dev = [
"cython>=0.29.20",
"pytest",
"sphinx",
- "twine",
]
[build-system]
@@ -60,6 +61,11 @@ doctest_optionflags = [
"NORMALIZE_WHITESPACE",
"ELLIPSIS",
]
+filterwarnings = [
+ "error",
+ "ignore::UserWarning",
+ "ignore::RuntimeWarning",
+]
[tool.coverage.run]
source = ["src"]
@@ -100,3 +106,27 @@ ignore = [
"test",
"test/*",
]
+
+[tool.cibuildwheel]
+build-verbosity = 1
+skip = [
+ "*musllinux_aarch64*",
+ # Skipped due to unavailability of Python binary and NumPy for Win-ARM64
+ "cp310-win_arm64",
+]
+
+test-skip = [
+ # Emulated testing is slow, so trust that the Python 3.14 test is good enough on aarch64
+ # (takes about 5 minutes per wheel to build, and 5 minutes to test)
+ "cp3{10,11,12,13}-*_aarch64",
+]
+test-groups = "dev"
+test-command = [
+ '''python -c "import cftime; print(f'cftime v{cftime.__version__}')"''',
+ "python -m pytest -vv {package}/test"
+]
+#manylinux-x86_64-image = "manylinux2014"
+#manylinux-aarch64-image = "manylinux2014"
+manylinux-x86_64-image = "manylinux_2_28"
+manylinux-aarch64-image = "manylinux_2_28"
+environment = { CFTIME_LIMITED_API = "1" }
=====================================
setup.py
=====================================
@@ -1,5 +1,6 @@
import os
import sys
+import sysconfig
import numpy
from Cython.Build import cythonize
@@ -29,6 +30,22 @@ NAME = 'cftime'
CFTIME_DIR = os.path.join(SRCDIR, NAME)
CYTHON_FNAME = os.path.join(CFTIME_DIR, '_{}.pyx'.format(NAME))
+USE_PY_LIMITED_API = (
+ # require opt-in (builds are specialized by default)
+ os.getenv('CFTIME_LIMITED_API', '0') == '1'
+ # Cython + numpy + limited API de facto requires Python >=3.11
+ and sys.version_info >= (3, 11)
+ # as of Python 3.14t, free-threaded builds don't support the limited API
+ and not sysconfig.get_config_var("Py_GIL_DISABLED")
+)
+ABI3_TARGET_VERSION = "".join(str(_) for _ in sys.version_info[:2])
+ABI3_TARGET_HEX = hex(sys.hexversion & 0xFFFF00F0)
+
+if USE_PY_LIMITED_API:
+ DEFINE_MACROS += [(("Py_LIMITED_API", ABI3_TARGET_HEX))]
+ SETUP_OPTIONS = {"bdist_wheel": {"py_limited_api": f"cp{ABI3_TARGET_VERSION}"}}
+else:
+ SETUP_OPTIONS = {}
class CleanCython(Command):
description = 'Purge artifacts built by Cython'
@@ -60,6 +77,7 @@ if ((FLAG_COVERAGE in sys.argv or os.environ.get('CYTHON_COVERAGE', None))
}
DEFINE_MACROS += [('CYTHON_TRACE', '1'),
('CYTHON_TRACE_NOGIL', '1')]
+
if FLAG_COVERAGE in sys.argv:
sys.argv.remove(FLAG_COVERAGE)
print('enable: "linetrace" Cython compiler directive')
@@ -71,7 +89,8 @@ else:
extension = Extension('{}._{}'.format(NAME, NAME),
sources=[os.path.relpath(CYTHON_FNAME, BASEDIR)],
define_macros=DEFINE_MACROS,
- include_dirs=[numpy.get_include(),])
+ include_dirs=[numpy.get_include()],
+ py_limited_api=USE_PY_LIMITED_API)
ext_modules = cythonize(
extension,
@@ -82,4 +101,5 @@ else:
setup(
cmdclass={'clean_cython': CleanCython},
ext_modules=ext_modules,
+ options=SETUP_OPTIONS,
)
=====================================
src/cftime/__init__.py
=====================================
@@ -7,7 +7,7 @@ from ._cftime import (microsec_units, millisec_units,
from ._cftime import CFWarning
# these will be removed in a future release
from ._cftime import (DatetimeNoLeap, DatetimeAllLeap, Datetime360Day,
- Datetime360Day, DatetimeJulian,
+ Datetime360Day, DatetimeJulian, DatetimeTAI,
DatetimeGregorian, DatetimeProlepticGregorian)
=====================================
src/cftime/_cftime.pyx
=====================================
@@ -30,7 +30,7 @@ _units = microsec_units+millisec_units+sec_units+min_units+hr_units+day_units
# '366_day'=='all_leap','365_day'=='noleap')
# see http://cfconventions.org/cf-conventions/cf-conventions#calendar
# for definitions.
-_calendars = ['standard', 'gregorian', 'proleptic_gregorian',
+_calendars = ['standard', 'gregorian', 'proleptic_gregorian', 'tai',
'noleap', 'julian', 'all_leap', '365_day', '366_day', '360_day']
_idealized_calendars= ['all_leap','noleap','366_day','365_day','360_day']
# Following are number of days per month
@@ -83,7 +83,7 @@ def _datesplit(timestr):
return units.lower(), remainder
-def _dateparse(timestr,calendar,has_year_zero=None):
+def _dateparse(timestr, calendar, has_year_zero=None):
"""parse a string of the form time-units since yyyy-mm-dd hh:mm:ss,
return a datetime instance"""
# same as version in cftime, but returns a timezone naive
@@ -109,6 +109,9 @@ def _dateparse(timestr,calendar,has_year_zero=None):
# parse the date string.
year, month, day, hour, minute, second, microsecond, utc_offset =\
_parse_date( isostring.strip() )
+ if calendar == 'tai':
+ if year < 1958 or utc_offset:
+ raise ValueError('TAI calendar must have a reference date of 1958-01-01T00:00:00 or later (with no utc offset)')
if year == 0 and not has_year_zero and calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
msg='zero not allowed as a reference year when has_year_zero=False'
raise ValueError(msg)
@@ -157,7 +160,7 @@ def date2num(dates, units, calendar=None, has_year_zero=None, longdouble=False):
**calendar**: describes the calendar to be used in the time calculations.
All the values currently defined in the
`CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
- Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
+ Valid calendars **'standard', 'gregorian', 'proleptic_gregorian', 'tai',
'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
Default is `None` which means the calendar associated with the first
input datetime instance will be used.
@@ -206,6 +209,11 @@ def date2num(dates, units, calendar=None, has_year_zero=None, longdouble=False):
mask = dates.mask
ismasked = True
+ # Convert numpy.datetime64 to a list to convert them to datetime instances.
+ if hasattr(dates, "dtype"):
+ if np.issubdtype(dates.dtype, np.datetime64):
+ dates = dates.tolist()
+
# are all input dates 'real' python datetime objects?
dates = np.asanyarray(dates) # convert to numpy array
shape = dates.shape # save shape of input
@@ -373,6 +381,7 @@ UNIT_CONVERSION_FACTORS = {
DATE_TYPES = {
"proleptic_gregorian": DatetimeProlepticGregorian,
+ "tai": DatetimeTAI,
"standard": DatetimeGregorian,
"noleap": DatetimeNoLeap,
"365_day": DatetimeNoLeap,
@@ -537,7 +546,7 @@ def num2date(
**calendar**: describes the calendar used in the time calculations.
All the values currently defined in the
`CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
- Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
+ Valid calendars **'standard', 'gregorian', 'proleptic_gregorian', 'tai',
'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
Default is **'standard'**, which is a mixed Julian/Gregorian calendar.
@@ -652,7 +661,7 @@ def date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None)
**calendar**: describes the calendar to be used in the time calculations.
All the values currently defined in the
`CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
- Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
+ Valid calendars **'standard', 'gregorian', 'proleptic_gregorian', 'tai',
'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
Default is `None` which means the calendar associated with the first
input datetime instance will be used.
@@ -864,7 +873,7 @@ def _date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None
order.
**calendar**: Describes the calendar used in the time calculation.
- Valid calendars 'standard', 'gregorian', 'proleptic_gregorian'
+ Valid calendars 'standard', 'gregorian', 'proleptic_gregorian', 'tai',
'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
Default is 'standard', which is a mixed Julian/Gregorian calendar
If `calendar` is None, its value is given by `nctime.calendar` or
@@ -911,7 +920,7 @@ def time2index(times, nctime, calendar=None, select='exact'):
order.
**calendar**: Describes the calendar used in the time calculation.
- Valid calendars 'standard', 'gregorian', 'proleptic_gregorian'
+ Valid calendars 'standard', 'gregorian', 'proleptic_gregorian', 'tai',
'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
Default is `standard`, which is a mixed Julian/Gregorian calendar
If `calendar` is None, its value is given by `nctime.calendar` or
@@ -1072,7 +1081,7 @@ for cftime.datetime instances using
All the calendars currently defined in the
`CF metadata convention <http://cfconventions.org/cf-conventions/cf-conventions#calendar>`__ are supported.
-Valid calendars are 'standard', 'gregorian', 'proleptic_gregorian'
+Valid calendars are 'standard', 'gregorian', 'proleptic_gregorian', 'tai',
'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
Default is 'standard', which is a mixed Julian/Gregorian calendar.
'standard' and 'gregorian' are synonyms, as are 'all_leap'/'366_day'
@@ -1152,6 +1161,12 @@ The default format of the string produced by strftime is controlled by self.form
# and has a year zero, so for now this is the default in cftime.
if calendar in ['julian','gregorian','standard'] and year <= 0:
warnings.warn(cfwarnmsg,category=CFWarning)
+ # raise exception if date before 1958-01-01 requested for tai calendar.
+ if calendar == 'tai':
+ if year < 1958:
+ raise ValueError('dates before 1958-01-01 not allowed in TAI calendar')
+ if has_year_zero:
+ raise ValueError('year zero not allowed in TAI calendar')
# raise exception if year zero requested but has_year_zero set
# to False (issue #248).
if year == 0 and has_year_zero==False:
@@ -1185,7 +1200,7 @@ The default format of the string produced by strftime is controlled by self.form
self.calendar = calendar
self.datetime_compatible = False
assert_valid_date(self, is_leap_julian, False, has_year_zero=has_year_zero)
- elif calendar == 'proleptic_gregorian':
+ elif calendar == 'proleptic_gregorian' or calendar == 'tai':
self.calendar = calendar
self.datetime_compatible = True
assert_valid_date(self, is_leap_proleptic_gregorian, False, has_year_zero=has_year_zero)
@@ -1275,7 +1290,7 @@ The default format of the string produced by strftime is controlled by self.form
if getattr(pydatetime, 'tzinfo',None) is not None:
pydatetime = pydatetime.replace(tzinfo=None) - pydatetime.utcoffset()
compatible_date =\
- calendar == 'proleptic_gregorian' or \
+ calendar == 'proleptic_gregorian' or calendar == 'tai' or \
(calendar in ['gregorian','standard'] and (pydatetime.year > 1582 or \
(pydatetime.year == 1582 and pydatetime.month > 10) or \
(pydatetime.year == 1582 and pydatetime.month == 10 and pydatetime.day > 15)))
@@ -1480,7 +1495,7 @@ The default format of the string produced by strftime is controlled by self.form
units = 'days since -4712-1-1-12'
else:
units = 'days since -4713-1-1-12'
- elif calendar == 'proleptic_gregorian':
+ elif calendar == 'proleptic_gregorian' or calendar == 'tai':
if has_year_zero:
units = 'days since -4713-11-24-12'
else:
@@ -1566,6 +1581,9 @@ The default format of the string produced by strftime is controlled by self.form
elif calendar == 'proleptic_gregorian':
#return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),calendar=calendar,has_year_zero=has_year_zero)
return DatetimeProlepticGregorian(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=has_year_zero)
+ elif calendar == 'tai':
+ #return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),calendar=calendar,has_year_zero=has_year_zero)
+ return DatetimeTAI(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=has_year_zero)
else:
return NotImplemented
@@ -1626,6 +1644,10 @@ datetime object."""
#return self.__class__(*add_timedelta(self, -other,
# is_leap_proleptic_gregorian, False, has_year_zero),calendar=self.calendar,has_year_zero=self.has_year_zero)
return DatetimeProlepticGregorian(*add_timedelta(self, -other, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=self.has_year_zero)
+ elif self.calendar == 'tai':
+ #return self.__class__(*add_timedelta(self, -other,
+ # is_leap_proleptic_gregorian, False, has_year_zero),calendar=self.calendar,has_year_zero=self.has_year_zero)
+ return DatetimeTAI(*add_timedelta(self, -other, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=self.has_year_zero)
else:
return NotImplemented
else:
@@ -1692,7 +1714,7 @@ cdef _strftime(datetime dt, fmt):
year = year + ((2000 - year) // 28) * 28
timetuple = dt.timetuple()
s1 = time.strftime(fmt1, (year,) + timetuple[1:])
- twodigityear = 'y' in fmt1
+ twodigityear = '%y' in fmt1
if twodigityear:
sites1 = _findall(s1, str(year)[-2:])
else:
@@ -1944,7 +1966,7 @@ cdef _is_leap(int year, calendar, has_year_zero=None):
tyear = year + 1
else:
tyear = year
- if calendar == 'proleptic_gregorian' or (calendar == 'standard' and year > 1581):
+ if calendar == 'proleptic_gregorian' or calendar == 'tai' or (calendar == 'standard' and year > 1581):
if tyear % 4: # not divisible by 4
leap = False
elif tyear % 100: # not divisible by 100
@@ -2040,7 +2062,7 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
elif calendar == '366_day':
return year*366 + _cumdayspermonth_leap[month-1] + day - 1
- # handle standard, julian, proleptic_gregorian calendars.
+ # handle standard, julian, tai, proleptic_gregorian calendars.
if year == 0 and not has_year_zero:
raise ValueError('year zero does not exist in the %s calendar' %\
calendar)
@@ -2066,7 +2088,7 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
jday_greg -= 31739
if calendar == 'julian':
return jday_jul
- elif calendar == 'proleptic_gregorian':
+ elif calendar == 'proleptic_gregorian' or calendar == 'tai':
return jday_greg
elif calendar in ['standard','gregorian']:
# check for invalid days in mixed calendar (there are 10 missing)
@@ -2080,7 +2102,7 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
else:
return jday_greg
-# legacy calendar specific sub-classes (will be removed in a future release).
+# legacy calendar specific sub-classes (may be removed in a future release).
@cython.embedsignature(True)
cdef class DatetimeNoLeap(datetime):
@@ -2141,3 +2163,13 @@ but allows for dates that don't exist in the proleptic gregorian calendar.
def __init__(self, *args, **kwargs):
kwargs['calendar']='proleptic_gregorian'
super().__init__( *args, **kwargs)
+
+ at cython.embedsignature(True)
+cdef class DatetimeTAI(datetime):
+ """
+Phony datetime object which mimics the python datetime object,
+but allows for dates that don't exist in the proleptic gregorian calendar.
+ """
+ def __init__(self, *args, **kwargs):
+ kwargs['calendar']='tai'
+ super().__init__( *args, **kwargs)
=====================================
test/test_cftime.py
=====================================
@@ -3,7 +3,7 @@ from cftime import datetime as datetimex
from cftime import real_datetime
from cftime import (Datetime360Day, DatetimeAllLeap,
DatetimeGregorian, DatetimeJulian, DatetimeNoLeap,
- DatetimeProlepticGregorian, _parse_date,
+ DatetimeProlepticGregorian, DatetimeTAI, _parse_date,
date2index, date2num, num2date, UNIT_CONVERSION_FACTORS)
import copy
import unittest
@@ -204,6 +204,9 @@ class cftimeTestCase(unittest.TestCase):
self.assertTrue(d.strftime(dateformat) == d2.strftime(dateformat))
# make sure two digit years work in strftime (issue #362)
self.assertTrue(d.strftime(dateformat2) == d2.strftime(dateformat2))
+ # issue #401 (check for %y not y)
+ t = cftime.DatetimeGregorian(2026,4,14,0)
+ self.assertTrue(t.strftime('year=%Y') == 'year=2026')
# check day of year.
ndayr = d.timetuple()[7]
self.assertTrue(ndayr == 125)
@@ -2170,6 +2173,22 @@ def test_date2num_missing_data():
assert out is np.ma.masked
+def test_date2num_numpy_datetime64():
+ # Array
+ array = np.array([
+ np.datetime64(123, "s"),
+ np.datetime64(124, "s"),
+ np.datetime64(125, "s")
+ ])
+ out = date2num(array, units="seconds since 1970-01-01T00:00:00", calendar="gregorian")
+ assert ((out == np.array([123, 124, 125]))).all()
+
+ # Scalar
+ array = np.datetime64(123, "s")
+ out = date2num(array, units="seconds since 1970-01-01T00:00:00", calendar="gregorian")
+ assert out == np.int64(123)
+
+
def test_num2date_preserves_shape():
# The optimized num2date algorithm operates on a flattened array. This
# check ensures that the original shape of the times is restored in the
@@ -2248,6 +2267,77 @@ def test_num2date_precision():
assert np.ma.is_masked(date2[0])
assert date[1] == date2[1]
+# NOTE: using pytest style tests -- these won't run without pytest
+# but it looks like you're using pytest, so this is cleaner and easier
+
+# There's really no reason to put these in a class, but it does organize things
+class Test_tai:
+ """
+ tests specific to the tai calendar
+ """
+ def test_dateparse_valid(self):
+ """
+ It should raise for an epoch before 1958
+ """
+ # This is directly testing the _dateparse function
+ # Which is a "proper" unit test, but also testing an internal function.
+ timestring = "seconds since 1958-01-01T00:00:00"
+ basedate = cftime._dateparse(timestring, 'tai')
+
+ print(repr(basedate))
+
+ assert basedate == cftime.datetime(1958, 1, 1, 0, 0, 0, 0, calendar='tai', has_year_zero=False)
+
+ def test_dateparse_before_1958(self):
+ """
+ It should raise for an epoch before 1958
+ """
+ # This is directly testing the _dateparse function
+ # Which is a "proper" unit test, but also testing an internal function.
+ timestring = "seconds since 1957-01-01T00:00:00"
+ with pytest.raises(ValueError):
+ basedate = cftime._dateparse(timestring, 'tai')
+ # cftime._dateparse(timestring, 'tai', has_year_zero=None)
+ print(basedate)
+
+ def test_dateparse_with_offset(self):
+ """
+ It should raise if there's an offset
+ """
+ # This is directly testing the _dateparse function
+ # Which is a "proper" unit test, but also testing an internal function.
+ timestring = "seconds since 1965-01-01T00:00:00+08:00"
+ with pytest.raises(ValueError):
+ basedate = cftime._dateparse(timestring, 'tai')
+ # cftime._dateparse(timestring, 'tai', has_year_zero=None)
+ print(basedate)
+
+ def test_year_zero(self):
+ """
+ tai does not have a year zero -- not sure this is worth testing, but for full coverage.
+ """
+ # This is directly testing the _dateparse function
+ # Which is a "proper" unit test, but also testing an internal function.
+ timestring = "seconds since 1965-01-01T00:00:00"
+ with pytest.raises(ValueError):
+ basedate = cftime._dateparse(timestring, 'tai', has_year_zero=True)
+ # cftime._dateparse(timestring, 'tai', has_year_zero=None)
+ print(basedate)
+
+ def test_creation_valid(self):
+ dt = DatetimeTAI(2025, 1, 9, 14, 18)
+
+ assert dt.calendar == 'tai'
+ assert dt.has_year_zero is False
+
+ print(repr(dt))
+
+ def test_creation_before_1958(self):
+ with pytest.raises(ValueError):
+ dt = DatetimeTAI(1957, 1, 9, 14, 18)
+ print(repr(dt))
+
+
if __name__ == '__main__':
unittest.main()
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/2111e831a4c3df831d7a3a3142498e9e8fecec37...2169d617487bf503374c32372751ec9305f415a9
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/2111e831a4c3df831d7a3a3142498e9e8fecec37...2169d617487bf503374c32372751ec9305f415a9
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20260918/9f86d2c4/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list