Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rust extension module #101

Merged
merged 26 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
94f31ed
Rust extension module with maturin
ariebovenberg Mar 29, 2024
18a83b4
Make rust extension optional
ariebovenberg Apr 1, 2024
7b81dd6
First classes in rust extension
ariebovenberg Apr 2, 2024
2fc7ccf
Add other datetime types, redesign DateDelta
ariebovenberg Apr 29, 2024
fc2f77f
Improve error handling
ariebovenberg May 20, 2024
a060e69
Add missing methods, improve error handling
ariebovenberg May 28, 2024
7b1d82c
patch up GC/ref handling, 3.13 support
ariebovenberg Jun 10, 2024
5d7ce10
Improve docs, get pure-Python version working again
ariebovenberg Jun 10, 2024
0c39c8c
rationalize method names
ariebovenberg Jun 13, 2024
2e6e53a
fix leftover todos and coverage
ariebovenberg Jun 13, 2024
6cc336c
Fixes for wheels across platforms
ariebovenberg Jun 16, 2024
def6619
fix image link in README
ariebovenberg Jun 19, 2024
3b43929
Additional wheels for other archs
ariebovenberg Jun 19, 2024
a4c0e8f
add flag to see whether rust extension is loaded
ariebovenberg Jun 20, 2024
e5bfa10
additional testing for internal function
ariebovenberg Jun 20, 2024
857caab
add benchmark readme
ariebovenberg Jun 20, 2024
1ef0ab7
docs improvements
ariebovenberg Jun 20, 2024
d517867
small refactorings across extension module
ariebovenberg Jun 20, 2024
9475917
improve robustness of conversions near bounds
ariebovenberg Jun 23, 2024
14876cf
Drop "local" terminolgy from system timezone
ariebovenberg Jun 23, 2024
38cfb27
fix failing tests
ariebovenberg Jun 24, 2024
d93a57f
Replace UTCDateTime with Instant
ariebovenberg Jun 25, 2024
f18b5ba
Rename naive to local, cleanup docs
ariebovenberg Jun 28, 2024
1d4b722
make disambiguate argument required for relevant methods
ariebovenberg Jun 28, 2024
7c35f86
Add ignore_dst and disambiguate arg to relevant methods
ariebovenberg Jun 29, 2024
ce7885a
prepare next release
ariebovenberg Jul 4, 2024
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
Prev Previous commit
Next Next commit
Fixes for wheels across platforms
  • Loading branch information
ariebovenberg committed Jun 18, 2024
commit 6cc336cf1b0e1bcaa01bfa3cce6f4ac4c8876420
9 changes: 2 additions & 7 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
workflow_dispatch:

jobs:
test-python-version:
test-python-versions:
name: Test Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
Expand All @@ -20,11 +20,6 @@ jobs:
"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
Expand Down Expand Up @@ -88,7 +83,7 @@ jobs:
"3.11",
"3.12",
"3.13-dev",
# NOTE: pypy/pytest fails sometimes (https://github.com/pypy/pypy/issues/3959)
# # NOTE: pypy/pytest fails sometimes (https://github.com/pypy/pypy/issues/3959)
"pypy3.9",
"pypy3.10"
]
Expand Down
13 changes: 5 additions & 8 deletions .github/workflows/publish.yml → .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -31,7 +32,7 @@ jobs:
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: cibw-wheels-linux-${{ matrix.target }}
name: wheels-linux-${{ matrix.target }}
path: dist/*.whl

windows:
Expand Down Expand Up @@ -75,7 +76,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: wheels-macos-${{ matrix.target }}
path: dist
path: dist/*.whl

sdist:
runs-on: ubuntu-latest
Expand All @@ -101,11 +102,7 @@ jobs:
- name: Publish to PyPI
run: |
pip install twine
twine upload dist/*
twine upload --non-interactive --disable-progress-bar --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-*/*
8 changes: 4 additions & 4 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

</div>

Expand All @@ -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)
Expand All @@ -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: ...
```

Expand All @@ -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:

<div align="center">

| | Whenever | datetime | Arrow | Pendulum |
|-------------------|:--------:|:--------:|:-----:|:--------:|
| DST-safe | ✅ | ❌ | ❌ | ⚠️ |
| Typed aware/naive | ✅ | ❌ | ❌ | ❌ |
| Fast | ✅ | ✅ | ❌ | ❌ |

</div>

[**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,
Expand All @@ -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).
Expand Down
22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ 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" }

[[tool.cibuildwheel.overrides]]
select = "*-musllinux*"
before-all = "curl -sSf https://sh.rustup.rs | sh -s -- -y && apk add tzdata"

[build-system]
build-backend = "setuptools.build_meta"
requires = ["setuptools", "wheel", "setuptools-rust"]
53 changes: 51 additions & 2 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i32> {
// 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))
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -898,6 +898,55 @@ 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_dt_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
}
}

#[inline]
pub(crate) unsafe fn get_time_tzinfo(dt: *mut PyObject) -> *mut PyObject {
#[cfg(Py_3_10)]
{
PyDateTime_TIME_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]
Expand Down
10 changes: 4 additions & 6 deletions src/local_datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ impl OffsetDateTime {
pub(crate) unsafe fn to_local_system(self, py_api: &PyDateTime_CAPI) -> PyResult<Self> {
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 {
Expand All @@ -81,8 +80,7 @@ impl Instant {
) -> PyResult<OffsetDateTime> {
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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions src/naive_datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_dt_tzinfo(dt);
if tzinfo != Py_None() {
Err(value_err!(
"datetime must be naive, but got tzinfo={}",
Expand Down Expand Up @@ -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_dt_tzinfo(parsed);
if tzinfo != Py_None() {
Err(value_err!(
"datetime must be naive, but got tzinfo={}",
Expand Down Expand Up @@ -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 {
Expand Down
27 changes: 12 additions & 15 deletions src/offset_datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<Self>> {
debug_assert!(PyObject_IsInstance(dt, state.py_api.DateTimeType.cast()).is_positive());
let tzinfo = PyDateTime_DATE_GET_TZINFO(dt);
let tzinfo = get_dt_tzinfo(dt);
Ok(match PyObject_IsInstance(tzinfo, state.timezone_type) {
1 => OffsetDateTime::new(
Date {
Expand Down Expand Up @@ -389,7 +389,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.without_offset().small_shift_unchecked(-odt.offset_secs);
Expand Down Expand Up @@ -859,17 +859,16 @@ unsafe fn format_rfc2822(slf: *mut PyObject, _: *mut PyObject) -> PyReturn {
py_api: datetime_api,
..
} = State::for_obj(slf);
PyObject_CallOneArg(
call1(
format_rfc2822,
OffsetDateTime::extract(slf).to_py(datetime_api)?,
)
.as_result()
}

#[cfg(Py_3_10)]
unsafe fn parse_rfc2822(cls: *mut PyObject, s_obj: *mut PyObject) -> PyReturn {
let state = State::for_type(cls.cast());
let py_dt = PyObject_CallOneArg(state.parse_rfc2822, s_obj).as_result()?;
let py_dt = call1(state.parse_rfc2822, s_obj)?;
defer_decref!(py_dt);
OffsetDateTime::from_py(py_dt, state)?
.ok_or_else(|| {
Expand All @@ -889,16 +888,14 @@ unsafe fn parse_rfc2822(cls: *mut PyObject, s_obj: *mut PyObject) -> PyReturn {
if !s_obj.is_str() {
Err(type_err!("Argument must be a string"))?
}
let py_dt = PyObject_CallOneArg(state.parse_rfc2822, s_obj)
.as_result()
.map_err(|e| {
if PyErr_ExceptionMatches(PyExc_TypeError) != 0 {
PyErr_Clear();
value_err!("Invalid format: {}", s_obj.repr())
} else {
e
}
})?;
let py_dt = call1(state.parse_rfc2822, s_obj).map_err(|e| {
if PyErr_ExceptionMatches(PyExc_TypeError) != 0 {
PyErr_Clear();
value_err!("Invalid format: {}", s_obj.repr())
} else {
e
}
})?;
defer_decref!(py_dt);
OffsetDateTime::from_py(py_dt, state)?
.ok_or_else(|| {
Expand Down
2 changes: 1 addition & 1 deletion src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ unsafe fn from_py_time(type_: *mut PyObject, time: *mut PyObject) -> PyReturn {
if PyTime_Check(time) == 0 {
Err(type_err!("argument must be a Time"))?
}
if PyDateTime_TIME_GET_TZINFO(time) != Py_None() {
if get_time_tzinfo(time) != Py_None() {
Err(value_err!("time with timezone is not supported"))?
}
// FUTURE: check `fold=0`?
Expand Down
Loading
Loading