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> { debug_assert!(PyObject_IsInstance(dt, state.py_api.DateTimeType.cast()).is_positive()); - let tzinfo = PyDateTime_DATE_GET_TZINFO(dt); + let tzinfo = get_tzinfo(dt); Ok(match PyObject_IsInstance(tzinfo, state.timezone_type) { 1 => OffsetDateTime::new( Date { @@ -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); @@ -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(|| { @@ -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(|| { diff --git a/src/time.rs b/src/time.rs index 3a79d92a..dc2aa5ce 100644 --- a/src/time.rs +++ b/src/time.rs @@ -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_tzinfo(time) != Py_None() { Err(value_err!("time with timezone is not supported"))? } // FUTURE: check `fold=0`? diff --git a/src/utc_datetime.rs b/src/utc_datetime.rs index eab78efb..4fea5f06 100644 --- a/src/utc_datetime.rs +++ b/src/utc_datetime.rs @@ -192,7 +192,7 @@ impl Instant { } pub(crate) unsafe fn from_py(dt: *mut PyObject, state: &State) -> Option { - let tzinfo = PyDateTime_DATE_GET_TZINFO(dt); + let tzinfo = get_tzinfo(dt); (tzinfo == state.py_api.TimeZone_UTC).then_some(Instant::from_datetime( Date { year: PyDateTime_GET_YEAR(dt) as u16, @@ -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() || tzinfo == module.py_api.TimeZone_UTC) { Err(value_err!( "datetime must have UTC tzinfo, but got {}", @@ -798,7 +798,7 @@ unsafe fn to_tz(slf: &mut PyObject, tz: &mut PyObject) -> PyReturn { .. } = State::for_obj(slf); let DateTime { date, time } = Instant::extract(slf).to_datetime(); - let zoneinfo = PyObject_CallOneArg(zoneinfo_type, tz).as_result()?; + let zoneinfo = call1(zoneinfo_type, tz)?; defer_decref!(zoneinfo); ZonedDateTime::from_utc(py_api, date, time, zoneinfo)?.to_obj(zoned_datetime_type) } @@ -857,23 +857,21 @@ unsafe fn parse_rfc2822(cls: *mut PyObject, s_obj: *mut PyObject) -> PyReturn { if !s_obj.is_str() { Err(type_err!("Expected a string"))?; } - 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 - } - })?; + 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 + } + })?; } #[cfg(Py_3_10)] { - dt = PyObject_CallOneArg(state.parse_rfc2822, s_obj).as_result()?; + dt = call1(state.parse_rfc2822, s_obj)?; } defer_decref!(dt); - let tzinfo = PyDateTime_DATE_GET_TZINFO(dt); + let tzinfo = get_tzinfo(dt); if tzinfo == state.py_api.TimeZone_UTC || (tzinfo == Py_None() && s_obj.to_str()?.unwrap().contains("-0000")) { diff --git a/src/zoned_datetime.rs b/src/zoned_datetime.rs index ac22d7cd..24cdefdc 100644 --- a/src/zoned_datetime.rs +++ b/src/zoned_datetime.rs @@ -101,9 +101,9 @@ impl ZonedDateTime { }: Time, zoneinfo: *mut PyObject, ) -> PyResult { - let dt = PyObject_CallMethodOneArg( + let dt = methcall1( zoneinfo, - steal!("fromutc".to_py()?), + "fromutc", steal!(DateTime_FromDateAndTime( year.into(), month.into(), @@ -115,8 +115,7 @@ impl ZonedDateTime { zoneinfo, DateTimeType, )), - ) - .as_result()?; + )?; defer_decref!(dt); // Don't need to use the checked constructor since we know @@ -264,7 +263,7 @@ unsafe fn __new__(cls: *mut PyTypeObject, args: *mut PyObject, kwargs: *mut PyOb if tz.is_null() { return Err(type_err!("tz argument is required")); } - let zoneinfo = PyObject_CallOneArg(zoneinfo_type, tz).as_result()?; + let zoneinfo = call1(zoneinfo_type, tz)?; defer_decref!(zoneinfo); let date = Date::from_longs(year, month, day).ok_or_value_err("Invalid date")?; @@ -543,7 +542,7 @@ unsafe fn to_tz(slf: &mut PyObject, tz: &mut PyObject) -> PyReturn { py_api, .. } = State::for_type(cls); - let new_zoneinfo = PyObject_CallOneArg(zoneinfo_type, tz).as_result()?; + let new_zoneinfo = call1(zoneinfo_type, tz)?; defer_decref!(new_zoneinfo); let zdt = ZonedDateTime::extract(slf); let DateTime { date, time, .. } = zdt.without_offset().small_shift_unchecked(-zdt.offset_secs); @@ -560,7 +559,7 @@ pub(crate) unsafe fn unpickle(module: &mut PyObject, args: &[*mut PyObject]) -> .. } = State::for_mod(module); let mut packed = args[0].to_bytes()?.ok_or_type_err("Invalid pickle data")?; - let zoneinfo = PyObject_CallOneArg(zoneinfo_type, args[1]).as_result()?; + let zoneinfo = call1(zoneinfo_type, args[1])?; defer_decref!(zoneinfo); if packed.len() != mem::size_of::() + mem::size_of::() * 5 + mem::size_of::() * 2 { @@ -605,9 +604,9 @@ unsafe fn py_datetime(slf: &mut PyObject, _: &mut PyObject) -> PyReturn { }, .. } = State::for_obj(slf); - PyObject_CallMethodOneArg( + methcall1( zdt.zoneinfo, - steal!("fromutc".to_py()?), + "fromutc", steal!(DateTime_FromDateAndTime( year.into(), month.into(), @@ -620,7 +619,6 @@ unsafe fn py_datetime(slf: &mut PyObject, _: &mut PyObject) -> PyReturn { DateTimeType, )), ) - .as_result() } unsafe fn to_utc(slf: &mut PyObject, _: &mut PyObject) -> PyReturn { @@ -825,7 +823,7 @@ unsafe fn replace( .to_long()? .ok_or_type_err("nanosecond must be an integer")? } else if name == str_tz { - zoneinfo = PyObject_CallOneArg(zoneinfo_type, value).as_result()?; + zoneinfo = call1(zoneinfo_type, value)?; defer_decref!(zoneinfo); } else if name == str_disambiguate { dis = Disambiguate::parse( @@ -873,7 +871,7 @@ unsafe fn now(cls: *mut PyObject, tz: *mut PyObject) -> PyReturn { zoneinfo_type, .. } = State::for_type(cls.cast()); - let zoneinfo = PyObject_CallOneArg(zoneinfo_type, tz).as_result()? as *mut PyObject; + let zoneinfo = call1(zoneinfo_type, tz)? as *mut PyObject; defer_decref!(zoneinfo); let (timestamp, subsec) = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) { Ok(dur) => (dur.as_secs() as f64, dur.subsec_nanos()), @@ -918,7 +916,7 @@ unsafe fn from_py_datetime(cls: *mut PyObject, dt: *mut PyObject) -> PyReturn { if PyDateTime_Check(dt) == 0 { Err(type_err!("Argument must be a datetime.datetime instance"))?; } - let tzinfo = PyDateTime_DATE_GET_TZINFO(dt); + let tzinfo = get_tzinfo(dt); // NOTE: it has to be exactly a `ZoneInfo`, since subclasses // could theoretically introduce circular references. @@ -1042,7 +1040,7 @@ unsafe fn check_from_timestamp_args_return_zoneinfo( args.len() + kwargs.len() )) } else if kwargs[0].0 == str_tz { - PyObject_CallOneArg(zoneinfo_type, kwargs[0].1).as_result() + call1(zoneinfo_type, kwargs[0].1) } else { Err(type_err!( "{}() got an unexpected keyword argument {}", @@ -1193,11 +1191,10 @@ unsafe fn parse_common_iso(cls: *mut PyObject, s_obj: *mut PyObject) -> PyReturn exc_invalid_offset, .. } = State::for_type(cls.cast()); - let zoneinfo = PyObject_CallOneArg( + let zoneinfo = call1( zoneinfo_type, steal!(std::str::from_utf8_unchecked(&s[1..s.len() - 1]).to_py()?), - ) - .as_result()?; + )?; defer_decref!(zoneinfo); let offset_valid = match OffsetResult::for_tz(py_api, date, time, zoneinfo)? { OffsetResult::Unambiguous(o) => o == offset_secs, diff --git a/tests/test_local_datetime.py b/tests/test_local_datetime.py index 6fabdecf..efd68301 100644 --- a/tests/test_local_datetime.py +++ b/tests/test_local_datetime.py @@ -1,4 +1,5 @@ import pickle +import platform import re from copy import copy, deepcopy from datetime import datetime as py_datetime, timedelta, timezone @@ -1388,7 +1389,11 @@ def test_invalid(self): with pytest.raises(TypeError, match="foo"): d.replace_time(Time(1, 2, 3), foo="raise") # type: ignore[call-arg] + @pytest.mark.skipif( + platform.machine() == "armv7l", reason="time_t out of range" + ) def test_out_of_range_due_to_offset(self): + print(f"platform.machine() == {platform.machine()}") with local_ams_tz(): d = UTCDateTime.MIN.to_local_system() with pytest.raises(