diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 244af02a..69dc79c0 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -8,73 +8,68 @@ on:
workflow_dispatch:
jobs:
- test-python-version:
- name: Test Python ${{ matrix.python-version }}
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- python-version: [
- "3.9",
- "3.10",
- "3.11",
- "3.12",
- "3.13-dev",
- # FUTURE: pypy builds current fail. Uncomment when fixed.
- # Low prio because pure-Python version is available.
- # NOTE: pypy/pytest fails sometimes (https://github.com/pypy/pypy/issues/3959)
- # "pypy3.9",
- # "pypy3.10"
- ]
- steps:
- - uses: actions/checkout@v4
- - uses: actions-rust-lang/setup-rust-toolchain@v1
- - uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
+ # test-python-versions:
+ # name: Test Python ${{ matrix.python-version }}
+ # runs-on: ubuntu-latest
+ # strategy:
+ # fail-fast: false
+ # matrix:
+ # python-version: [
+ # "3.9",
+ # "3.10",
+ # "3.11",
+ # "3.12",
+ # "3.13-dev",
+ # ]
+ # steps:
+ # - uses: actions/checkout@v4
+ # - uses: actions-rust-lang/setup-rust-toolchain@v1
+ # - uses: actions/setup-python@v5
+ # with:
+ # python-version: ${{ matrix.python-version }}
- - name: "Test Rust"
- if: ${{ (matrix.os == 'ubuntu-latest') && (matrix.python-version == '3.12') }}
- run: |
- cargo test
+ # - name: "Test Rust"
+ # if: ${{ (matrix.os == 'ubuntu-latest') && (matrix.python-version == '3.12') }}
+ # run: |
+ # cargo test
- - name: Install and test
- shell: bash
- run: |
- pip install .
- pip install -r requirements/test.txt
- pytest tests/
+ # - name: Install and test
+ # shell: bash
+ # run: |
+ # pip install .
+ # pip install -r requirements/test.txt
+ # pytest tests/
- Test-os:
- name: Test on ${{ matrix.os }}
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
- steps:
- - uses: actions/checkout@v4
- - uses: actions-rust-lang/setup-rust-toolchain@v1
+ # Test-os:
+ # name: Test on ${{ matrix.os }}
+ # runs-on: ${{ matrix.os }}
+ # strategy:
+ # fail-fast: false
+ # matrix:
+ # os: [ubuntu-latest, windows-latest, macos-latest]
+ # steps:
+ # - uses: actions/checkout@v4
+ # - uses: actions-rust-lang/setup-rust-toolchain@v1
- - uses: actions/setup-python@v5
- if: ${{ !(matrix.os == 'windows-latest') }}
- with:
- python-version: '3.12'
+ # - uses: actions/setup-python@v5
+ # if: ${{ !(matrix.os == 'windows-latest') }}
+ # with:
+ # python-version: '3.12'
- # ensure 32-bit target is tested
- - uses: actions/setup-python@v5
- if: ${{ matrix.os == 'windows-latest' }}
- with:
- python-version: '3.12'
- architecture: x86
+ # # ensure 32-bit target is tested
+ # - uses: actions/setup-python@v5
+ # if: ${{ matrix.os == 'windows-latest' }}
+ # with:
+ # python-version: '3.12'
+ # architecture: x86
- - name: Install and test
- shell: bash
- run: |
- pip install -e .
- pip install -r requirements/test.txt
- pytest tests/
+ # - name: Install and test
+ # shell: bash
+ # run: |
+ # pip install -e .
+ # pip install -r requirements/test.txt
+ # pytest tests/
test-pure-python:
name: Test pure Python version
@@ -83,14 +78,14 @@ jobs:
fail-fast: false
matrix:
python-version: [
- "3.9",
- "3.10",
- "3.11",
+ # "3.9",
+ # "3.10",
+ # "3.11",
"3.12",
- "3.13-dev",
- # NOTE: pypy/pytest fails sometimes (https://github.com/pypy/pypy/issues/3959)
- "pypy3.9",
- "pypy3.10"
+ # "3.13-dev",
+ # # NOTE: pypy/pytest fails sometimes (https://github.com/pypy/pypy/issues/3959)
+ # "pypy3.9",
+ # "pypy3.10"
]
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/publish.yml b/.github/workflows/wheels.yml
similarity index 92%
rename from .github/workflows/publish.yml
rename to .github/workflows/wheels.yml
index 30cac681..6e404797 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/wheels.yml
@@ -16,6 +16,7 @@ jobs:
linux:
runs-on: ubuntu-latest
strategy:
+ fail-fast: false # TODO: unset
matrix:
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
steps:
@@ -101,11 +102,7 @@ jobs:
- name: Publish to PyPI
run: |
pip install twine
- twine upload dist/*
+ twine upload --non-interactive --skip-existing wheels-*/*
env:
- TWINE_UESRNAME: __token__
+ TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
- TWINE_REPOSITORY: testpypi
- with:
- command: upload
- args: --non-interactive --skip-existing wheels-*/*
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 2e427f1c..d2dec0e5 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,13 +16,13 @@
**Rationale**: Nanosecond precision is the standard for most modern
datetime libraries.
-- Unified `(from_)canonical_format` methods with `(from_)common_iso8601` methods
- into `(format|parse)_common_iso` methods.
+- Unified `[from_]canonical_format` methods with `[from_]common_iso8601` methods
+ into `[format|parse]_common_iso` methods.
**Rationale**: This cuts down on the number of methods; the performance benefits
- aren't worth the extra clutter.
+ of separate methods aren't worth the clutter.
-- Renamed `(from_)(rfc3339|rfc2822)` methods to `(format|parse)_(rfc3339|rfc2822)`.
+- Renamed `[from_][rfc3339|rfc2822]` methods to `[format|parse]_[rfc3339|rfc2822]`.
**Rationale**: Consistency with other methods.
diff --git a/Cargo.toml b/Cargo.toml
index b0b6eebc..3c26d109 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,8 +29,8 @@ extension-module = ["pyo3/extension-module"]
name = "benchmarks"
path = "benchmarks/rust/main.rs"
-# TODO: replace git repos with proper versions once this fix has
-# been released: https://github.com/PyO3/pyo3/issues/4093
+# TODO: replace git repos with proper versions once 0.22 is released.
+# we're waiting on https://github.com/PyO3/pyo3/issues/4093
[dependencies]
# pyo3-ffi = { version = "^0.21.0", default_features = false, features = ["extension-module"]}
# pyo3 = { version = "^0.21.0", features = ["extension-module"] }
diff --git a/README.md b/README.md
index 138b4237..6f5e1ed4 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ It's also **way faster** than other third-party libraries—and usually the stan
[🚀 Changelog](https://whenever.readthedocs.io/en/latest/changelog.html) |
[❓ FAQ](https://whenever.readthedocs.io/en/latest/faq.html) |
[🗺️ Roadmap](#roadmap) |
-[💬 Issues & discussions](https://github.com/ariebovenberg/whenever/issues)
+[💬 Issues & feedback](https://github.com/ariebovenberg/whenever/issues)
@@ -65,7 +65,7 @@ Two points stand out:
```
Note this isn't a bug, but a design decision that DST is only considered
- when calculations involve *two different timezones*.
+ when calculations involve *two* timezones.
If you think this is surprising, you
[are](https://github.com/python/cpython/issues/91618)
[not](https://github.com/python/cpython/issues/116035)
@@ -76,7 +76,7 @@ Two points stand out:
but there's no way to enforce this in the type system!
```python
- # Should this be a naive or aware datetime? Can't tell!
+ # Does this expect naive or aware? Can't tell!
def schedule_meeting(at: datetime) -> None: ...
```
@@ -85,12 +85,16 @@ Two points stand out:
There are two other popular third-party libraries, but they don't (fully)
address these issues. Here's how they compare to *whenever* and the standard library:
+
+
| | Whenever | datetime | Arrow | Pendulum |
|-------------------|:--------:|:--------:|:-----:|:--------:|
| DST-safe | ✅ | ❌ | ❌ | ⚠️ |
| Typed aware/naive | ✅ | ❌ | ❌ | ❌ |
| Fast | ✅ | ✅ | ❌ | ❌ |
+
+
[**Arrow**](https://pypi.org/project/arrow/)
is probably the most historically popular 3rd party datetime library.
It attempts to provide a more "friendly" API than the standard library,
@@ -100,7 +104,7 @@ of types to just one (``arrow.Arrow``) means that it's even harder
for typecheckers to catch mistakes.
[**Pendulum**](https://pypi.org/project/pendulum/)
-came in the scene in 2016, promising better DST-handling,
+arrived on the scene in 2016, promising better DST-handling,
as well as improved performance.
However, it only fixes [*some* DST-related pitfalls](https://dev.arie.bovenberg.net/blog/python-datetime-pitfalls/#datetime-library-scorecard),
and its performance has significantly [degraded over time](https://github.com/sdispater/pendulum/issues/818).
diff --git a/pyproject.toml b/pyproject.toml
index d6b12f24..e48607d5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,6 +83,23 @@ module = [
]
ignore_missing_imports = true
+[tool.cibuildwheel]
+skip = ["pp*", "*-musllinux_i686"]
+test-command = "pytest -s {project}/tests"
+test-requires = "pytest pytest-benchmark hypothesis pytest-mypy-plugins"
+environment = {
+ PATH = "$HOME/.cargo/bin:$PATH",
+}
+
+[tool.cibuildwheel.linux]
+before-all = "curl -sSf https://sh.rustup.rs | sh -s -- -y"
+
+[tool.cibuildwheel.windows]
+before-all = "rustup target add i686-pc-windows-msvc"
+environment = {
+ PATH = "$UserProfile\\.cargo\\bin;$PATH"
+}
+
[build-system]
build-backend = "setuptools.build_meta"
requires = ["setuptools", "wheel", "setuptools-rust"]
diff --git a/src/common.rs b/src/common.rs
index fc33c5ff..35e98dc7 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -621,7 +621,7 @@ pub(crate) unsafe fn newref<'a>(obj: *mut PyObject) -> &'a mut PyObject {
pub(crate) unsafe fn offset_from_py_dt(dt: *mut PyObject) -> PyResult {
// OPTIMIZE: is calling ZoneInfo.utcoffset() faster?
- let delta = PyObject_CallMethodNoArgs(dt, steal!("utcoffset".to_py()?)).as_result()?;
+ let delta = methcall0(dt, "utcoffset")?;
defer_decref!(delta);
Ok(PyDateTime_DELTA_GET_DAYS(delta) * 86_400 + PyDateTime_DELTA_GET_SECONDS(delta))
}
@@ -681,7 +681,7 @@ unsafe fn local_offset(
)
.as_result()?;
defer_decref!(naive);
- let aware = PyObject_CallMethodNoArgs(naive, steal!("astimezone".to_py()?)).as_result()?;
+ let aware = methcall0(naive, "astimezone")?;
defer_decref!(aware);
let kwargs = PyDict_New().as_result()?;
defer_decref!(kwargs);
@@ -898,6 +898,38 @@ pub(crate) const fn hashmask(hash: Py_hash_t) -> Py_hash_t {
}
}
+#[inline]
+pub(crate) unsafe fn call1(func: *mut PyObject, arg: *mut PyObject) -> PyReturn {
+ PyObject_CallOneArg(func, arg).as_result()
+}
+
+#[inline]
+pub(crate) unsafe fn methcall1(slf: *mut PyObject, name: &str, arg: *mut PyObject) -> PyReturn {
+ PyObject_CallMethodOneArg(slf, name.to_py()?, arg).as_result()
+}
+
+#[inline]
+pub(crate) unsafe fn methcall0(slf: *mut PyObject, name: &str) -> PyReturn {
+ PyObject_CallMethodNoArgs(slf, steal!(name.to_py()?)).as_result()
+}
+
+#[inline]
+pub(crate) unsafe fn get_tzinfo(dt: *mut PyObject) -> *mut PyObject {
+ #[cfg(Py_3_10)]
+ {
+ PyDateTime_DATE_GET_TZINFO(dt)
+ }
+ #[cfg(not(Py_3_10))]
+ {
+ let tzinfo = PyObject_GetAttrString(dt, c"tzinfo".as_ptr());
+ // To keep things consistent with the Py3.10 code above,
+ // we need to decref it, turning it into a borrowed reference.
+ // We can assume the parent datetime keeps it alive.
+ Py_DECREF(tzinfo);
+ tzinfo
+ }
+}
+
// from stackoverflow.com/questions/5889238
#[cfg(target_pointer_width = "64")]
#[inline]
diff --git a/src/local_datetime.rs b/src/local_datetime.rs
index eba8480d..a578cd7f 100644
--- a/src/local_datetime.rs
+++ b/src/local_datetime.rs
@@ -53,8 +53,7 @@ impl OffsetDateTime {
pub(crate) unsafe fn to_local_system(self, py_api: &PyDateTime_CAPI) -> PyResult {
let dt_original = self.to_py(py_api)?;
defer_decref!(dt_original);
- let dt_new =
- PyObject_CallMethodNoArgs(dt_original, steal!("astimezone".to_py()?)).as_result()?;
+ let dt_new = methcall0(dt_original, "astimezone")?;
defer_decref!(dt_new);
Ok(OffsetDateTime::new_unchecked(
Date {
@@ -81,8 +80,7 @@ impl Instant {
) -> PyResult {
let dt_utc = self.to_py(py_api)?;
defer_decref!(dt_utc);
- let dt_new =
- PyObject_CallMethodNoArgs(dt_utc, steal!("astimezone".to_py()?)).as_result()?;
+ let dt_new = methcall0(dt_utc, "astimezone")?;
defer_decref!(dt_new);
Ok(OffsetDateTime::new_unchecked(
Date {
@@ -376,7 +374,7 @@ unsafe fn to_tz(slf: *mut PyObject, tz: *mut PyObject) -> PyReturn {
zoned_datetime_type,
..
} = State::for_type(type_);
- let zoneinfo = PyObject_CallOneArg(zoneinfo_type, tz).as_result()?;
+ let zoneinfo = call1(zoneinfo_type, tz)?;
defer_decref!(zoneinfo);
let odt = OffsetDateTime::extract(slf);
let DateTime { date, time } = odt
@@ -648,7 +646,7 @@ unsafe fn now(cls: *mut PyObject, _: *mut PyObject) -> PyReturn {
)
.as_result()?;
defer_decref!(utc_dt);
- let local_dt = PyObject_CallMethodNoArgs(utc_dt, steal!("astimezone".to_py()?)).as_result()?;
+ let local_dt = methcall0(utc_dt, "astimezone")?;
defer_decref!(local_dt);
OffsetDateTime::new_unchecked(
Date {
diff --git a/src/naive_datetime.rs b/src/naive_datetime.rs
index 57919c2b..836b9b2d 100644
--- a/src/naive_datetime.rs
+++ b/src/naive_datetime.rs
@@ -491,7 +491,7 @@ unsafe fn from_py_datetime(type_: *mut PyObject, dt: *mut PyObject) -> PyReturn
if PyDateTime_Check(dt) == 0 {
Err(type_err!("argument must be datetime.datetime"))?
}
- let tzinfo = PyDateTime_DATE_GET_TZINFO(dt);
+ let tzinfo = get_tzinfo(dt);
if tzinfo != Py_None() {
Err(value_err!(
"datetime must be naive, but got tzinfo={}",
@@ -592,7 +592,7 @@ unsafe fn strptime(cls: *mut PyObject, args: &[*mut PyObject]) -> PyReturn {
)
.as_result()?;
defer_decref!(parsed);
- let tzinfo = PyDateTime_DATE_GET_TZINFO(parsed);
+ let tzinfo = get_tzinfo(parsed);
if tzinfo != Py_None() {
Err(value_err!(
"datetime must be naive, but got tzinfo={}",
@@ -657,7 +657,7 @@ unsafe fn assume_tz(
}
let dis = Disambiguate::from_only_kwarg(kwargs, str_disambiguate, "assume_tz")?;
- let zoneinfo = PyObject_CallOneArg(zoneinfo_type, args[0]).as_result()?;
+ let zoneinfo = call1(zoneinfo_type, args[0])?;
defer_decref!(zoneinfo);
ZonedDateTime::from_naive(py_api, date, time, zoneinfo, dis)?
.map_err(|e| match e {
diff --git a/src/offset_datetime.rs b/src/offset_datetime.rs
index 045cc84e..6e080dc7 100644
--- a/src/offset_datetime.rs
+++ b/src/offset_datetime.rs
@@ -127,7 +127,7 @@ impl OffsetDateTime {
// Returns None if the tzinfo is incorrect, or the UTC time is out of bounds
pub(crate) unsafe fn from_py(dt: *mut PyObject, state: &State) -> PyResult