diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fffcefc..fb5e4bfa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,9 +53,9 @@ jobs: { os: "windows-latest", python-architecture: "x86", rust-target: "i686-pc-windows-msvc" }, ] exclude: - # There is no 64-bit pypy on windows + # See https://github.com/PyO3/pyo3/pull/1215 for why. - python-version: pypy3 - platform: { os: "windows-latest", python-architecture: "x64" } + platform: { os: "windows-latest" } include: # Test minimal supported Rust version - rust: 1.39.0 diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml index 27458e63..8dee8876 100644 --- a/.github/workflows/guide.yml +++ b/.github/workflows/guide.yml @@ -5,6 +5,7 @@ on: branches: - master release: + types: [published] env: CARGO_TERM_COLOR: always @@ -12,6 +13,8 @@ env: jobs: deploy: runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.prepare_tag.outputs.tag_name }} steps: - uses: actions/checkout@v2 @@ -44,3 +47,23 @@ jobs: publish_dir: ./gh-pages-build/ destination_dir: ${{ steps.prepare_tag.outputs.tag_name }} full_commit_message: 'Upload documentation for ${{ steps.prepare_tag.outputs.tag_name }}' + + release: + needs: deploy + runs-on: ubuntu-latest + if: ${{ github.event_name == 'release' }} + steps: + - name: Create latest tag redirect + env: + TAG_NAME: ${{ needs.deploy.outputs.tag_name }} + run: | + mkdir public + echo "" > public/index.html + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3.7.0-8 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./public/ + full_commit_message: 'Release ${{ needs.deploy.outputs.tag_name }}' + keep_files: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0a9770..52a4d7b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,66 +7,81 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added -- Add FFI definitions `Py_FinalizeEx`, `PyOS_getsig`, `PyOS_setsig`. [#1021](https://github.com/PyO3/pyo3/pull/1021) -- Add `Python::with_gil` for executing a closure with the Python GIL. [#1037](https://github.com/PyO3/pyo3/pull/1037) -- Implement `Debug` for `PyIterator`. [#1051](https://github.com/PyO3/pyo3/pull/1051) -- Implement type information for conversion failures. [#1050](https://github.com/PyO3/pyo3/pull/1050) -- Add `PyBytes::new_with` and `PyByteArray::new_with` for initialising Python-allocated bytes and bytearrays using a closure. [#1074](https://github.com/PyO3/pyo3/pull/1074) -- Add `Py::as_ref` and `Py::into_ref`. [#1098](https://github.com/PyO3/pyo3/pull/1098) -- Add optional implementations of `ToPyObject`, `IntoPy`, and `FromPyObject` for [hashbrown](https://crates.io/crates/hashbrown)'s `HashMap` and `HashSet` types. The `hashbrown` feature must be enabled for these implementations to be built. [#1114](https://github.com/PyO3/pyo3/pull/1114/) -- Allow other `Result` types when using `#[pyfunction]`. [#1106](https://github.com/PyO3/pyo3/issues/1106). -- Add `#[derive(FromPyObject)]` macro for enums and structs. [#1065](https://github.com/PyO3/pyo3/pull/1065) -- Add macro attribute to `#[pyfn]` and `#[pyfunction]` to pass the module of a Python function to the function - body. [#1143](https://github.com/PyO3/pyo3/pull/1143) -- Add `add_function()` and `add_submodule()` functions to `PyModule` [#1143](https://github.com/PyO3/pyo3/pull/1143) -- Add native `PyCFunction` and `PyFunction` types, change `add_function` to take a wrapper returning - a `&PyCFunction`instead of `PyObject`. [#1163](https://github.com/PyO3/pyo3/pull/1163) +- Add support for keyword-only arguments without default values in `#[pyfunction]`. [#1209](https://github.com/PyO3/pyo3/pull/1209) +- Add a wrapper for `PyErr_CheckSignals()` as `Python::check_signals()`. [#1214](https://github.com/PyO3/pyo3/pull/1214) ### Changed -- Exception types have been renamed from e.g. `RuntimeError` to `PyRuntimeError`, and are now accessible by `&T` or `Py` similar to other Python-native types. The old names continue to exist but are deprecated. [#1024](https://github.com/PyO3/pyo3/pull/1024) [#1115](https://github.com/PyO3/pyo3/pull/1115) +- Fields of `PyMethodDef`, `PyGetterDef`, `PySetterDef`, and `PyClassAttributeDef` are now private. [#1169](https://github.com/PyO3/pyo3/pull/1169) + +### Fixed +- Fix invalid document for protocol methods. [#1169](https://github.com/PyO3/pyo3/pull/1169) + +## [0.12.1] - 2020-09-16 +### Fixed +- Fix building for a 32-bit Python on 64-bit Windows with a 64-bit Rust toolchain. [#1179](https://github.com/PyO3/pyo3/pull/1179) +- Fix building on platforms where `c_char` is `u8`. [#1182](https://github.com/PyO3/pyo3/pull/1182) + +## [0.12.0] - 2020-09-12 +### Added +- Add FFI definitions `Py_FinalizeEx`, `PyOS_getsig`, and `PyOS_setsig`. [#1021](https://github.com/PyO3/pyo3/pull/1021) +- Add `PyString::to_str` for accessing `PyString` as `&str`. [#1023](https://github.com/PyO3/pyo3/pull/1023) +- Add `Python::with_gil` for executing a closure with the Python GIL. [#1037](https://github.com/PyO3/pyo3/pull/1037) +- Add type information to failures in `PyAny::downcast()`. [#1050](https://github.com/PyO3/pyo3/pull/1050) +- Implement `Debug` for `PyIterator`. [#1051](https://github.com/PyO3/pyo3/pull/1051) +- Add `PyBytes::new_with` and `PyByteArray::new_with` for initialising `bytes` and `bytearray` objects using a closure. [#1074](https://github.com/PyO3/pyo3/pull/1074) +- Add `#[derive(FromPyObject)]` macro for enums and structs. [#1065](https://github.com/PyO3/pyo3/pull/1065) +- Add `Py::as_ref` and `Py::into_ref` for converting `Py` to `&T`. [#1098](https://github.com/PyO3/pyo3/pull/1098) +- Add ability to return `Result` types other than `PyResult` from `#[pyfunction]`, `#[pymethod]` and `#[pyproto]` functions. [#1106](https://github.com/PyO3/pyo3/pull/1118). +- Implement `ToPyObject`, `IntoPy`, and `FromPyObject` for [hashbrown](https://crates.io/crates/hashbrown)'s `HashMap` and `HashSet` types (requires the `hashbrown` feature). [#1114](https://github.com/PyO3/pyo3/pull/1114) +- Add `#[pyfunction(pass_module)]` and `#[pyfn(pass_module)]` to pass the module object as the first function argument. [#1143](https://github.com/PyO3/pyo3/pull/1143) +- Add `PyModule::add_function` and `PyModule::add_submodule` as typed alternatives to `PyModule::add_wrapped`. [#1143](https://github.com/PyO3/pyo3/pull/1143) +- Add native `PyCFunction` and `PyFunction` types. [#1163](https://github.com/PyO3/pyo3/pull/1163) + +### Changed +- Rework exception types: [#1024](https://github.com/PyO3/pyo3/pull/1024) [#1115](https://github.com/PyO3/pyo3/pull/1115) + - Rename exception types from e.g. `RuntimeError` to `PyRuntimeError`. The old names continue to exist but are deprecated. + - Exception objects are now accessible as `&T` or `Py`, just like other Python-native types. - Rename `PyException::py_err()` to `PyException::new_err()`. - Rename `PyUnicodeDecodeErr::new_err()` to `PyUnicodeDecodeErr::new()`. - Remove `PyStopIteration::stop_iteration()`. -- Correct FFI definitions `Py_SetProgramName` and `Py_SetPythonHome` to take `*const` argument instead of `*mut`. [#1021](https://github.com/PyO3/pyo3/pull/1021) -- Rename `PyString::to_string` to `to_str`, change return type `Cow` to `&str`. [#1023](https://github.com/PyO3/pyo3/pull/1023) -- Correct FFI definition `_PyLong_AsByteArray` `*mut c_uchar` argument instead of `*const c_uchar`. [#1029](https://github.com/PyO3/pyo3/pull/1029) -- Add `T: Send` bound to the return value of `Python::allow_threads`. [#1036](https://github.com/PyO3/pyo3/pull/1036) -- Rename `PYTHON_SYS_EXECUTABLE` to `PYO3_PYTHON`. The old name will continue to work but will be undocumented, and will be removed in a future release. [#1039](https://github.com/PyO3/pyo3/pull/1039) -- `PyType::as_type_ptr` is no longer `unsafe`. [#1047](https://github.com/PyO3/pyo3/pull/1047) -- Change `PyIterator::from_object` to return `PyResult` instead of `Result`. [#1051](https://github.com/PyO3/pyo3/pull/1051) +- Require `T: Send` for the return value `T` of `Python::allow_threads`. [#1036](https://github.com/PyO3/pyo3/pull/1036) +- Rename `PYTHON_SYS_EXECUTABLE` to `PYO3_PYTHON`. The old name will continue to work (undocumented) but will be removed in a future release. [#1039](https://github.com/PyO3/pyo3/pull/1039) +- Remove `unsafe` from signature of `PyType::as_type_ptr`. [#1047](https://github.com/PyO3/pyo3/pull/1047) +- Change return type of `PyIterator::from_object` to `PyResult` (was `Result`). [#1051](https://github.com/PyO3/pyo3/pull/1051) - `IntoPy` is no longer implied by `FromPy`. [#1063](https://github.com/PyO3/pyo3/pull/1063) -- `PyObject` is now just a type alias for `Py`. [#1063](https://github.com/PyO3/pyo3/pull/1063) -- Rework PyErr to be compatible with the `std::error::Error` trait: [#1067](https://github.com/PyO3/pyo3/pull/1067) [#1115](https://github.com/PyO3/pyo3/pull/1115) +- Change `PyObject` to be a type alias for `Py`. [#1063](https://github.com/PyO3/pyo3/pull/1063) +- Rework `PyErr` to be compatible with the `std::error::Error` trait: [#1067](https://github.com/PyO3/pyo3/pull/1067) [#1115](https://github.com/PyO3/pyo3/pull/1115) - Implement `Display`, `Error`, `Send` and `Sync` for `PyErr` and `PyErrArguments`. - - Add `PyErr::instance()` which returns `&PyBaseException`. + - Add `PyErr::instance()` for accessing `PyErr` as `&PyBaseException`. - `PyErr`'s fields are now an implementation detail. The equivalent values can be accessed with `PyErr::ptype()`, `PyErr::pvalue()` and `PyErr::ptraceback()`. - - Change `PyErr::print()` and `PyErr::print_and_set_sys_last_vars()` to take `&self` instead of `self`. + - Change receiver of `PyErr::print()` and `PyErr::print_and_set_sys_last_vars()` to `&self` (was `self`). - Remove `PyErrValue`, `PyErr::from_value`, `PyErr::into_normalized()`, and `PyErr::normalize()`. - - Remove `PyException::into()` and `Into>` for `PyErr` and `PyException`. -- Change `#[pyproto]` to return NotImplemented for operators for which Python can try a reversed operation. #[1072](https://github.com/PyO3/pyo3/pull/1072) -- `PyModule::add` now uses `IntoPy` instead of `ToPyObject`. #[1124](https://github.com/PyO3/pyo3/pull/1124) -- Add nested modules as `&PyModule` instead of using the wrapper generated by `#[pymodule]`. [#1143](https://github.com/PyO3/pyo3/pull/1143) + - Remove `PyException::into()`. + - Remove `Into>` for `PyErr` and `PyException`. +- Change methods generated by `#[pyproto]` to return `NotImplemented` if Python should try a reversed operation. #[1072](https://github.com/PyO3/pyo3/pull/1072) +- Change argument to `PyModule::add` to `impl IntoPy` (was `impl ToPyObject`). #[1124](https://github.com/PyO3/pyo3/pull/1124) ### Removed +- Remove many exception and `PyErr` APIs; see the "changed" section above. [#1024](https://github.com/PyO3/pyo3/pull/1024) [#1067](https://github.com/PyO3/pyo3/pull/1067) [#1115](https://github.com/PyO3/pyo3/pull/1115) +- Remove `PyString::to_string` (use new `PyString::to_str`). [#1023](https://github.com/PyO3/pyo3/pull/1023) - Remove `PyString::as_bytes`. [#1023](https://github.com/PyO3/pyo3/pull/1023) - Remove `Python::register_any`. [#1023](https://github.com/PyO3/pyo3/pull/1023) - Remove `GILGuard::acquire` from the public API. Use `Python::acquire_gil` or `Python::with_gil`. [#1036](https://github.com/PyO3/pyo3/pull/1036) -- Remove `FromPy`. [#1063](https://github.com/PyO3/pyo3/pull/1063) -- Remove `AsPyRef`. [#1098](https://github.com/PyO3/pyo3/pull/1098) +- Remove the `FromPy` trait. [#1063](https://github.com/PyO3/pyo3/pull/1063) +- Remove the `AsPyRef` trait. [#1098](https://github.com/PyO3/pyo3/pull/1098) ### Fixed -- Conversion from types with an `__index__` method to Rust BigInts. [#1027](https://github.com/PyO3/pyo3/pull/1027) -- Fix segfault with #[pyclass(dict, unsendable)] [#1058](https://github.com/PyO3/pyo3/pull/1058) -- Don't rely on the order of structmembers to compute offsets in PyCell. Related to - [#1058](https://github.com/PyO3/pyo3/pull/1058). [#1059](https://github.com/PyO3/pyo3/pull/1059) -- Allows `&Self` as a `#[pymethods]` argument again. [#1071](https://github.com/PyO3/pyo3/pull/1071) -- Fix best-effort build against PyPy 3.6. #[1092](https://github.com/PyO3/pyo3/pull/1092) -- Improve lifetime elision in `#[pyproto]`. [#1093](https://github.com/PyO3/pyo3/pull/1093) -- Fix python configuration detection when cross-compiling. [#1095](https://github.com/PyO3/pyo3/pull/1095) -- Link against libpython on android with `extension-module` set. [#1095](https://github.com/PyO3/pyo3/pull/1095) -- Fix support for both `__add__` and `__radd__` in the `+` operator when both are defined in `PyNumberProtocol` - (and similar for all other reversible operators). [#1107](https://github.com/PyO3/pyo3/pull/1107) -- Associate Python functions with their module by passing the Module and Module name [#1143](https://github.com/PyO3/pyo3/pull/1143) +- Correct FFI definitions `Py_SetProgramName` and `Py_SetPythonHome` to take `*const` arguments (was `*mut`). [#1021](https://github.com/PyO3/pyo3/pull/1021) +- Fix `FromPyObject` for `num_bigint::BigInt` for Python objects with an `__index__` method. [#1027](https://github.com/PyO3/pyo3/pull/1027) +- Correct FFI definition `_PyLong_AsByteArray` to take `*mut c_uchar` argument (was `*const c_uchar`). [#1029](https://github.com/PyO3/pyo3/pull/1029) +- Fix segfault with `#[pyclass(dict, unsendable)]`. [#1058](https://github.com/PyO3/pyo3/pull/1058) [#1059](https://github.com/PyO3/pyo3/pull/1059) +- Fix using `&Self` as an argument type for functions in a `#[pymethods]` block. [#1071](https://github.com/PyO3/pyo3/pull/1071) +- Fix best-effort build against PyPy 3.6. [#1092](https://github.com/PyO3/pyo3/pull/1092) +- Fix many cases of lifetime elision in `#[pyproto]` implementations. [#1093](https://github.com/PyO3/pyo3/pull/1093) +- Fix detection of Python build configuration when cross-compiling. [#1095](https://github.com/PyO3/pyo3/pull/1095) +- Always link against libpython on android with the `extension-module` feature. [#1095](https://github.com/PyO3/pyo3/pull/1095) +- Fix the `+` operator not trying `__radd__` when both `__add__` and `__radd__` are defined in `PyNumberProtocol` (and similar for all other reversible operators). [#1107](https://github.com/PyO3/pyo3/pull/1107) +- Fix building with Anaconda python. [#1175](https://github.com/PyO3/pyo3/pull/1175) ## [0.11.1] - 2020-06-30 ### Added @@ -493,11 +508,13 @@ Yanked - Allow to add gc support without implementing PyGCProtocol #57 - Refactor `PyErr` implementation. Drop `py` parameter from constructor. -## 0.1.0 - 07-23-2017 +## [0.1.0] - 07-23-2017 ### Added - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.11.1...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.12.1...HEAD +[0.12.1]: https://github.com/pyo3/pyo3/compare/v0.12.0...v0.12.1 +[0.12.0]: https://github.com/pyo3/pyo3/compare/v0.11.1...v0.12.0 [0.11.1]: https://github.com/pyo3/pyo3/compare/v0.11.0...v0.11.1 [0.11.0]: https://github.com/pyo3/pyo3/compare/v0.10.1...v0.11.0 [0.10.1]: https://github.com/pyo3/pyo3/compare/v0.10.0...v0.10.1 @@ -529,3 +546,4 @@ Yanked [0.2.2]: https://github.com/pyo3/pyo3/compare/v0.2.1...v0.2.2 [0.2.1]: https://github.com/pyo3/pyo3/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/pyo3/pyo3/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/PyO3/pyo3/tree/0.1.0 diff --git a/Cargo.toml b/Cargo.toml index fa8c3c5e..230bd52a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.11.1" +version = "0.12.1" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -22,8 +22,8 @@ libc = "0.2.62" parking_lot = "0.11.0" num-bigint = { version = "0.3", optional = true } num-complex = { version = "0.3", optional = true } -paste = { version = "0.1.6", optional = true } -pyo3cls = { path = "pyo3cls", version = "=0.11.1", optional = true } +paste = { version = "1.0.1", optional = true } +pyo3cls = { path = "pyo3cls", version = "=0.12.1", optional = true } unindent = { version = "0.1.4", optional = true } hashbrown = { version = "0.9", optional = true } diff --git a/README.md b/README.md index 2e57a358..e33f7614 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3] -version = "0.11.1" +version = "0.12.1" features = ["extension-module"] ``` @@ -99,7 +99,7 @@ use it to run Python code, add `pyo3` to your `Cargo.toml` like this: ```toml [dependencies] -pyo3 = "0.11.1" +pyo3 = "0.12.1" ``` Example program displaying the value of `sys.version` and the current user name: @@ -135,7 +135,7 @@ about this topic. ## Tools and libraries * [maturin](https://github.com/PyO3/maturin) _Zero configuration build tool for Rust-made Python extensions_. * [setuptools-rust](https://github.com/PyO3/setuptools-rust) _Setuptools plugin for Rust support_. - * [pyo3-built](https://github.com/PyO3/pyo3-built) _Simple macro to expose metadata obtained with the [`built`](https://crates.io/crates/built) crate as a [`PyDict`](https://pyo3.github.io/pyo3/pyo3/struct.PyDict.html)_ + * [pyo3-built](https://github.com/PyO3/pyo3-built) _Simple macro to expose metadata obtained with the [`built`](https://crates.io/crates/built) crate as a [`PyDict`](https://docs.rs/pyo3/0.12.0/pyo3/types/struct.PyDict.html)_ * [rust-numpy](https://github.com/PyO3/rust-numpy) _Rust binding of NumPy C-API_ * [dict-derive](https://github.com/gperinazzo/dict-derive) _Derive FromPyObject to automatically transform Python dicts into Rust structs_ * [pyo3-log](https://github.com/vorner/pyo3-log) _Bridge from Rust to Python logging_ @@ -152,7 +152,7 @@ about this topic. * [Rogue-Gym](https://github.com/kngwyu/rogue-gym) _Customizable rogue-like game for AI experiments_ * Contains an example of building wheels on Azure Pipelines * [fastuuid](https://github.com/thedrow/fastuuid/) _Python bindings to Rust's UUID library_ - * [python-ext-wasm](https://github.com/wasmerio/python-ext-wasm) _Python library to run WebAssembly binaries_ + * [wasmer-python](https://github.com/wasmerio/wasmer-python) _Python library to run WebAssembly binaries_ * [mocpy](https://github.com/cds-astro/mocpy) _Astronomical Python library offering data structures for describing any arbitrary coverage regions on the unit sphere_ * [tokenizers](https://github.com/huggingface/tokenizers/tree/master/bindings/python) _Python bindings to the Hugging Face tokenizers (NLP) written in Rust_ diff --git a/build.rs b/build.rs index a1572181..3d7b7035 100644 --- a/build.rs +++ b/build.rs @@ -148,7 +148,9 @@ impl CrossCompileConfig { } fn cross_compiling() -> Result> { - if env::var("TARGET")? == env::var("HOST")? { + let target = env::var("TARGET")?; + let host = env::var("HOST")?; + if target == host || (target == "i686-pc-windows-msvc" && host == "x86_64-pc-windows-msvc") { return Ok(None); } @@ -685,9 +687,15 @@ import platform import struct import sys import sysconfig +import os.path PYPY = platform.python_implementation() == "PyPy" +# Anaconda based python distributions have a static python executable, but include +# the shared library. Use the shared library for embedding to avoid rust trying to +# LTO the static library (and failing with newer gcc's, because it is old). +ANACONDA = os.path.exists(os.path.join(sys.prefix, 'conda-meta')) + try: base_prefix = sys.base_prefix except AttributeError: @@ -702,7 +710,7 @@ if libdir is not None: print("libdir", libdir) print("ld_version", sysconfig.get_config_var('LDVERSION') or sysconfig.get_config_var('py_version_short')) print("base_prefix", base_prefix) -print("shared", PYPY or bool(sysconfig.get_config_var('Py_ENABLE_SHARED'))) +print("shared", PYPY or ANACONDA or bool(sysconfig.get_config_var('Py_ENABLE_SHARED'))) print("executable", sys.executable) print("calcsize_pointer", struct.calcsize("P")) "#; diff --git a/examples/rustapi_module/tox.ini b/examples/rustapi_module/tox.ini index fa74ab8e..7a7d545d 100644 --- a/examples/rustapi_module/tox.ini +++ b/examples/rustapi_module/tox.ini @@ -3,7 +3,7 @@ envlist = py35, py36, py37, py38, - pypy36 + pypy3 minversion = 3.4.0 skip_missing_interpreters = true isolated_build = true diff --git a/examples/word-count/tox.ini b/examples/word-count/tox.ini index a6e31299..7a7d545d 100644 --- a/examples/word-count/tox.ini +++ b/examples/word-count/tox.ini @@ -3,7 +3,7 @@ envlist = py35, py36, py37, py38, - pypy35 + pypy3 minversion = 3.4.0 skip_missing_interpreters = true isolated_build = true diff --git a/guide/src/class.md b/guide/src/class.md index 4e6ce160..d4db1106 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -757,14 +757,14 @@ impl pyo3::IntoPy for MyClass { } pub struct Pyo3MethodsInventoryForMyClass { - methods: &'static [pyo3::class::PyMethodDefType], + methods: Vec, } impl pyo3::class::methods::PyMethodsInventory for Pyo3MethodsInventoryForMyClass { - fn new(methods: &'static [pyo3::class::PyMethodDefType]) -> Self { + fn new(methods: Vec) -> Self { Self { methods } } - fn get(&self) -> &'static [pyo3::class::PyMethodDefType] { - self.methods + fn get(&'static self) -> &'static [pyo3::class::PyMethodDefType] { + &self.methods } } impl pyo3::class::methods::HasMethodsInventory for MyClass { diff --git a/guide/src/function.md b/guide/src/function.md index 0e0277f3..a8b59785 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -173,15 +173,31 @@ Type: builtin_function_or_method ## Closures -Currently, there are no conversions between `Fn`s in Rust and callables in Python. This would definitely be possible and very useful, so contributions are welcome. In the meantime, you can do the following: +Currently, there are no conversions between `Fn`s in Rust and callables in Python. This would +definitely be possible and very useful, so contributions are welcome. In the meantime, you can do +the following: ### Calling Python functions in Rust -You can use [`PyAny::is_callable`] to check if you have a callable object. `is_callable` will return `true` for functions (including lambdas), methods and objects with a `__call__` method. You can call the object with [`PyAny::call`] with the args as first parameter and the kwargs (or `None`) as second parameter. There are also [`PyAny::call0`] with no args and [`PyAny::call1`] with only positional args. +You can pass Python `def`'d functions and built-in functions to Rust functions `[PyFunction]` +corresponds to regular Python functions while `[PyCFunction]` describes built-ins such as +`repr()`. + +You can also use [`PyAny::is_callable`] to check if you have a callable object. `is_callable` will +return `true` for functions (including lambdas), methods and objects with a `__call__` method. +You can call the object with [`PyAny::call`] with the args as first parameter and the kwargs +(or `None`) as second parameter. There are also [`PyAny::call0`] with no args and [`PyAny::call1`] +with only positional args. ### Calling Rust functions in Python -If you have a static function, you can expose it with `#[pyfunction]` and use [`wrap_pyfunction!`] to get the corresponding [`PyObject`]. For dynamic functions, e.g. lambdas and functions that were passed as arguments, you must put them in some kind of owned container, e.g. a `Box`. (A long-term solution will be a special container similar to wasm-bindgen's `Closure`). You can then use a `#[pyclass]` struct with that container as a field as a way to pass the function over the FFI barrier. You can even make that class callable with `__call__` so it looks like a function in Python code. +If you have a static function, you can expose it with `#[pyfunction]` and use [`wrap_pyfunction!`] +to get the corresponding [`PyCFunction`]. For dynamic functions, e.g. lambdas and functions that +were passed as arguments, you must put them in some kind of owned container, e.g. a `Box`. +(A long-term solution will be a special container similar to wasm-bindgen's `Closure`). You can +then use a `#[pyclass]` struct with that container as a field as a way to pass the function over +the FFI barrier. You can even make that class callable with `__call__` so it looks like a function +in Python code. [`PyAny::is_callable`]: https://docs.rs/pyo3/latest/pyo3/struct.PyAny.html#tymethod.is_callable [`PyAny::call`]: https://docs.rs/pyo3/latest/pyo3/struct.PyAny.html#tymethod.call @@ -233,3 +249,15 @@ fn module_with_fn(py: Python, m: &PyModule) -> PyResult<()> { # fn main() {} ``` + +## Accessing the FFI functions + +In order to make Rust functions callable from Python, PyO3 generates a +`extern "C" Fn(slf: *mut PyObject, args: *mut PyObject, kwargs: *mut PyObject) -> *mut Pyobject` +function and embeds the call to the Rust function inside this FFI-wrapper function. This +wrapper handles extraction of the regular arguments and the keyword arguments from the input +`PyObjects`. Since this function is not user-defined but required to build a `PyCFunction`, PyO3 +offers the `raw_pycfunction!()` macro to get the identifier of this generated wrapper. + +The `wrap_pyfunction` macro can be used to directly get a `PyCFunction` given a +`#[pyfunction]` and a `PyModule`: `wrap_pyfunction!(rust_fun, module)`. \ No newline at end of file diff --git a/pyo3-derive-backend/Cargo.toml b/pyo3-derive-backend/Cargo.toml index 6f0a45ee..80ef5d1f 100644 --- a/pyo3-derive-backend/Cargo.toml +++ b/pyo3-derive-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-derive-backend" -version = "0.11.1" +version = "0.12.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] diff --git a/pyo3-derive-backend/src/from_pyobject.rs b/pyo3-derive-backend/src/from_pyobject.rs index 656532be..b8d70b44 100644 --- a/pyo3-derive-backend/src/from_pyobject.rs +++ b/pyo3-derive-backend/src/from_pyobject.rs @@ -133,7 +133,9 @@ impl<'a> Container<'a> { "Cannot derive FromPyObject for empty structs and variants.", )); } - let transparent = attrs.iter().any(ContainerAttribute::transparent); + let transparent = attrs + .iter() + .any(|attr| *attr == ContainerAttribute::Transparent); if transparent { Self::check_transparent_len(fields)?; } @@ -182,7 +184,6 @@ impl<'a> Container<'a> { let err_name = attrs .iter() .find_map(|a| a.annotation()) - .cloned() .unwrap_or_else(|| path.segments.last().unwrap().ident.to_string()); let v = Container { @@ -305,18 +306,10 @@ enum ContainerAttribute { } impl ContainerAttribute { - /// Return whether this attribute is `Transparent` - fn transparent(&self) -> bool { - match self { - ContainerAttribute::Transparent => true, - _ => false, - } - } - /// Convenience method to access `ErrorAnnotation`. - fn annotation(&self) -> Option<&String> { + fn annotation(&self) -> Option { match self { - ContainerAttribute::ErrorAnnotation(s) => Some(s), + ContainerAttribute::ErrorAnnotation(s) => Some(s.to_string()), _ => None, } } diff --git a/pyo3-derive-backend/src/method.rs b/pyo3-derive-backend/src/method.rs index 5697df95..492ca08c 100644 --- a/pyo3-derive-backend/src/method.rs +++ b/pyo3-derive-backend/src/method.rs @@ -286,7 +286,7 @@ impl<'a> FnSpec<'a> { pub fn default_value(&self, name: &syn::Ident) -> Option { for s in self.attrs.iter() { match *s { - Argument::Arg(ref path, ref opt) => { + Argument::Arg(ref path, ref opt) | Argument::Kwarg(ref path, ref opt) => { if path.is_ident(name) { if let Some(ref val) = opt { let i: syn::Expr = syn::parse_str(&val).unwrap(); @@ -294,12 +294,6 @@ impl<'a> FnSpec<'a> { } } } - Argument::Kwarg(ref path, ref opt) => { - if path.is_ident(name) { - let i: syn::Expr = syn::parse_str(&opt).unwrap(); - return Some(quote!(#i)); - } - } _ => (), } } diff --git a/pyo3-derive-backend/src/module.rs b/pyo3-derive-backend/src/module.rs index 657d1134..3959fb81 100644 --- a/pyo3-derive-backend/src/module.rs +++ b/pyo3-derive-backend/src/module.rs @@ -200,7 +200,7 @@ pub fn add_fn_to_module( doc, }; - let doc = &spec.doc; + let doc = syn::LitByteStr::new(spec.doc.value().as_bytes(), spec.doc.span()); let python_name = &spec.python_name; @@ -212,7 +212,16 @@ pub fn add_fn_to_module( fn #function_wrapper_ident<'a>( args: impl Into> ) -> pyo3::PyResult<&'a pyo3::types::PyCFunction> { - pyo3::types::PyCFunction::new_with_keywords(#wrapper_ident, stringify!(#python_name), #doc, args.into()) + let name = concat!(stringify!(#python_name), "\0"); + let name = std::ffi::CStr::from_bytes_with_nul(name.as_bytes()).unwrap(); + let doc = std::ffi::CStr::from_bytes_with_nul(#doc).unwrap(); + pyo3::types::PyCFunction::internal_new( + name, + doc, + pyo3::class::PyMethodType::PyCFunctionWithKeywords(#wrapper_ident), + pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS, + args.into(), + ) } }) } diff --git a/pyo3-derive-backend/src/pyclass.rs b/pyo3-derive-backend/src/pyclass.rs index 693d5004..bc2fd66e 100644 --- a/pyo3-derive-backend/src/pyclass.rs +++ b/pyo3-derive-backend/src/pyclass.rs @@ -215,14 +215,14 @@ fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream { quote! { #[doc(hidden)] pub struct #inventory_cls { - methods: &'static [pyo3::class::PyMethodDefType], + methods: Vec, } impl pyo3::class::methods::PyMethodsInventory for #inventory_cls { - fn new(methods: &'static [pyo3::class::PyMethodDefType]) -> Self { + fn new(methods: Vec) -> Self { Self { methods } } - fn get(&self) -> &'static [pyo3::class::PyMethodDefType] { - self.methods + fn get(&'static self) -> &'static [pyo3::class::PyMethodDefType] { + &self.methods } } @@ -483,7 +483,7 @@ fn impl_descriptors( pyo3::inventory::submit! { #![crate = pyo3] { type Inventory = <#cls as pyo3::class::methods::HasMethodsInventory>::Methods; - ::new(&[#(#py_methods),*]) + ::new(vec![#(#py_methods),*]) } } }) diff --git a/pyo3-derive-backend/src/pyfunction.rs b/pyo3-derive-backend/src/pyfunction.rs index 80ac1cf3..bfb943ff 100644 --- a/pyo3-derive-backend/src/pyfunction.rs +++ b/pyo3-derive-backend/src/pyfunction.rs @@ -14,7 +14,7 @@ pub enum Argument { VarArgs(syn::Path), KeywordArgs(syn::Path), Arg(syn::Path, Option), - Kwarg(syn::Path, String), + Kwarg(syn::Path, Option), } /// The attributes of the pyfunction macro @@ -90,12 +90,10 @@ impl PyFunctionAttr { )); } if self.has_varargs { - return Err(syn::Error::new_spanned( - item, - "Positional argument or varargs(*) is not allowed after *", - )); + self.arguments.push(Argument::Kwarg(path.clone(), None)); + } else { + self.arguments.push(Argument::Arg(path.clone(), None)); } - self.arguments.push(Argument::Arg(path.clone(), None)); Ok(()) } @@ -128,7 +126,8 @@ impl PyFunctionAttr { self.kw_arg_is_ok(item)?; if self.has_varargs { // kw only - self.arguments.push(Argument::Kwarg(name.clone(), value)); + self.arguments + .push(Argument::Kwarg(name.clone(), Some(value))); } else { self.has_kw = true; self.arguments @@ -251,7 +250,34 @@ mod test { Argument::Arg(parse_quote! {test1}, None), Argument::Arg(parse_quote! {test2}, Some("None".to_owned())), Argument::VarArgsSeparator, - Argument::Kwarg(parse_quote! {test3}, "None".to_owned()), + Argument::Kwarg(parse_quote! {test3}, Some("None".to_owned())), + ] + ); + + let args = items(quote! {"*", test1, test2}).unwrap(); + assert!( + args == vec![ + Argument::VarArgsSeparator, + Argument::Kwarg(parse_quote! {test1}, None), + Argument::Kwarg(parse_quote! {test2}, None), + ] + ); + + let args = items(quote! {"*", test1, test2="None"}).unwrap(); + assert!( + args == vec![ + Argument::VarArgsSeparator, + Argument::Kwarg(parse_quote! {test1}, None), + Argument::Kwarg(parse_quote! {test2}, Some("None".to_owned())), + ] + ); + + let args = items(quote! {"*", test1="None", test2}).unwrap(); + assert!( + args == vec![ + Argument::VarArgsSeparator, + Argument::Kwarg(parse_quote! {test1}, Some("None".to_owned())), + Argument::Kwarg(parse_quote! {test2}, None), ] ); } @@ -265,7 +291,7 @@ mod test { Argument::Arg(parse_quote! {test1}, None), Argument::Arg(parse_quote! {test2}, Some("None".to_owned())), Argument::VarArgs(parse_quote! {args}), - Argument::Kwarg(parse_quote! {test3}, "None".to_owned()), + Argument::Kwarg(parse_quote! {test3}, Some("None".to_owned())), Argument::KeywordArgs(parse_quote! {kwargs}), ] ); diff --git a/pyo3-derive-backend/src/pyimpl.rs b/pyo3-derive-backend/src/pyimpl.rs index 00999af5..e59cb43f 100644 --- a/pyo3-derive-backend/src/pyimpl.rs +++ b/pyo3-derive-backend/src/pyimpl.rs @@ -43,7 +43,7 @@ pub fn impl_methods(ty: &syn::Type, impls: &mut Vec) -> syn::Resu pyo3::inventory::submit! { #![crate = pyo3] { type Inventory = <#ty as pyo3::class::methods::HasMethodsInventory>::Methods; - ::new(&[#( + ::new(vec![#( #(#cfg_attributes)* #methods ),*]) diff --git a/pyo3-derive-backend/src/pymethod.rs b/pyo3-derive-backend/src/pymethod.rs index 62ced447..6afb66ef 100644 --- a/pyo3-derive-backend/src/pymethod.rs +++ b/pyo3-derive-backend/src/pymethod.rs @@ -570,12 +570,11 @@ pub fn impl_py_method_def(spec: &FnSpec, wrapper: &TokenStream) -> TokenStream { pyo3::class::PyMethodDefType::Method({ #wrapper - pyo3::class::PyMethodDef { - ml_name: stringify!(#python_name), - ml_meth: pyo3::class::PyMethodType::PyCFunction(__wrap), - ml_flags: pyo3::ffi::METH_NOARGS, - ml_doc: #doc, - } + pyo3::class::PyMethodDef::cfunction( + concat!(stringify!(#python_name), "\0"), + __wrap, + #doc + ) }) } } else { @@ -583,12 +582,12 @@ pub fn impl_py_method_def(spec: &FnSpec, wrapper: &TokenStream) -> TokenStream { pyo3::class::PyMethodDefType::Method({ #wrapper - pyo3::class::PyMethodDef { - ml_name: stringify!(#python_name), - ml_meth: pyo3::class::PyMethodType::PyCFunctionWithKeywords(__wrap), - ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS, - ml_doc: #doc, - } + pyo3::class::PyMethodDef::cfunction_with_keywords( + concat!(stringify!(#python_name), "\0"), + __wrap, + 0, + #doc + ) }) } } @@ -601,12 +600,7 @@ pub fn impl_py_method_def_new(spec: &FnSpec, wrapper: &TokenStream) -> TokenStre pyo3::class::PyMethodDefType::New({ #wrapper - pyo3::class::PyMethodDef { - ml_name: stringify!(#python_name), - ml_meth: pyo3::class::PyMethodType::PyNewFunc(__wrap), - ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS, - ml_doc: #doc, - } + pyo3::class::PyMethodDef::new_func(concat!(stringify!(#python_name), "\0"), __wrap, #doc) }) } } @@ -618,13 +612,12 @@ pub fn impl_py_method_def_class(spec: &FnSpec, wrapper: &TokenStream) -> TokenSt pyo3::class::PyMethodDefType::Class({ #wrapper - pyo3::class::PyMethodDef { - ml_name: stringify!(#python_name), - ml_meth: pyo3::class::PyMethodType::PyCFunctionWithKeywords(__wrap), - ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS | + pyo3::class::PyMethodDef::cfunction_with_keywords( + concat!(stringify!(#python_name), "\0"), + __wrap, pyo3::ffi::METH_CLASS, - ml_doc: #doc, - } + #doc + ) }) } } @@ -636,12 +629,12 @@ pub fn impl_py_method_def_static(spec: &FnSpec, wrapper: &TokenStream) -> TokenS pyo3::class::PyMethodDefType::Static({ #wrapper - pyo3::class::PyMethodDef { - ml_name: stringify!(#python_name), - ml_meth: pyo3::class::PyMethodType::PyCFunctionWithKeywords(__wrap), - ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS | pyo3::ffi::METH_STATIC, - ml_doc: #doc, - } + pyo3::class::PyMethodDef::cfunction_with_keywords( + concat!(stringify!(#python_name), "\0"), + __wrap, + pyo3::ffi::METH_STATIC, + #doc + ) }) } } @@ -652,10 +645,7 @@ pub fn impl_py_method_class_attribute(spec: &FnSpec<'_>, wrapper: &TokenStream) pyo3::class::PyMethodDefType::ClassAttribute({ #wrapper - pyo3::class::PyClassAttributeDef { - name: stringify!(#python_name), - meth: __wrap, - } + pyo3::class::PyClassAttributeDef::new(concat!(stringify!(#python_name), "\0"), __wrap) }) } } @@ -666,10 +656,7 @@ pub fn impl_py_const_class_attribute(spec: &ConstSpec, wrapper: &TokenStream) -> pyo3::class::PyMethodDefType::ClassAttribute({ #wrapper - pyo3::class::PyClassAttributeDef { - name: stringify!(#python_name), - meth: __wrap, - } + pyo3::class::PyClassAttributeDef::new(concat!(stringify!(#python_name), "\0"), __wrap) }) } } @@ -681,12 +668,12 @@ pub fn impl_py_method_def_call(spec: &FnSpec, wrapper: &TokenStream) -> TokenStr pyo3::class::PyMethodDefType::Call({ #wrapper - pyo3::class::PyMethodDef { - ml_name: stringify!(#python_name), - ml_meth: pyo3::class::PyMethodType::PyCFunctionWithKeywords(__wrap), - ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS, - ml_doc: #doc, - } + pyo3::class::PyMethodDef::cfunction_with_keywords( + concat!(stringify!(#python_name), "\0"), + __wrap, + pyo3::ffi::METH_STATIC, + #doc + ) }) } } @@ -700,11 +687,7 @@ pub(crate) fn impl_py_setter_def( pyo3::class::PyMethodDefType::Setter({ #wrapper - pyo3::class::PySetterDef { - name: stringify!(#python_name), - meth: __wrap, - doc: #doc, - } + pyo3::class::PySetterDef::new(concat!(stringify!(#python_name), "\0"), __wrap, #doc) }) } } @@ -718,11 +701,7 @@ pub(crate) fn impl_py_getter_def( pyo3::class::PyMethodDefType::Getter({ #wrapper - pyo3::class::PyGetterDef { - name: stringify!(#python_name), - meth: __wrap, - doc: #doc, - } + pyo3::class::PyGetterDef::new(concat!(stringify!(#python_name), "\0"), __wrap, #doc) }) } } diff --git a/pyo3-derive-backend/src/pyproto.rs b/pyo3-derive-backend/src/pyproto.rs index c9bebbe0..7c6cbf1a 100644 --- a/pyo3-derive-backend/src/pyproto.rs +++ b/pyo3-derive-backend/src/pyproto.rs @@ -95,12 +95,12 @@ fn impl_proto_impl( py_methods.push(quote! { pyo3::class::PyMethodDefType::Method({ #method - pyo3::class::PyMethodDef { - ml_name: stringify!(#name), - ml_meth: pyo3::class::PyMethodType::PyCFunctionWithKeywords(__wrap), - ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS | #coexist, - ml_doc: "" - } + pyo3::class::PyMethodDef::cfunction_with_keywords( + concat!(stringify!(#name), "\0"), + __wrap, + #coexist, + "\0" + ) }) }); } @@ -123,7 +123,7 @@ fn inventory_submission(py_methods: Vec, ty: &syn::Type) -> TokenSt pyo3::inventory::submit! { #![crate = pyo3] { type Inventory = <#ty as pyo3::class::methods::HasMethodsInventory>::Methods; - ::new(&[#(#py_methods),*]) + ::new(vec![#(#py_methods),*]) } } } diff --git a/pyo3cls/Cargo.toml b/pyo3cls/Cargo.toml index 2d9a7325..40113998 100644 --- a/pyo3cls/Cargo.toml +++ b/pyo3cls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3cls" -version = "0.11.1" +version = "0.12.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,4 +16,4 @@ proc-macro = true [dependencies] quote = "1" syn = { version = "1", features = ["full", "extra-traits"] } -pyo3-derive-backend = { path = "../pyo3-derive-backend", version = "=0.11.1" } +pyo3-derive-backend = { path = "../pyo3-derive-backend", version = "=0.12.1" } diff --git a/src/buffer.rs b/src/buffer.rs index 2e7c4013..a80f1839 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -132,18 +132,12 @@ fn standard_element_type_from_type_char(type_char: u8) -> ElementType { #[cfg(target_endian = "little")] fn is_matching_endian(c: u8) -> bool { - match c { - b'@' | b'=' | b'<' => true, - _ => false, - } + c == b'@' || c == b'=' || c == b'>' } #[cfg(target_endian = "big")] fn is_matching_endian(c: u8) -> bool { - match c { - b'@' | b'=' | b'>' | b'!' => true, - _ => false, - } + c == b'@' || c == b'=' || c == b'>' || c == b'!' } /// Trait implemented for possible element types of `PyBuffer`. diff --git a/src/class/methods.rs b/src/class/methods.rs index a34260d6..e8d10a65 100644 --- a/src/class/methods.rs +++ b/src/class/methods.rs @@ -2,7 +2,7 @@ use crate::{ffi, PyObject, Python}; use libc::c_int; -use std::ffi::CString; +use std::ffi::CStr; use std::fmt; /// `PyMethodDefType` represents different types of Python callable objects. @@ -35,32 +35,32 @@ pub enum PyMethodType { PyInitFunc(ffi::initproc), } -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub struct PyMethodDef { - pub ml_name: &'static str, - pub ml_meth: PyMethodType, - pub ml_flags: c_int, - pub ml_doc: &'static str, + pub(crate) ml_name: &'static CStr, + pub(crate) ml_meth: PyMethodType, + pub(crate) ml_flags: c_int, + pub(crate) ml_doc: &'static CStr, } #[derive(Copy, Clone)] pub struct PyClassAttributeDef { - pub name: &'static str, - pub meth: for<'p> fn(Python<'p>) -> PyObject, + pub(crate) name: &'static CStr, + pub(crate) meth: for<'p> fn(Python<'p>) -> PyObject, } -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub struct PyGetterDef { - pub name: &'static str, - pub meth: ffi::getter, - pub doc: &'static str, + pub(crate) name: &'static CStr, + pub(crate) meth: ffi::getter, + doc: &'static CStr, } -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub struct PySetterDef { - pub name: &'static str, - pub meth: ffi::setter, - pub doc: &'static str, + pub(crate) name: &'static CStr, + pub(crate) meth: ffi::setter, + doc: &'static CStr, } unsafe impl Sync for PyMethodDef {} @@ -73,7 +73,67 @@ unsafe impl Sync for PySetterDef {} unsafe impl Sync for ffi::PyGetSetDef {} +fn get_name(name: &str) -> &CStr { + CStr::from_bytes_with_nul(name.as_bytes()) + .expect("Method name must be terminated with NULL byte") +} + +fn get_doc(doc: &str) -> &CStr { + CStr::from_bytes_with_nul(doc.as_bytes()).expect("Document must be terminated with NULL byte") +} + impl PyMethodDef { + pub(crate) fn get_new_func(&self) -> Option { + if let PyMethodType::PyNewFunc(new_func) = self.ml_meth { + Some(new_func) + } else { + None + } + } + + pub(crate) fn get_cfunction_with_keywords(&self) -> Option { + if let PyMethodType::PyCFunctionWithKeywords(func) = self.ml_meth { + Some(func) + } else { + None + } + } + + /// Define a function with no `*args` and `**kwargs`. + pub fn cfunction(name: &'static str, cfunction: ffi::PyCFunction, doc: &'static str) -> Self { + Self { + ml_name: get_name(name), + ml_meth: PyMethodType::PyCFunction(cfunction), + ml_flags: ffi::METH_NOARGS, + ml_doc: get_doc(doc), + } + } + + /// Define a `__new__` function. + pub fn new_func(name: &'static str, newfunc: ffi::newfunc, doc: &'static str) -> Self { + Self { + ml_name: get_name(name), + ml_meth: PyMethodType::PyNewFunc(newfunc), + ml_flags: ffi::METH_VARARGS | ffi::METH_KEYWORDS, + ml_doc: get_doc(doc), + } + } + + /// Define a function that can take `*args` and `**kwargs`. + pub fn cfunction_with_keywords( + name: &'static str, + cfunction: ffi::PyCFunctionWithKeywords, + flags: c_int, + doc: &'static str, + ) -> Self { + Self { + ml_name: get_name(name), + ml_meth: PyMethodType::PyCFunctionWithKeywords(cfunction), + ml_flags: flags | ffi::METH_VARARGS | ffi::METH_KEYWORDS, + ml_doc: get_doc(doc), + } + } + /// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef` pub fn as_method_def(&self) -> ffi::PyMethodDef { let meth = match self.ml_meth { @@ -84,12 +144,20 @@ impl PyMethodDef { }; ffi::PyMethodDef { - ml_name: CString::new(self.ml_name) - .expect("Method name must not contain NULL byte") - .into_raw(), + ml_name: self.ml_name.as_ptr(), ml_meth: Some(meth), ml_flags: self.ml_flags, - ml_doc: self.ml_doc.as_ptr() as *const _, + ml_doc: self.ml_doc.as_ptr(), + } + } +} + +impl PyClassAttributeDef { + /// Define a class attribute. + pub fn new(name: &'static str, meth: for<'p> fn(Python<'p>) -> PyObject) -> Self { + Self { + name: get_name(name), + meth, } } } @@ -105,30 +173,44 @@ impl fmt::Debug for PyClassAttributeDef { } impl PyGetterDef { + /// Define a getter. + pub fn new(name: &'static str, getter: ffi::getter, doc: &'static str) -> Self { + Self { + name: get_name(name), + meth: getter, + doc: get_doc(doc), + } + } + /// Copy descriptor information to `ffi::PyGetSetDef` pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) { if dst.name.is_null() { - dst.name = CString::new(self.name) - .expect("Method name must not contain NULL byte") - .into_raw(); + dst.name = self.name.as_ptr() as _; } if dst.doc.is_null() { - dst.doc = self.doc.as_ptr() as *mut libc::c_char; + dst.doc = self.doc.as_ptr() as _; } dst.get = Some(self.meth); } } impl PySetterDef { + /// Define a setter. + pub fn new(name: &'static str, setter: ffi::setter, doc: &'static str) -> Self { + Self { + name: get_name(name), + meth: setter, + doc: get_doc(doc), + } + } + /// Copy descriptor information to `ffi::PyGetSetDef` pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) { if dst.name.is_null() { - dst.name = CString::new(self.name) - .expect("Method name must not contain NULL byte") - .into_raw(); + dst.name = self.name.as_ptr() as _; } if dst.doc.is_null() { - dst.doc = self.doc.as_ptr() as *mut libc::c_char; + dst.doc = self.doc.as_ptr() as _; } dst.set = Some(self.meth); } @@ -150,10 +232,10 @@ pub trait PyMethods { #[cfg(feature = "macros")] pub trait PyMethodsInventory: inventory::Collect { /// Create a new instance - fn new(methods: &'static [PyMethodDefType]) -> Self; + fn new(methods: Vec) -> Self; /// Returns the methods for a single `#[pymethods] impl` block - fn get(&self) -> &'static [PyMethodDefType]; + fn get(&'static self) -> &'static [PyMethodDefType]; } /// Implemented for `#[pyclass]` in our proc macro code. diff --git a/src/class/mod.rs b/src/class/mod.rs index 9630a6aa..5fc986cb 100644 --- a/src/class/mod.rs +++ b/src/class/mod.rs @@ -13,6 +13,7 @@ pub mod descr; pub mod gc; pub mod iter; pub mod mapping; +#[doc(hidden)] pub mod methods; pub mod number; pub mod proto_methods; @@ -27,6 +28,7 @@ pub use self::descr::PyDescrProtocol; pub use self::gc::{PyGCProtocol, PyTraverseError, PyVisit}; pub use self::iter::PyIterProtocol; pub use self::mapping::PyMappingProtocol; +#[doc(hidden)] pub use self::methods::{ PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, PyMethodType, PySetterDef, }; diff --git a/src/exceptions.rs b/src/exceptions.rs index adab81be..a8973670 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -6,6 +6,7 @@ use crate::type_object::PySizedLayout; use crate::{ffi, PyResult, Python}; use std::ffi::CStr; use std::ops; +use std::os::raw::c_char; /// The boilerplate to convert between a Rust type and a Python exception. #[macro_export] @@ -445,7 +446,7 @@ impl PyUnicodeDecodeError { unsafe { py.from_owned_ptr_or_err(ffi::PyUnicodeDecodeError_Create( encoding.as_ptr(), - input.as_ptr() as *const i8, + input.as_ptr() as *const c_char, input.len() as ffi::Py_ssize_t, range.start as ffi::Py_ssize_t, range.end as ffi::Py_ssize_t, diff --git a/src/lib.rs b/src/lib.rs index c5e5e760..2d3a246c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ //! crate-type = ["cdylib"] //! //! [dependencies.pyo3] -//! version = "0.11.1" +//! version = "0.12.1" //! features = ["extension-module"] //! ``` //! @@ -109,7 +109,7 @@ //! //! ```toml //! [dependencies] -//! pyo3 = "0.11.1" +//! pyo3 = "0.12.1" //! ``` //! //! Example program displaying the value of `sys.version`: @@ -222,6 +222,25 @@ macro_rules! wrap_pyfunction { /// Returns the function that is called in the C-FFI. /// /// Use this together with `#[pyfunction]` and [types::PyCFunction]. +/// ``` +/// use pyo3::prelude::*; +/// use pyo3::types::PyCFunction; +/// use pyo3::raw_pycfunction; +/// +/// #[pyfunction] +/// fn some_fun() -> PyResult<()> { +/// Ok(()) +/// } +/// +/// #[pymodule] +/// fn module(_py: Python, module: &PyModule) -> PyResult<()> { +/// let ffi_wrapper_fun = raw_pycfunction!(some_fun); +/// let docs = "Some documentation string with null-termination\0"; +/// let py_cfunction = +/// PyCFunction::new_with_keywords(ffi_wrapper_fun, "function_name", docs, module.into())?; +/// module.add_function(py_cfunction) +/// } +/// ``` #[macro_export] macro_rules! raw_pycfunction { ($function_name: ident) => {{ diff --git a/src/pyclass.rs b/src/pyclass.rs index aa21881b..c3fe0231 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -6,7 +6,7 @@ use crate::derive_utils::PyBaseTypeUtils; use crate::pyclass_slots::{PyClassDict, PyClassWeakRef}; use crate::type_object::{type_flags, PyLayout}; use crate::types::PyAny; -use crate::{class, ffi, PyCell, PyErr, PyNativeType, PyResult, PyTypeInfo, Python}; +use crate::{ffi, PyCell, PyErr, PyNativeType, PyResult, PyTypeInfo, Python}; use std::convert::TryInto; use std::ffi::CString; use std::marker::PhantomData; @@ -309,7 +309,7 @@ fn py_class_flags() -> c_uint { pub(crate) fn py_class_attributes() -> impl Iterator { T::py_methods().into_iter().filter_map(|def| match def { - PyMethodDefType::ClassAttribute(attr) => Some(*attr), + PyMethodDefType::ClassAttribute(attr) => Some(attr.to_owned()), _ => None, }) } @@ -341,16 +341,12 @@ fn py_class_method_defs() -> ( for def in T::py_methods() { match *def { PyMethodDefType::New(ref def) => { - if let class::methods::PyMethodType::PyNewFunc(meth) = def.ml_meth { - new = Some(meth) - } + new = def.get_new_func(); + debug_assert!(new.is_some()); } PyMethodDefType::Call(ref def) => { - if let class::methods::PyMethodType::PyCFunctionWithKeywords(meth) = def.ml_meth { - call = Some(meth) - } else { - panic!("Method type is not supoorted by tp_call slot") - } + call = def.get_cfunction_with_keywords(); + debug_assert!(call.is_some()); } PyMethodDefType::Method(ref def) | PyMethodDefType::Class(ref def) @@ -374,19 +370,17 @@ fn py_class_properties() -> Vec { for def in T::py_methods() { match *def { PyMethodDefType::Getter(ref getter) => { - let name = getter.name.to_string(); - if !defs.contains_key(&name) { - let _ = defs.insert(name.clone(), ffi::PyGetSetDef_INIT); + if !defs.contains_key(getter.name) { + let _ = defs.insert(getter.name.to_owned(), ffi::PyGetSetDef_INIT); } - let def = defs.get_mut(&name).expect("Failed to call get_mut"); + let def = defs.get_mut(getter.name).expect("Failed to call get_mut"); getter.copy_to(def); } PyMethodDefType::Setter(ref setter) => { - let name = setter.name.to_string(); - if !defs.contains_key(&name) { - let _ = defs.insert(name.clone(), ffi::PyGetSetDef_INIT); + if !defs.contains_key(setter.name) { + let _ = defs.insert(setter.name.to_owned(), ffi::PyGetSetDef_INIT); } - let def = defs.get_mut(&name).expect("Failed to call get_mut"); + let def = defs.get_mut(setter.name).expect("Failed to call get_mut"); setter.copy_to(def); } _ => (), diff --git a/src/python.rs b/src/python.rs index 88c372b8..bdd15bab 100644 --- a/src/python.rs +++ b/src/python.rs @@ -498,6 +498,26 @@ impl<'p> Python<'p> { pub fn xdecref(self, ptr: T) { unsafe { ffi::Py_XDECREF(ptr.into_ptr()) }; } + + /// Lets the Python interpreter check for pending signals and invoke the + /// corresponding signal handlers. This can run arbitrary Python code. + /// + /// If an exception is raised by the signal handler, or the default signal + /// handler raises an exception (such as `KeyboardInterrupt` for `SIGINT`), + /// an `Err` is returned. + /// + /// This is a wrapper of the C function `PyErr_CheckSignals()`. It is good + /// practice to call this regularly in a long-running calculation since + /// SIGINT and other signals handled by Python code are left pending for its + /// entire duration. + pub fn check_signals(self) -> PyResult<()> { + let v = unsafe { ffi::PyErr_CheckSignals() }; + if v == -1 { + Err(PyErr::fetch(self)) + } else { + Ok(()) + } + } } #[cfg(test)] diff --git a/src/type_object.rs b/src/type_object.rs index d923d10c..35d039bf 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -222,18 +222,15 @@ impl LazyStaticType { fn initialize_tp_dict( py: Python, type_object: *mut ffi::PyObject, - items: Vec<(&'static str, PyObject)>, + items: Vec<(&'static std::ffi::CStr, PyObject)>, ) -> PyResult<()> { // We hold the GIL: the dictionary update can be considered atomic from // the POV of other threads. for (key, val) in items { - crate::types::with_tmp_string(py, key, |key| { - let ret = unsafe { ffi::PyObject_SetAttr(type_object, key, val.into_ptr()) }; - if ret < 0 { - return Err(PyErr::fetch(py)); - } - Ok(()) - })?; + let ret = unsafe { ffi::PyObject_SetAttrString(type_object, key.as_ptr(), val.into_ptr()) }; + if ret < 0 { + return Err(PyErr::fetch(py)); + } } Ok(()) } diff --git a/src/types/function.rs b/src/types/function.rs index 5acb3ddd..832a52b9 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -3,7 +3,7 @@ use std::ffi::{CStr, CString}; use crate::derive_utils::PyFunctionArguments; use crate::exceptions::PyValueError; use crate::prelude::*; -use crate::{class, ffi, AsPyPointer, PyMethodType}; +use crate::{ffi, AsPyPointer, PyMethodDef, PyMethodType}; /// Represents a builtin Python function object. #[repr(transparent)] @@ -11,60 +11,69 @@ pub struct PyCFunction(PyAny); pyobject_native_var_type!(PyCFunction, ffi::PyCFunction_Type, ffi::PyCFunction_Check); +fn get_name(name: &str) -> PyResult<&'static CStr> { + let cstr = CString::new(name) + .map_err(|_| PyValueError::new_err("Function name cannot contain contain NULL byte."))?; + Ok(Box::leak(cstr.into_boxed_c_str())) +} + +fn get_doc(doc: &str) -> PyResult<&'static CStr> { + let cstr = CString::new(doc) + .map_err(|_| PyValueError::new_err("Document cannot contain contain NULL byte."))?; + Ok(Box::leak(cstr.into_boxed_c_str())) +} + impl PyCFunction { /// Create a new built-in function with keywords. + /// + /// See [raw_pycfunction] for documentation on how to get the `fun` argument. pub fn new_with_keywords<'a>( fun: ffi::PyCFunctionWithKeywords, name: &str, - doc: &'static str, + doc: &str, py_or_module: PyFunctionArguments<'a>, - ) -> PyResult<&'a PyCFunction> { - let fun = PyMethodType::PyCFunctionWithKeywords(fun); - Self::new_(fun, name, doc, py_or_module) + ) -> PyResult<&'a Self> { + Self::internal_new( + get_name(name)?, + get_doc(doc)?, + PyMethodType::PyCFunctionWithKeywords(fun), + ffi::METH_VARARGS | ffi::METH_KEYWORDS, + py_or_module, + ) } /// Create a new built-in function without keywords. pub fn new<'a>( fun: ffi::PyCFunction, name: &str, - doc: &'static str, + doc: &str, py_or_module: PyFunctionArguments<'a>, - ) -> PyResult<&'a PyCFunction> { - let fun = PyMethodType::PyCFunction(fun); - Self::new_(fun, name, doc, py_or_module) + ) -> PyResult<&'a Self> { + Self::internal_new( + get_name(name)?, + get_doc(doc)?, + PyMethodType::PyCFunction(fun), + ffi::METH_NOARGS, + py_or_module, + ) } - fn new_<'a>( - fun: class::PyMethodType, - name: &str, - doc: &'static str, + #[doc(hidden)] + pub fn internal_new<'a>( + name: &'static CStr, + doc: &'static CStr, + method_type: PyMethodType, + flags: std::os::raw::c_int, py_or_module: PyFunctionArguments<'a>, - ) -> PyResult<&'a PyCFunction> { + ) -> PyResult<&'a Self> { let (py, module) = py_or_module.into_py_and_maybe_module(); - let doc: &'static CStr = CStr::from_bytes_with_nul(doc.as_bytes()) - .map_err(|_| PyValueError::new_err("docstring must end with NULL byte."))?; - let name = CString::new(name.as_bytes()).map_err(|_| { - PyValueError::new_err("Function name cannot contain contain NULL byte.") - })?; - let def = match fun { - PyMethodType::PyCFunction(fun) => ffi::PyMethodDef { - ml_name: name.into_raw() as _, - ml_meth: Some(fun), - ml_flags: ffi::METH_VARARGS, - ml_doc: doc.as_ptr() as _, - }, - PyMethodType::PyCFunctionWithKeywords(fun) => ffi::PyMethodDef { - ml_name: name.into_raw() as _, - ml_meth: Some(unsafe { std::mem::transmute(fun) }), - ml_flags: ffi::METH_VARARGS | ffi::METH_KEYWORDS, - ml_doc: doc.as_ptr() as _, - }, - _ => { - return Err(PyValueError::new_err( - "Only PyCFunction and PyCFunctionWithKeywords are valid.", - )) - } + let method_def = PyMethodDef { + ml_name: name, + ml_meth: method_type, + ml_flags: flags, + ml_doc: doc, }; + let def = method_def.as_method_def(); let (mod_ptr, module_name) = if let Some(m) = module { let mod_ptr = m.as_ptr(); let name = m.name()?.into_py(py); diff --git a/tests/test_methods.rs b/tests/test_methods.rs index b6b8a7f8..9c62b3f0 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -235,6 +235,21 @@ impl MethArgs { [a.to_object(py), args.into(), kwargs.to_object(py)].to_object(py) } + #[args("*", a = 2, b = 3)] + fn get_kwargs_only_with_defaults(&self, a: i32, b: i32) -> PyResult { + Ok(a + b) + } + + #[args("*", a, b)] + fn get_kwargs_only(&self, a: i32, b: i32) -> PyResult { + Ok(a + b) + } + + #[args("*", a = 1, b)] + fn get_kwargs_only_with_some_default(&self, a: i32, b: i32) -> PyResult { + Ok(a + b) + } + #[args(a, b = 2, "*", c = 3)] fn get_pos_arg_kw_sep1(&self, a: i32, b: i32, c: i32) -> PyResult { Ok(a + b + c) @@ -308,6 +323,53 @@ fn meth_args() { py_expect_exception!(py, inst, "inst.get_pos_arg_kw(1, a=1)", PyTypeError); py_expect_exception!(py, inst, "inst.get_pos_arg_kw(b=2)", PyTypeError); + py_run!(py, inst, "assert inst.get_kwargs_only_with_defaults() == 5"); + py_run!( + py, + inst, + "assert inst.get_kwargs_only_with_defaults(a = 8) == 11" + ); + py_run!( + py, + inst, + "assert inst.get_kwargs_only_with_defaults(b = 8) == 10" + ); + py_run!( + py, + inst, + "assert inst.get_kwargs_only_with_defaults(a = 1, b = 1) == 2" + ); + py_run!( + py, + inst, + "assert inst.get_kwargs_only_with_defaults(b = 1, a = 1) == 2" + ); + + py_run!(py, inst, "assert inst.get_kwargs_only(a = 1, b = 1) == 2"); + py_run!(py, inst, "assert inst.get_kwargs_only(b = 1, a = 1) == 2"); + + py_run!( + py, + inst, + "assert inst.get_kwargs_only_with_some_default(a = 2, b = 1) == 3" + ); + py_run!( + py, + inst, + "assert inst.get_kwargs_only_with_some_default(b = 1) == 2" + ); + py_run!( + py, + inst, + "assert inst.get_kwargs_only_with_some_default(b = 1, a = 2) == 3" + ); + py_expect_exception!( + py, + inst, + "inst.get_kwargs_only_with_some_default()", + PyTypeError + ); + py_run!(py, inst, "assert inst.get_pos_arg_kw_sep1(1) == 6"); py_run!(py, inst, "assert inst.get_pos_arg_kw_sep1(1, 2) == 6"); py_run!( @@ -315,6 +377,21 @@ fn meth_args() { inst, "assert inst.get_pos_arg_kw_sep1(1, 2, c=13) == 16" ); + py_run!( + py, + inst, + "assert inst.get_pos_arg_kw_sep1(a=1, b=2, c=13) == 16" + ); + py_run!( + py, + inst, + "assert inst.get_pos_arg_kw_sep1(b=2, c=13, a=1) == 16" + ); + py_run!( + py, + inst, + "assert inst.get_pos_arg_kw_sep1(c=13, b=2, a=1) == 16" + ); py_expect_exception!(py, inst, "inst.get_pos_arg_kw_sep1(1, 2, 3)", PyTypeError); py_run!(py, inst, "assert inst.get_pos_arg_kw_sep2(1) == 6"); diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 8a1e52d2..521c12f2 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -116,7 +116,7 @@ fn test_raw_function() { let gil = Python::acquire_gil(); let py = gil.python(); let raw_func = raw_pycfunction!(optional_bool); - let fun = PyCFunction::new_with_keywords(raw_func, "fun", "\0", py.into()).unwrap(); + let fun = PyCFunction::new_with_keywords(raw_func, "fun", "", py.into()).unwrap(); let res = fun.call((), None).unwrap().extract::<&str>().unwrap(); assert_eq!(res, "Some(true)"); let res = fun.call((false,), None).unwrap().extract::<&str>().unwrap(); diff --git a/tests/ui/invalid_macro_args.rs b/tests/ui/invalid_macro_args.rs index f99f5814..241d35c6 100644 --- a/tests/ui/invalid_macro_args.rs +++ b/tests/ui/invalid_macro_args.rs @@ -5,11 +5,6 @@ fn pos_after_kw(py: Python, a: i32, b: i32) -> PyObject { [a.to_object(py), vararg.into()].to_object(py) } -#[pyfunction(a, "*", b)] -fn pos_after_separator(py: Python, a: i32, b: i32) -> PyObject { - [a.to_object(py), vararg.into()].to_object(py) -} - #[pyfunction(kwargs = "**", a = 5)] fn kw_after_kwargs(py: Python, kwargs: &PyDict, a: i32) -> PyObject { [a.to_object(py), vararg.into()].to_object(py) diff --git a/tests/ui/invalid_macro_args.stderr b/tests/ui/invalid_macro_args.stderr index 1e9dab29..20e0dee0 100644 --- a/tests/ui/invalid_macro_args.stderr +++ b/tests/ui/invalid_macro_args.stderr @@ -4,14 +4,8 @@ error: Positional argument or varargs(*) is not allowed after keyword arguments 3 | #[pyfunction(a = 5, b)] | ^ -error: Positional argument or varargs(*) is not allowed after * - --> $DIR/invalid_macro_args.rs:8:22 - | -8 | #[pyfunction(a, "*", b)] - | ^ - error: Keyword argument or kwargs(**) is not allowed after kwargs(**) - --> $DIR/invalid_macro_args.rs:13:29 - | -13 | #[pyfunction(kwargs = "**", a = 5)] - | ^^^^^ + --> $DIR/invalid_macro_args.rs:8:29 + | +8 | #[pyfunction(kwargs = "**", a = 5)] + | ^^^^^