Skip to content

Commit

Permalink
Fixes for wheels across platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
ariebovenberg committed Jun 18, 2024
1 parent 2e6e53a commit ba2f06b
Show file tree
Hide file tree
Showing 14 changed files with 147 additions and 83 deletions.
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
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 Down Expand Up @@ -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-*/*
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

0 comments on commit ba2f06b

Please sign in to comment.