From 1531ac8d7e1df022081d080c2a7abb39c9ee40ee Mon Sep 17 00:00:00 2001 From: KRunchPL Date: Sat, 6 Nov 2021 15:24:39 +0100 Subject: [PATCH 01/30] #1959 - Add Python Typing Hints to the Guide --- guide/src/SUMMARY.md | 1 + guide/src/python_typing_hints.md | 145 +++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 147 insertions(+) create mode 100644 guide/src/python_typing_hints.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 995a1680..fe117f61 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -31,4 +31,5 @@ [Appendix A: Migration Guide](migration.md) [Appendix B: PyO3 and rust-cpython](rust_cpython.md) [Appendix C: Trait bounds](trait_bounds.md) +[Appendix D: Python typing hints](python_typing_hints.md) [CHANGELOG](changelog.md) diff --git a/guide/src/python_typing_hints.md b/guide/src/python_typing_hints.md new file mode 100644 index 00000000..47ec5b89 --- /dev/null +++ b/guide/src/python_typing_hints.md @@ -0,0 +1,145 @@ + +# Typing and IDE hints for you Python package +PyO3 provides an easy to use interface to code native Python libraries in Rust. The accompanying Maturin allows you to build and publish them as a package. Yet, for the better user experience, Python libraries should provide typing hints and documentation for all public entities, so that IDEs can show them during development and type analyzing tools such as `mypy` can use them to properly verify the code. + +Currently the best solution for the problem is to maintain manually the `*.pyi` files and ship them along with the package. + +## The `pyi` files introduction + +`pyi` (an abbreviation for `Python Interface`) is called a `Stub File` in most of the documentations related to them. Very good definition of what it is can be found in [old MyPy documentation](https://github.com/python/mypy/wiki/Creating-Stubs-For-Python-Modules): +> A stubs file only contains a description of the public interface of the module without any implementations. + +Probably most Python developers encountered them already when trying to use the IDE "Go to Definition" function on any builtin type. For example the definitions of few standard exceptions look like this: + +```python +class BaseException(object): + args: Tuple[Any, ...] + __cause__: BaseException | None + __context__: BaseException | None + __suppress_context__: bool + __traceback__: TracebackType | None + def __init__(self, *args: object) -> None: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def with_traceback(self: _TBE, tb: TracebackType | None) -> _TBE: ... + +class SystemExit(BaseException): + code: int + +class Exception(BaseException): ... + +class StopIteration(Exception): + value: Any +``` + +As we can see those are not full definitions containing implementation, but just a description of interface. It is usually all that is needed by the user of the library. + +### What does the PEPs say? + +As of the time of writing this documentation the `pyi` files are referenced in three PEPs. + +[PEP8 - Style Guide for Python Code - #Function Annotations](https://www.python.org/dev/peps/pep-0008/#function-annotations) (last point) recommends all third party library creators to provide stub files as the source of knowledge about the package for type checker tools. + +> (...) it is expected that users of third party library packages may want to run type checkers over those packages. For this purpose [PEP 484](https://www.python.org/dev/peps/pep-0484) recommends the use of stub files: .pyi files that are read by the type checker in preference of the corresponding .py files. (...) + +[PEP484 - Type Hints - #Stub Files](https://www.python.org/dev/peps/pep-0484/#stub-files) defines stub files as follows. +> Stub files are files containing type hints that are only for use by the type checker, not at runtime. + +It contains a specification for them (highly recommended reading, since it contains at least one thing that is not used in normal Python code) and also some general information about where to store the stub files. + +[PEP561 - Distributing and Packaging Type Information](https://www.python.org/dev/peps/pep-0561/) describes in detail how to build packages that will enable type checking. In particular it contains information about how the stub files must be distributed in order for type checkers to use them. + +## How to do it? + +[PEP561](https://www.python.org/dev/peps/pep-0561/) recognizes three ways of distributing type information: + +* `inline` - the typing is placed directly in source (`py`) files; +* `separate package with stub files` - the typing is placed in `pyi` files distributed in their own, separate package; +* `in-package stub files` - the typing is placed in `pyi` files distributed in the same package as source files. + +The first way is tricky with PyO3 since we do not have `py` files. When it will be investigated and necessary changes are implemented, this document will be updated. + +The second way is easy to do, and the whole work can be fully separated from the main library code. The example repo for the package with stub files can be found in [PEP561 references section](https://www.python.org/dev/peps/pep-0561/#references): [Stub package repository](https://github.com/ethanhs/stub-package) + +The third way is described below. + +### Including `pyi` files in your PyO3/Maturin build package + +When source files are in the same package as stub files, they should be placed next to each other. We need a way to do that with Maturin. Also, in order to mark our package as typing-enabled we need to add an empty file named `py.typed` to the package. + +#### Your custom Python package files + + Fortunately the Maturin provides easy way to add files to package ([documentation](https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects)). You just need to create a folder with the name of your module next to the `Cargo.toml` file (for customization see documentation linked above). + +The folder structure would be: + +```text +my-project +├── Cargo.toml +├── my_project +│ ├── __init__.py +│ ├── my_project.pyi +│ └── py.typed +├── pyproject.toml +├── Readme.md +└── src + └── lib.rs +``` + +Let's go a little bit more into details on the files inside the package folder. + +#### `__init__.py` content + +As we now specify our own package content, we have to provide the `__init__.py` file, so the folder is treated as a package and we can import things from it. We can always use the same content that the Maturin creates for us if we do not specify a python source folder. For PyO3 bindings it would be: + +```python +from .my_project import * +``` + +That way everything that is exposed by our native module can be imported directly from the package. + +#### `py.typed` requirement + +As stated in [PEP561](https://www.python.org/dev/peps/pep-0561/): +> Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing. This marker applies recursively: if a top-level package includes it, all its sub-packages MUST support type checking as well. + +If we do not include that file, some IDEs might still use our `pyi` files to show hints, but the type checkers might not. MyPy will raise an error in this situation: + +```text +error: Skipping analyzing "my_project": found module but no type hints or library stubs +``` + +The file is just a marker file, so it should be empty. + +#### `my_project.pyi` content + +Our module stub file. This document does not aim at describing how to write them, since you can find a lot of documentation on it, starting from already quoted [PEP484](https://www.python.org/dev/peps/pep-0484/#stub-files). + +The example can look like this: + +```python +class Car: + """ + A class representing a car. + + :param body_type: the name of body type, e.g. hatchback, sedan + :param horsepower: power of the engine in horsepower + """ + def __init__(self, body_type: str, horsepower: int) -> None: ... + + @classmethod + def from_unique_name(cls, name: str) -> 'Car': + """ + Creates a Car based on unique name + + :param name: model name of a car to be created + :return: a Car instance with default data + """ + + def best_color(self) -> str: + """ + Gets the best color for the car. + + :return: the name of the color our great algorithm thinks is the best for this car + """ +``` diff --git a/src/lib.rs b/src/lib.rs index 2d0ca39b..527c5bfa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -405,6 +405,7 @@ pub mod doc_test { doctest!("guide/src/trait_bounds.md", guide_trait_bounds_md); doctest!("guide/src/types.md", guide_types_md); doctest!("guide/src/faq.md", faq); + doctest!("guide/src/python_typing_hints.md", guide_python_typing_hints); // deliberate choice not to test guide/ecosystem because those pages depend on external crates // such as pyo3_asyncio. From 5011ac7ce859ba0ecca5a3cc28c3ff21867b0942 Mon Sep 17 00:00:00 2001 From: KRunchPL Date: Sat, 6 Nov 2021 17:26:43 +0100 Subject: [PATCH 02/30] Fix formatting --- src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 527c5bfa..07297153 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -405,7 +405,10 @@ pub mod doc_test { doctest!("guide/src/trait_bounds.md", guide_trait_bounds_md); doctest!("guide/src/types.md", guide_types_md); doctest!("guide/src/faq.md", faq); - doctest!("guide/src/python_typing_hints.md", guide_python_typing_hints); + doctest!( + "guide/src/python_typing_hints.md", + guide_python_typing_hints + ); // deliberate choice not to test guide/ecosystem because those pages depend on external crates // such as pyo3_asyncio. From a0716ce519fc2fdfd51347e5997a8b15839e59ba Mon Sep 17 00:00:00 2001 From: KRunchPL Date: Sat, 6 Nov 2021 17:29:02 +0100 Subject: [PATCH 03/30] Description of simple solution implemented in Maturin guide --- guide/src/python_typing_hints.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/guide/src/python_typing_hints.md b/guide/src/python_typing_hints.md index 47ec5b89..8104b3f5 100644 --- a/guide/src/python_typing_hints.md +++ b/guide/src/python_typing_hints.md @@ -1,5 +1,5 @@ - # Typing and IDE hints for you Python package + PyO3 provides an easy to use interface to code native Python libraries in Rust. The accompanying Maturin allows you to build and publish them as a package. Yet, for the better user experience, Python libraries should provide typing hints and documentation for all public entities, so that IDEs can show them during development and type analyzing tools such as `mypy` can use them to properly verify the code. Currently the best solution for the problem is to maintain manually the `*.pyi` files and ship them along with the package. @@ -7,6 +7,7 @@ Currently the best solution for the problem is to maintain manually the `*.pyi` ## The `pyi` files introduction `pyi` (an abbreviation for `Python Interface`) is called a `Stub File` in most of the documentations related to them. Very good definition of what it is can be found in [old MyPy documentation](https://github.com/python/mypy/wiki/Creating-Stubs-For-Python-Modules): + > A stubs file only contains a description of the public interface of the module without any implementations. Probably most Python developers encountered them already when trying to use the IDE "Go to Definition" function on any builtin type. For example the definitions of few standard exceptions look like this: @@ -43,6 +44,7 @@ As of the time of writing this documentation the `pyi` files are referenced in t > (...) it is expected that users of third party library packages may want to run type checkers over those packages. For this purpose [PEP 484](https://www.python.org/dev/peps/pep-0484) recommends the use of stub files: .pyi files that are read by the type checker in preference of the corresponding .py files. (...) [PEP484 - Type Hints - #Stub Files](https://www.python.org/dev/peps/pep-0484/#stub-files) defines stub files as follows. + > Stub files are files containing type hints that are only for use by the type checker, not at runtime. It contains a specification for them (highly recommended reading, since it contains at least one thing that is not used in normal Python code) and also some general information about where to store the stub files. @@ -67,9 +69,24 @@ The third way is described below. When source files are in the same package as stub files, they should be placed next to each other. We need a way to do that with Maturin. Also, in order to mark our package as typing-enabled we need to add an empty file named `py.typed` to the package. -#### Your custom Python package files +#### If you do not have other Python files - Fortunately the Maturin provides easy way to add files to package ([documentation](https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects)). You just need to create a folder with the name of your module next to the `Cargo.toml` file (for customization see documentation linked above). +If you do not need to add any other Python files apart from `pyi` to the package, the Maturin provides a way to do most of the work for you. As documented in [Maturin Guide](https://github.com/PyO3/maturin/blob/084cfaced651b28616aeea1f818bdc933a536bfe/guide/src/project_layout.md#adding-python-type-information) the only thing you need to do is create a stub file for your module named `.pyi` in your project root and Maturin will do the rest. + +```text +my-rust-project/ +├── Cargo.toml +├── my_project.pyi # <<< add type stubs for Rust functions in the my_project module here +├── pyproject.toml +└── src + └── lib.rs +``` + +For example of `pyi` file see [`my_project.pyi` content](#my_projectpyi-content) section. + +#### If you need other Python files + +If you need to add other Python files apart from `pyi` to the package, you can do it also, but that requires some more work. Maturin provides easy way to add files to package ([documentation](https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects)). You just need to create a folder with the name of your module next to the `Cargo.toml` file (for customization see documentation linked above). The folder structure would be: @@ -79,6 +96,7 @@ my-project ├── my_project │ ├── __init__.py │ ├── my_project.pyi +│ ├── other_python_file.py │ └── py.typed ├── pyproject.toml ├── Readme.md @@ -88,7 +106,7 @@ my-project Let's go a little bit more into details on the files inside the package folder. -#### `__init__.py` content +##### `__init__.py` content As we now specify our own package content, we have to provide the `__init__.py` file, so the folder is treated as a package and we can import things from it. We can always use the same content that the Maturin creates for us if we do not specify a python source folder. For PyO3 bindings it would be: @@ -98,7 +116,7 @@ from .my_project import * That way everything that is exposed by our native module can be imported directly from the package. -#### `py.typed` requirement +##### `py.typed` requirement As stated in [PEP561](https://www.python.org/dev/peps/pep-0561/): > Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing. This marker applies recursively: if a top-level package includes it, all its sub-packages MUST support type checking as well. @@ -111,7 +129,7 @@ error: Skipping analyzing "my_project": found module but no type hints or librar The file is just a marker file, so it should be empty. -#### `my_project.pyi` content +##### `my_project.pyi` content Our module stub file. This document does not aim at describing how to write them, since you can find a lot of documentation on it, starting from already quoted [PEP484](https://www.python.org/dev/peps/pep-0484/#stub-files). From 3d1bcae3aa1e5b96b949be9d0b88624439da1ea6 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 7 Nov 2021 12:57:40 +0000 Subject: [PATCH 04/30] guide: document `#[pyo3(from_py_with)]` --- guide/src/class.md | 106 +++++++++++++ guide/src/function.md | 337 +++++++++++++++++------------------------ pyo3-macros/src/lib.rs | 3 +- 3 files changed, 250 insertions(+), 196 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 7c75c419..9ab5bf96 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -696,6 +696,112 @@ num=44 num=-1 ``` +## Making class method signatures available to Python + +The [`#[pyo3(text_signature = "...")]`](./function.md#text_signature) option for `#[pyfunction]` also works for classes and methods: + +```rust +# #![allow(dead_code)] +use pyo3::prelude::*; +use pyo3::types::PyType; + +// it works even if the item is not documented: +#[pyclass] +#[pyo3(text_signature = "(c, d, /)")] +struct MyClass {} + +#[pymethods] +impl MyClass { + // the signature for the constructor is attached + // to the struct definition instead. + #[new] + fn new(c: i32, d: &str) -> Self { + Self {} + } + // the self argument should be written $self + #[pyo3(text_signature = "($self, e, f)")] + fn my_method(&self, e: i32, f: i32) -> i32 { + e + f + } + #[classmethod] + #[pyo3(text_signature = "(cls, e, f)")] + fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 { + e + f + } + #[staticmethod] + #[pyo3(text_signature = "(e, f)")] + fn my_static_method(e: i32, f: i32) -> i32 { + e + f + } +} +# +# fn main() -> PyResult<()> { +# Python::with_gil(|py| { +# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; +# let module = PyModule::new(py, "my_module")?; +# module.add_class::()?; +# let class = module.getattr("MyClass")?; +# +# if cfg!(not(Py_LIMITED_API)) || py.version_info() >= (3, 10) { +# let doc: String = class.getattr("__doc__")?.extract()?; +# assert_eq!(doc, ""); +# +# let sig: String = inspect +# .call1((class,))? +# .call_method0("__str__")? +# .extract()?; +# assert_eq!(sig, "(c, d, /)"); +# } else { +# let doc: String = class.getattr("__doc__")?.extract()?; +# assert_eq!(doc, ""); +# +# inspect.call1((class,)).expect_err("`text_signature` on classes is not compatible with compilation in `abi3` mode until Python 3.10 or greater"); +# } +# +# { +# let method = class.getattr("my_method")?; +# +# assert!(method.getattr("__doc__")?.is_none()); +# +# let sig: String = inspect +# .call1((method,))? +# .call_method0("__str__")? +# .extract()?; +# assert_eq!(sig, "(self, /, e, f)"); +# } +# +# { +# let method = class.getattr("my_class_method")?; +# +# assert!(method.getattr("__doc__")?.is_none()); +# +# let sig: String = inspect +# .call1((method,))? +# .call_method0("__str__")? +# .extract()?; +# assert_eq!(sig, "(cls, e, f)"); +# } +# +# { +# let method = class.getattr("my_static_method")?; +# +# assert!(method.getattr("__doc__")?.is_none()); +# +# let sig: String = inspect +# .call1((method,))? +# .call_method0("__str__")? +# .extract()?; +# assert_eq!(sig, "(e, f)"); +# } +# +# Ok(()) +# }) +# } +``` + +Note that `text_signature` on classes is not compatible with compilation in +`abi3` mode until Python 3.10 or greater. + ## Implementation details The `#[pyclass]` macros rely on a lot of conditional code generation: each `#[pyclass]` can optionally have a `#[pymethods]` block as well as several different possible `#[pyproto]` trait implementations. diff --git a/guide/src/function.md b/guide/src/function.md index aaf4ccb0..e027b99c 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -1,8 +1,8 @@ # Python Functions -PyO3 supports two ways to define a free function in Python. Both require registering the function to a [module](./module.md). +The `#[pyfunction]` attribute is used to define a Python function from a Rust function. Once defined, the function needs to be added to a [module](./module.md) using the `wrap_pyfunction!` macro. -One way is annotating a function with `#[pyfunction]` and then adding it to the module using the `wrap_pyfunction!` macro. +The following example defines a function called `double` in a Python module called `my_extension`: ```rust use pyo3::prelude::*; @@ -19,50 +19,27 @@ fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { } ``` -Alternatively, there is a shorthand: the function can be placed inside the module definition and -annotated with `#[pyfn]`, as below: +This chapter of the guide explains full usage of the `#[pyfunction]` attribute. The following topics are covered: -```rust -use pyo3::prelude::*; - -#[pymodule] -fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { - - #[pyfn(m)] - fn double(x: usize) -> usize { - x * 2 - } - - Ok(()) -} -``` - -`#[pyfn(m)]` is just syntactic sugar for `#[pyfunction]`, and takes all the same options -documented in the rest of this chapter. The code above is expanded to the following: - -```rust -use pyo3::prelude::*; - -#[pymodule] -fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { - - #[pyfunction] - fn double(x: usize) -> usize { - x * 2 - } - - m.add_function(wrap_pyfunction!(double, m)?)?; - Ok(()) -} -``` +- [Function options](#function-options) + - [`#[pyo3(name = "...")]`](#name) + - [`#[pyo3(text_signature = "...")]`](#text_signature) + - [`#[pyo3(pass_module)]`](#pass_module) +- [Argument parsing](#argument-parsing) + - [`#[pyo3(from_py_with = "...")]`](#from_py_with) +- [Advanced function patterns](#advanced-function-patterns) +- [`#[pyfn]` shorthand](#pyfn-shorthand) ## Function options The `#[pyo3]` attribute can be used to modify properties of the generated Python function. It can take any combination of the following options: - - `#[pyo3(name = "...")]` + - `#[pyo3(name = "...")]` - Overrides the name generated in Python: + Overrides the name exposed to Python. + + In the following example, the Rust function `no_args_py` will be added to the Python module + `module_with_functions` as the Python function `no_args`: ```rust use pyo3::prelude::*; @@ -84,6 +61,63 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python # }); ``` + - `#[pyo3(text_signature = "...")]` + + Sets the function signature visible in Python tooling (such as via [`inspect.signature`]). + + The example below creates a function `add` which has a signature describing two positional-only + arguments `a` and `b`. + + ```rust + use pyo3::prelude::*; + + /// This function adds two unsigned 64-bit integers. + #[pyfunction] + #[pyo3(text_signature = "(a, b, /)")] + fn add(a: u64, b: u64) -> u64 { + a + b + } + # + # fn main() -> PyResult<()> { + # Python::with_gil(|py| { + # let fun = pyo3::wrap_pyfunction!(add, py)?; + # + # let doc: String = fun.getattr("__doc__")?.extract()?; + # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); + # + # let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; + # let sig: String = inspect + # .call1((fun,))? + # .call_method0("__str__")? + # .extract()?; + # assert_eq!(sig, "(a, b, /)"); + # + # Ok(()) + # }) + # } + ``` + + - `#[pyo3(pass_module)]` + + Set this option to make PyO3 pass the containing module as the first argument to the function. It is then possible to use the module in the function body. The first argument **must** be of type `&PyModule`. + + The following example creates a function `pyfunction_with_module` which returns the containing module's name (i.e. `module_with_fn`): + + ```rust + use pyo3::prelude::*; + + #[pyfunction] + #[pyo3(pass_module)] + fn pyfunction_with_module(module: &PyModule) -> PyResult<&str> { + module.name() + } + + #[pymodule] + fn module_with_fn(py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?) + } + ``` + ## Argument parsing The `#[pyfunction]` attribute supports specifying details of argument parsing. The details are given in the section ["Method arguments" of the Classes chapter](class.md#method-arguments). Here is an example for a function that accepts arbitrary keyword arguments (`**kwargs` in Python syntax) and returns the number that was passed: @@ -104,146 +138,38 @@ fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> { } ``` -## Making the function signature available to Python +### Per-argument options -In order to make the function signature available to Python to be retrieved via -`inspect.signature`, use the `#[pyo3(text_signature)]` annotation as in the example -below. The `/` signifies the end of positional-only arguments. (This -is not a feature of this library in particular, but the general format used by -CPython for annotating signatures of built-in functions.) +The `#[pyo3]` attribute can be used on individual arguments to modify properties of them in the generated function. It can take any combination of the following options: -```rust -use pyo3::prelude::*; + - `#[pyo3(from_py_with = "...")]` -/// This function adds two unsigned 64-bit integers. -#[pyfunction] -#[pyo3(text_signature = "(a, b, /)")] -fn add(a: u64, b: u64) -> u64 { - a + b -} -# -# fn main() -> PyResult<()> { -# Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(add, py)?; -# -# let doc: String = fun.getattr("__doc__")?.extract()?; -# assert_eq!(doc, "This function adds two unsigned 64-bit integers."); -# -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; -# let sig: String = inspect -# .call1((fun,))? -# .call_method0("__str__")? -# .extract()?; -# assert_eq!(sig, "(a, b, /)"); -# -# Ok(()) -# }) -# } -``` + Set this on an option to specify a custom function to convert the function argument from Python to the desired Rust type, instead of using the default `FromPyObject` extraction. The function signature must be `fn(&PyAny) -> PyResult` where `T` is the Rust type of the argument. -This also works for classes and methods: + The following example uses `from_py_with` to convert the input Python object to its length: -```rust -# #![allow(dead_code)] -use pyo3::prelude::*; -use pyo3::types::PyType; + ```rust + use pyo3::prelude::*; -// it works even if the item is not documented: -#[pyclass] -#[pyo3(text_signature = "(c, d, /)")] -struct MyClass {} - -#[pymethods] -impl MyClass { - // the signature for the constructor is attached - // to the struct definition instead. - #[new] - fn new(c: i32, d: &str) -> Self { - Self {} + fn get_length(obj: &PyAny) -> PyResult { + let length = obj.len()?; + Ok(length) } - // the self argument should be written $self - #[pyo3(text_signature = "($self, e, f)")] - fn my_method(&self, e: i32, f: i32) -> i32 { - e + f - } - #[classmethod] - #[pyo3(text_signature = "(cls, e, f)")] - fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 { - e + f - } - #[staticmethod] - #[pyo3(text_signature = "(e, f)")] - fn my_static_method(e: i32, f: i32) -> i32 { - e + f - } -} -# -# fn main() -> PyResult<()> { -# Python::with_gil(|py| { -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; -# let module = PyModule::new(py, "my_module")?; -# module.add_class::()?; -# let class = module.getattr("MyClass")?; -# -# if cfg!(not(Py_LIMITED_API)) || py.version_info() >= (3, 10) { -# let doc: String = class.getattr("__doc__")?.extract()?; -# assert_eq!(doc, ""); -# -# let sig: String = inspect -# .call1((class,))? -# .call_method0("__str__")? -# .extract()?; -# assert_eq!(sig, "(c, d, /)"); -# } else { -# let doc: String = class.getattr("__doc__")?.extract()?; -# assert_eq!(doc, ""); -# -# inspect.call1((class,)).expect_err("`text_signature` on classes is not compatible with compilation in `abi3` mode until Python 3.10 or greater"); -# } -# -# { -# let method = class.getattr("my_method")?; -# -# assert!(method.getattr("__doc__")?.is_none()); -# -# let sig: String = inspect -# .call1((method,))? -# .call_method0("__str__")? -# .extract()?; -# assert_eq!(sig, "(self, /, e, f)"); -# } -# -# { -# let method = class.getattr("my_class_method")?; -# -# assert!(method.getattr("__doc__")?.is_none()); -# -# let sig: String = inspect -# .call1((method,))? -# .call_method0("__str__")? -# .extract()?; -# assert_eq!(sig, "(cls, e, f)"); -# } -# -# { -# let method = class.getattr("my_static_method")?; -# -# assert!(method.getattr("__doc__")?.is_none()); -# -# let sig: String = inspect -# .call1((method,))? -# .call_method0("__str__")? -# .extract()?; -# assert_eq!(sig, "(e, f)"); -# } -# -# Ok(()) -# }) -# } -``` -Note that `text_signature` on classes is not compatible with compilation in -`abi3` mode until Python 3.10 or greater. + #[pyfunction] + fn object_length( + #[pyo3(from_py_with = "get_length")] argument: usize + ) -> usize { + argument + } + + # Python::with_gil(|py| { + # let f = pyo3::wrap_pyfunction!(object_length)(py).unwrap(); + # assert_eq!(f.call1((vec![1, 2, 3],)).unwrap().extract::().unwrap(), 3); + # }); + ``` + +## Advanced function patterns ### Making the function signature available to Python (old method) @@ -288,7 +214,7 @@ Docstring: This function adds two unsigned 64-bit integers. Type: builtin_function_or_method ``` -## Closures +### 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 @@ -325,29 +251,7 @@ in Python code. [`PyFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFunction.html [`PyCFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyCFunction.html -### Accessing the module of a function - -It is possible to access the module of a `#[pyfunction]` in the function body by using `#[pyo3(pass_module)]` option: - -```rust -use pyo3::prelude::*; - -#[pyfunction] -#[pyo3(pass_module)] -fn pyfunction_with_module(module: &PyModule) -> PyResult<&str> { - module.name() -} - -#[pymodule] -fn module_with_fn(py: Python, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?) -} -``` - -If `pass_module` is set, the first argument **must** be the `&PyModule`. It is then possible to use the module -in the function body. - -## Accessing the FFI functions +### Accessing the FFI functions In order to make Rust functions callable from Python, PyO3 generates an `extern "C"` function whose exact signature depends on the Rust signature. (PyO3 chooses the optimal @@ -357,3 +261,46 @@ arguments from the input `PyObject`s. The `wrap_pyfunction` macro can be used to directly get a `PyCFunction` given a `#[pyfunction]` and a `PyModule`: `wrap_pyfunction!(rust_fun, module)`. + +## `#[pyfn]` shorthand + +There is a shorthand to `#[pyfunction]` and `wrap_pymodule!`: the function can be placed inside the module definition and +annotated with `#[pyfn]`. To simplify PyO3, it is expected that `#[pyfn]` may be removed in a future release (See [#694](https://github.com/PyO3/pyo3/issues/694)). + +An example of `#[pyfn]` is below: + +```rust +use pyo3::prelude::*; + +#[pymodule] +fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { + + #[pyfn(m)] + fn double(x: usize) -> usize { + x * 2 + } + + Ok(()) +} +``` + +`#[pyfn(m)]` is just syntactic sugar for `#[pyfunction]`, and takes all the same options +documented in the rest of this chapter. The code above is expanded to the following: + +```rust +use pyo3::prelude::*; + +#[pymodule] +fn my_extension(py: Python, m: &PyModule) -> PyResult<()> { + + #[pyfunction] + fn double(x: usize) -> usize { + x * 2 + } + + m.add_function(wrap_pyfunction!(double, m)?)?; + Ok(()) +} +``` + +[`inspect.signature`]: https://docs.python.org/3/library/inspect.html#inspect.signature diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index d9155399..b8ab3f6a 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -125,7 +125,7 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { /// A proc macro used to expose methods to Python. /// -/// Methods within a `#[pymethods]` block can be annotated with the following: +/// Methods within a `#[pymethods]` block can be annotated with as well as the following: /// /// | Annotation | Description | /// | :- | :- | @@ -135,6 +135,7 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { /// | [`#[classmethod]`][7] | Defines the method as a classmethod, like Python's `@classmethod` decorator.| /// | [`#[classattr]`][9] | Defines a class variable. | /// | [`#[args]`][10] | Define a method's default arguments and allows the function to receive `*args` and `**kwargs`. | +/// | [`#[pyo3( | Any of the `#[pyo3]` options supported on [`macro@pyfunction`]. | /// /// For more on creating class methods, /// see the [class section of the guide][1]. From b0af3ec25f25d99a6e851a41cd73e6fa2395f3af Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Wed, 10 Nov 2021 08:34:54 +0000 Subject: [PATCH 05/30] docs: pyo3 config files --- Architecture.md | 38 ++++++++++++++++++-------- guide/pyo3_version.py | 3 ++ guide/src/building_and_distribution.md | 6 +++- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/Architecture.md b/Architecture.md index 2734371b..474381d4 100644 --- a/Architecture.md +++ b/Architecture.md @@ -28,8 +28,9 @@ To summarize, there are six main parts to the PyO3 codebase. - [`src/class`] 5. [Procedural macros to simplify usage for users.](#5-procedural-macros-to-simplify-usage-for-users) - [`src/derive_utils.rs`], [`pyo3-macros`] and [`pyo3-macros-backend`] -6. [`build.rs`](#6-buildrs) +6. [`build.rs` and `pyo3-build-config`](#6-buildrs-and-pyo3-build-config) - [`build.rs`](https://github.com/PyO3/pyo3/tree/main/build.rs) + - [`pyo3-build-config`] ## 1. Low-level bindings of Python/C API @@ -50,7 +51,7 @@ With `Py_LIMITED_API`, we can build a Python-version-agnostic binary called an `Py_37` means that the API is available from Python >= 3.7. There are also `Py_38`, `Py_39`, and so on. `PyPy` means that the API definition is for PyPy. -Those flags are set in [`build.rs`](#6-buildrs). +Those flags are set in [`build.rs`](#6-buildrs-and-pyo3-build-config). ## 2. Bindings to Python objects @@ -166,25 +167,37 @@ some internal tricks for making `#[pyproto]` flexible. [`src/derive_utils.rs`] contains some utilities used in code generated by these proc-macros, such as parsing function arguments. -## 6. `build.rs` +## 6. `build.rs` and `pyo3-build-config` -PyO3's [`build.rs`](https://github.com/PyO3/pyo3/tree/main/build.rs) is relatively long -(about 900 lines) to support multiple architectures, interpreters, and usages. -Below is a non-exhaustive list of its functionality: +PyO3 supports a wide range of OSes, interpreters and use cases. The correct environment must be +detected at build time in order to set up relevant conditional compilation correctly. This logic +is captured in the [`pyo3-build-config`] crate, which is a `build-dependency` of `pyo3` and +`pyo3-macros`, and can also be used by downstream users in the same way. -- Cross-compiling support. - - If `TARGET` architecture and `HOST` architecture differ, we find cross compile information - from environment variables (`PYO3_CROSS_LIB_DIR` and `PYO3_CROSS_PYTHON_VERSION`) or system files. +In [`pyo3-build-config`]'s `build.rs` the build environment is detected and inlined into the crate +as a "config file". This works in all cases except for cross-compiling, where it is necessary to +capture this from the `pyo3` `build.rs` to get some extra environment variables that Cargo doesn't +set for build dependencies. + +The `pyo3` `build.rs` also runs some safety checks such as ensuring the Python version detected is +actually supported. + +Some of the functionality of `pyo3-build-config`: - Find the interpreter for build and detect the Python version. - - We have to set some version flags like `Py_37`. - - If the interpreter is PyPy, we set `PyPy`. - - If `PYO3_NO_PYTHON` environment variable is set then the interpreter detection is bypassed + - We have to set some version flags like `#[cfg(Py_3_7)]`. + - If the interpreter is PyPy, we set `#[cfg(PyPy)`. + - If the `PYO3_CONFIG_FILE` environment variable is set then that file's contents will be used + instead of any detected configuration. + - If the `PYO3_NO_PYTHON` environment variable is set then the interpreter detection is bypassed entirely and only abi3 extensions can be built. - Check if we are building a Python extension. - If we are building an extension (e.g., Python library installable by `pip`), we don't link `libpython`. Currently we use the `extension-module` feature for this purpose. This may change in the future. See [#1123](https://github.com/PyO3/pyo3/pull/1123). +- Cross-compiling configuration + - If `TARGET` architecture and `HOST` architecture differ, we find cross compile information + from environment variables (`PYO3_CROSS_LIB_DIR` and `PYO3_CROSS_PYTHON_VERSION`) or system files. @@ -194,6 +207,7 @@ Below is a non-exhaustive list of its functionality: [`pyo3-macros`]: https://github.com/PyO3/pyo3/tree/main/pyo3-macros [`pyo3-macros-backend`]: https://github.com/PyO3/pyo3/tree/main/pyo3-macros-backend +[`pyo3-build-config`]: https://github.com/PyO3/pyo3/tree/main/pyo3-build-config diff --git a/guide/pyo3_version.py b/guide/pyo3_version.py index bc729e4c..7595b5a7 100644 --- a/guide/pyo3_version.py +++ b/guide/pyo3_version.py @@ -18,11 +18,13 @@ PYO3_VERSION_TAG = os.environ.get("PYO3_VERSION_TAG", "main") if PYO3_VERSION_TAG == "main": PYO3_DOCS_URL = "https://pyo3.rs/main/doc" + PYO3_DOCS_VERSION = "latest" PYO3_CRATE_VERSION = 'git = "https://github.com/pyo3/pyo3"' else: # v0.13.2 -> 0.13.2 version = PYO3_VERSION_TAG.lstrip("v") PYO3_DOCS_URL = f"https://docs.rs/pyo3/{version}" + PYO3_DOCS_VERSION = version PYO3_CRATE_VERSION = f'version = "{version}"' @@ -35,6 +37,7 @@ def replace_section_content(section): section["Chapter"]["content"] .replace("{{#PYO3_VERSION_TAG}}", PYO3_VERSION_TAG) .replace("{{#PYO3_DOCS_URL}}", PYO3_DOCS_URL) + .replace("{{#PYO3_DOCS_VERSION}}", PYO3_DOCS_VERSION) .replace("{{#PYO3_CRATE_VERSION}}", PYO3_CRATE_VERSION) ) diff --git a/guide/src/building_and_distribution.md b/guide/src/building_and_distribution.md index 05da889d..0d63dd6c 100644 --- a/guide/src/building_and_distribution.md +++ b/guide/src/building_and_distribution.md @@ -44,7 +44,11 @@ Caused by: build_flags=WITH_THREAD ``` -> Note: if you save the output config to a file, it is possible to manually override the contents and feed it back into PyO3 using the `PYO3_CONFIG_FILE` env var. For now, this is an advanced feature that should not be needed for most users. The format of the config file and its contents are deliberately unstable and undocumented. If you have a production use-case for this config file, please file an issue and help us stabilize it! +### Advanced: config files + +If you save the above output config from `PYO3_PRINT_CONFIG` to a file, it is possible to manually override the contents and feed it back into PyO3 using the `PYO3_CONFIG_FILE` env var. + +If your build environment is unusual enough that PyO3's regular configuration detection doesn't work, using a config file like this will give you the flexibility to make PyO3 work for you. To see the full set of options supported, see the documentation for the [`InterpreterConfig` struct](https://docs.rs/pyo3-build-config/{{#PYO3_DOCS_VERSION}}/pyo3_build_config/struct.InterpreterConfig.html). ## Building Python extension modules From a156158d7bad0e5deec99305590466ddb9d88539 Mon Sep 17 00:00:00 2001 From: mejrs Date: Wed, 10 Nov 2021 22:37:20 +0100 Subject: [PATCH 06/30] add doc_auto_cfg feature --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2d0ca39b..dbd24840 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![cfg_attr(feature = "nightly", feature(specialization))] -#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![cfg_attr( docsrs, // rustdoc:: is not supported on msrv deny( From 23778f5386f5254f4c5ad038a39a295ef779ead2 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 7 Nov 2021 08:16:15 +0000 Subject: [PATCH 07/30] pymethods: test and document opt-out of protos --- CHANGELOG.md | 10 +++ guide/src/class/protocols.md | 37 ++++++++ pyo3-macros-backend/src/konst.rs | 21 +++-- pyo3-macros-backend/src/pyimpl.rs | 9 +- pyo3-macros-backend/src/pymethod.rs | 130 ++++++++++++++++++++-------- tests/test_proto_methods.rs | 74 +++++++++++++++- 6 files changed, 234 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fdb5114..2d5bc19e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ PyO3 versions, please see the [migration guide](https://pyo3.rs/latest/migration The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- `#[classattr]` constants with a known magic method name (which is lowercase) no longer trigger lint warnings expecting constants to be uppercase. [#1969](https://github.com/PyO3/pyo3/pull/1969) + +### Fixed + +- Fix creating `#[classattr]` by functions with the name of a known magic method. [#1969](https://github.com/PyO3/pyo3/pull/1969) + ## [0.15.0] - 2021-11-03 ### Packaging diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 251447be..cc3e11c3 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -47,6 +47,25 @@ given signatures should be interpreted as follows: - `__str__() -> object (str)` - `__repr__() -> object (str)` - `__hash__() -> isize` +
+ Disabling Python's default hash + + By default, all `#[pyclass]` types have a default hash implementation from Python. Types which should not be hashable can override this by setting `__hash__` to `None`. This is the same mechanism as for a pure-Python class. This is done like so: + + ```rust + # use pyo3::prelude::*; + # + #[pyclass] + struct NotHashable { } + + #[pymethods] + impl NotHashable { + #[classattr] + const __hash__: Option = None; + } + ``` +
+ - `__richcmp__(, object, pyo3::basic::CompareOp) -> object` - `__getattr__(, object) -> object` - `__setattr__(, object, object) -> ()` @@ -140,6 +159,24 @@ TODO; see [#1884](https://github.com/PyO3/pyo3/issues/1884) - `__len__() -> usize` - `__contains__(, object) -> bool` +
+ Disabling Python's default contains + + By default, all `#[pyclass]` types with an `__iter__` method support a default implementation of the `in` operator. Types which do not want this can override this by setting `__contains__` to `None`. This is the same mechanism as for a pure-Python class. This is done like so: + + ```rust + # use pyo3::prelude::*; + # + #[pyclass] + struct NoContains { } + + #[pymethods] + impl NoContains { + #[classattr] + const __contains__: Option = None; + } + ``` +
- `__getitem__(, object) -> object` - `__setitem__(, object, object) -> ()` - `__delitem__(, object) -> ()` diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index ddece4f0..d0bc458d 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crate::{ attributes::{ self, get_deprecated_name_attribute, get_pyo3_options, is_attribute_ident, take_attributes, @@ -5,7 +7,7 @@ use crate::{ }, deprecations::Deprecations, }; -use proc_macro2::TokenStream; +use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::{ ext::IdentExt, @@ -20,15 +22,18 @@ pub struct ConstSpec { } impl ConstSpec { + pub fn python_name(&self) -> Cow { + if let Some(name) = &self.attributes.name { + Cow::Borrowed(&name.0) + } else { + Cow::Owned(self.rust_ident.unraw()) + } + } + /// Null-terminated Python name pub fn null_terminated_python_name(&self) -> TokenStream { - if let Some(name) = &self.attributes.name { - let name = format!("{}\0", name.0); - quote!({#name}) - } else { - let name = format!("{}\0", self.rust_ident.unraw().to_string()); - quote!(#name) - } + let name = format!("{}\0", self.python_name()); + quote!({#name}) } } diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 885c2540..54e248c9 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; use crate::{ konst::{ConstAttributes, ConstSpec}, pyfunction::PyFunctionOptions, - pymethod, + pymethod::{self, is_proto_method}, }; use proc_macro2::TokenStream; use pymethod::GeneratedPyMethod; @@ -79,6 +79,13 @@ pub fn impl_methods( let attrs = get_cfg_attributes(&konst.attrs); let meth = gen_py_const(ty, &spec); methods.push(quote!(#(#attrs)* #meth)); + if is_proto_method(&spec.python_name().to_string()) { + // If this is a known protocol method e.g. __contains__, then allow this + // symbol even though it's not an uppercase constant. + konst + .attrs + .push(syn::parse_quote!(#[allow(non_upper_case_globals)])); + } } } _ => (), diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 39758a90..eb8611dc 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -24,6 +24,63 @@ pub enum GeneratedPyMethod { SlotTraitImpl(String, TokenStream), } +pub struct PyMethod<'a> { + kind: PyMethodKind, + method_name: String, + spec: FnSpec<'a>, +} + +enum PyMethodKind { + Fn, + Proto(PyMethodProtoKind), +} + +impl PyMethodKind { + fn from_name(name: &str) -> Self { + if let Some(slot_def) = pyproto(name) { + PyMethodKind::Proto(PyMethodProtoKind::Slot(slot_def)) + } else if name == "__call__" { + PyMethodKind::Proto(PyMethodProtoKind::Call) + } else if let Some(slot_fragment_def) = pyproto_fragment(name) { + PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(slot_fragment_def)) + } else { + PyMethodKind::Fn + } + } +} + +enum PyMethodProtoKind { + Slot(&'static SlotDef), + Call, + SlotFragment(&'static SlotFragmentDef), +} + +impl<'a> PyMethod<'a> { + fn parse( + sig: &'a mut syn::Signature, + meth_attrs: &mut Vec, + options: PyFunctionOptions, + ) -> Result { + let spec = FnSpec::parse(sig, meth_attrs, options)?; + + let method_name = spec.python_name.to_string(); + let kind = PyMethodKind::from_name(&method_name); + + Ok(Self { + kind, + method_name, + spec, + }) + } +} + +pub fn is_proto_method(name: &str) -> bool { + match PyMethodKind::from_name(name) { + PyMethodKind::Fn => false, + PyMethodKind::Proto(_) => true, + } +} + pub fn gen_py_method( cls: &syn::Type, sig: &mut syn::Signature, @@ -33,56 +90,55 @@ pub fn gen_py_method( check_generic(sig)?; ensure_not_async_fn(sig)?; ensure_function_options_valid(&options)?; - let spec = FnSpec::parse(sig, &mut *meth_attrs, options)?; + let method = PyMethod::parse(sig, &mut *meth_attrs, options)?; + let spec = &method.spec; - let method_name = spec.python_name.to_string(); - - if let Some(slot_def) = pyproto(&method_name) { - ensure_no_forbidden_protocol_attributes(&spec, &method_name)?; - let slot = slot_def.generate_type_slot(cls, &spec)?; - return Ok(GeneratedPyMethod::Proto(slot)); - } else if method_name == "__call__" { - ensure_no_forbidden_protocol_attributes(&spec, &method_name)?; - return Ok(GeneratedPyMethod::Proto(impl_call_slot(cls, spec)?)); - } - - if let Some(slot_fragment_def) = pyproto_fragment(&method_name) { - ensure_no_forbidden_protocol_attributes(&spec, &method_name)?; - let proto = slot_fragment_def.generate_pyproto_fragment(cls, &spec)?; - return Ok(GeneratedPyMethod::SlotTraitImpl(method_name, proto)); - } - - Ok(match &spec.tp { + Ok(match (method.kind, &spec.tp) { + // Class attributes go before protos so that class attributes can be used to set proto + // method to None. + (_, FnType::ClassAttribute) => { + GeneratedPyMethod::Method(impl_py_class_attribute(cls, spec)) + } + (PyMethodKind::Proto(proto_kind), _) => { + ensure_no_forbidden_protocol_attributes(spec, &method.method_name)?; + match proto_kind { + PyMethodProtoKind::Slot(slot_def) => { + let slot = slot_def.generate_type_slot(cls, spec)?; + GeneratedPyMethod::Proto(slot) + } + PyMethodProtoKind::Call => { + GeneratedPyMethod::Proto(impl_call_slot(cls, method.spec)?) + } + PyMethodProtoKind::SlotFragment(slot_fragment_def) => { + let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec)?; + GeneratedPyMethod::SlotTraitImpl(method.method_name, proto) + } + } + } // ordinary functions (with some specialties) - FnType::Fn(_) => GeneratedPyMethod::Method(impl_py_method_def(cls, &spec, None)?), - FnType::FnClass => GeneratedPyMethod::Method(impl_py_method_def( + (_, FnType::Fn(_)) => GeneratedPyMethod::Method(impl_py_method_def(cls, spec, None)?), + (_, FnType::FnClass) => GeneratedPyMethod::Method(impl_py_method_def( cls, - &spec, + spec, Some(quote!(::pyo3::ffi::METH_CLASS)), )?), - FnType::FnStatic => GeneratedPyMethod::Method(impl_py_method_def( + (_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def( cls, - &spec, + spec, Some(quote!(::pyo3::ffi::METH_STATIC)), )?), // special prototypes - FnType::FnNew => GeneratedPyMethod::TraitImpl(impl_py_method_def_new(cls, &spec)?), - FnType::ClassAttribute => GeneratedPyMethod::Method(impl_py_class_attribute(cls, &spec)), - FnType::Getter(self_type) => GeneratedPyMethod::Method(impl_py_getter_def( + (_, FnType::FnNew) => GeneratedPyMethod::TraitImpl(impl_py_method_def_new(cls, spec)?), + + (_, FnType::Getter(self_type)) => GeneratedPyMethod::Method(impl_py_getter_def( cls, - PropertyType::Function { - self_type, - spec: &spec, - }, + PropertyType::Function { self_type, spec }, )?), - FnType::Setter(self_type) => GeneratedPyMethod::Method(impl_py_setter_def( + (_, FnType::Setter(self_type)) => GeneratedPyMethod::Method(impl_py_setter_def( cls, - PropertyType::Function { - self_type, - spec: &spec, - }, + PropertyType::Function { self_type, spec }, )?), - FnType::FnModule => { + (_, FnType::FnModule) => { unreachable!("methods cannot be FnModule") } }) diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index d5324bf6..0b6c30d4 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -1,11 +1,14 @@ use pyo3::exceptions::PyValueError; -use pyo3::types::{PySlice, PyType}; +use pyo3::types::{PyList, PySlice, PyType}; use pyo3::{exceptions::PyAttributeError, prelude::*}; use pyo3::{ffi, py_run, AsPyPointer, PyCell}; use std::{isize, iter}; mod common; +#[pyclass] +struct EmptyClass; + #[pyclass] struct ExampleClass { #[pyo3(get, set)] @@ -560,3 +563,72 @@ assert c.counter.count == 1 .map_err(|e| e.print(py)) .unwrap(); } + +#[pyclass] +struct NotHashable; + +#[pymethods] +impl NotHashable { + #[classattr] + const __hash__: Option = None; +} + +#[test] +fn test_hash_opt_out() { + // By default Python provides a hash implementation, which can be disabled by setting __hash__ + // to None. + Python::with_gil(|py| { + let empty = Py::new(py, EmptyClass).unwrap(); + py_assert!(py, empty, "hash(empty) is not None"); + + let not_hashable = Py::new(py, NotHashable).unwrap(); + py_expect_exception!(py, not_hashable, "hash(not_hashable)", PyTypeError); + }) +} + +/// Class with __iter__ gets default contains from CPython. +#[pyclass] +struct DefaultedContains; + +#[pymethods] +impl DefaultedContains { + fn __iter__(&self, py: Python) -> PyObject { + PyList::new(py, &["a", "b", "c"]) + .as_ref() + .iter() + .unwrap() + .into() + } +} + +#[pyclass] +struct NoContains; + +#[pymethods] +impl NoContains { + fn __iter__(&self, py: Python) -> PyObject { + PyList::new(py, &["a", "b", "c"]) + .as_ref() + .iter() + .unwrap() + .into() + } + + // Equivalent to the opt-out const form in NotHashable above, just more verbose, to confirm this + // also works. + #[classattr] + fn __contains__() -> Option { + None + } +} + +#[test] +fn test_contains_opt_out() { + Python::with_gil(|py| { + let defaulted_contains = Py::new(py, DefaultedContains).unwrap(); + py_assert!(py, defaulted_contains, "'a' in defaulted_contains"); + + let no_contains = Py::new(py, NoContains).unwrap(); + py_expect_exception!(py, no_contains, "'a' in no_contains", PyTypeError); + }) +} From 436b8b0b35bf20f621f4202836ba22d1fdf032f3 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Fri, 12 Nov 2021 21:13:28 +0000 Subject: [PATCH 08/30] err: tidy up some uses of PyErr::occurred --- src/types/floatob.rs | 10 ++++++---- src/types/iterator.rs | 8 +------- src/types/num.rs | 26 ++++++++++++++------------ 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/types/floatob.rs b/src/types/floatob.rs index 1cd66510..01c33a2d 100644 --- a/src/types/floatob.rs +++ b/src/types/floatob.rs @@ -52,11 +52,13 @@ impl<'source> FromPyObject<'source> for f64 { fn extract(obj: &'source PyAny) -> PyResult { let v = unsafe { ffi::PyFloat_AsDouble(obj.as_ptr()) }; - if v == -1.0 && PyErr::occurred(obj.py()) { - Err(PyErr::fetch(obj.py())) - } else { - Ok(v) + if v == -1.0 { + if let Some(err) = PyErr::take(obj.py()) { + return Err(err); + } } + + Ok(v) } } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index c3039498..11726a1d 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -58,13 +58,7 @@ impl<'p> Iterator for &'p PyIterator { match unsafe { py.from_owned_ptr_or_opt(ffi::PyIter_Next(self.0.as_ptr())) } { Some(obj) => Some(Ok(obj)), - None => { - if PyErr::occurred(py) { - Some(Err(PyErr::fetch(py))) - } else { - None - } - } + None => PyErr::take(py).map(Err), } } } diff --git a/src/types/num.rs b/src/types/num.rs index 11cfba7d..c9e05eb0 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -10,18 +10,6 @@ use std::convert::TryFrom; use std::i64; use std::os::raw::c_long; -fn err_if_invalid_value( - py: Python, - invalid_value: T, - actual_value: T, -) -> PyResult { - if actual_value == invalid_value && PyErr::occurred(py) { - Err(PyErr::fetch(py)) - } else { - Ok(actual_value) - } -} - macro_rules! int_fits_larger_int { ($rust_type:ty, $larger_type:ty) => { impl ToPyObject for $rust_type { @@ -273,6 +261,20 @@ mod slow_128bit_int_conversion { int_convert_128!(u128, u64); } +fn err_if_invalid_value( + py: Python, + invalid_value: T, + actual_value: T, +) -> PyResult { + if actual_value == invalid_value { + if let Some(err) = PyErr::take(py) { + return Err(err); + } + } + + Ok(actual_value) +} + #[cfg(test)] mod test_128bit_intergers { use super::*; From ae05020b13413a67b6e78c8dfd218765ed2db969 Mon Sep 17 00:00:00 2001 From: Moriyoshi Koizumi Date: Thu, 17 Jun 2021 20:28:50 +0900 Subject: [PATCH 09/30] Support Py::as_ref() and Py::into_ref() for PySequence, PyIterator and PyMapping. --- CHANGELOG.md | 4 ++++ src/types/iterator.rs | 44 +++++++++++++++++++++++++++++++--- src/types/mapping.rs | 51 +++++++++++++++++++++++++++++++++++---- src/types/sequence.rs | 56 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 143 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d5bc19e..b4e86580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add implementations for `Py::as_ref()` and `Py::into_ref()` for `Py`, `Py` and `Py`. [#1682](https://github.com/PyO3/pyo3/pull/1682) + ### Changed - `#[classattr]` constants with a known magic method name (which is lowercase) no longer trigger lint warnings expecting constants to be uppercase. [#1969](https://github.com/PyO3/pyo3/pull/1969) diff --git a/src/types/iterator.rs b/src/types/iterator.rs index c3039498..a8bf8ca3 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -2,7 +2,7 @@ // // based on Daniel Grunwald's https://github.com/dgrunwald/rust-cpython -use crate::{ffi, AsPyPointer, PyAny, PyErr, PyResult, Python}; +use crate::{ffi, AsPyPointer, IntoPyPointer, Py, PyAny, PyErr, PyNativeType, PyResult, Python}; #[cfg(any(not(Py_LIMITED_API), Py_3_8))] use crate::{PyDowncastError, PyTryFrom}; @@ -94,6 +94,22 @@ impl<'v> PyTryFrom<'v> for PyIterator { } } +impl Py { + /// Borrows a GIL-bound reference to the PyIterator. By binding to the GIL lifetime, this + /// allows the GIL-bound reference to not require `Python` for any of its methods. + pub fn as_ref<'py>(&'py self, _py: Python<'py>) -> &'py PyIterator { + let any = self.as_ptr() as *const PyAny; + unsafe { PyNativeType::unchecked_downcast(&*any) } + } + + /// Similar to [`as_ref`](#method.as_ref), and also consumes this `Py` and registers the + /// Python object reference in PyO3's object storage. The reference count for the Python + /// object will not be decreased until the GIL lifetime ends. + pub fn into_ref(self, py: Python) -> &PyIterator { + unsafe { py.from_owned_ptr(self.into_ptr()) } + } +} + #[cfg(test)] mod tests { use super::PyIterator; @@ -101,8 +117,8 @@ mod tests { use crate::gil::GILPool; use crate::types::{PyDict, PyList}; #[cfg(any(not(Py_LIMITED_API), Py_3_8))] - use crate::{Py, PyAny, PyTryFrom}; - use crate::{Python, ToPyObject}; + use crate::PyTryFrom; + use crate::{Py, PyAny, Python, ToPyObject}; use indoc::indoc; #[test] @@ -209,4 +225,26 @@ mod tests { assert_eq!(obj, iter.into()); }); } + + #[test] + fn test_as_ref() { + Python::with_gil(|py| { + let iter: Py = PyAny::iter(PyList::empty(py)).unwrap().into(); + let mut iter_ref: &PyIterator = iter.as_ref(py); + assert!(iter_ref.next().is_none()); + }) + } + + #[test] + fn test_into_ref() { + Python::with_gil(|py| { + let bare_iter = PyAny::iter(PyList::empty(py)).unwrap(); + assert_eq!(bare_iter.get_refcnt(), 1); + let iter: Py = bare_iter.into(); + assert_eq!(bare_iter.get_refcnt(), 2); + let mut iter_ref = iter.into_ref(py); + assert!(iter_ref.next().is_none()); + assert_eq!(iter_ref.get_refcnt(), 2); + }) + } } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 90d8b5cc..651bcc56 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -2,9 +2,10 @@ use crate::err::{PyDowncastError, PyErr, PyResult}; use crate::types::{PyAny, PySequence}; -use crate::AsPyPointer; -use crate::{ffi, ToPyObject}; -use crate::{PyTryFrom, ToBorrowedObject}; +use crate::{ + ffi, AsPyPointer, IntoPyPointer, Py, PyNativeType, PyTryFrom, Python, ToBorrowedObject, + ToPyObject, +}; /// Represents a reference to a Python object supporting the mapping protocol. #[repr(transparent)] @@ -120,11 +121,31 @@ impl<'v> PyTryFrom<'v> for PyMapping { } } +impl Py { + /// Borrows a GIL-bound reference to the PyMapping. By binding to the GIL lifetime, this + /// allows the GIL-bound reference to not require `Python` for any of its methods. + pub fn as_ref<'py>(&'py self, _py: Python<'py>) -> &'py PyMapping { + let any = self.as_ptr() as *const PyAny; + unsafe { PyNativeType::unchecked_downcast(&*any) } + } + + /// Similar to [`as_ref`](#method.as_ref), and also consumes this `Py` and registers the + /// Python object reference in PyO3's object storage. The reference count for the Python + /// object will not be decreased until the GIL lifetime ends. + pub fn into_ref(self, py: Python) -> &PyMapping { + unsafe { py.from_owned_ptr(self.into_ptr()) } + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; - use crate::{exceptions::PyKeyError, types::PyTuple, Python}; + use crate::{ + exceptions::PyKeyError, + types::{PyDict, PyTuple}, + Python, + }; use super::*; @@ -256,4 +277,26 @@ mod tests { assert_eq!(32 + 42 + 123, values_sum); }); } + + #[test] + fn test_as_ref() { + Python::with_gil(|py| { + let mapping: Py = PyDict::new(py).as_mapping().into(); + let mapping_ref: &PyMapping = mapping.as_ref(py); + assert_eq!(mapping_ref.len().unwrap(), 0); + }) + } + + #[test] + fn test_into_ref() { + Python::with_gil(|py| { + let bare_mapping = PyDict::new(py).as_mapping(); + assert_eq!(bare_mapping.get_refcnt(), 1); + let mapping: Py = bare_mapping.into(); + assert_eq!(bare_mapping.get_refcnt(), 2); + let mapping_ref = mapping.into_ref(py); + assert_eq!(mapping_ref.len().unwrap(), 0); + assert_eq!(mapping_ref.get_refcnt(), 2); + }) + } } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 1b5a5a95..61f48569 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -1,10 +1,10 @@ // Copyright (c) 2017-present PyO3 Project and Contributors use crate::err::{self, PyDowncastError, PyErr, PyResult}; -use crate::ffi; use crate::internal_tricks::get_ssize_index; use crate::types::{PyAny, PyList, PyTuple}; -use crate::AsPyPointer; +use crate::{ffi, PyNativeType}; +use crate::{AsPyPointer, IntoPyPointer, Py, Python}; use crate::{FromPyObject, PyTryFrom, ToBorrowedObject}; /// Represents a reference to a Python object supporting the sequence protocol. @@ -335,12 +335,36 @@ impl<'v> PyTryFrom<'v> for PySequence { } } +impl Py { + /// Borrows a GIL-bound reference to the PySequence. By binding to the GIL lifetime, this + /// allows the GIL-bound reference to not require `Python` for any of its methods. + /// + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::types::{PyList, PySequence}; + /// # Python::with_gil(|py| { + /// let seq: Py = PyList::empty(py).as_sequence().into(); + /// let seq: &PySequence = seq.as_ref(py); + /// assert_eq!(seq.len().unwrap(), 0); + /// # }); + /// ``` + pub fn as_ref<'py>(&'py self, _py: Python<'py>) -> &'py PySequence { + let any = self.as_ptr() as *const PyAny; + unsafe { PyNativeType::unchecked_downcast(&*any) } + } + + /// Similar to [`as_ref`](#method.as_ref), and also consumes this `Py` and registers the + /// Python object reference in PyO3's object storage. The reference count for the Python + /// object will not be decreased until the GIL lifetime ends. + pub fn into_ref(self, py: Python) -> &PySequence { + unsafe { py.from_owned_ptr(self.into_ptr()) } + } +} + #[cfg(test)] mod tests { use crate::types::{PyList, PySequence}; - use crate::AsPyPointer; - use crate::Python; - use crate::{PyObject, PyTryFrom, ToPyObject}; + use crate::{AsPyPointer, Py, PyObject, PyTryFrom, Python, ToPyObject}; fn get_object() -> PyObject { // Convenience function for getting a single unique object @@ -818,4 +842,26 @@ mod tests { assert!(seq_from.list().is_ok()); }); } + + #[test] + fn test_as_ref() { + Python::with_gil(|py| { + let seq: Py = PyList::empty(py).as_sequence().into(); + let seq_ref: &PySequence = seq.as_ref(py); + assert_eq!(seq_ref.len().unwrap(), 0); + }) + } + + #[test] + fn test_into_ref() { + Python::with_gil(|py| { + let bare_seq = PyList::empty(py).as_sequence(); + assert_eq!(bare_seq.get_refcnt(), 1); + let seq: Py = bare_seq.into(); + assert_eq!(bare_seq.get_refcnt(), 2); + let seq_ref = seq.into_ref(py); + assert_eq!(seq_ref.len().unwrap(), 0); + assert_eq!(seq_ref.get_refcnt(), 2); + }) + } } From 41280c01cd4cd9864c1fe609c2e04c3b6aad36d1 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Wed, 10 Nov 2021 08:05:30 +0000 Subject: [PATCH 10/30] ci: try running coverage for all OSes Co-authored-by: Taiki Endo --- .github/workflows/ci.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b149615..b20a26a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -266,7 +266,11 @@ jobs: coverage: needs: [fmt] - runs-on: ubuntu-latest + name: coverage-${{ matrix.os }} + strategy: + matrix: + os: ["windows", "macos", "ubuntu"] + runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 @@ -280,12 +284,19 @@ jobs: target key: coverage-cargo-${{ hashFiles('**/Cargo.toml') }} continue-on-error: true + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + profile: minimal + components: llvm-tools-preview - name: install cargo-llvm-cov + shell: bash run: | - wget https://github.com/taiki-e/cargo-llvm-cov/releases/download/v${CARGO_LLVM_COV_VERSION}/cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz -qO- | tar -xzvf - - mv cargo-llvm-cov ~/.cargo/bin + host=$(rustc -Vv | grep host | sed 's/host: //') + curl -fsSL https://github.com/taiki-e/cargo-llvm-cov/releases/download/v${CARGO_LLVM_COV_VERSION}/cargo-llvm-cov-"$host".tar.gz | tar xzf - -C ~/.cargo/bin env: - CARGO_LLVM_COV_VERSION: 0.1.10 + CARGO_LLVM_COV_VERSION: 0.1.11 - uses: actions-rs/toolchain@v1 with: toolchain: nightly @@ -299,8 +310,10 @@ jobs: cargo llvm-cov --package $ALL_PACKAGES --no-report --features $(make list_all_additive_features) cargo llvm-cov --package $ALL_PACKAGES --no-report --features abi3 $(make list_all_additive_features) cargo llvm-cov --package $ALL_PACKAGES --no-run --lcov --output-path coverage.lcov + shell: bash env: ALL_PACKAGES: pyo3 pyo3-build-config pyo3-macros-backend pyo3-macros - uses: codecov/codecov-action@v2 with: file: coverage.lcov + name: ${{ matrix.os }} From 2325c283b0180eaacc65190191c122f6282f524b Mon Sep 17 00:00:00 2001 From: messense Date: Mon, 15 Nov 2021 13:57:47 +0800 Subject: [PATCH 11/30] Fix mingw platform detection --- CHANGELOG.md | 1 + pyo3-build-config/src/impl_.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4e86580..bac059e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix creating `#[classattr]` by functions with the name of a known magic method. [#1969](https://github.com/PyO3/pyo3/pull/1969) +- Fix mingw platform detection. [#1993](https://github.com/PyO3/pyo3/pull/1993) ## [0.15.0] - 2021-11-03 diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 4690b6e7..aaf2f3c9 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -202,7 +202,7 @@ print_if_set("libdir", get_config_var("LIBDIR")) print_if_set("base_prefix", base_prefix) print("executable", sys.executable) print("calcsize_pointer", struct.calcsize("P")) -print("mingw", get_platform() == "mingw") +print("mingw", get_platform().startswith("mingw")) "#; let output = run_python_script(interpreter.as_ref(), SCRIPT)?; let map: HashMap = parse_script_output(&output); From 88ca6bbbab54d8f059b2395234e3a4545e8a7d1d Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Wed, 10 Nov 2021 09:33:00 +0000 Subject: [PATCH 12/30] types: add PyTraceback --- CHANGELOG.md | 1 + src/err/err_state.rs | 4 +-- src/err/mod.rs | 6 ++-- src/types/mod.rs | 2 ++ src/types/traceback.rs | 82 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/types/traceback.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b4e86580..28610230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add implementations for `Py::as_ref()` and `Py::into_ref()` for `Py`, `Py` and `Py`. [#1682](https://github.com/PyO3/pyo3/pull/1682) +- Add `PyTraceback` type to represent and format Python tracebacks. [#1977](https://github.com/PyO3/pyo3/pull/1977) ### Changed diff --git a/src/err/err_state.rs b/src/err/err_state.rs index b1390dc1..97a55ec5 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -2,7 +2,7 @@ use crate::{ exceptions::{PyBaseException, PyTypeError}, ffi, type_object::PyTypeObject, - types::PyType, + types::{PyTraceback, PyType}, AsPyPointer, IntoPy, IntoPyPointer, Py, PyObject, Python, }; @@ -10,7 +10,7 @@ use crate::{ pub(crate) struct PyErrStateNormalized { pub ptype: Py, pub pvalue: Py, - pub ptraceback: Option, + pub ptraceback: Option>, } pub(crate) enum PyErrState { diff --git a/src/err/mod.rs b/src/err/mod.rs index db8e2571..db23c678 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -2,7 +2,7 @@ use crate::panic::PanicException; use crate::type_object::PyTypeObject; -use crate::types::PyType; +use crate::types::{PyTraceback, PyType}; use crate::{ exceptions::{self, PyBaseException}, ffi, @@ -201,7 +201,7 @@ impl PyErr { /// assert_eq!(err.ptraceback(py), None); /// }); /// ``` - pub fn ptraceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyAny> { + pub fn ptraceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { self.normalized(py) .ptraceback .as_ref() @@ -497,7 +497,7 @@ impl PyErr { *self_state = Some(PyErrState::Normalized(PyErrStateNormalized { ptype: Py::from_owned_ptr_or_opt(py, ptype).expect("Exception type missing"), pvalue: Py::from_owned_ptr_or_opt(py, pvalue).expect("Exception value missing"), - ptraceback: PyObject::from_owned_ptr_or_opt(py, ptraceback), + ptraceback: Py::from_owned_ptr_or_opt(py, ptraceback), })); match self_state { diff --git a/src/types/mod.rs b/src/types/mod.rs index db09044a..f670a32d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -26,6 +26,7 @@ pub use self::slice::{PySlice, PySliceIndices}; #[cfg(all(not(Py_LIMITED_API), target_endian = "little"))] pub use self::string::PyStringData; pub use self::string::{PyString, PyString as PyUnicode}; +pub use self::traceback::PyTraceback; pub use self::tuple::PyTuple; pub use self::typeobject::PyType; @@ -237,5 +238,6 @@ mod sequence; mod set; mod slice; mod string; +mod traceback; mod tuple; mod typeobject; diff --git a/src/types/traceback.rs b/src/types/traceback.rs new file mode 100644 index 00000000..b83f2e93 --- /dev/null +++ b/src/types/traceback.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2017-present PyO3 Project and Contributors + +use crate::err::{error_on_minusone, PyResult}; +use crate::ffi; +use crate::types::PyString; +use crate::{AsPyPointer, PyAny}; + +/// Represents a Python traceback. +#[repr(transparent)] +pub struct PyTraceback(PyAny); + +pyobject_native_type_core!( + PyTraceback, + ffi::PyTraceBack_Type, + #checkfunction=ffi::PyTraceBack_Check +); + +impl PyTraceback { + /// Formats the traceback as a string. + /// + /// This does not include the exception type and value. The exception type and value can be + /// formatted using the `Display` implementation for `PyErr`. + /// + /// # Example + /// + /// The following code formats a Python traceback and exception pair from Rust: + /// + /// ```rust + /// # use pyo3::{Python, PyResult}; + /// # let result: PyResult<()> = + /// Python::with_gil(|py| { + /// let err = py + /// .run("raise Exception('banana')", None, None) + /// .expect_err("raise will create a Python error"); + /// + /// let traceback = err.ptraceback(py).expect("raised exception will have a traceback"); + /// assert_eq!( + /// format!("{}{}", traceback.format()?, err), + /// "\ + /// Traceback (most recent call last): + /// File \"\", line 1, in + /// Exception: banana\ + /// " + /// ); + /// Ok(()) + /// }) + /// # ; + /// # result.expect("example failed"); + /// ``` + pub fn format(&self) -> PyResult { + let py = self.py(); + let string_io = py.import("io")?.getattr("StringIO")?.call0()?; + let result = unsafe { ffi::PyTraceBack_Print(self.as_ptr(), string_io.as_ptr()) }; + error_on_minusone(py, result)?; + let formatted = string_io + .getattr("getvalue")? + .call0()? + .downcast::()? + .to_str()? + .to_owned(); + Ok(formatted) + } +} + +#[cfg(test)] +mod tests { + use crate::Python; + + #[test] + fn format_traceback() { + Python::with_gil(|py| { + let err = py + .run("raise Exception('banana')", None, None) + .expect_err("raising should have given us an error"); + + assert_eq!( + err.ptraceback(py).unwrap().format().unwrap(), + "Traceback (most recent call last):\n File \"\", line 1, in \n" + ); + }) + } +} From 73c3911748ee0cd2a2a239498f89aaf8945bd2f5 Mon Sep 17 00:00:00 2001 From: messense Date: Mon, 15 Nov 2021 10:44:26 +0800 Subject: [PATCH 13/30] Don't emit `Py_LIMITED_API` cfg for PyPy --- CHANGELOG.md | 1 + pyo3-build-config/src/impl_.rs | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bac059e6..e115b50a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix creating `#[classattr]` by functions with the name of a known magic method. [#1969](https://github.com/PyO3/pyo3/pull/1969) +- Fix build failure on PyPy when abi3 features are activated. [#1991](https://github.com/PyO3/pyo3/pull/1991) - Fix mingw platform detection. [#1993](https://github.com/PyO3/pyo3/pull/1993) ## [0.15.0] - 2021-11-03 diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index aaf2f3c9..191a70bd 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -135,10 +135,6 @@ impl InterpreterConfig { println!("cargo:rustc-cfg=Py_3_{}", i); } - if self.abi3 { - println!("cargo:rustc-cfg=Py_LIMITED_API"); - } - if self.implementation.is_pypy() { println!("cargo:rustc-cfg=PyPy"); if self.abi3 { @@ -147,7 +143,9 @@ impl InterpreterConfig { See https://foss.heptapod.net/pypy/pypy/-/issues/3397 for more information." ); } - }; + } else if self.abi3 { + println!("cargo:rustc-cfg=Py_LIMITED_API"); + } for flag in &self.build_flags.0 { println!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag) From e16024fc95761eeb587b9b236f43a335dd5b815c Mon Sep 17 00:00:00 2001 From: messense Date: Mon, 15 Nov 2021 12:11:30 +0800 Subject: [PATCH 14/30] Test PyPy abi3 build on CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b20a26a4..46c04767 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -211,6 +211,10 @@ jobs: - name: Build (all additive features) run: cargo build --lib --tests --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}" + - if: ${{ startsWith(matrix.python-version, 'pypy') }} + name: Build PyPy (abi3-py36) + run: cargo build --lib --tests --no-default-features --features "abi3-py36 ${{ steps.settings.outputs.all_additive_features }}" + # Run tests (except on PyPy, because no embedding API). - if: ${{ !startsWith(matrix.python-version, 'pypy') }} name: Test From f01595163c3411d92704bc796e8ec4da7a06ed07 Mon Sep 17 00:00:00 2001 From: messense Date: Mon, 15 Nov 2021 13:24:29 +0800 Subject: [PATCH 15/30] Don't link to python3.lib for PyPy on Windows --- pyo3-build-config/src/impl_.rs | 61 +++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 191a70bd..6a73b3b9 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -219,7 +219,12 @@ print("mingw", get_platform().startswith("mingw")) let implementation = map["implementation"].parse()?; let lib_name = if cfg!(windows) { - default_lib_name_windows(version, abi3, map["mingw"].as_str() == "True") + default_lib_name_windows( + version, + implementation, + abi3, + map["mingw"].as_str() == "True", + ) } else { default_lib_name_unix( version, @@ -987,7 +992,12 @@ fn windows_hardcoded_cross_compile( version, shared: true, abi3, - lib_name: Some(default_lib_name_windows(version, abi3, false)), + lib_name: Some(default_lib_name_windows( + version, + PythonImplementation::CPython, + abi3, + false, + )), lib_dir: cross_compile_config.lib_dir.to_str().map(String::from), executable: None, pointer_width: None, @@ -1026,8 +1036,13 @@ fn load_cross_compile_config( // This contains only the limited ABI symbols. const WINDOWS_ABI3_LIB_NAME: &str = "python3"; -fn default_lib_name_windows(version: PythonVersion, abi3: bool, mingw: bool) -> String { - if abi3 { +fn default_lib_name_windows( + version: PythonVersion, + implementation: PythonImplementation, + abi3: bool, + mingw: bool, +) -> String { + if abi3 && !implementation.is_pypy() { WINDOWS_ABI3_LIB_NAME.to_owned() } else if mingw { // https://packages.msys2.org/base/mingw-w64-python @@ -1387,22 +1402,52 @@ mod tests { #[test] fn default_lib_name_windows() { + use PythonImplementation::*; assert_eq!( - super::default_lib_name_windows(PythonVersion { major: 3, minor: 6 }, false, false), + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 6 }, + CPython, + false, + false + ), "python36", ); assert_eq!( - super::default_lib_name_windows(PythonVersion { major: 3, minor: 6 }, true, false), + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 6 }, + CPython, + true, + false + ), "python3", ); assert_eq!( - super::default_lib_name_windows(PythonVersion { major: 3, minor: 6 }, false, true), + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 6 }, + CPython, + false, + true + ), "python3.6", ); assert_eq!( - super::default_lib_name_windows(PythonVersion { major: 3, minor: 6 }, true, true), + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 6 }, + CPython, + true, + true + ), "python3", ); + assert_eq!( + super::default_lib_name_windows( + PythonVersion { major: 3, minor: 6 }, + PyPy, + true, + false + ), + "python36", + ); } #[test] From 0e4027711233e021f1f868de20ed9c237795d614 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Mon, 15 Nov 2021 09:01:05 +0100 Subject: [PATCH 16/30] add missing annotation to PyCounter.__call__ This patch annotates the `__call__` method of `PyCounter` in example: callable objects with `#[args(args="*", kwargs="**")]`. Without it (at least in PyO3 0.15.0) the example fails with `TypeError: counter.__call__() missing 1 required positional argument: 'args'`. --- guide/src/class/protocols.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index cc3e11c3..d8dcda1c 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -98,7 +98,7 @@ impl PyCounter { fn __new__(wraps: Py) -> Self { PyCounter { count: 0, wraps } } - + #[args(args="*", kwargs="**")] fn __call__( &mut self, py: Python, From 1df68e852ea3cdab1ea9644996ef0922e7d016df Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 14 Nov 2021 08:52:39 +0000 Subject: [PATCH 17/30] allow_threads: switch from `catch_unwind` to guard pattern --- CHANGELOG.md | 1 + src/python.rs | 59 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee61feb..9b7bb34b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix creating `#[classattr]` by functions with the name of a known magic method. [#1969](https://github.com/PyO3/pyo3/pull/1969) +- Fix use of `catch_unwind` in `allow_threads` which can cause fatal crashes. [#1989](https://github.com/PyO3/pyo3/pull/1989) - Fix build failure on PyPy when abi3 features are activated. [#1991](https://github.com/PyO3/pyo3/pull/1991) - Fix mingw platform detection. [#1993](https://github.com/PyO3/pyo3/pull/1993) diff --git a/src/python.rs b/src/python.rs index 4f2fdceb..911bf6bd 100644 --- a/src/python.rs +++ b/src/python.rs @@ -326,7 +326,7 @@ impl<'py> Python<'py> { /// py.allow_threads(move || { /// // An example of an "expensive" Rust calculation /// let sum = numbers.iter().sum(); - /// + /// /// Ok(sum) /// }) /// } @@ -367,26 +367,30 @@ impl<'py> Python<'py> { F: Send + FnOnce() -> T, T: Send, { + // Use a guard pattern to handle reacquiring the GIL, so that the GIL will be reacquired + // even if `f` panics. + + struct RestoreGuard { + count: usize, + tstate: *mut ffi::PyThreadState, + } + + impl Drop for RestoreGuard { + fn drop(&mut self) { + gil::GIL_COUNT.with(|c| c.set(self.count)); + unsafe { + ffi::PyEval_RestoreThread(self.tstate); + } + } + } + // The `Send` bound on the closure prevents the user from // transferring the `Python` token into the closure. let count = gil::GIL_COUNT.with(|c| c.replace(0)); let tstate = unsafe { ffi::PyEval_SaveThread() }; - // Unwinding right here corrupts the Python interpreter state and leads to weird - // crashes such as stack overflows. We will catch the unwind and resume as soon as - // we've restored the GIL state. - // - // Because we will resume unwinding as soon as the GIL state is fixed, we can assert - // that the closure is unwind safe. - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); - // Restore GIL state - gil::GIL_COUNT.with(|c| c.set(count)); - unsafe { - ffi::PyEval_RestoreThread(tstate); - } - - // Now that the GIL state has been safely reset, we can unwind if a panic was caught. - result.unwrap_or_else(|payload| std::panic::resume_unwind(payload)) + let _guard = RestoreGuard { count, tstate }; + f() } /// Evaluates a Python expression in the given context and returns the result. @@ -840,6 +844,29 @@ mod tests { }); } + #[test] + fn test_allow_threads_releases_and_acquires_gil() { + Python::with_gil(|py| { + let b = std::sync::Arc::new(std::sync::Barrier::new(2)); + + let b2 = b.clone(); + std::thread::spawn(move || Python::with_gil(|_| b2.wait())); + + py.allow_threads(|| { + // If allow_threads does not release the GIL, this will deadlock because + // the thread spawned above will never be able to acquire the GIL. + b.wait(); + }); + + unsafe { + // If the GIL is not reacquired at the end of allow_threads, this call + // will crash the Python interpreter. + let tstate = ffi::PyEval_SaveThread(); + ffi::PyEval_RestoreThread(tstate); + } + }); + } + #[test] fn test_allow_threads_panics_safely() { Python::with_gil(|py| { From 26ccc1ab378be67212720b0defa6d63f23db913a Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Mon, 15 Nov 2021 15:45:01 +0000 Subject: [PATCH 18/30] macros: fix panic in __get__ implementation --- CHANGELOG.md | 1 + pyo3-macros-backend/src/pymethod.rs | 23 ++++++++++++++++++++--- tests/test_proto_methods.rs | 18 +++++++++++++++--- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b7bb34b..2a416212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix use of `catch_unwind` in `allow_threads` which can cause fatal crashes. [#1989](https://github.com/PyO3/pyo3/pull/1989) - Fix build failure on PyPy when abi3 features are activated. [#1991](https://github.com/PyO3/pyo3/pull/1991) - Fix mingw platform detection. [#1993](https://github.com/PyO3/pyo3/pull/1993) +- Fix panic in `__get__` implementation when accessing descriptor on type object. [#1997](https://github.com/PyO3/pyo3/pull/1997) ## [0.15.0] - 2021-11-03 diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index eb8611dc..13a49f92 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -483,8 +483,8 @@ const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") const __RICHCMP__: SlotDef = SlotDef::new("Py_tp_richcompare", "richcmpfunc") .extract_error_mode(ExtractErrorMode::NotImplemented) .arguments(&[Ty::Object, Ty::CompareOp]); -const __GET__: SlotDef = - SlotDef::new("Py_tp_descr_get", "descrgetfunc").arguments(&[Ty::Object, Ty::Object]); +const __GET__: SlotDef = SlotDef::new("Py_tp_descr_get", "descrgetfunc") + .arguments(&[Ty::MaybeNullObject, Ty::MaybeNullObject]); const __ITER__: SlotDef = SlotDef::new("Py_tp_iter", "getiterfunc"); const __NEXT__: SlotDef = SlotDef::new("Py_tp_iternext", "iternextfunc").return_conversion( TokenGenerator(|| quote! { ::pyo3::class::iter::IterNextOutput::<_, _> }), @@ -606,6 +606,7 @@ fn pyproto(method_name: &str) -> Option<&'static SlotDef> { #[derive(Clone, Copy)] enum Ty { Object, + MaybeNullObject, NonNullObject, CompareOp, Int, @@ -617,7 +618,7 @@ enum Ty { impl Ty { fn ffi_type(self) -> TokenStream { match self { - Ty::Object => quote! { *mut ::pyo3::ffi::PyObject }, + Ty::Object | Ty::MaybeNullObject => quote! { *mut ::pyo3::ffi::PyObject }, Ty::NonNullObject => quote! { ::std::ptr::NonNull<::pyo3::ffi::PyObject> }, Ty::Int | Ty::CompareOp => quote! { ::std::os::raw::c_int }, Ty::PyHashT => quote! { ::pyo3::ffi::Py_hash_t }, @@ -645,6 +646,22 @@ impl Ty { ); extract_object(cls, arg.ty, ident, extract) } + Ty::MaybeNullObject => { + let extract = handle_error( + extract_error_mode, + py, + quote! { + #py.from_borrowed_ptr::<::pyo3::PyAny>( + if #ident.is_null() { + ::pyo3::ffi::Py_None() + } else { + #ident + } + ).extract() + }, + ); + extract_object(cls, arg.ty, ident, extract) + } Ty::NonNullObject => { let extract = handle_error( extract_error_mode, diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 0b6c30d4..ad1ce58f 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -548,11 +548,23 @@ fn descr_getset() { r#" class Class: counter = Counter() + +# access via type +counter = Class.counter +assert counter.count == 1 + +# access with instance directly +assert Counter.__get__(counter, Class()).count == 2 + +# access via instance c = Class() -c.counter # count += 1 -assert c.counter.count == 2 -c.counter = Counter() assert c.counter.count == 3 + +# __set__ +c.counter = Counter() +assert c.counter.count == 4 + +# __delete__ del c.counter assert c.counter.count == 1 "# From d9a3f67287f3ace3a01734d3fe9e33ce71f19c09 Mon Sep 17 00:00:00 2001 From: Dan Svoboda Date: Mon, 15 Nov 2021 14:04:53 -0500 Subject: [PATCH 19/30] Fix broken relative markdown link in guide This link needs to point up one dir to the location of class.md --- guide/src/conversions/traits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index a102d47f..d0089ef1 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -17,7 +17,7 @@ variety of Rust types, which you can check out in the implementor list of [`FromPyObject`]. [`FromPyObject`] is also implemented for your own Rust types wrapped as Python -objects (see [the chapter about classes](class.md)). There, in order to both be +objects (see [the chapter about classes](../class.md)). There, in order to both be able to operate on mutable references *and* satisfy Rust's rules of non-aliasing mutable references, you have to extract the PyO3 reference wrappers [`PyRef`] and [`PyRefMut`]. They work like the reference wrappers of From eb5059acc7312bd03b793b2423d32b9d3b5dcc2e Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 18 Nov 2021 07:34:22 +0000 Subject: [PATCH 20/30] release: 0.15.1 --- CHANGELOG.md | 5 +++-- Cargo.toml | 6 +++--- README.md | 4 ++-- pyo3-build-config/Cargo.toml | 2 +- pyo3-macros-backend/Cargo.toml | 4 ++-- pyo3-macros/Cargo.toml | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a416212..91c378a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ PyO3 versions, please see the [migration guide](https://pyo3.rs/latest/migration The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.15.1] - 2021-11-19 ### Added @@ -987,7 +987,8 @@ Yanked - Initial release -[unreleased]: https://github.com/pyo3/pyo3/compare/v0.15.0...HEAD +[unreleased]: https://github.com/pyo3/pyo3/compare/v0.15.1...HEAD +[0.15.1]: https://github.com/pyo3/pyo3/compare/v0.15.0...v0.15.1 [0.15.0]: https://github.com/pyo3/pyo3/compare/v0.14.5...v0.15.0 [0.14.5]: https://github.com/pyo3/pyo3/compare/v0.14.4...v0.14.5 [0.14.4]: https://github.com/pyo3/pyo3/compare/v0.14.3...v0.14.4 diff --git a/Cargo.toml b/Cargo.toml index b7790271..fb90960b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.15.0" +version = "0.15.1" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -20,7 +20,7 @@ libc = "0.2.62" parking_lot = "0.11.0" # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.15.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.15.1", optional = true } # indoc must stay at 0.3.x for Rust 1.41 compatibility indoc = { version = "0.3.6", optional = true } paste = { version = "0.1.18", optional = true } @@ -56,7 +56,7 @@ serde_json = "1.0.61" pyo3 = { path = ".", default-features = false, features = ["macros", "auto-initialize"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "0.15.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "0.15.1", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index fdfb1c2a..71242c0c 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3] -version = "0.15.0" +version = "0.15.1" features = ["extension-module"] ``` @@ -108,7 +108,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.15.0" +version = "0.15.1" features = ["auto-initialize"] ``` diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 2ade560e..d908ed81 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.15.0" +version = "0.15.1" description = "Build configuration for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index a72239e5..5abd99af 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.15.0" +version = "0.15.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2018" [dependencies] quote = { version = "1", default-features = false } proc-macro2 = { version = "1", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "0.15.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "0.15.1", features = ["resolve-config"] } [dependencies.syn] version = "1" diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 1cd663d6..a7e4550c 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.15.0" +version = "0.15.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -19,4 +19,4 @@ multiple-pymethods = [] [dependencies] quote = "1" syn = { version = "1", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.15.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.15.1" } From fc3478c75e91af93123cdbc018f421cd6806cc1b Mon Sep 17 00:00:00 2001 From: saidvandeklundert Date: Fri, 19 Nov 2021 09:20:25 +0100 Subject: [PATCH 21/30] Add link to article in readme (#2005) * add Article * Update README.md Co-authored-by: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Co-authored-by: Said van de Klundert Co-authored-by: David Hewitt <1939362+davidhewitt@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fdfb1c2a..58b13134 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,7 @@ about this topic. ## Articles and other media +- [Calling Rust from Python using PyO3](https://saidvandeklundert.net/learn/2021-11-18-calling-rust-from-python-using-pyo3/) - Nov 18, 2021 - [davidhewitt's 2021 talk at Rust Manchester meetup](https://www.youtube.com/watch?v=-XyWG_klSAw&t=320s) - Aug 19, 2021 - [Incrementally porting a small Python project to Rust](https://blog.waleedkhan.name/port-python-to-rust/) - Apr 29, 2021 - [Vortexa - Integrating Rust into Python](https://www.vortexa.com/insight/integrating-rust-into-python) - Apr 12, 2021 From 6a65f98bd20f6d5d74ef205a8172ce22a1c21f2e Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Thu, 18 Nov 2021 11:38:47 +0000 Subject: [PATCH 22/30] msrv: bump to 1.48 --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 8 ++++ Cargo.toml | 13 ++---- README.md | 4 +- pyo3-macros-backend/src/method.rs | 11 ++--- pyo3-macros-backend/src/proto_method.rs | 4 +- pyo3-macros-backend/src/pymethod.rs | 11 ++--- pyo3-macros-backend/src/utils.rs | 12 ++--- src/buffer.rs | 7 +-- src/class/impl_.rs | 8 ++-- src/ffi/ceval.rs | 1 - src/gil.rs | 51 +++++++++++---------- src/lib.rs | 2 +- src/pyclass.rs | 16 +++---- src/types/string.rs | 5 +- tests/test_compile_error.rs | 11 +---- tests/test_not_msrv.rs | 4 +- tests/ui/abi3_nativetype_inheritance.stderr | 18 ++++---- tests/ui/deprecations.rs | 3 -- tests/ui/pyclass_send.stderr | 4 +- 20 files changed, 82 insertions(+), 113 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46c04767..b6dba645 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,7 +123,7 @@ jobs: rust-target: "x86_64-apple-darwin", } # Test minimal supported Rust version - - rust: 1.41.1 + - rust: 1.48.0 python-version: "3.10" platform: { diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c378a8..db17f5f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ PyO3 versions, please see the [migration guide](https://pyo3.rs/latest/migration The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Packaging + +- Update MSRV to Rust 1.48. [#2004](https://github.com/PyO3/pyo3/pull/2004) +- Update `indoc` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) +- Update `paste` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) + ## [0.15.1] - 2021-11-19 ### Added diff --git a/Cargo.toml b/Cargo.toml index fb90960b..cd8fb632 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,8 @@ parking_lot = "0.11.0" # support crates for macros feature pyo3-macros = { path = "pyo3-macros", version = "=0.15.1", optional = true } -# indoc must stay at 0.3.x for Rust 1.41 compatibility -indoc = { version = "0.3.6", optional = true } -paste = { version = "0.1.18", optional = true } +indoc = { version = "1.0.3", optional = true } +paste = { version = "1.0.6", optional = true } unindent = { version = "0.1.4", optional = true } # support crate for multiple-pymethods feature @@ -32,7 +31,7 @@ inventory = { version = "0.1.4", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0", optional = true } -eyre = { version = ">= 0.4, < 0.7" , optional = true } +eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.12", optional = true } indexmap = { version = ">= 1.6, < 1.8", optional = true } num-bigint = { version = "0.4", optional = true } @@ -41,11 +40,7 @@ serde = { version = "1.0", optional = true } [dev-dependencies] assert_approx_eq = "1.1.0" -# O.3.5 uses the matches! macro, which isn't compatible with Rust 1.41 -criterion = "=0.3.4" -# half and bitflags use if/match in const fn, which isn't compatible with Rust 1.41 -half = "=1.7.1" -bitflags = "=1.2.1" +criterion = "0.3.5" trybuild = "1.0.49" rustversion = "1.0" # 1.0.0 requires Rust 1.50 diff --git a/README.md b/README.md index 9066d147..34d8630e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![benchmark](https://github.com/PyO3/pyo3/actions/workflows/bench.yml/badge.svg)](https://pyo3.rs/dev/bench/) [![codecov](https://codecov.io/gh/PyO3/pyo3/branch/main/graph/badge.svg)](https://codecov.io/gh/PyO3/pyo3) [![crates.io](https://img.shields.io/crates/v/pyo3)](https://crates.io/crates/pyo3) -[![minimum rustc 1.41](https://img.shields.io/badge/rustc-1.41+-blue.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) +[![minimum rustc 1.48](https://img.shields.io/badge/rustc-1.48+-blue.svg)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) [![dev chat](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/PyO3/Lobby) [![contributing notes](https://img.shields.io/badge/contribute-on%20github-Green)](https://github.com/PyO3/pyo3/blob/main/Contributing.md) @@ -18,7 +18,7 @@ PyO3 supports the following software versions: - Python 3.6 and up (CPython and PyPy) - - Rust 1.41 and up + - Rust 1.48 and up You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn. diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index e57ef337..6a7b8432 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -352,15 +352,12 @@ impl<'a> FnSpec<'a> { parse_method_receiver(first_arg) }; - #[allow(clippy::manual_strip)] // for strip_prefix replacement supporting rust < 1.45 // strip get_ or set_ let strip_fn_name = |prefix: &'static str| { - let ident = name.unraw().to_string(); - if ident.starts_with(prefix) { - Some(syn::Ident::new(&ident[prefix.len()..], ident.span())) - } else { - None - } + name.unraw() + .to_string() + .strip_prefix(prefix) + .map(|stripped| syn::Ident::new(stripped, name.span())) }; let (fn_type, skip_first_arg, fixed_convention) = match fn_type_attr { diff --git a/pyo3-macros-backend/src/proto_method.rs b/pyo3-macros-backend/src/proto_method.rs index 3c14843f..648a9734 100644 --- a/pyo3-macros-backend/src/proto_method.rs +++ b/pyo3-macros-backend/src/proto_method.rs @@ -15,13 +15,11 @@ pub struct MethodProto { } impl MethodProto { - // TODO: workaround for no unsized casts in const fn on Rust 1.45 (stable in 1.46) - const EMPTY_ARGS: &'static [&'static str] = &[]; pub const fn new(name: &'static str, proto: &'static str) -> Self { MethodProto { name, proto, - args: MethodProto::EMPTY_ARGS, + args: &[], with_self: false, with_result: true, } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 13a49f92..1b5951a4 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -399,14 +399,9 @@ pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType) -> Resul /// Split an argument of pyo3::Python from the front of the arg list, if present fn split_off_python_arg<'a>(args: &'a [FnArg<'a>]) -> (Option<&FnArg>, &[FnArg]) { - if args - .get(0) - .map(|py| utils::is_python(py.ty)) - .unwrap_or(false) - { - (Some(&args[0]), &args[1..]) - } else { - (None, args) + match args { + [py, args @ ..] if utils::is_python(py.ty) => (Some(py), args), + args => (None, args), } } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index b0353cdc..3c3e5102 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -62,8 +62,6 @@ pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> { #[derive(Clone)] pub struct PythonDoc(TokenStream); -// TODO(#1782) use strip_prefix on Rust 1.45 or greater -#[allow(clippy::manual_strip)] /// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a null-terminated string /// e.g. concat!("...", "\n", "\0") pub fn get_doc( @@ -107,11 +105,11 @@ pub fn get_doc( // Strip single left space from literal strings, if needed. // e.g. `/// Hello world` expands to #[doc = " Hello world"] let doc_line = lit_str.value(); - if doc_line.starts_with(' ') { - syn::LitStr::new(&doc_line[1..], lit_str.span()).to_tokens(tokens) - } else { - lit_str.to_tokens(tokens) - } + doc_line + .strip_prefix(' ') + .map(|stripped| syn::LitStr::new(stripped, lit_str.span())) + .unwrap_or(lit_str) + .to_tokens(tokens); } else { // This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)] token_stream.to_tokens(tokens) diff --git a/src/buffer.rs b/src/buffer.rs index 07f6321a..f155bb11 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -75,12 +75,7 @@ impl ElementType { pub fn from_format(format: &CStr) -> ElementType { match format.to_bytes() { [char] | [b'@', char] => native_element_type_from_type_char(*char), - [modifier, char] - if (*modifier == b'=' - || *modifier == b'<' - || *modifier == b'>' - || *modifier == b'!') => - { + [modifier, char] if matches!(modifier, b'=' | b'<' | b'>' | b'!') => { standard_element_type_from_type_char(*char) } _ => ElementType::Unknown, diff --git a/src/class/impl_.rs b/src/class/impl_.rs index e9fb6492..7924b1bf 100644 --- a/src/class/impl_.rs +++ b/src/class/impl_.rs @@ -542,7 +542,6 @@ pub unsafe extern "C" fn alloc_with_freelist( /// # Safety /// - `obj` must be a valid pointer to an instance of T (not a subclass). /// - The GIL must be held. -#[allow(clippy::collapsible_if)] // for if cfg! pub unsafe extern "C" fn free_with_freelist(obj: *mut c_void) { let obj = obj as *mut ffi::PyObject; debug_assert_eq!( @@ -560,10 +559,9 @@ pub unsafe extern "C" fn free_with_freelist(obj: *mut c_ }; free(obj as *mut c_void); - if cfg!(Py_3_8) { - if ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) != 0 { - ffi::Py_DECREF(ty as *mut ffi::PyObject); - } + #[cfg(Py_3_8)] + if ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) != 0 { + ffi::Py_DECREF(ty as *mut ffi::PyObject); } } } diff --git a/src/ffi/ceval.rs b/src/ffi/ceval.rs index 202c2b1e..4f1dab8b 100644 --- a/src/ffi/ceval.rs +++ b/src/ffi/ceval.rs @@ -52,7 +52,6 @@ extern "C" { fn _Py_CheckRecursiveCall(_where: *mut c_char) -> c_int; } -// TODO // skipped Py_EnterRecursiveCall // skipped Py_LeaveRecursiveCall diff --git a/src/gil.rs b/src/gil.rs index 588d45f1..0832ec6d 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -69,33 +69,36 @@ pub(crate) fn gil_is_acquired() -> bool { /// # } /// ``` #[cfg(not(PyPy))] -#[allow(clippy::collapsible_if)] // for if cfg! pub fn prepare_freethreaded_python() { // Protect against race conditions when Python is not yet initialized and multiple threads // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against // concurrent initialization of the Python runtime by other users of the Python C API. START.call_once_force(|_| unsafe { - if cfg!(not(Py_3_7)) { - // Use call_once_force because if initialization panics, it's okay to try again. - if ffi::Py_IsInitialized() != 0 { - if ffi::PyEval_ThreadsInitialized() == 0 { - // We can only safely initialize threads if this thread holds the GIL. - assert!( - !ffi::PyGILState_GetThisThreadState().is_null(), - "Python threading is not initialized and cannot be initialized by this \ - thread, because it is not the thread which initialized Python." - ); - ffi::PyEval_InitThreads(); - } - } else { - ffi::Py_InitializeEx(0); - ffi::PyEval_InitThreads(); + // Use call_once_force because if initialization panics, it's okay to try again. - // Release the GIL. - ffi::PyEval_SaveThread(); + // TODO(#1782) - Python 3.6 legacy code + #[cfg(not(Py_3_7))] + if ffi::Py_IsInitialized() != 0 { + if ffi::PyEval_ThreadsInitialized() == 0 { + // We can only safely initialize threads if this thread holds the GIL. + assert!( + !ffi::PyGILState_GetThisThreadState().is_null(), + "Python threading is not initialized and cannot be initialized by this \ + thread, because it is not the thread which initialized Python." + ); + ffi::PyEval_InitThreads(); } - } else if ffi::Py_IsInitialized() == 0 { - // In Python 3.7 and up PyEval_InitThreads is irrelevant. + } else { + ffi::Py_InitializeEx(0); + ffi::PyEval_InitThreads(); + + // Release the GIL. + ffi::PyEval_SaveThread(); + } + + // In Python 3.7 and up PyEval_InitThreads is irrelevant. + #[cfg(Py_3_7)] + if ffi::Py_IsInitialized() == 0 { ffi::Py_InitializeEx(0); // Release the GIL. @@ -134,7 +137,6 @@ pub fn prepare_freethreaded_python() { /// # } /// ``` #[cfg(not(PyPy))] -#[allow(clippy::collapsible_if)] // for if cfg! pub unsafe fn with_embedded_python_interpreter(f: F) -> R where F: for<'p> FnOnce(Python<'p>) -> R, @@ -149,10 +151,9 @@ where // Changed in version 3.7: This function is now called by Py_Initialize(), so you don’t have to // call it yourself anymore. - if cfg!(not(Py_3_7)) { - if ffi::PyEval_ThreadsInitialized() == 0 { - ffi::PyEval_InitThreads(); - } + #[cfg(not(Py_3_7))] + if ffi::PyEval_ThreadsInitialized() == 0 { + ffi::PyEval_InitThreads(); } // Safe: the GIL is already held because of the Py_IntializeEx call. diff --git a/src/lib.rs b/src/lib.rs index 14bc7466..08e476b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,7 +111,7 @@ //! //! PyO3 supports the following software versions: //! - Python 3.6 and up (CPython and PyPy) -//! - Rust 1.41 and up +//! - Rust 1.48 and up //! //! # Example: Building a native Python module //! diff --git a/src/pyclass.rs b/src/pyclass.rs index fc8a4efa..132fe599 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -83,7 +83,8 @@ where slots.push(ffi::Py_tp_free, free as _); } - if cfg!(Py_3_9) { + #[cfg(Py_3_9)] + { let members = py_class_members::(); if !members.is_empty() { slots.push(ffi::Py_tp_members, into_raw(members)) @@ -155,7 +156,8 @@ fn tp_init_additional(type_object: *mut ffi::PyTypeObject) { // Setting buffer protocols via slots doesn't work until Python 3.9, so on older versions we // must manually fixup the type object. - if cfg!(not(Py_3_9)) { + #[cfg(not(Py_3_9))] + { if let Some(buffer) = T::get_buffer() { unsafe { (*(*type_object).tp_as_buffer).bf_getbuffer = buffer.bf_getbuffer; @@ -166,7 +168,8 @@ fn tp_init_additional(type_object: *mut ffi::PyTypeObject) { // Setting tp_dictoffset and tp_weaklistoffset via slots doesn't work until Python 3.9, so on // older versions again we must fixup the type object. - if cfg!(not(Py_3_9)) { + #[cfg(not(Py_3_9))] + { // __dict__ support if let Some(dict_offset) = PyCell::::dict_offset() { unsafe { @@ -258,12 +261,6 @@ fn py_class_members() -> Vec { members } -// Stub needed since the `if cfg!()` above still compiles contained code. -#[cfg(not(Py_3_9))] -fn py_class_members() -> Vec { - vec![] -} - const PY_GET_SET_DEF_INIT: ffi::PyGetSetDef = ffi::PyGetSetDef { name: ptr::null_mut(), get: None, @@ -272,7 +269,6 @@ const PY_GET_SET_DEF_INIT: ffi::PyGetSetDef = ffi::PyGetSetDef { closure: ptr::null_mut(), }; -#[allow(clippy::collapsible_if)] // for if cfg! fn py_class_properties( is_dummy: bool, for_each_method_def: &dyn Fn(&mut dyn FnMut(&[PyMethodDefType])), diff --git a/src/types/string.rs b/src/types/string.rs index 4c5ff52d..28eb89f6 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -227,8 +227,9 @@ impl PyString { pub unsafe fn data(&self) -> PyResult> { let ptr = self.as_ptr(); - if cfg!(not(Py_3_12)) { - #[allow(deprecated)] + #[cfg(not(Py_3_12))] + #[allow(deprecated)] + { let ready = ffi::PyUnicode_READY(ptr); if ready != 0 { // Exception was created on failure. diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 4751f774..4daab267 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -23,22 +23,15 @@ fn _test_compile_errors() { t.compile_fail("tests/ui/invalid_pymethods.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); t.compile_fail("tests/ui/invalid_argument_attributes.rs"); + t.compile_fail("tests/ui/missing_clone.rs"); t.compile_fail("tests/ui/reject_generics.rs"); + t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); - tests_rust_1_48(&t); tests_rust_1_49(&t); tests_rust_1_54(&t); tests_rust_1_55(&t); tests_rust_1_56(&t); - #[rustversion::since(1.48)] - fn tests_rust_1_48(t: &trybuild::TestCases) { - t.compile_fail("tests/ui/missing_clone.rs"); - t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); - } - #[rustversion::before(1.48)] - fn tests_rust_1_48(_t: &trybuild::TestCases) {} - #[rustversion::since(1.49)] fn tests_rust_1_49(t: &trybuild::TestCases) { t.compile_fail("tests/ui/deprecations.rs"); diff --git a/tests/test_not_msrv.rs b/tests/test_not_msrv.rs index d432c980..b484520e 100644 --- a/tests/test_not_msrv.rs +++ b/tests/test_not_msrv.rs @@ -2,9 +2,7 @@ //! but can't even be cfg-ed out on MSRV because the compiler doesn't support //! the syntax. -// TODO(#1782) rustversion attribute can't go on modules until Rust 1.42, so this -// funky dance has to happen... +#[rustversion::since(1.54)] mod requires_1_54 { - #[rustversion::since(1.54)] include!("not_msrv/requires_1_54.rs"); } diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index a90d4044..6faab088 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -6,15 +6,15 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied | = note: required because of the requirements on the impl of `PyClassBaseType` for `PyDict` note: required by a bound in `PyClassBaseType` - --> src/class/impl_.rs:766:1 + --> src/class/impl_.rs | -766 | / pub trait PyClassBaseType: Sized { -767 | | type Dict; -768 | | type WeakRef; -769 | | type LayoutAsBase: PyCellLayout; + | / pub trait PyClassBaseType: Sized { + | | type Dict; + | | type WeakRef; + | | type LayoutAsBase: PyCellLayout; ... | -772 | | type Initializer: PyObjectInit; -773 | | } + | | type Initializer: PyObjectInit; + | | } | |_^ required by this bound in `PyClassBaseType` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -26,8 +26,8 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied | = note: required because of the requirements on the impl of `PyClassBaseType` for `PyDict` note: required by a bound in `ThreadCheckerInherited` - --> src/class/impl_.rs:753:47 + --> src/class/impl_.rs | -753 | pub struct ThreadCheckerInherited(PhantomData, U::ThreadChecker); + | pub struct ThreadCheckerInherited(PhantomData, U::ThreadChecker); | ^^^^^^^^^^^^^^^ required by this bound in `ThreadCheckerInherited` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 50236b39..7938fbe6 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -50,6 +50,3 @@ fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { fn main() { } - - -// TODO: ensure name deprecated on #[pyfunction] and #[pymodule] diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 56217f1e..531a4e9d 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -11,8 +11,8 @@ note: required because it appears within the type `NotThreadSafe` 5 | struct NotThreadSafe { | ^^^^^^^^^^^^^ note: required by a bound in `ThreadCheckerStub` - --> src/class/impl_.rs:710:33 + --> src/class/impl_.rs | -710 | pub struct ThreadCheckerStub(PhantomData); + | pub struct ThreadCheckerStub(PhantomData); | ^^^^ required by this bound in `ThreadCheckerStub` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) From b9cc10f3fb9efbdbd662aa1c92613deb71eb5931 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Fri, 19 Nov 2021 14:19:24 +0000 Subject: [PATCH 23/30] pyo3-build-config: don't enable resolve-config by default --- CHANGELOG.md | 1 + guide/src/features.md | 8 ++------ pyo3-build-config/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db17f5f0..dd999126 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update MSRV to Rust 1.48. [#2004](https://github.com/PyO3/pyo3/pull/2004) - Update `indoc` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) - Update `paste` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) +- `pyo3-build-config` no longer enables the `resolve-config` feature by default. [#2008](https://github.com/PyO3/pyo3/pull/2008) ## [0.15.1] - 2021-11-19 diff --git a/guide/src/features.md b/guide/src/features.md index 86447c9d..73ec50b9 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -74,10 +74,8 @@ The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use R ### `resolve-config` The `resolve-config` feature of the `pyo3-build-config` crate controls whether that crate's -build script automatically resolves a Python interpreter / build configuration. Disabling -this feature enables this crate to be used in *library mode*. This may be desirable for -use cases where you want to read or write PyO3 build configuration files or resolve -metadata about a Python interpreter. +build script automatically resolves a Python interpreter / build configuration. This feature is primarily useful when building PyO3 +itself. By default this feature is not enabled, meaning you can freely use `pyo3-build-config` as a standalone library to read or write PyO3 build configuration files or resolve metadata about a Python interpreter. ## Optional Dependencies @@ -127,5 +125,3 @@ struct User { permissions: Vec> } ``` - - diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index d908ed81..7e248083 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" once_cell = "1" [features] -default = ["resolve-config"] +default = [] # Attempt to resolve a Python interpreter config for building in the build # script. If this feature isn't enabled, the build script no-ops. From 91caa814d0533127f09160c81039a18da517338a Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Fri, 19 Nov 2021 16:49:24 +0100 Subject: [PATCH 24/30] Add `Py::setattr` method --- CHANGELOG.md | 4 ++++ src/instance.rs | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db17f5f0..4b536823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update `indoc` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) - Update `paste` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) +### Added + +- Add `Py::setattr` method. [#2009](https://github.com/PyO3/pyo3/pull/2009) + ## [0.15.1] - 2021-11-19 ### Added diff --git a/src/instance.rs b/src/instance.rs index 269d22f3..b8f7ab69 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -528,6 +528,21 @@ impl Py { }) } + /// Sets an attribute value. + /// + /// This is equivalent to the Python expression `self.attr_name = value`. + pub fn setattr(&self, py: Python, attr_name: N, value: V) -> PyResult<()> + where + N: ToPyObject, + V: ToPyObject, + { + attr_name.with_borrowed_ptr(py, move |attr_name| { + value.with_borrowed_ptr(py, |value| unsafe { + err::error_on_minusone(py, ffi::PyObject_SetAttr(self.as_ptr(), attr_name, value)) + }) + }) + } + /// Calls the object. /// /// This is equivalent to the Python expression `self(*args, **kwargs)`. From c9a4cd1f87c99333e4f32bf6941c74b39d09e3d7 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Fri, 19 Nov 2021 13:33:56 +0000 Subject: [PATCH 25/30] deprecations: remove items deprecated in PyO3 0.14 --- CHANGELOG.md | 4 ++ guide/src/class.md | 1 - pyo3-macros-backend/src/attributes.rs | 66 ------------------ pyo3-macros-backend/src/defs.rs | 64 ++---------------- pyo3-macros-backend/src/deprecations.rs | 8 --- pyo3-macros-backend/src/konst.rs | 10 +-- pyo3-macros-backend/src/module.rs | 51 +++----------- pyo3-macros-backend/src/pyclass.rs | 13 +--- pyo3-macros-backend/src/pyfunction.rs | 33 ++------- pyo3-macros-backend/src/pyproto.rs | 1 - pyo3-macros/src/lib.rs | 18 ++--- src/class/basic.rs | 29 -------- src/class/context.rs | 49 -------------- src/class/descr.rs | 33 --------- src/class/impl_.rs | 1 - src/class/mapping.rs | 15 ----- src/class/mod.rs | 2 - src/class/number.rs | 29 -------- src/class/pyasync.rs | 38 ----------- src/ffi/import.rs | 5 +- src/ffi/intrcheck.rs | 5 +- src/impl_/deprecations.rs | 24 ------- src/types/module.rs | 44 +----------- tests/test_arithmetics_protos.rs | 6 -- tests/test_compile_error.rs | 1 + tests/test_mapping.rs | 21 ------ tests/test_module.rs | 25 ------- tests/test_pyproto.rs | 90 +------------------------ tests/ui/deprecations.rs | 36 ---------- tests/ui/deprecations.stderr | 74 ++------------------ tests/ui/invalid_pymodule_args.rs | 8 +++ tests/ui/invalid_pymodule_args.stderr | 5 ++ 32 files changed, 54 insertions(+), 755 deletions(-) delete mode 100644 src/class/context.rs create mode 100644 tests/ui/invalid_pymodule_args.rs create mode 100644 tests/ui/invalid_pymodule_args.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index db17f5f0..3ecdc356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update `indoc` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) - Update `paste` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) +## Removed + +- Remove all functionality deprecated in PyO3 0.14. [#2007](https://github.com/PyO3/pyo3/pull/2007) + ## [0.15.1] - 2021-11-19 ### Added diff --git a/guide/src/class.md b/guide/src/class.md index 9ab5bf96..9ba39ed7 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -865,7 +865,6 @@ impl pyo3::class::impl_::PyClassImpl for MyClass { visitor(collector.py_class_descriptors()); visitor(collector.object_protocol_methods()); visitor(collector.async_protocol_methods()); - visitor(collector.context_protocol_methods()); visitor(collector.descr_protocol_methods()); visitor(collector.mapping_protocol_methods()); visitor(collector.number_protocol_methods()); diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 04dcc054..a0569848 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -1,13 +1,10 @@ use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, - spanned::Spanned, token::Comma, Attribute, ExprPath, Ident, LitStr, Result, Token, }; -use crate::deprecations::{Deprecation, Deprecations}; - pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); @@ -113,66 +110,3 @@ pub fn take_pyo3_options(attrs: &mut Vec) -> Result syn::Result> { - match attr.parse_meta() { - Ok(syn::Meta::NameValue(syn::MetaNameValue { - path, - lit: syn::Lit::Str(s), - .. - })) if path.is_ident("name") => { - deprecations.push(Deprecation::NameAttribute, attr.span()); - Ok(Some(NameAttribute(s.parse()?))) - } - _ => Ok(None), - } -} - -pub fn get_deprecated_text_signature_attribute( - attr: &syn::Attribute, - deprecations: &mut Deprecations, -) -> syn::Result> { - match attr.parse_meta() { - Ok(syn::Meta::NameValue(syn::MetaNameValue { - path, - lit: syn::Lit::Str(lit), - .. - })) if path.is_ident("text_signature") => { - let text_signature = TextSignatureAttribute { - kw: syn::parse_quote!(text_signature), - eq_token: syn::parse_quote!(=), - lit, - }; - deprecations.push( - crate::deprecations::Deprecation::TextSignatureAttribute, - attr.span(), - ); - Ok(Some(text_signature)) - } - _ => Ok(None), - } -} - -pub fn take_deprecated_text_signature_attribute( - attrs: &mut Vec, - deprecations: &mut Deprecations, -) -> syn::Result> { - let mut text_signature = None; - let mut attrs_out = Vec::with_capacity(attrs.len()); - for attr in attrs.drain(..) { - if let Some(value) = get_deprecated_text_signature_attribute(&attr, deprecations)? { - ensure_spanned!( - text_signature.is_none(), - attr.span() => "text_signature attribute already specified previously" - ); - text_signature = Some(value); - } else { - attrs_out.push(attr); - } - } - *attrs = attrs_out; - Ok(text_signature) -} diff --git a/pyo3-macros-backend/src/defs.rs b/pyo3-macros-backend/src/defs.rs index bdda83b6..be90f137 100644 --- a/pyo3-macros-backend/src/defs.rs +++ b/pyo3-macros-backend/src/defs.rs @@ -107,13 +107,6 @@ impl PyMethod { can_coexist: true, } } - const fn new(name: &'static str, proto: &'static str) -> Self { - PyMethod { - name, - proto, - can_coexist: false, - } - } } /// Represents a slot definition. @@ -156,20 +149,13 @@ pub const OBJECT: Proto = Proto { .has_self(), MethodProto::new("__str__", "PyObjectStrProtocol").has_self(), MethodProto::new("__repr__", "PyObjectReprProtocol").has_self(), - MethodProto::new("__format__", "PyObjectFormatProtocol") - .args(&["Format"]) - .has_self(), MethodProto::new("__hash__", "PyObjectHashProtocol").has_self(), - MethodProto::new("__bytes__", "PyObjectBytesProtocol").has_self(), MethodProto::new("__richcmp__", "PyObjectRichcmpProtocol") .args(&["Other"]) .has_self(), MethodProto::new("__bool__", "PyObjectBoolProtocol").has_self(), ], - py_methods: &[ - PyMethod::new("__format__", "FormatProtocolImpl"), - PyMethod::new("__bytes__", "BytesProtocolImpl"), - ], + py_methods: &[], slot_defs: &[ SlotDef::new(&["__str__"], "Py_tp_str", "str"), SlotDef::new(&["__repr__"], "Py_tp_repr", "repr"), @@ -194,15 +180,8 @@ pub const ASYNC: Proto = Proto { MethodProto::new("__await__", "PyAsyncAwaitProtocol").args(&["Receiver"]), MethodProto::new("__aiter__", "PyAsyncAiterProtocol").args(&["Receiver"]), MethodProto::new("__anext__", "PyAsyncAnextProtocol").args(&["Receiver"]), - MethodProto::new("__aenter__", "PyAsyncAenterProtocol").has_self(), - MethodProto::new("__aexit__", "PyAsyncAexitProtocol") - .args(&["ExcType", "ExcValue", "Traceback"]) - .has_self(), - ], - py_methods: &[ - PyMethod::new("__aenter__", "PyAsyncAenterProtocolImpl"), - PyMethod::new("__aexit__", "PyAsyncAexitProtocolImpl"), ], + py_methods: &[], slot_defs: &[ SlotDef::new(&["__await__"], "Py_am_await", "await_"), SlotDef::new(&["__aiter__"], "Py_am_aiter", "aiter"), @@ -228,22 +207,6 @@ pub const BUFFER: Proto = Proto { ], }; -pub const CONTEXT: Proto = Proto { - name: "Context", - module: "::pyo3::class::context", - methods: &[ - MethodProto::new("__enter__", "PyContextEnterProtocol").has_self(), - MethodProto::new("__exit__", "PyContextExitProtocol") - .args(&["ExcType", "ExcValue", "Traceback"]) - .has_self(), - ], - py_methods: &[ - PyMethod::new("__enter__", "PyContextEnterProtocolImpl"), - PyMethod::new("__exit__", "PyContextExitProtocolImpl"), - ], - slot_defs: &[], -}; - pub const GC: Proto = Proto { name: "GC", module: "::pyo3::class::gc", @@ -268,17 +231,8 @@ pub const DESCR: Proto = Proto { methods: &[ MethodProto::new("__get__", "PyDescrGetProtocol").args(&["Receiver", "Inst", "Owner"]), MethodProto::new("__set__", "PyDescrSetProtocol").args(&["Receiver", "Inst", "Value"]), - MethodProto::new("__delete__", "PyDescrDelProtocol") - .args(&["Inst"]) - .has_self(), - MethodProto::new("__set_name__", "PyDescrSetNameProtocol") - .args(&["Inst"]) - .has_self(), - ], - py_methods: &[ - PyMethod::new("__delete__", "PyDescrDelProtocolImpl"), - PyMethod::new("__set_name__", "PyDescrNameProtocolImpl"), ], + py_methods: &[], slot_defs: &[ SlotDef::new(&["__get__"], "Py_tp_descr_get", "descr_get"), SlotDef::new(&["__set__"], "Py_tp_descr_set", "descr_set"), @@ -313,12 +267,8 @@ pub const MAPPING: Proto = Proto { MethodProto::new("__delitem__", "PyMappingDelItemProtocol") .args(&["Key"]) .has_self(), - MethodProto::new("__reversed__", "PyMappingReversedProtocol").has_self(), ], - py_methods: &[PyMethod::new( - "__reversed__", - "PyMappingReversedProtocolImpl", - )], + py_methods: &[], slot_defs: &[ SlotDef::new(&["__len__"], "Py_mp_length", "len"), SlotDef::new(&["__getitem__"], "Py_mp_subscript", "getitem"), @@ -492,13 +442,9 @@ pub const NUM: Proto = Proto { MethodProto::new("__pos__", "PyNumberPosProtocol").has_self(), MethodProto::new("__abs__", "PyNumberAbsProtocol").has_self(), MethodProto::new("__invert__", "PyNumberInvertProtocol").has_self(), - MethodProto::new("__complex__", "PyNumberComplexProtocol").has_self(), MethodProto::new("__int__", "PyNumberIntProtocol").has_self(), MethodProto::new("__float__", "PyNumberFloatProtocol").has_self(), MethodProto::new("__index__", "PyNumberIndexProtocol").has_self(), - MethodProto::new("__round__", "PyNumberRoundProtocol") - .args(&["NDigits"]) - .has_self(), ], py_methods: &[ PyMethod::coexist("__radd__", "PyNumberRAddProtocolImpl"), @@ -515,8 +461,6 @@ pub const NUM: Proto = Proto { PyMethod::coexist("__rand__", "PyNumberRAndProtocolImpl"), PyMethod::coexist("__rxor__", "PyNumberRXorProtocolImpl"), PyMethod::coexist("__ror__", "PyNumberROrProtocolImpl"), - PyMethod::new("__complex__", "PyNumberComplexProtocolImpl"), - PyMethod::new("__round__", "PyNumberRoundProtocolImpl"), ], slot_defs: &[ SlotDef::new(&["__add__", "__radd__"], "Py_nb_add", "add_radd"), diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index b178d4e8..4bf3d624 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -2,20 +2,12 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote_spanned, ToTokens}; pub enum Deprecation { - NameAttribute, - PyfnNameArgument, - PyModuleNameArgument, - TextSignatureAttribute, CallAttribute, } impl Deprecation { fn ident(&self, span: Span) -> syn::Ident { let string = match self { - Deprecation::NameAttribute => "NAME_ATTRIBUTE", - Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT", - Deprecation::PyModuleNameArgument => "PYMODULE_NAME_ARGUMENT", - Deprecation::TextSignatureAttribute => "TEXT_SIGNATURE_ATTRIBUTE", Deprecation::CallAttribute => "CALL_ATTRIBUTE", }; syn::Ident::new(string, span) diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index d0bc458d..c11c828d 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,10 +1,7 @@ use std::borrow::Cow; use crate::{ - attributes::{ - self, get_deprecated_name_attribute, get_pyo3_options, is_attribute_ident, take_attributes, - NameAttribute, - }, + attributes::{self, get_pyo3_options, is_attribute_ident, take_attributes, NameAttribute}, deprecations::Deprecations, }; use proc_macro2::{Ident, TokenStream}; @@ -81,11 +78,6 @@ impl ConstAttributes { } } Ok(true) - } else if let Some(name) = - get_deprecated_name_attribute(attr, &mut attributes.deprecations)? - { - attributes.set_name(name)?; - Ok(true) } else { Ok(false) } diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 43c42d3f..5adc02d4 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,15 +2,10 @@ //! Code generation for the function that initializes a python module and adds classes and function. use crate::{ - attributes::{self, take_pyo3_options}, - deprecations::Deprecations, + attributes::{self, is_attribute_ident, take_attributes, take_pyo3_options, NameAttribute}, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, utils::PythonDoc, }; -use crate::{ - attributes::{is_attribute_ident, take_attributes, NameAttribute}, - deprecations::Deprecation, -}; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{ @@ -23,23 +18,11 @@ use syn::{ pub struct PyModuleOptions { name: Option, - deprecations: Deprecations, } impl PyModuleOptions { - pub fn from_pymodule_arg_and_attrs( - deprecated_pymodule_name_arg: Option, - attrs: &mut Vec, - ) -> Result { - let mut deprecations = Deprecations::new(); - if let Some(name) = &deprecated_pymodule_name_arg { - deprecations.push(Deprecation::PyModuleNameArgument, name.span()); - } - - let mut options: PyModuleOptions = PyModuleOptions { - name: deprecated_pymodule_name_arg, - deprecations, - }; + pub fn from_attrs(attrs: &mut Vec) -> Result { + let mut options: PyModuleOptions = PyModuleOptions { name: None }; for option in take_pyo3_options(attrs)? { match option { @@ -65,7 +48,6 @@ impl PyModuleOptions { /// module pub fn py_init(fnname: &Ident, options: PyModuleOptions, doc: PythonDoc) -> TokenStream { let name = options.name.unwrap_or_else(|| fnname.unraw()); - let deprecations = options.deprecations; let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site()); quote! { @@ -79,8 +61,6 @@ pub fn py_init(fnname: &Ident, options: PyModuleOptions, doc: PythonDoc) -> Toke static DOC: &str = #doc; static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) }; - #deprecations - ::pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) }) } } @@ -129,23 +109,10 @@ impl Parse for PyFnArgs { let _: Comma = input.parse()?; - let mut deprecated_name_argument = None; - if let Ok(lit_str) = input.parse::() { - deprecated_name_argument = Some(lit_str); - if !input.is_empty() { - let _: Comma = input.parse()?; - } - } - - let mut options: PyFunctionOptions = input.parse()?; - if let Some(lit_str) = deprecated_name_argument { - options.set_name(NameAttribute(lit_str.parse()?))?; - options - .deprecations - .push(Deprecation::PyfnNameArgument, lit_str.span()); - } - - Ok(Self { modname, options }) + Ok(Self { + modname, + options: input.parse()?, + }) } } @@ -167,7 +134,9 @@ fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result syn::Result { - let mut options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?; - if let Some(text_signature) = - take_deprecated_text_signature_attribute(&mut class.attrs, &mut options.deprecations)? - { - options.set_text_signature(text_signature)?; - } + let options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?; let doc = utils::get_doc( &class.attrs, options @@ -569,7 +561,6 @@ fn impl_class( visitor(collector.py_class_descriptors()); visitor(collector.object_protocol_methods()); visitor(collector.async_protocol_methods()); - visitor(collector.context_protocol_methods()); visitor(collector.descr_protocol_methods()); visitor(collector.mapping_protocol_methods()); visitor(collector.number_protocol_methods()); diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index a5017e1b..87e8ffda 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -2,9 +2,8 @@ use crate::{ attributes::{ - self, get_deprecated_name_attribute, get_deprecated_text_signature_attribute, - get_pyo3_options, take_attributes, FromPyWithAttribute, NameAttribute, - TextSignatureAttribute, + self, get_pyo3_options, take_attributes, take_pyo3_options, FromPyWithAttribute, + NameAttribute, TextSignatureAttribute, }, deprecations::Deprecations, method::{self, CallingConvention, FnArg}, @@ -303,34 +302,10 @@ impl Parse for PyFunctionOption { impl PyFunctionOptions { pub fn from_attrs(attrs: &mut Vec) -> syn::Result { let mut options = PyFunctionOptions::default(); - options.take_pyo3_options(attrs)?; + options.add_attributes(take_pyo3_options(attrs)?)?; Ok(options) } - pub fn take_pyo3_options(&mut self, attrs: &mut Vec) -> syn::Result<()> { - take_attributes(attrs, |attr| { - if let Some(pyo3_attributes) = get_pyo3_options(attr)? { - self.add_attributes(pyo3_attributes)?; - Ok(true) - } else if let Some(name) = get_deprecated_name_attribute(attr, &mut self.deprecations)? - { - self.set_name(name)?; - Ok(true) - } else if let Some(text_signature) = - get_deprecated_text_signature_attribute(attr, &mut self.deprecations)? - { - self.add_attributes(std::iter::once(PyFunctionOption::TextSignature( - text_signature, - )))?; - Ok(true) - } else { - Ok(false) - } - })?; - - Ok(()) - } - pub fn add_attributes( &mut self, attrs: impl IntoIterator, @@ -379,7 +354,7 @@ pub fn build_py_function( ast: &mut syn::ItemFn, mut options: PyFunctionOptions, ) -> syn::Result { - options.take_pyo3_options(&mut ast.attrs)?; + options.add_attributes(take_pyo3_options(&mut ast.attrs)?)?; Ok(impl_wrap_pyfunction(ast, options)?.1) } diff --git a/pyo3-macros-backend/src/pyproto.rs b/pyo3-macros-backend/src/pyproto.rs index 23e5d8a5..779cce25 100644 --- a/pyo3-macros-backend/src/pyproto.rs +++ b/pyo3-macros-backend/src/pyproto.rs @@ -18,7 +18,6 @@ pub fn build_py_proto(ast: &mut syn::ItemImpl) -> syn::Result { Some(segment) if segment.ident == "PyAsyncProtocol" => &defs::ASYNC, Some(segment) if segment.ident == "PyMappingProtocol" => &defs::MAPPING, Some(segment) if segment.ident == "PyIterProtocol" => &defs::ITER, - Some(segment) if segment.ident == "PyContextProtocol" => &defs::CONTEXT, Some(segment) if segment.ident == "PySequenceProtocol" => &defs::SEQ, Some(segment) if segment.ident == "PyNumberProtocol" => &defs::NUM, Some(segment) if segment.ident == "PyDescrProtocol" => &defs::DESCR, diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index b8ab3f6a..a6612d19 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -12,7 +12,7 @@ use pyo3_macros_backend::{ PyFunctionOptions, PyModuleOptions, }; use quote::quote; -use syn::parse_macro_input; +use syn::{parse::Nothing, parse_macro_input}; /// A proc macro used to implement Python modules. /// @@ -31,19 +31,11 @@ use syn::parse_macro_input; /// /// [1]: https://pyo3.rs/latest/module.html #[proc_macro_attribute] -pub fn pymodule(attr: TokenStream, input: TokenStream) -> TokenStream { +pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { + parse_macro_input!(args as Nothing); + let mut ast = parse_macro_input!(input as syn::ItemFn); - - let deprecated_pymodule_name_arg = if attr.is_empty() { - None - } else { - Some(parse_macro_input!(attr as syn::Ident)) - }; - - let options = match PyModuleOptions::from_pymodule_arg_and_attrs( - deprecated_pymodule_name_arg, - &mut ast.attrs, - ) { + let options = match PyModuleOptions::from_attrs(&mut ast.attrs) { Ok(options) => options, Err(e) => return e.to_compile_error().into(), }; diff --git a/src/class/basic.rs b/src/class/basic.rs index 1e34d493..9d4d569b 100644 --- a/src/class/basic.rs +++ b/src/class/basic.rs @@ -81,17 +81,6 @@ pub trait PyObjectProtocol<'p>: PyClass { unimplemented!() } - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__format__` in `#[pymethods]` instead of in a protocol" - )] - fn __format__(&'p self, format_spec: Self::Format) -> Self::Result - where - Self: PyObjectFormatProtocol<'p>, - { - unimplemented!() - } - fn __hash__(&'p self) -> Self::Result where Self: PyObjectHashProtocol<'p>, @@ -99,17 +88,6 @@ pub trait PyObjectProtocol<'p>: PyClass { unimplemented!() } - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__bytes__` in `#[pymethods]` instead of in a protocol" - )] - fn __bytes__(&'p self) -> Self::Result - where - Self: PyObjectBytesProtocol<'p>, - { - unimplemented!() - } - fn __richcmp__(&'p self, other: Self::Other, op: CompareOp) -> Self::Result where Self: PyObjectRichcmpProtocol<'p>, @@ -143,19 +121,12 @@ pub trait PyObjectStrProtocol<'p>: PyObjectProtocol<'p> { pub trait PyObjectReprProtocol<'p>: PyObjectProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyObjectFormatProtocol<'p>: PyObjectProtocol<'p> { - type Format: FromPyObject<'p>; - type Result: IntoPyCallbackOutput; -} pub trait PyObjectHashProtocol<'p>: PyObjectProtocol<'p> { type Result: IntoPyCallbackOutput; } pub trait PyObjectBoolProtocol<'p>: PyObjectProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyObjectBytesProtocol<'p>: PyObjectProtocol<'p> { - type Result: IntoPyCallbackOutput; -} pub trait PyObjectRichcmpProtocol<'p>: PyObjectProtocol<'p> { type Other: FromPyObject<'p>; type Result: IntoPyCallbackOutput; diff --git a/src/class/context.rs b/src/class/context.rs deleted file mode 100644 index 4fc8d6c5..00000000 --- a/src/class/context.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2017-present PyO3 Project and Contributors - -//! Context manager api -//! Trait and support implementation for context manager api - -use crate::callback::IntoPyCallbackOutput; -use crate::{PyClass, PyObject}; - -/// Context manager interface -#[allow(unused_variables)] -pub trait PyContextProtocol<'p>: PyClass { - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__enter__` in `#[pymethods]` instead of in a protocol" - )] - fn __enter__(&'p mut self) -> Self::Result - where - Self: PyContextEnterProtocol<'p>, - { - unimplemented!() - } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__exit__` in `#[pymethods]` instead of in a protocol" - )] - fn __exit__( - &'p mut self, - exc_type: Option, - exc_value: Option, - traceback: Option, - ) -> Self::Result - where - Self: PyContextExitProtocol<'p>, - { - unimplemented!() - } -} - -pub trait PyContextEnterProtocol<'p>: PyContextProtocol<'p> { - type Result: IntoPyCallbackOutput; -} - -pub trait PyContextExitProtocol<'p>: PyContextProtocol<'p> { - type ExcType: crate::FromPyObject<'p>; - type ExcValue: crate::FromPyObject<'p>; - type Traceback: crate::FromPyObject<'p>; - type Result: IntoPyCallbackOutput; -} diff --git a/src/class/descr.rs b/src/class/descr.rs index 9cc775a9..1c693d44 100644 --- a/src/class/descr.rs +++ b/src/class/descr.rs @@ -6,7 +6,6 @@ //! https://docs.python.org/3/reference/datamodel.html#implementing-descriptors) use crate::callback::IntoPyCallbackOutput; -use crate::types::PyAny; use crate::{FromPyObject, PyClass, PyObject}; use std::os::raw::c_int; @@ -30,28 +29,6 @@ pub trait PyDescrProtocol<'p>: PyClass { { unimplemented!() } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__delete__` in `#[pymethods]` instead of in a protocol" - )] - fn __delete__(&'p self, instance: &'p PyAny) -> Self::Result - where - Self: PyDescrDeleteProtocol<'p>, - { - unimplemented!() - } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__set_name__` in `#[pymethods]` instead of in a protocol" - )] - fn __set_name__(&'p self, instance: &'p PyAny) -> Self::Result - where - Self: PyDescrSetNameProtocol<'p>, - { - unimplemented!() - } } pub trait PyDescrGetProtocol<'p>: PyDescrProtocol<'p> { @@ -68,15 +45,5 @@ pub trait PyDescrSetProtocol<'p>: PyDescrProtocol<'p> { type Result: IntoPyCallbackOutput<()>; } -pub trait PyDescrDeleteProtocol<'p>: PyDescrProtocol<'p> { - type Inst: FromPyObject<'p>; - type Result: IntoPyCallbackOutput<()>; -} - -pub trait PyDescrSetNameProtocol<'p>: PyDescrProtocol<'p> { - type Inst: FromPyObject<'p>; - type Result: IntoPyCallbackOutput<()>; -} - py_ternarys_func!(descr_get, PyDescrGetProtocol, Self::__get__); py_ternarys_func!(descr_set, PyDescrSetProtocol, Self::__set__, c_int); diff --git a/src/class/impl_.rs b/src/class/impl_.rs index 7924b1bf..63435a38 100644 --- a/src/class/impl_.rs +++ b/src/class/impl_.rs @@ -670,7 +670,6 @@ slots_trait!(PyMethodsProtocolSlots, methods_protocol_slots); methods_trait!(PyObjectProtocolMethods, object_protocol_methods); methods_trait!(PyAsyncProtocolMethods, async_protocol_methods); -methods_trait!(PyContextProtocolMethods, context_protocol_methods); methods_trait!(PyDescrProtocolMethods, descr_protocol_methods); methods_trait!(PyMappingProtocolMethods, mapping_protocol_methods); methods_trait!(PyNumberProtocolMethods, number_protocol_methods); diff --git a/src/class/mapping.rs b/src/class/mapping.rs index 9961c5d1..b6868521 100644 --- a/src/class/mapping.rs +++ b/src/class/mapping.rs @@ -36,17 +36,6 @@ pub trait PyMappingProtocol<'p>: PyClass { { unimplemented!() } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__reversed__` in `#[pymethods]` instead of in a protocol" - )] - fn __reversed__(&'p self) -> Self::Result - where - Self: PyMappingReversedProtocol<'p>, - { - unimplemented!() - } } // The following are a bunch of marker traits used to detect @@ -72,10 +61,6 @@ pub trait PyMappingDelItemProtocol<'p>: PyMappingProtocol<'p> { type Result: IntoPyCallbackOutput<()>; } -pub trait PyMappingReversedProtocol<'p>: PyMappingProtocol<'p> { - type Result: IntoPyCallbackOutput; -} - py_len_func!(len, PyMappingLenProtocol, Self::__len__); py_binary_func!(getitem, PyMappingGetItemProtocol, Self::__getitem__); py_func_set!(setitem, PyMappingSetItemProtocol, Self::__setitem__); diff --git a/src/class/mod.rs b/src/class/mod.rs index 4b7543aa..0e889573 100644 --- a/src/class/mod.rs +++ b/src/class/mod.rs @@ -8,7 +8,6 @@ mod macros; pub mod basic; #[cfg(not(Py_LIMITED_API))] pub mod buffer; -pub mod context; pub mod descr; pub mod gc; #[doc(hidden)] @@ -24,7 +23,6 @@ pub mod sequence; pub use self::basic::PyObjectProtocol; #[cfg(not(Py_LIMITED_API))] pub use self::buffer::PyBufferProtocol; -pub use self::context::PyContextProtocol; pub use self::descr::PyDescrProtocol; pub use self::gc::{PyGCProtocol, PyTraverseError, PyVisit}; pub use self::iter::PyIterProtocol; diff --git a/src/class/number.rs b/src/class/number.rs index 903744eb..2b0c2ade 100644 --- a/src/class/number.rs +++ b/src/class/number.rs @@ -283,16 +283,6 @@ pub trait PyNumberProtocol<'p>: PyClass { { unimplemented!() } - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__complex__` in `#[pymethods]` instead of in a protocol" - )] - fn __complex__(&'p self) -> Self::Result - where - Self: PyNumberComplexProtocol<'p>, - { - unimplemented!() - } fn __int__(&'p self) -> Self::Result where Self: PyNumberIntProtocol<'p>, @@ -311,16 +301,6 @@ pub trait PyNumberProtocol<'p>: PyClass { { unimplemented!() } - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__round__` in `#[pymethods]` instead of in a protocol" - )] - fn __round__(&'p self, ndigits: Option) -> Self::Result - where - Self: PyNumberRoundProtocol<'p>, - { - unimplemented!() - } } pub trait PyNumberAddProtocol<'p>: PyNumberProtocol<'p> { @@ -569,10 +549,6 @@ pub trait PyNumberInvertProtocol<'p>: PyNumberProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyNumberComplexProtocol<'p>: PyNumberProtocol<'p> { - type Result: IntoPyCallbackOutput; -} - pub trait PyNumberIntProtocol<'p>: PyNumberProtocol<'p> { type Result: IntoPyCallbackOutput; } @@ -581,11 +557,6 @@ pub trait PyNumberFloatProtocol<'p>: PyNumberProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyNumberRoundProtocol<'p>: PyNumberProtocol<'p> { - type NDigits: FromPyObject<'p>; - type Result: IntoPyCallbackOutput; -} - pub trait PyNumberIndexProtocol<'p>: PyNumberProtocol<'p> { type Result: IntoPyCallbackOutput; } diff --git a/src/class/pyasync.rs b/src/class/pyasync.rs index ebfde9fa..f79cd7ac 100644 --- a/src/class/pyasync.rs +++ b/src/class/pyasync.rs @@ -37,33 +37,6 @@ pub trait PyAsyncProtocol<'p>: PyClass { { unimplemented!() } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__aenter__` in `#[pymethods]` instead of in a protocol" - )] - fn __aenter__(&'p mut self) -> Self::Result - where - Self: PyAsyncAenterProtocol<'p>, - { - unimplemented!() - } - - #[deprecated( - since = "0.14.0", - note = "prefer implementing `__aexit__` in `#[pymethods]` instead of in a protocol" - )] - fn __aexit__( - &'p mut self, - exc_type: Option, - exc_value: Option, - traceback: Option, - ) -> Self::Result - where - Self: PyAsyncAexitProtocol<'p>, - { - unimplemented!() - } } pub trait PyAsyncAwaitProtocol<'p>: PyAsyncProtocol<'p> { @@ -81,17 +54,6 @@ pub trait PyAsyncAnextProtocol<'p>: PyAsyncProtocol<'p> { type Result: IntoPyCallbackOutput; } -pub trait PyAsyncAenterProtocol<'p>: PyAsyncProtocol<'p> { - type Result: IntoPyCallbackOutput; -} - -pub trait PyAsyncAexitProtocol<'p>: PyAsyncProtocol<'p> { - type ExcType: crate::FromPyObject<'p>; - type ExcValue: crate::FromPyObject<'p>; - type Traceback: crate::FromPyObject<'p>; - type Result: IntoPyCallbackOutput; -} - py_unarys_func!(await_, PyAsyncAwaitProtocol, Self::__await__); py_unarys_func!(aiter, PyAsyncAiterProtocol, Self::__aiter__); py_unarys_func!(anext, PyAsyncAnextProtocol, Self::__anext__); diff --git a/src/ffi/import.rs b/src/ffi/import.rs index 81d85c3b..0edb58cc 100644 --- a/src/ffi/import.rs +++ b/src/ffi/import.rs @@ -69,10 +69,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyImport_ReloadModule")] pub fn PyImport_ReloadModule(m: *mut PyObject) -> *mut PyObject; #[cfg(not(Py_3_9))] - #[deprecated( - since = "0.14.0", - note = "Removed in Python 3.9 as it was \"For internal use only\"." - )] + #[deprecated(note = "Removed in Python 3.9 as it was \"For internal use only\".")] pub fn PyImport_Cleanup(); pub fn PyImport_ImportFrozenModuleObject(name: *mut PyObject) -> c_int; pub fn PyImport_ImportFrozenModule(name: *const c_char) -> c_int; diff --git a/src/ffi/intrcheck.rs b/src/ffi/intrcheck.rs index ffd6c79c..c777356a 100644 --- a/src/ffi/intrcheck.rs +++ b/src/ffi/intrcheck.rs @@ -4,10 +4,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyOS_InterruptOccurred")] pub fn PyOS_InterruptOccurred() -> c_int; #[cfg(not(Py_3_10))] - #[deprecated( - since = "0.14.0", - note = "Not documented in Python API; see Python 3.10 release notes" - )] + #[deprecated(note = "Not documented in Python API; see Python 3.10 release notes")] pub fn PyOS_InitInterrupts(); #[cfg(any(not(Py_LIMITED_API), Py_3_7))] diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 1fbabf9e..c0faa0cf 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -1,28 +1,4 @@ //! Symbols used to denote deprecated usages of PyO3's proc macros. -#[deprecated( - since = "0.14.0", - note = "use `#[pyo3(name = \"...\")]` instead of `#[name = \"...\"]`" -)] -pub const NAME_ATTRIBUTE: () = (); - -#[deprecated( - since = "0.14.0", - note = "use `#[pyfn(m)] #[pyo3(name = \"...\")]` instead of `#[pyfn(m, \"...\")]`" -)] -pub const PYFN_NAME_ARGUMENT: () = (); - -#[deprecated( - since = "0.14.0", - note = "use `#[pymodule] #[pyo3(name = \"...\")]` instead of `#[pymodule(...)]`" -)] -pub const PYMODULE_NAME_ARGUMENT: () = (); - -#[deprecated( - since = "0.14.0", - note = "use `#[pyo3(text_signature = \"...\")]` instead of `#[text_signature = \"...\"]`" -)] -pub const TEXT_SIGNATURE_ATTRIBUTE: () = (); - #[deprecated(since = "0.15.0", note = "use `fn __call__` instead of `#[call]`")] pub const CALL_ATTRIBUTE: () = (); diff --git a/src/types/module.rs b/src/types/module.rs index e3cbcf16..57bf9035 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -8,9 +8,9 @@ use crate::exceptions; use crate::ffi; use crate::pyclass::PyClass; use crate::type_object::PyTypeObject; +use crate::types::PyCFunction; use crate::types::{PyAny, PyDict, PyList}; -use crate::types::{PyCFunction, PyTuple}; -use crate::{AsPyPointer, IntoPy, Py, PyObject, Python}; +use crate::{AsPyPointer, IntoPy, PyObject, Python}; use std::ffi::{CStr, CString}; use std::str; @@ -367,46 +367,6 @@ impl PyModule { let name = fun.getattr("__name__")?.extract()?; self.add(name, fun) } - - /// Calls a function in the module. - /// - /// This is equivalent to the Python expression `module.name(*args, **kwargs)`. - #[deprecated( - since = "0.14.0", - note = "use getattr(name)?.call(args, kwargs) instead" - )] - pub fn call( - &self, - name: &str, - args: impl IntoPy>, - kwargs: Option<&PyDict>, - ) -> PyResult<&PyAny> { - self.getattr(name)?.call(args, kwargs) - } - - /// Calls a function in the module with only positional arguments. - /// - /// This is equivalent to the Python expression `module.name(*args)`. - #[deprecated(since = "0.14.0", note = "use getattr(name)?.call1(args) instead")] - pub fn call1(&self, name: &str, args: impl IntoPy>) -> PyResult<&PyAny> { - self.getattr(name)?.call1(args) - } - - /// Calls a function in the module without arguments. - /// - /// This is equivalent to the Python expression `module.name()`. - #[deprecated(since = "0.14.0", note = "use getattr(name)?.call0() instead")] - pub fn call0(&self, name: &str) -> PyResult<&PyAny> { - self.getattr(name)?.call0() - } - - /// Gets a member from the module. - /// - /// This is equivalent to the Python expression `module.name`. - #[deprecated(since = "0.14.0", note = "use getattr(name) instead")] - pub fn get(&self, name: &str) -> PyResult<&PyAny> { - self.getattr(name) - } } #[cfg(test)] diff --git a/tests/test_arithmetics_protos.rs b/tests/test_arithmetics_protos.rs index 9036d737..0083a96a 100644 --- a/tests/test_arithmetics_protos.rs +++ b/tests/test_arithmetics_protos.rs @@ -38,10 +38,6 @@ impl PyNumberProtocol for UnaryArithmetic { fn __abs__(&self) -> Self { Self::new(self.inner.abs()) } - - fn __round__(&self, _ndigits: Option) -> Self { - Self::new(self.inner.round()) - } } #[test] @@ -53,8 +49,6 @@ fn unary_arithmetic() { py_run!(py, c, "assert repr(-c) == 'UA(-2.7)'"); py_run!(py, c, "assert repr(+c) == 'UA(2.7)'"); py_run!(py, c, "assert repr(abs(c)) == 'UA(2.7)'"); - py_run!(py, c, "assert repr(round(c)) == 'UA(3)'"); - py_run!(py, c, "assert repr(round(c, 1)) == 'UA(3)'"); } #[pyclass] diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 4daab267..99decdc4 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -22,6 +22,7 @@ fn _test_compile_errors() { t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); + t.compile_fail("tests/ui/invalid_pymodule_args.rs"); t.compile_fail("tests/ui/invalid_argument_attributes.rs"); t.compile_fail("tests/ui/missing_clone.rs"); t.compile_fail("tests/ui/reject_generics.rs"); diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 42710094..8bbfcad5 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -1,5 +1,3 @@ -#![allow(deprecated)] // for deprecated protocol methods - use std::collections::HashMap; use pyo3::exceptions::PyKeyError; @@ -59,16 +57,6 @@ impl PyMappingProtocol for Mapping { Ok(()) } } - - /// not an actual reversed implementation, just to demonstrate that the method is callable. - fn __reversed__(&self) -> PyObject { - let gil = Python::acquire_gil(); - self.index - .keys() - .cloned() - .collect::>() - .into_py(gil.python()) - } } /// Return a dict with `m = Mapping(['1', '2', '3'])`. @@ -117,12 +105,3 @@ fn test_delitem() { py_expect_exception!(py, *d, "del m[-1]", PyTypeError); py_expect_exception!(py, *d, "del m['4']", PyKeyError); } - -#[test] -fn test_reversed() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let d = map_dict(py); - py_assert!(py, *d, "set(reversed(m)) == {'1', '2', '3'}"); -} diff --git a/tests/test_module.rs b/tests/test_module.rs index 18efb5bf..3660bcae 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -23,10 +23,6 @@ impl ValueClass { #[pyclass(module = "module")] struct LocatedClass {} -fn sum_as_string(a: i64, b: i64) -> String { - format!("{}", a + b) -} - #[pyfunction] /// Doubles the given value fn double(x: usize) -> usize { @@ -36,12 +32,6 @@ fn double(x: usize) -> usize { /// This module is implemented in Rust. #[pymodule] fn module_with_functions(_py: Python, m: &PyModule) -> PyResult<()> { - #![allow(deprecated)] - #[pyfn(m, "sum_as_string")] - fn function_with_deprecated_name(_py: Python, a: i64, b: i64) -> String { - sum_as_string(a, b) - } - #[pyfn(m)] #[pyo3(name = "no_parameters")] fn function_with_name() -> usize { @@ -89,7 +79,6 @@ fn test_module_with_functions() { *d, "module_with_functions.__doc__ == 'This module is implemented in Rust.'" ); - py_assert!(py, *d, "module_with_functions.sum_as_string(1, 2) == '3'"); py_assert!(py, *d, "module_with_functions.no_parameters() == 42"); py_assert!(py, *d, "module_with_functions.foo == 'bar'"); py_assert!(py, *d, "module_with_functions.AnonClass != None"); @@ -438,20 +427,6 @@ fn test_module_functions_with_module() { ); } -#[test] -#[allow(deprecated)] -fn test_module_with_deprecated_name() { - #[pymodule(custom_name)] - fn my_module(_py: Python, _m: &PyModule) -> PyResult<()> { - Ok(()) - } - - Python::with_gil(|py| { - let m = pyo3::wrap_pymodule!(custom_name)(py); - py_assert!(py, m, "m.__name__ == 'custom_name'"); - }) -} - #[test] fn test_module_doc_hidden() { #[doc(hidden)] diff --git a/tests/test_pyproto.rs b/tests/test_pyproto.rs index da2dc0de..263b38e2 100644 --- a/tests/test_pyproto.rs +++ b/tests/test_pyproto.rs @@ -1,8 +1,6 @@ -#![allow(deprecated)] // for deprecated protocol methods - use pyo3::class::{ - PyAsyncProtocol, PyContextProtocol, PyDescrProtocol, PyIterProtocol, PyMappingProtocol, - PyObjectProtocol, PySequenceProtocol, + PyAsyncProtocol, PyDescrProtocol, PyIterProtocol, PyMappingProtocol, PyObjectProtocol, + PySequenceProtocol, }; use pyo3::exceptions::{PyIndexError, PyValueError}; use pyo3::prelude::*; @@ -91,14 +89,6 @@ impl PyObjectProtocol for StringMethods { fn __repr__(&self) -> &'static str { "repr" } - - fn __format__(&self, format_spec: String) -> String { - format!("format({})", format_spec) - } - - fn __bytes__(&self) -> &'static [u8] { - b"bytes" - } } #[test] @@ -109,12 +99,6 @@ fn string_methods() { let obj = Py::new(py, StringMethods {}).unwrap(); py_assert!(py, obj, "str(obj) == 'str'"); py_assert!(py, obj, "repr(obj) == 'repr'"); - py_assert!(py, obj, "'{0:x}'.format(obj) == 'format(x)'"); - py_assert!(py, obj, "bytes(obj) == b'bytes'"); - - // Test that `__bytes__` takes no arguments (should be METH_NOARGS) - py_assert!(py, obj, "obj.__bytes__() == b'bytes'"); - py_expect_exception!(py, obj, "obj.__bytes__('unexpected argument')", PyTypeError); } #[pyclass] @@ -298,25 +282,6 @@ fn setdelitem() { assert_eq!(c.val, None); } -#[pyclass] -struct Reversed {} - -#[pyproto] -impl PyMappingProtocol for Reversed { - fn __reversed__(&self) -> &'static str { - "I am reversed" - } -} - -#[test] -fn reversed() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let c = Py::new(py, Reversed {}).unwrap(); - py_run!(py, c, "assert reversed(c) == 'I am reversed'"); -} - #[pyclass] struct Contains {} @@ -338,57 +303,6 @@ fn contains() { py_expect_exception!(py, c, "assert 'wrong type' not in c", PyTypeError); } -#[pyclass] -struct ContextManager { - exit_called: bool, -} - -#[pyproto] -impl PyContextProtocol for ContextManager { - fn __enter__(&mut self) -> i32 { - 42 - } - - fn __exit__( - &mut self, - ty: Option<&PyType>, - _value: Option<&PyAny>, - _traceback: Option<&PyAny>, - ) -> bool { - let gil = Python::acquire_gil(); - self.exit_called = true; - ty == Some(gil.python().get_type::()) - } -} - -#[test] -fn context_manager() { - let gil = Python::acquire_gil(); - let py = gil.python(); - - let c = PyCell::new(py, ContextManager { exit_called: false }).unwrap(); - py_run!(py, c, "with c as x: assert x == 42"); - { - let mut c = c.borrow_mut(); - assert!(c.exit_called); - c.exit_called = false; - } - py_run!(py, c, "with c as x: raise ValueError"); - { - let mut c = c.borrow_mut(); - assert!(c.exit_called); - c.exit_called = false; - } - py_expect_exception!( - py, - c, - "with c as x: raise NotImplementedError", - PyNotImplementedError - ); - let c = c.borrow(); - assert!(c.exit_called); -} - #[test] fn test_basics() { let gil = Python::acquire_gil(); diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 7938fbe6..5ad70f9c 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -2,28 +2,6 @@ use pyo3::prelude::*; -#[pyclass] -#[text_signature = "()"] -struct TestClass { - num: u32, -} - -#[pymethods] -impl TestClass { - #[classattr] - #[name = "num"] - const DEPRECATED_NAME_CONSTANT: i32 = 0; - - #[name = "num"] - #[text_signature = "()"] - fn deprecated_name_pymethod(&self) { } - - #[staticmethod] - #[name = "custom_static"] - #[text_signature = "()"] - fn deprecated_name_staticmethod() {} -} - #[pyclass] struct DeprecatedCall; @@ -33,20 +11,6 @@ impl DeprecatedCall { fn deprecated_call(&self) {} } -#[pyfunction] -#[name = "foo"] -#[text_signature = "()"] -fn deprecated_name_pyfunction() { } - -#[pymodule(deprecated_module_name)] -fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { - #[pyfn(m, "some_name")] - #[text_signature = "()"] - fn deprecated_name_pyfn() { } - - Ok(()) -} - fn main() { } diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index d0131a9c..d71c1927 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,77 +1,11 @@ -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:14:5 +error: use of deprecated constant `pyo3::impl_::deprecations::CALL_ATTRIBUTE`: use `fn __call__` instead of `#[call]` + --> tests/ui/deprecations.rs:10:7 | -14 | #[name = "num"] - | ^ +10 | #[call] + | ^^^^ | note: the lint level is defined here --> tests/ui/deprecations.rs:1:9 | 1 | #![deny(deprecated)] | ^^^^^^^^^^ - -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:17:5 - | -17 | #[name = "num"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:18:5 - | -18 | #[text_signature = "()"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:22:5 - | -22 | #[name = "custom_static"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:23:5 - | -23 | #[text_signature = "()"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::CALL_ATTRIBUTE`: use `fn __call__` instead of `#[call]` - --> tests/ui/deprecations.rs:32:7 - | -32 | #[call] - | ^^^^ - -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:37:1 - | -37 | #[name = "foo"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:38:1 - | -38 | #[text_signature = "()"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::PYFN_NAME_ARGUMENT`: use `#[pyfn(m)] #[pyo3(name = "...")]` instead of `#[pyfn(m, "...")]` - --> tests/ui/deprecations.rs:43:15 - | -43 | #[pyfn(m, "some_name")] - | ^^^^^^^^^^^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:44:5 - | -44 | #[text_signature = "()"] - | ^ - -error: use of deprecated constant `pyo3::impl_::deprecations::PYMODULE_NAME_ARGUMENT`: use `#[pymodule] #[pyo3(name = "...")]` instead of `#[pymodule(...)]` - --> tests/ui/deprecations.rs:41:12 - | -41 | #[pymodule(deprecated_module_name)] - | ^^^^^^^^^^^^^^^^^^^^^^ - -error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:6:1 - | -6 | #[text_signature = "()"] - | ^ diff --git a/tests/ui/invalid_pymodule_args.rs b/tests/ui/invalid_pymodule_args.rs new file mode 100644 index 00000000..a11ec937 --- /dev/null +++ b/tests/ui/invalid_pymodule_args.rs @@ -0,0 +1,8 @@ +use pyo3::prelude::*; + +#[pymodule(some_arg)] +fn module(_py: Python, m: &PyModule) -> PyResult<()> { + Ok(()) +} + +fn main(){} diff --git a/tests/ui/invalid_pymodule_args.stderr b/tests/ui/invalid_pymodule_args.stderr new file mode 100644 index 00000000..933b6d60 --- /dev/null +++ b/tests/ui/invalid_pymodule_args.stderr @@ -0,0 +1,5 @@ +error: unexpected token + --> tests/ui/invalid_pymodule_args.rs:3:12 + | +3 | #[pymodule(some_arg)] + | ^^^^^^^^ From 70030f130dd9ad67e49af12a1ac9cb04c2ec0ca7 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Fri, 19 Nov 2021 10:45:27 +0000 Subject: [PATCH 26/30] python: drop support for 3.6 --- .github/workflows/ci.yml | 33 ++---- CHANGELOG.md | 1 + Cargo.toml | 1 - README.md | 2 +- guide/src/building_and_distribution.md | 10 +- guide/src/debugging.md | 2 +- guide/src/features.md | 2 +- guide/src/migration.md | 6 ++ pyo3-build-config/Cargo.toml | 1 - pyo3-build-config/build.rs | 2 +- pyo3-build-config/src/impl_.rs | 138 ++++++++----------------- pyo3-build-config/src/lib.rs | 2 +- src/class/methods.rs | 8 +- src/err/mod.rs | 7 +- src/exceptions.rs | 12 +-- src/ffi/abstract_.rs | 4 +- src/ffi/ceval.rs | 1 - src/ffi/cpython/import.rs | 16 +-- src/ffi/cpython/unicodeobject.rs | 5 - src/ffi/datetime.rs | 12 +-- src/ffi/intrcheck.rs | 5 +- src/ffi/methodobject.rs | 6 +- src/ffi/pystate.rs | 2 +- src/ffi/sliceobject.rs | 12 --- src/ffi/unicodeobject.rs | 5 +- src/gil.rs | 50 +-------- src/lib.rs | 4 +- src/python.rs | 10 +- tests/test_py36_init.rs | 9 -- tox.ini | 8 +- 30 files changed, 104 insertions(+), 272 deletions(-) delete mode 100644 tests/test_py36_init.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6dba645..60668064 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: - name: Run cargo checks run: | set -x - VERSIONS=("3.6" "3.7" "3.8" "3.9" "3.10") + VERSIONS=("3.7" "3.8" "3.9" "3.10") for VERSION in ${VERSIONS[@]}; do echo "version=$VERSION" > config.txt echo "suppress_build_script_link_lines=true" >> config.txt @@ -76,7 +76,7 @@ jobs: fail-fast: false # If one platform fails, allow the rest to keep testing. matrix: rust: [stable] - python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy-3.6, pypy-3.7, pypy-3.8] + python-version: [3.7, 3.8, 3.9, "3.10", pypy-3.7, pypy-3.8] platform: [ { @@ -101,27 +101,12 @@ jobs: }, ] exclude: - # PyPy 3.6 is EOL and not working on macos-latest (now macos-11) - - python-version: pypy-3.6 - platform: { os: "macos-latest", python-architecture: "x64" } - # There is no 64-bit pypy on windows for pypy-3.6 - - python-version: pypy-3.6 - platform: { os: "windows-latest", python-architecture: "x64" } - # PyPy 3.7 on Windows doesn't release 32-bit builds any more + # PyPy doesn't release 32-bit Windows builds any more - python-version: pypy-3.7 platform: { os: "windows-latest", python-architecture: "x86" } - python-version: pypy-3.8 platform: { os: "windows-latest", python-architecture: "x86" } include: - # PyPy3.6 still runs on macos-10.15 - - rust: stable - python-version: pypy-3.6 - platform: - { - os: "macos-10.15", - python-architecture: "x64", - rust-target: "x86_64-apple-darwin", - } # Test minimal supported Rust version - rust: 1.48.0 python-version: "3.10" @@ -212,8 +197,8 @@ jobs: run: cargo build --lib --tests --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}" - if: ${{ startsWith(matrix.python-version, 'pypy') }} - name: Build PyPy (abi3-py36) - run: cargo build --lib --tests --no-default-features --features "abi3-py36 ${{ steps.settings.outputs.all_additive_features }}" + name: Build PyPy (abi3-py37) + run: cargo build --lib --tests --no-default-features --features "abi3-py37 ${{ steps.settings.outputs.all_additive_features }}" # Run tests (except on PyPy, because no embedding API). - if: ${{ !startsWith(matrix.python-version, 'pypy') }} @@ -225,10 +210,10 @@ jobs: name: Test (abi3) run: cargo test --no-default-features --features "abi3 ${{ steps.settings.outputs.all_additive_features }}" - # Run tests again, for abi3-py36 (the minimal Python version) - - if: ${{ (!startsWith(matrix.python-version, 'pypy')) && (matrix.python-version != '3.6') }} - name: Test (abi3-py36) - run: cargo test --no-default-features --features "abi3-py36 ${{ steps.settings.outputs.all_additive_features }}" + # Run tests again, for abi3-py37 (the minimal Python version) + - if: ${{ (!startsWith(matrix.python-version, 'pypy')) && (matrix.python-version != '3.7') }} + name: Test (abi3-py37) + run: cargo test --no-default-features --features "abi3-py37 ${{ steps.settings.outputs.all_additive_features }}" - name: Test proc-macro code run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index df479ff1..bb88dd88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update MSRV to Rust 1.48. [#2004](https://github.com/PyO3/pyo3/pull/2004) - Update `indoc` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) - Update `paste` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) +- Drop support for Python 3.6, remove `abi3-py36` feature. [#2006](https://github.com/PyO3/pyo3/pull/2006) - `pyo3-build-config` no longer enables the `resolve-config` feature by default. [#2008](https://github.com/PyO3/pyo3/pull/2008) ### Added diff --git a/Cargo.toml b/Cargo.toml index cd8fb632..4b21a6c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,7 +71,6 @@ extension-module = [] abi3 = ["pyo3-build-config/abi3"] # With abi3, we can manually set the minimum Python version. -abi3-py36 = ["abi3-py37", "pyo3-build-config/abi3-py36"] abi3-py37 = ["abi3-py38", "pyo3-build-config/abi3-py37"] abi3-py38 = ["abi3-py39", "pyo3-build-config/abi3-py38"] abi3-py39 = ["abi3-py310", "pyo3-build-config/abi3-py39"] diff --git a/README.md b/README.md index 34d8630e..c07787b2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## Usage PyO3 supports the following software versions: - - Python 3.6 and up (CPython and PyPy) + - Python 3.7 and up (CPython and PyPy) - Rust 1.48 and up You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn. diff --git a/guide/src/building_and_distribution.md b/guide/src/building_and_distribution.md index 0d63dd6c..0644859d 100644 --- a/guide/src/building_and_distribution.md +++ b/guide/src/building_and_distribution.md @@ -13,7 +13,7 @@ PyO3 uses a build script (backed by the [`pyo3-build-config`] crate) to determin - The `python` executable (if it's a Python 3 interpreter). - The `python3` executable. -You can override the Python interpreter by setting the `PYO3_PYTHON` environment variable, e.g. `PYO3_PYTHON=python3.6`, `PYO3_PYTHON=/usr/bin/python3.9`, or even a PyPy interpreter `PYO3_PYTHON=pypy3`. +You can override the Python interpreter by setting the `PYO3_PYTHON` environment variable, e.g. `PYO3_PYTHON=python3.7`, `PYO3_PYTHON=/usr/bin/python3.9`, or even a PyPy interpreter `PYO3_PYTHON=pypy3`. Once the Python interpreter is located, `pyo3-build-config` executes it to query the information in the `sysconfig` module which is needed to configure the rest of the compilation. @@ -145,16 +145,16 @@ See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https:// #### Minimum Python version for `abi3` -Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py36`, `abi3-py37`, `abi-py38` etc. to set the minimum required Python version for your `abi3` wheel. -For example, if you set the `abi3-py36` feature, your extension wheel can be used on all Python 3 versions from Python 3.6 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp36-abi3-manylinux2020_x86_64.whl`. +Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py37`, `abi3-py38`, `abi3-py39` etc. to set the minimum required Python version for your `abi3` wheel. +For example, if you set the `abi3-py37` feature, your extension wheel can be used on all Python 3 versions from Python 3.7 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp37-abi3-manylinux2020_x86_64.whl`. As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building_and_distribution/multiple_python_versions.html#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. -PyO3 is only able to link your extension module to api3 version up to and including your host Python version. E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.6, the build will fail. +PyO3 is only able to link your extension module to api3 version up to and including your host Python version. E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.7, the build will fail. As an advanced feature, you can build PyO3 wheel without calling Python interpreter with the environment variable `PYO3_NO_PYTHON` set. On unix systems this works unconditionally; on Windows you must also set the `RUSTFLAGS` evironment variable to contain `-L native=/path/to/python/libs` so that the linker can find `python3.lib`. -> Note: If you set more that one of these api version feature flags the highest version always wins. For example, with both `abi3-py36` and `abi3-py38` set, PyO3 would build a wheel which supports Python 3.8 and up. +> Note: If you set more that one of these api version feature flags the lowest version always wins. For example, with both `abi3-py37` and `abi3-py38` set, PyO3 would build a wheel which supports Python 3.7 and up. #### Missing features diff --git a/guide/src/debugging.md b/guide/src/debugging.md index b7df5b40..3a624d2d 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -24,7 +24,7 @@ Valgrind is a tool to detect memory management bugs such as memory leaks. You first need to install a debug build of Python, otherwise Valgrind won't produce usable results. In Ubuntu there's e.g. a `python3-dbg` package. -Activate an environment with the debug interpreter and recompile. If you're on Linux, use `ldd` with the name of your binary and check that you're linking e.g. `libpython3.6dm.so.1.0` instead of `libpython3.6m.so.1.0`. +Activate an environment with the debug interpreter and recompile. If you're on Linux, use `ldd` with the name of your binary and check that you're linking e.g. `libpython3.7d.so.1.0` instead of `libpython3.7.so.1.0`. [Download the suppressions file for cpython](https://raw.githubusercontent.com/python/cpython/master/Misc/valgrind-python.supp). diff --git a/guide/src/features.md b/guide/src/features.md index 73ec50b9..5af4f3ec 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -24,7 +24,7 @@ See the [building and distribution](building_and_distribution.md#py_limited_apia ### The `abi3-pyXY` features -(`abi3-py36`, `abi3-py37`, `abi3-py38`, `abi3-py39`, and `abi3-py310`) +(`abi3-py37`, `abi3-py38`, `abi3-py39`, and `abi3-py310`) These features are extensions of the `abi3` feature to specify the exact minimum Python version which the multiple-version-wheel will support. diff --git a/guide/src/migration.md b/guide/src/migration.md index 56a714ac..5431d009 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -3,6 +3,12 @@ This guide can help you upgrade code through breaking changes from one PyO3 version to the next. For a detailed list of all changes, see the [CHANGELOG](changelog.md). +## from 0.15.* to 0.16 + +### Drop support for older technogies + +PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. This enables ore use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project. + ## from 0.14.* to 0.15 ### Changes in sequence indexing diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 7e248083..9dfdcd82 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -21,7 +21,6 @@ default = [] resolve-config = [] abi3 = [] -abi3-py36 = ["abi3-py37"] abi3-py37 = ["abi3-py38"] abi3-py38 = ["abi3-py39"] abi3-py39 = ["abi3-py310"] diff --git a/pyo3-build-config/build.rs b/pyo3-build-config/build.rs index 7e2cae9b..45a5bb71 100644 --- a/pyo3-build-config/build.rs +++ b/pyo3-build-config/build.rs @@ -63,7 +63,7 @@ pub fn abi3_config() -> Option { abi3: true, lib_name: None, lib_dir: None, - build_flags: BuildFlags::abi3(), + build_flags: BuildFlags::default(), pointer_width: None, executable: None, shared: true, diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 6a73b3b9..7fc37126 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -18,7 +18,7 @@ use crate::{ }; /// Minimum Python version PyO3 supports. -const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 6 }; +const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; /// Maximum Python version that can be used as minimum required Python version with abi3. const ABI3_MAX_MINOR: u8 = 9; @@ -131,7 +131,10 @@ impl InterpreterConfig { pub fn emit_pyo3_cfgs(&self) { // This should have been checked during pyo3-build-config build time. assert!(self.version >= MINIMUM_SUPPORTED_VERSION); - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version.minor { + + // pyo3-build-config was released when Python 3.6 was supported, so minimum flag to emit is + // Py_3_6 (to avoid silently breaking users who depend on this cfg). + for i in 6..=self.version.minor { println!("cargo:rustc-cfg=Py_3_{}", i); } @@ -258,7 +261,7 @@ print("mingw", get_platform().startswith("mingw")) lib_dir, executable: map.get("executable").cloned(), pointer_width: Some(calcsize_pointer * 8), - build_flags: BuildFlags::from_interpreter(interpreter)?.fixup(version, implementation), + build_flags: BuildFlags::from_interpreter(interpreter)?.fixup(version), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], }) @@ -280,7 +283,7 @@ print("mingw", get_platform().startswith("mingw")) macro_rules! parse_value { ($variable:ident, $value:ident) => { - $variable = Some($value.parse().context(format!( + $variable = Some($value.trim().parse().context(format!( concat!( "failed to parse ", stringify!($variable), @@ -347,14 +350,7 @@ print("mingw", get_platform().startswith("mingw")) lib_dir, executable, pointer_width, - build_flags: build_flags.unwrap_or_else(|| { - if abi3 { - BuildFlags::abi3() - } else { - BuildFlags::default() - } - .fixup(version, implementation) - }), + build_flags: build_flags.unwrap_or_default(), suppress_build_script_link_lines: suppress_build_script_link_lines.unwrap_or(false), extra_build_script_lines, }) @@ -557,7 +553,6 @@ fn cross_compiling() -> Result> { #[allow(non_camel_case_types)] #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum BuildFlag { - WITH_THREAD, Py_DEBUG, Py_REF_DEBUG, Py_TRACE_REFS, @@ -578,7 +573,6 @@ impl FromStr for BuildFlag { type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { match s { - "WITH_THREAD" => Ok(BuildFlag::WITH_THREAD), "Py_DEBUG" => Ok(BuildFlag::Py_DEBUG), "Py_REF_DEBUG" => Ok(BuildFlag::Py_REF_DEBUG), "Py_TRACE_REFS" => Ok(BuildFlag::Py_TRACE_REFS), @@ -605,9 +599,7 @@ impl FromStr for BuildFlag { pub struct BuildFlags(pub HashSet); impl BuildFlags { - const ALL: [BuildFlag; 5] = [ - // TODO: Remove WITH_THREAD once Python 3.6 support dropped (as it's always on). - BuildFlag::WITH_THREAD, + const ALL: [BuildFlag; 4] = [ BuildFlag::Py_DEBUG, BuildFlag::Py_REF_DEBUG, BuildFlag::Py_TRACE_REFS, @@ -636,9 +628,10 @@ impl BuildFlags { /// the interpreter and printing variables of interest from /// sysconfig.get_config_vars. fn from_interpreter(interpreter: impl AsRef) -> Result { - // If we're on a Windows host, then Python won't have any useful config vars + // sysconfig is missing all the flags on windows, so we can't actually + // query the interpreter directly for its build flags. if cfg!(windows) { - return Ok(Self::windows_hardcoded()); + return Ok(Self::new()); } let mut script = String::from("import sysconfig\n"); @@ -665,21 +658,7 @@ impl BuildFlags { Ok(Self(flags)) } - fn windows_hardcoded() -> Self { - // sysconfig is missing all the flags on windows, so we can't actually - // query the interpreter directly for its build flags. - let mut flags = HashSet::new(); - flags.insert(BuildFlag::WITH_THREAD); - Self(flags) - } - - pub fn abi3() -> Self { - let mut flags = HashSet::new(); - flags.insert(BuildFlag::WITH_THREAD); - Self(flags) - } - - fn fixup(mut self, version: PythonVersion, implementation: PythonImplementation) -> Self { + fn fixup(mut self, version: PythonVersion) -> Self { if self.0.contains(&BuildFlag::Py_DEBUG) { self.0.insert(BuildFlag::Py_REF_DEBUG); if version <= PythonVersion::PY37 { @@ -688,11 +667,6 @@ impl BuildFlags { } } - // WITH_THREAD is always on for Python 3.7, and for PyPy. - if implementation == PythonImplementation::PyPy || version >= PythonVersion::PY37 { - self.0.insert(BuildFlag::WITH_THREAD); - } - self } } @@ -717,7 +691,7 @@ impl FromStr for BuildFlags { fn from_str(value: &str) -> Result { let mut flags = HashSet::new(); - for flag in value.split(',') { + for flag in value.split_terminator(',') { flags.insert(flag.parse().unwrap()); } Ok(BuildFlags(flags)) @@ -821,7 +795,7 @@ for key in KEYS: )), executable: None, pointer_width, - build_flags: BuildFlags::from_config_map(&sysconfigdata).fixup(version, implementation), + build_flags: BuildFlags::from_config_map(&sysconfigdata).fixup(version), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], }) @@ -913,8 +887,6 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec vec![f.path()], // Python 3.7+ sysconfigdata with platform specifics Ok(f) if starts_with(f, "_sysconfigdata_") && ends_with(f, "py") => vec![f.path()], Ok(f) if f.metadata().map_or(false, |metadata| metadata.is_dir()) => { @@ -969,7 +941,6 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec meth.0, PyMethodType::PyCFunctionWithKeywords(meth) => unsafe { std::mem::transmute(meth.0) }, - #[cfg(all(Py_3_7, not(Py_LIMITED_API)))] + #[cfg(not(Py_LIMITED_API))] PyMethodType::PyCFunctionFastWithKeywords(meth) => unsafe { std::mem::transmute(meth.0) }, diff --git a/src/err/mod.rs b/src/err/mod.rs index db23c678..a5c89a47 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -683,12 +683,7 @@ mod tests { let mut fields = debug_str["PyErr { ".len()..debug_str.len() - 2].split(", "); assert_eq!(fields.next().unwrap(), "type: "); - if py.version_info() >= (3, 7) { - assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); - } else { - // Python 3.6 and below formats the repr differently - assert_eq!(fields.next().unwrap(), ("value: Exception('banana',)")); - } + assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); let traceback = fields.next().unwrap(); assert!(traceback.starts_with("traceback: Some(= (3, 7) { - assert_eq!(format!("{:?}", exc), "Exception('banana')"); - } else { - assert_eq!(format!("{:?}", exc), "Exception('banana',)"); - } + assert_eq!(format!("{:?}", exc), "Exception('banana')"); let source = exc.source().expect("cause should exist"); - if py.version_info() >= (3, 7) { - assert_eq!(format!("{:?}", source), "TypeError('peach')"); - } else { - assert_eq!(format!("{:?}", source), "TypeError('peach',)"); - } + assert_eq!(format!("{:?}", source), "TypeError('peach')"); let source_source = source.source(); assert!(source_source.is_none(), "source_source should be None"); diff --git a/src/ffi/abstract_.rs b/src/ffi/abstract_.rs index 3fa035b3..07e02da4 100644 --- a/src/ffi/abstract_.rs +++ b/src/ffi/abstract_.rs @@ -91,7 +91,7 @@ extern "C" { pub fn PyObject_GetIter(arg1: *mut PyObject) -> *mut PyObject; } -// Defined as this macro in Python 3.6, 3.7 limited API, but relies on +// Defined as this macro in Python limited API, but relies on // non-limited PyTypeObject. Don't expose this since it cannot be used. #[cfg(not(any(Py_LIMITED_API, PyPy)))] #[inline] @@ -156,7 +156,7 @@ extern "C" { pub fn PyNumber_Or(o1: *mut PyObject, o2: *mut PyObject) -> *mut PyObject; } -// Defined as this macro in Python 3.6, 3.7 limited API, but relies on +// Defined as this macro in Python limited API, but relies on // non-limited PyTypeObject. Don't expose this since it cannot be used. #[cfg(not(any(Py_LIMITED_API, PyPy)))] #[inline] diff --git a/src/ffi/ceval.rs b/src/ffi/ceval.rs index 4f1dab8b..4031daa7 100644 --- a/src/ffi/ceval.rs +++ b/src/ffi/ceval.rs @@ -67,7 +67,6 @@ extern "C" { pub fn PyEval_RestoreThread(arg1: *mut PyThreadState); } -#[cfg(py_sys_config = "WITH_THREAD")] extern "C" { #[cfg_attr(PyPy, link_name = "PyPyEval_ThreadsInitialized")] pub fn PyEval_ThreadsInitialized() -> c_int; diff --git a/src/ffi/cpython/import.rs b/src/ffi/cpython/import.rs index 00fe424b..f342c0e3 100644 --- a/src/ffi/cpython/import.rs +++ b/src/ffi/cpython/import.rs @@ -6,33 +6,19 @@ use std::os::raw::{c_char, c_int, c_uchar}; extern "C" { pub fn _PyImport_IsInitialized(state: *mut PyInterpreterState) -> c_int; // skipped _PyImport_GetModuleId - #[cfg(Py_3_7)] pub fn _PyImport_SetModule(name: *mut PyObject, module: *mut PyObject) -> c_int; - #[cfg(Py_3_7)] pub fn _PyImport_SetModuleString(name: *const c_char, module: *mut PyObject) -> c_int; pub fn _PyImport_AcquireLock(); pub fn _PyImport_ReleaseLock() -> c_int; - #[cfg(not(Py_3_7))] - pub fn _PyImport_FindBuiltin(name: *const c_char) -> *mut PyObject; - #[cfg(all(Py_3_7, not(Py_3_9)))] + #[cfg(not(Py_3_9))] pub fn _PyImport_FindBuiltin(name: *const c_char, modules: *mut PyObject) -> *mut PyObject; #[cfg(not(Py_3_11))] pub fn _PyImport_FindExtensionObject(a: *mut PyObject, b: *mut PyObject) -> *mut PyObject; - #[cfg(not(Py_3_7))] - pub fn _PyImport_FixupBuiltin(module: *mut PyObject, name: *const c_char) -> c_int; - #[cfg(Py_3_7)] pub fn _PyImport_FixupBuiltin( module: *mut PyObject, name: *const c_char, modules: *mut PyObject, ) -> c_int; - #[cfg(not(Py_3_7))] - pub fn _PyImport_FixupExtensionObject( - a: *mut PyObject, - b: *mut PyObject, - c: *mut PyObject, - ) -> c_int; - #[cfg(Py_3_7)] pub fn _PyImport_FixupExtensionObject( a: *mut PyObject, b: *mut PyObject, diff --git a/src/ffi/cpython/unicodeobject.rs b/src/ffi/cpython/unicodeobject.rs index f357d91d..00b832fc 100644 --- a/src/ffi/cpython/unicodeobject.rs +++ b/src/ffi/cpython/unicodeobject.rs @@ -323,14 +323,9 @@ extern "C" { extern "C" { // skipped _PyUnicode_AsStringAndSize - #[cfg(Py_3_7)] #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8")] pub fn PyUnicode_AsUTF8(unicode: *mut PyObject) -> *const c_char; - #[cfg(not(Py_3_7))] - #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8")] - pub fn PyUnicode_AsUTF8(unicode: *mut PyObject) -> *mut c_char; - // skipped _PyUnicode_AsString pub fn PyUnicode_Encode( diff --git a/src/ffi/datetime.rs b/src/ffi/datetime.rs index 8510b96e..a7b97ca1 100644 --- a/src/ffi/datetime.rs +++ b/src/ffi/datetime.rs @@ -361,7 +361,7 @@ pub struct PyDateTime_CAPI { pub TimeType: *mut PyTypeObject, pub DeltaType: *mut PyTypeObject, pub TZInfoType: *mut PyTypeObject, - #[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] + #[cfg(not(all(PyPy, not(Py_3_8))))] pub TimeZone_UTC: *mut PyObject, pub Date_FromDate: unsafe extern "C" fn( year: c_int, @@ -395,7 +395,7 @@ pub struct PyDateTime_CAPI { normalize: c_int, cls: *mut PyTypeObject, ) -> *mut PyObject, - #[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] + #[cfg(not(all(PyPy, not(Py_3_8))))] pub TimeZone_FromTimeZone: unsafe extern "C" fn(offset: *mut PyObject, name: *mut PyObject) -> *mut PyObject, @@ -451,7 +451,7 @@ pub static PyDateTimeAPI: _PyDateTimeAPI_impl = _PyDateTimeAPI_impl { /// /// The type obtained by dereferencing this object is `&'static PyObject`. This may change in the /// future to be a more specific type representing that this is a `datetime.timezone` object. -#[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] +#[cfg(not(all(PyPy, not(Py_3_8))))] pub static PyDateTime_TimeZone_UTC: _PyDateTime_TimeZone_UTC_impl = _PyDateTime_TimeZone_UTC_impl { inner: &PyDateTimeAPI, }; @@ -609,12 +609,12 @@ impl Deref for _PyDateTimeAPI_impl { } #[doc(hidden)] -#[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] +#[cfg(not(all(PyPy, not(Py_3_8))))] pub struct _PyDateTime_TimeZone_UTC_impl { inner: &'static _PyDateTimeAPI_impl, } -#[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] +#[cfg(not(all(PyPy, not(Py_3_8))))] impl Deref for _PyDateTime_TimeZone_UTC_impl { type Target = crate::PyObject; @@ -661,7 +661,7 @@ mod tests { } #[test] - #[cfg(all(Py_3_7, any(not(PyPy), Py_3_8)))] + #[cfg(not(all(PyPy, not(Py_3_8))))] fn test_utc_timezone() { Python::with_gil(|py| { let utc_timezone = PyDateTime_TimeZone_UTC.as_ref(py); diff --git a/src/ffi/intrcheck.rs b/src/ffi/intrcheck.rs index ffd6c79c..3b2738bb 100644 --- a/src/ffi/intrcheck.rs +++ b/src/ffi/intrcheck.rs @@ -10,13 +10,10 @@ extern "C" { )] pub fn PyOS_InitInterrupts(); - #[cfg(any(not(Py_LIMITED_API), Py_3_7))] pub fn PyOS_BeforeFork(); - #[cfg(any(not(Py_LIMITED_API), Py_3_7))] pub fn PyOS_AfterFork_Parent(); - #[cfg(any(not(Py_LIMITED_API), Py_3_7))] pub fn PyOS_AfterFork_Child(); - #[cfg_attr(Py_3_7, deprecated(note = "use PyOS_AfterFork_Child instead"))] + #[deprecated(note = "use PyOS_AfterFork_Child instead")] #[cfg_attr(PyPy, link_name = "PyPyOS_AfterFork")] pub fn PyOS_AfterFork(); diff --git a/src/ffi/methodobject.rs b/src/ffi/methodobject.rs index b1b435d7..24dac732 100644 --- a/src/ffi/methodobject.rs +++ b/src/ffi/methodobject.rs @@ -31,7 +31,7 @@ pub unsafe fn PyCFunction_Check(op: *mut PyObject) -> c_int { pub type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; -#[cfg(all(Py_3_7, not(Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] pub type _PyCFunctionFast = unsafe extern "C" fn( slf: *mut PyObject, args: *mut *mut PyObject, @@ -45,7 +45,7 @@ pub type PyCFunctionWithKeywords = unsafe extern "C" fn( kwds: *mut PyObject, ) -> *mut PyObject; -#[cfg(all(Py_3_7, not(Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] pub type _PyCFunctionFastWithKeywords = unsafe extern "C" fn( slf: *mut PyObject, args: *const *mut PyObject, @@ -119,7 +119,7 @@ pub const METH_COEXIST: c_int = 0x0040; /* METH_FASTCALL indicates the PEP 590 Vectorcall calling format. It may be specified alone or with METH_KEYWORDS. */ -#[cfg(all(Py_3_7, not(Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] pub const METH_FASTCALL: c_int = 0x0080; // skipped METH_STACKLESS diff --git a/src/ffi/pystate.rs b/src/ffi/pystate.rs index a00af79d..4eb2049f 100644 --- a/src/ffi/pystate.rs +++ b/src/ffi/pystate.rs @@ -25,7 +25,7 @@ extern "C" { #[cfg(all(Py_3_8, not(PyPy)))] pub fn PyInterpreterState_GetDict(arg1: *mut PyInterpreterState) -> *mut PyObject; - #[cfg(all(Py_3_7, not(PyPy)))] + #[cfg(not(PyPy))] pub fn PyInterpreterState_GetID(arg1: *mut PyInterpreterState) -> i64; #[cfg(not(PyPy))] diff --git a/src/ffi/sliceobject.rs b/src/ffi/sliceobject.rs index fa73102d..31488bb4 100644 --- a/src/ffi/sliceobject.rs +++ b/src/ffi/sliceobject.rs @@ -52,20 +52,8 @@ extern "C" { stop: *mut Py_ssize_t, step: *mut Py_ssize_t, ) -> c_int; - - #[cfg(not(Py_3_7))] - #[cfg_attr(PyPy, link_name = "PyPySlice_GetIndicesEx")] - pub fn PySlice_GetIndicesEx( - r: *mut PyObject, - length: Py_ssize_t, - start: *mut Py_ssize_t, - stop: *mut Py_ssize_t, - step: *mut Py_ssize_t, - slicelength: *mut Py_ssize_t, - ) -> c_int; } -#[cfg(Py_3_7)] #[inline] pub unsafe fn PySlice_GetIndicesEx( slice: *mut PyObject, diff --git a/src/ffi/unicodeobject.rs b/src/ffi/unicodeobject.rs index 61a60f8c..2b02b37e 100644 --- a/src/ffi/unicodeobject.rs +++ b/src/ffi/unicodeobject.rs @@ -165,12 +165,9 @@ extern "C" { ) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8String")] pub fn PyUnicode_AsUTF8String(unicode: *mut PyObject) -> *mut PyObject; - #[cfg(any(Py_3_10, all(Py_3_7, not(Py_LIMITED_API))))] + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8AndSize")] pub fn PyUnicode_AsUTF8AndSize(unicode: *mut PyObject, size: *mut Py_ssize_t) -> *const c_char; - #[cfg(not(any(Py_3_7, Py_LIMITED_API)))] - #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUTF8AndSize")] - pub fn PyUnicode_AsUTF8AndSize(unicode: *mut PyObject, size: *mut Py_ssize_t) -> *mut c_char; #[cfg_attr(PyPy, link_name = "PyPyUnicode_DecodeUTF32")] pub fn PyUnicode_DecodeUTF32( string: *const c_char, diff --git a/src/gil.rs b/src/gil.rs index 0832ec6d..38e1db34 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -45,20 +45,12 @@ pub(crate) fn gil_is_acquired() -> bool { /// signal handling depends on the notion of a 'main thread', which must be the thread that /// initializes the Python interpreter. /// -/// If both the Python interpreter and Python threading are already initialized, this function has -/// no effect. +/// If the Python interpreter is already initialized, this function has no effect. /// /// This function is unavailable under PyPy because PyPy cannot be embedded in Rust (or any other /// software). Support for this is tracked on the /// [PyPy issue tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3286). /// -/// Python 3.6 only: If the Python interpreter is initialized but Python threading is not, this -/// function will initialize Python threading. -/// -/// # Panics -/// - Python 3.6 only: If this function needs to initialize Python threading but the calling thread -/// is not the thread which initialized Python, this function will panic. -/// /// # Examples /// ```rust /// use pyo3::prelude::*; @@ -75,29 +67,6 @@ pub fn prepare_freethreaded_python() { // concurrent initialization of the Python runtime by other users of the Python C API. START.call_once_force(|_| unsafe { // Use call_once_force because if initialization panics, it's okay to try again. - - // TODO(#1782) - Python 3.6 legacy code - #[cfg(not(Py_3_7))] - if ffi::Py_IsInitialized() != 0 { - if ffi::PyEval_ThreadsInitialized() == 0 { - // We can only safely initialize threads if this thread holds the GIL. - assert!( - !ffi::PyGILState_GetThisThreadState().is_null(), - "Python threading is not initialized and cannot be initialized by this \ - thread, because it is not the thread which initialized Python." - ); - ffi::PyEval_InitThreads(); - } - } else { - ffi::Py_InitializeEx(0); - ffi::PyEval_InitThreads(); - - // Release the GIL. - ffi::PyEval_SaveThread(); - } - - // In Python 3.7 and up PyEval_InitThreads is irrelevant. - #[cfg(Py_3_7)] if ffi::Py_IsInitialized() == 0 { ffi::Py_InitializeEx(0); @@ -149,14 +118,7 @@ where ffi::Py_InitializeEx(0); - // Changed in version 3.7: This function is now called by Py_Initialize(), so you don’t have to - // call it yourself anymore. - #[cfg(not(Py_3_7))] - if ffi::PyEval_ThreadsInitialized() == 0 { - ffi::PyEval_InitThreads(); - } - - // Safe: the GIL is already held because of the Py_IntializeEx call. + // Safety: the GIL is already held because of the Py_IntializeEx call. let pool = GILPool::new(); // Import the threading module - this ensures that it will associate this thread as the "main" @@ -231,14 +193,6 @@ impl GILGuard { Consider calling `pyo3::prepare_freethreaded_python()` before attempting \ to use Python APIs." ); - assert_ne!( - ffi::PyEval_ThreadsInitialized(), - 0, - "Python threading is not initalized and the `auto-initialize` feature is \ - not enabled.\n\n\ - Consider calling `pyo3::prepare_freethreaded_python()` before attempting \ - to use Python APIs." - ); }); } } diff --git a/src/lib.rs b/src/lib.rs index 08e476b7..7d7d44b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,7 @@ //! PyO3 uses `rustc`'s `--cfg` flags to enable or disable code used for different Python versions. //! If you want to do this for your own crate, you can do so with the [`pyo3-build-config`] crate. //! -//! - `Py_3_6`, `Py_3_7`, `Py_3_8`, `Py_3_9`, `Py_3_10`: Marks code that is only enabled when +//! - `Py_3_7`, `Py_3_8`, `Py_3_9`, `Py_3_10`: Marks code that is only enabled when //! compiling for a given minimum Python version. //! - `Py_LIMITED_API`: Marks code enabled when the `abi3` feature flag is enabled. //! - `PyPy` - Marks code enabled when compiling for PyPy. @@ -110,7 +110,7 @@ //! # Minimum supported Rust and Python versions //! //! PyO3 supports the following software versions: -//! - Python 3.6 and up (CPython and PyPy) +//! - Python 3.7 and up (CPython and PyPy) //! - Rust 1.48 and up //! //! # Example: Building a native Python module diff --git a/src/python.rs b/src/python.rs index 911bf6bd..52de8c25 100644 --- a/src/python.rs +++ b/src/python.rs @@ -550,9 +550,9 @@ impl<'py> Python<'py> { /// ```rust /// # use pyo3::Python; /// Python::with_gil(|py| { - /// // PyO3 supports Python 3.6 and up. - /// assert!(py.version_info() >= (3, 6)); - /// assert!(py.version_info() >= (3, 6, 0)); + /// // PyO3 supports Python 3.7 and up. + /// assert!(py.version_info() >= (3, 7)); + /// assert!(py.version_info() >= (3, 7, 0)); /// }); /// ``` pub fn version_info(self) -> PythonVersionInfo<'py> { @@ -891,10 +891,6 @@ mod tests { fn test_python_version_info() { Python::with_gil(|py| { let version = py.version_info(); - #[cfg(Py_3_6)] - assert!(version >= (3, 6)); - #[cfg(Py_3_6)] - assert!(version >= (3, 6, 0)); #[cfg(Py_3_7)] assert!(version >= (3, 7)); #[cfg(Py_3_7)] diff --git a/tests/test_py36_init.rs b/tests/test_py36_init.rs deleted file mode 100644 index d4f1c8e9..00000000 --- a/tests/test_py36_init.rs +++ /dev/null @@ -1,9 +0,0 @@ -// This test checks Python initialization on python 3.6, so needs to be standalone in its own process. - -#[cfg(not(PyPy))] -#[test] -fn test_py36_init_threads() { - unsafe { pyo3::ffi::Py_InitializeEx(0) }; - pyo3::prepare_freethreaded_python(); - assert_eq!(unsafe { pyo3::ffi::PyEval_ThreadsInitialized() }, 1); -} diff --git a/tox.ini b/tox.ini index 6bbebd29..a1cfba86 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] -envlist = py35, - py36, - py37, +envlist = py37, py38, -minversion = 3.4.0 + py39, + py310, +minversion = 3.7.0 skip_missing_interpreters = true [testenv] From a83c31a3af001690b1cf2dc9bd7acf27cc890102 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Thu, 11 Nov 2021 20:06:42 +0100 Subject: [PATCH 27/30] add PyType::is_subclass_of and PyAny::is_instance_of which get the type to check against as an arguments, as opposed to a compile-time generic type. --- CHANGELOG.md | 10 +++++++++ src/conversions/path.rs | 2 +- src/types/any.rs | 23 +++++++++++++++++++-- src/types/typeobject.rs | 43 ++++++++++++++++++++++++++++++--------- tests/test_inheritance.rs | 18 ++++++++++++++++ 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c23dfbf..68670039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add implementations for `Py::as_ref()` and `Py::into_ref()` for `Py`, `Py` and `Py`. [#1682](https://github.com/PyO3/pyo3/pull/1682) - Add `PyTraceback` type to represent and format Python tracebacks. [#1977](https://github.com/PyO3/pyo3/pull/1977) +### Added + +- Add `PyType::is_subclass_of` and `PyAny::is_instance_of` which operate not on + a type known at compile-time but a run-time type object. [#1985](https://github.com/PyO3/pyo3/pull/1985) + ### Changed - `#[classattr]` constants with a known magic method name (which is lowercase) no longer trigger lint warnings expecting constants to be uppercase. [#1969](https://github.com/PyO3/pyo3/pull/1969) @@ -42,6 +47,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix mingw platform detection. [#1993](https://github.com/PyO3/pyo3/pull/1993) - Fix panic in `__get__` implementation when accessing descriptor on type object. [#1997](https://github.com/PyO3/pyo3/pull/1997) +### Removed + +- Remove `PyType::is_instance`, which is unintuitive; instead of `typ.is_instance(obj)`, you should + now use `obj.is_instance_of(typ)`. [#1985](https://github.com/PyO3/pyo3/pull/1985) + ## [0.15.0] - 2021-11-03 ### Packaging diff --git a/src/conversions/path.rs b/src/conversions/path.rs index 91a41666..552dd8c6 100644 --- a/src/conversions/path.rs +++ b/src/conversions/path.rs @@ -20,7 +20,7 @@ impl FromPyObject<'_> for PathBuf { let py = ob.py(); let pathlib = py.import("pathlib")?; let pathlib_path: &PyType = pathlib.getattr("Path")?.downcast()?; - if pathlib_path.is_instance(ob)? { + if ob.is_instance_of(pathlib_path)? { let path_str = ob.call_method0("__str__")?; OsString::extract(path_str)? } else { diff --git a/src/types/any.rs b/src/types/any.rs index 8fc8640d..66b9ee76 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -667,9 +667,19 @@ impl PyAny { /// Checks whether this object is an instance of type `T`. /// - /// This is equivalent to the Python expression `isinstance(self, T)`. + /// This is equivalent to the Python expression `isinstance(self, T)`, + /// if the type `T` is known at compile time. pub fn is_instance(&self) -> PyResult { - T::type_object(self.py()).is_instance(self) + self.is_instance_of(T::type_object(self.py())) + } + + /// Checks whether this object is an instance of type `typ`. + /// + /// This is equivalent to the Python expression `isinstance(self, typ)`. + pub fn is_instance_of(&self, typ: &PyType) -> PyResult { + let result = unsafe { ffi::PyObject_IsInstance(self.as_ptr(), typ.as_ptr()) }; + err::error_on_minusone(self.py(), result)?; + Ok(result == 1) } /// Returns a GIL marker constrained to the lifetime of this type. @@ -682,6 +692,7 @@ impl PyAny { #[cfg(test)] mod tests { use crate::{ + type_object::PyTypeObject, types::{IntoPyDict, PyList, PyLong, PyModule}, Python, ToPyObject, }; @@ -782,4 +793,12 @@ mod tests { assert!(l.is_instance::().unwrap()); }); } + + #[test] + fn test_any_isinstance_of() { + Python::with_gil(|py| { + let l = vec![1u8, 2].to_object(py).into_ref(py); + assert!(l.is_instance_of(PyList::type_object(py)).unwrap()); + }); + } } diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 09a1f8f2..f91967b3 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -40,25 +40,48 @@ impl PyType { self.getattr("__qualname__")?.extract() } - /// Checks whether `self` is subclass of type `T`. + /// Checks whether `self` is a subclass of type `T`. /// - /// Equivalent to Python's `issubclass` function. + /// Equivalent to the Python expression `issubclass(self, T)`, if the type + /// `T` is known at compile time. pub fn is_subclass(&self) -> PyResult where T: PyTypeObject, { - let result = - unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), T::type_object(self.py()).as_ptr()) }; - err::error_on_minusone(self.py(), result)?; - Ok(result == 1) + self.is_subclass_of(T::type_object(self.py())) } - /// Check whether `obj` is an instance of `self`. + /// Checks whether `self` is a subclass of `other`. /// - /// Equivalent to Python's `isinstance` function. - pub fn is_instance(&self, obj: &T) -> PyResult { - let result = unsafe { ffi::PyObject_IsInstance(obj.as_ptr(), self.as_ptr()) }; + /// Equivalent to the Python expression `issubclass(self, other)`. + pub fn is_subclass_of(&self, other: &PyType) -> PyResult { + let result = unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), other.as_ptr()) }; err::error_on_minusone(self.py(), result)?; Ok(result == 1) } } + +#[cfg(test)] +mod tests { + use crate::{ + type_object::PyTypeObject, + types::{PyBool, PyLong}, + Python, + }; + + #[test] + fn test_type_is_subclass() { + Python::with_gil(|py| { + assert!(PyBool::type_object(py).is_subclass::().unwrap()); + }); + } + + #[test] + fn test_type_is_subclass_of() { + Python::with_gil(|py| { + let bool_type = PyBool::type_object(py); + let long_type = PyLong::type_object(py); + assert!(bool_type.is_subclass_of(long_type).unwrap()); + }); + } +} diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 01b124b8..8dcdd9b6 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -1,5 +1,6 @@ use pyo3::prelude::*; use pyo3::py_run; +use pyo3::type_object::PyTypeObject; use pyo3::types::IntoPyDict; @@ -102,6 +103,23 @@ fn mutation_fails() { assert_eq!(&e.to_string(), "RuntimeError: Already borrowed") } +#[test] +fn is_subclass_and_is_instance() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let sub_ty = SubClass::type_object(py); + let base_ty = BaseClass::type_object(py); + assert!(sub_ty.is_subclass::().unwrap()); + assert!(sub_ty.is_subclass_of(base_ty).unwrap()); + + let obj = PyCell::new(py, SubClass::new()).unwrap(); + assert!(obj.is_instance::().unwrap()); + assert!(obj.is_instance::().unwrap()); + assert!(obj.is_instance_of(sub_ty).unwrap()); + assert!(obj.is_instance_of(base_ty).unwrap()); +} + #[pyclass(subclass)] struct BaseClassWithResult { _val: usize, From 43893158b1d61ce2fbc603a96210e2dcc78e198a Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Fri, 12 Nov 2021 19:19:54 +0100 Subject: [PATCH 28/30] switch is_instance/is_instance_of, is_subclass/is_subclass_of --- CHANGELOG.md | 17 ++++++----------- guide/src/conversions/traits.md | 2 +- guide/src/ecosystem/async-await.md | 13 +++++++------ guide/src/exception.md | 26 ++++++++++++-------------- src/conversions/path.rs | 2 +- src/err/mod.rs | 21 ++++++++++++--------- src/exceptions.rs | 8 ++++---- src/types/any.rs | 30 +++++++++++++++--------------- src/types/bytearray.rs | 4 ++-- src/types/bytes.rs | 2 +- src/types/iterator.rs | 2 +- src/types/mapping.rs | 4 ++-- src/types/module.rs | 2 +- src/types/num.rs | 8 ++++---- src/types/typeobject.rs | 30 +++++++++++++++--------------- tests/test_inheritance.rs | 12 ++++++------ tests/test_proto_methods.rs | 2 +- 17 files changed, 91 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68670039..daa5d2be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `Py::setattr` method. [#2009](https://github.com/PyO3/pyo3/pull/2009) -## Removed +### Changed +- `PyType::is_subclass`, `PyErr::is_instance` and `PyAny::is_instance` now operate run-time type object instead of a type known at compile-time. The old behavior is still available as `PyType::is_subclass_of`, `PyErr::is_instance_of` and `PyAny::is_instance_of`. [#1985](https://github.com/PyO3/pyo3/pull/1985) + +### Removed + +- Remove `PyType::is_instance`, which is unintuitive; instead of `typ.is_instance(obj)`, use `obj.is_instance(typ)`. [#1985](https://github.com/PyO3/pyo3/pull/1985) - Remove all functionality deprecated in PyO3 0.14. [#2007](https://github.com/PyO3/pyo3/pull/2007) ## [0.15.1] - 2021-11-19 @@ -30,11 +35,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add implementations for `Py::as_ref()` and `Py::into_ref()` for `Py`, `Py` and `Py`. [#1682](https://github.com/PyO3/pyo3/pull/1682) - Add `PyTraceback` type to represent and format Python tracebacks. [#1977](https://github.com/PyO3/pyo3/pull/1977) -### Added - -- Add `PyType::is_subclass_of` and `PyAny::is_instance_of` which operate not on - a type known at compile-time but a run-time type object. [#1985](https://github.com/PyO3/pyo3/pull/1985) - ### Changed - `#[classattr]` constants with a known magic method name (which is lowercase) no longer trigger lint warnings expecting constants to be uppercase. [#1969](https://github.com/PyO3/pyo3/pull/1969) @@ -47,11 +47,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix mingw platform detection. [#1993](https://github.com/PyO3/pyo3/pull/1993) - Fix panic in `__get__` implementation when accessing descriptor on type object. [#1997](https://github.com/PyO3/pyo3/pull/1997) -### Removed - -- Remove `PyType::is_instance`, which is unintuitive; instead of `typ.is_instance(obj)`, you should - now use `obj.is_instance_of(typ)`. [#1985](https://github.com/PyO3/pyo3/pull/1985) - ## [0.15.0] - 2021-11-03 ### Packaging diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index d0089ef1..619aac80 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -413,7 +413,7 @@ enum RustyEnum { # { # let thing = b"foo".to_object(py); # let error = thing.extract::(py).unwrap_err(); -# assert!(error.is_instance::(py)); +# assert!(error.is_instance_of::(py)); # } # # Ok(()) diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 7c92ea46..37e1333f 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -536,12 +536,13 @@ fn main() -> PyResult<()> { pyo3_asyncio::async_std::run(py, async move { // verify that we are on a uvloop.Loop Python::with_gil(|py| -> PyResult<()> { - assert!(uvloop - .as_ref(py) - .getattr("Loop")? - .downcast::() - .unwrap() - .is_instance(pyo3_asyncio::async_std::get_current_loop(py)?)?); + assert!(pyo3_asyncio::async_std::get_current_loop(py)?.is_instance( + uvloop + .as_ref(py) + .getattr("Loop")? + .downcast::() + .unwrap() + )?); Ok(()) })?; diff --git a/guide/src/exception.md b/guide/src/exception.md index 63ee6dda..8806c164 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -100,21 +100,19 @@ PyErr::from_instance(py, err).restore(py); ## Checking exception types Python has an [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance) method to check an object's type. -In PyO3 every native type has access to the [`PyAny::is_instance`] method which does the same thing. +In PyO3 every object has the [`PyAny::is_instance`] and [`PyAny::is_instance_of`] methods which do the same thing. ```rust use pyo3::Python; use pyo3::types::{PyBool, PyList}; Python::with_gil(|py| { - assert!(PyBool::new(py, true).is_instance::().unwrap()); + assert!(PyBool::new(py, true).is_instance_of::().unwrap()); let list = PyList::new(py, &[1, 2, 3, 4]); - assert!(!list.is_instance::().unwrap()); - assert!(list.is_instance::().unwrap()); + assert!(!list.is_instance_of::().unwrap()); + assert!(list.is_instance_of::().unwrap()); }); ``` -[`PyAny::is_instance`] calls the underlying [`PyType::is_instance`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyType.html#method.is_instance) -method to do the actual work. To check the type of an exception, you can similarly do: @@ -123,7 +121,7 @@ To check the type of an exception, you can similarly do: # use pyo3::prelude::*; # Python::with_gil(|py| { # let err = PyTypeError::new_err(()); -err.is_instance::(py); +err.is_instance_of::(py); # }); ``` @@ -184,7 +182,7 @@ fn main() { Python::with_gil(|py| { let fun = pyo3::wrap_pyfunction!(connect, py).unwrap(); let err = fun.call1(("0.0.0.0",)).unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }); } ``` @@ -201,21 +199,21 @@ fn parse_int(s: String) -> PyResult { } # # use pyo3::exceptions::PyValueError; -# +# # fn main() { # Python::with_gil(|py| { # assert_eq!(parse_int(String::from("1")).unwrap(), 1); # assert_eq!(parse_int(String::from("1337")).unwrap(), 1337); -# +# # assert!(parse_int(String::from("-1")) # .unwrap_err() -# .is_instance::(py)); +# .is_instance_of::(py)); # assert!(parse_int(String::from("foo")) # .unwrap_err() -# .is_instance::(py)); +# .is_instance_of::(py)); # assert!(parse_int(String::from("13.37")) # .unwrap_err() -# .is_instance::(py)); +# .is_instance_of::(py)); # }) # } ``` @@ -257,5 +255,5 @@ defines exceptions for several standard library modules. [`PyErr`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html [`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/type.PyResult.html [`PyErr::from_instance`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html#method.from_instance -[`Python::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.is_instance [`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#method.is_instance +[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#method.is_instance_of diff --git a/src/conversions/path.rs b/src/conversions/path.rs index 552dd8c6..bd0eca69 100644 --- a/src/conversions/path.rs +++ b/src/conversions/path.rs @@ -20,7 +20,7 @@ impl FromPyObject<'_> for PathBuf { let py = ob.py(); let pathlib = py.import("pathlib")?; let pathlib_path: &PyType = pathlib.getattr("Path")?.downcast()?; - if ob.is_instance_of(pathlib_path)? { + if ob.is_instance(pathlib_path)? { let path_str = ob.call_method0("__str__")?; OsString::extract(path_str)? } else { diff --git a/src/err/mod.rs b/src/err/mod.rs index db23c678..3e2de790 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -180,7 +180,7 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); - /// assert!(err.is_instance::(py)); + /// assert!(err.is_instance_of::(py)); /// assert_eq!(err.pvalue(py).to_string(), "some type error"); /// }); /// ``` @@ -366,13 +366,16 @@ impl PyErr { } /// Returns true if the current exception is instance of `T`. - pub fn is_instance(&self, py: Python) -> bool + pub fn is_instance(&self, py: Python, typ: &PyType) -> bool { + unsafe { ffi::PyErr_GivenExceptionMatches(self.ptype_ptr(py), typ.as_ptr()) != 0 } + } + + /// Returns true if the current exception is instance of `T`. + pub fn is_instance_of(&self, py: Python) -> bool where T: PyTypeObject, { - unsafe { - ffi::PyErr_GivenExceptionMatches(self.ptype_ptr(py), T::type_object(py).as_ptr()) != 0 - } + self.is_instance(py, T::type_object(py)) } /// Retrieves the exception instance for this error. @@ -612,11 +615,11 @@ mod tests { fn set_valueerror() { Python::with_gil(|py| { let err: PyErr = exceptions::PyValueError::new_err("some exception message"); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); err.restore(py); assert!(PyErr::occurred(py)); let err = PyErr::fetch(py); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); assert_eq!(err.to_string(), "ValueError: some exception message"); }) } @@ -625,10 +628,10 @@ mod tests { fn invalid_error_type() { Python::with_gil(|py| { let err: PyErr = PyErr::new::(()); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); err.restore(py); let err = PyErr::fetch(py); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); assert_eq!( err.to_string(), "TypeError: exceptions must derive from BaseException" diff --git a/src/exceptions.rs b/src/exceptions.rs index 004e19f4..2020f5eb 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -267,7 +267,7 @@ fn always_throws() -> PyResult<()> { # Python::with_gil(|py| { # let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); # let err = fun.call0().expect_err(\"called a function that should always return an error but the return value was Ok\"); -# assert!(err.is_instance::(py)) +# assert!(err.is_instance_of::(py)) # }); ``` @@ -292,7 +292,7 @@ Python::with_gil(|py| { let error_type = match result { Ok(_) => \"Not an error\", - Err(error) if error.is_instance::(py) => \"" , $name, "\", + Err(error) if error.is_instance_of::(py) => \"" , $name, "\", Err(_) => \"Some other error\", }; @@ -611,7 +611,7 @@ macro_rules! test_exception { .unwrap_or($exc_ty::new_err("a test exception")) }; - assert!(err.is_instance::<$exc_ty>(py)); + assert!(err.is_instance_of::<$exc_ty>(py)); let value: &$exc_ty = err.instance(py).downcast().unwrap(); assert!(value.source().is_none()); @@ -619,7 +619,7 @@ macro_rules! test_exception { err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause"))); assert!(value.source().is_some()); - assert!($crate::PyErr::from(value).is_instance::<$exc_ty>(py)); + assert!($crate::PyErr::from(value).is_instance_of::<$exc_ty>(py)); }) } }; diff --git a/src/types/any.rs b/src/types/any.rs index 66b9ee76..2d45255b 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -74,7 +74,7 @@ impl PyAny { /// /// Python::with_gil(|py| { /// let dict = PyDict::new(py); - /// assert!(dict.is_instance::().unwrap()); + /// assert!(dict.is_instance_of::().unwrap()); /// let any: &PyAny = dict.as_ref(); /// assert!(any.downcast::().is_ok()); /// assert!(any.downcast::().is_err()); @@ -665,21 +665,21 @@ impl PyAny { unsafe { self.py().from_owned_ptr(ffi::PyObject_Dir(self.as_ptr())) } } + /// Checks whether this object is an instance of type `typ`. + /// + /// This is equivalent to the Python expression `isinstance(self, typ)`. + pub fn is_instance(&self, typ: &PyType) -> PyResult { + let result = unsafe { ffi::PyObject_IsInstance(self.as_ptr(), typ.as_ptr()) }; + err::error_on_minusone(self.py(), result)?; + Ok(result == 1) + } + /// Checks whether this object is an instance of type `T`. /// /// This is equivalent to the Python expression `isinstance(self, T)`, /// if the type `T` is known at compile time. - pub fn is_instance(&self) -> PyResult { - self.is_instance_of(T::type_object(self.py())) - } - - /// Checks whether this object is an instance of type `typ`. - /// - /// This is equivalent to the Python expression `isinstance(self, typ)`. - pub fn is_instance_of(&self, typ: &PyType) -> PyResult { - let result = unsafe { ffi::PyObject_IsInstance(self.as_ptr(), typ.as_ptr()) }; - err::error_on_minusone(self.py(), result)?; - Ok(result == 1) + pub fn is_instance_of(&self) -> PyResult { + self.is_instance(T::type_object(self.py())) } /// Returns a GIL marker constrained to the lifetime of this type. @@ -787,10 +787,10 @@ mod tests { fn test_any_isinstance() { Python::with_gil(|py| { let x = 5.to_object(py).into_ref(py); - assert!(x.is_instance::().unwrap()); + assert!(x.is_instance_of::().unwrap()); let l = vec![x, x].to_object(py).into_ref(py); - assert!(l.is_instance::().unwrap()); + assert!(l.is_instance_of::().unwrap()); }); } @@ -798,7 +798,7 @@ mod tests { fn test_any_isinstance_of() { Python::with_gil(|py| { let l = vec![1u8, 2].to_object(py).into_ref(py); - assert!(l.is_instance_of(PyList::type_object(py)).unwrap()); + assert!(l.is_instance(PyList::type_object(py)).unwrap()); }); } } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 29750a5c..a63dd4db 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -241,7 +241,7 @@ mod tests { fn test_from_err() { Python::with_gil(|py| { if let Err(err) = PyByteArray::from(py, &py.None()) { - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); } else { panic!("error"); } @@ -293,7 +293,7 @@ mod tests { assert!(py_bytearray_result .err() .unwrap() - .is_instance::(py)); + .is_instance_of::(py)); }) } } diff --git a/src/types/bytes.rs b/src/types/bytes.rs index af61a6fd..e733582b 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -174,7 +174,7 @@ mod tests { assert!(py_bytes_result .err() .unwrap() - .is_instance::(py)); + .is_instance_of::(py)); }); } } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index a2518732..243644cb 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -206,7 +206,7 @@ mod tests { let x = 5.to_object(py); let err = PyIterator::from_object(py, &x).unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }); } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 651bcc56..0ff430a0 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -180,7 +180,7 @@ mod tests { assert!(mapping .get_item(8i32) .unwrap_err() - .is_instance::(py)); + .is_instance_of::(py)); }); } @@ -216,7 +216,7 @@ mod tests { assert!(mapping .get_item(7i32) .unwrap_err() - .is_instance::(py)); + .is_instance_of::(py)); }); } diff --git a/src/types/module.rs b/src/types/module.rs index 57bf9035..8330f29d 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -146,7 +146,7 @@ impl PyModule { match self.getattr("__all__") { Ok(idx) => idx.downcast().map_err(PyErr::from), Err(err) => { - if err.is_instance::(self.py()) { + if err.is_instance_of::(self.py()) { let l = PyList::empty(self.py()); self.setattr("__all__", l).map_err(PyErr::from)?; Ok(l) diff --git a/src/types/num.rs b/src/types/num.rs index c9e05eb0..2128b937 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -345,7 +345,7 @@ mod test_128bit_intergers { Python::with_gil(|py| { let obj = py.eval("(1 << 130) * -1", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }) } @@ -354,7 +354,7 @@ mod test_128bit_intergers { Python::with_gil(|py| { let obj = py.eval("1 << 130", None, None).unwrap(); let err = obj.extract::().unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }) } } @@ -421,7 +421,7 @@ mod tests { let obj = ("123").to_object(py); let err = obj.extract::<$t>(py).unwrap_err(); - assert!(err.is_instance::(py)); + assert!(err.is_instance_of::(py)); }); } @@ -431,7 +431,7 @@ mod tests { let obj = (12.3).to_object(py); let err = obj.extract::<$t>(py).unwrap_err(); - assert!(err.is_instance::(py));}); + assert!(err.is_instance_of::(py));}); } #[test] diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index f91967b3..db8eccf3 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -40,24 +40,24 @@ impl PyType { self.getattr("__qualname__")?.extract() } + /// Checks whether `self` is a subclass of `other`. + /// + /// Equivalent to the Python expression `issubclass(self, other)`. + pub fn is_subclass(&self, other: &PyType) -> PyResult { + let result = unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), other.as_ptr()) }; + err::error_on_minusone(self.py(), result)?; + Ok(result == 1) + } + /// Checks whether `self` is a subclass of type `T`. /// /// Equivalent to the Python expression `issubclass(self, T)`, if the type /// `T` is known at compile time. - pub fn is_subclass(&self) -> PyResult + pub fn is_subclass_of(&self) -> PyResult where T: PyTypeObject, { - self.is_subclass_of(T::type_object(self.py())) - } - - /// Checks whether `self` is a subclass of `other`. - /// - /// Equivalent to the Python expression `issubclass(self, other)`. - pub fn is_subclass_of(&self, other: &PyType) -> PyResult { - let result = unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), other.as_ptr()) }; - err::error_on_minusone(self.py(), result)?; - Ok(result == 1) + self.is_subclass(T::type_object(self.py())) } } @@ -72,16 +72,16 @@ mod tests { #[test] fn test_type_is_subclass() { Python::with_gil(|py| { - assert!(PyBool::type_object(py).is_subclass::().unwrap()); + let bool_type = PyBool::type_object(py); + let long_type = PyLong::type_object(py); + assert!(bool_type.is_subclass(long_type).unwrap()); }); } #[test] fn test_type_is_subclass_of() { Python::with_gil(|py| { - let bool_type = PyBool::type_object(py); - let long_type = PyLong::type_object(py); - assert!(bool_type.is_subclass_of(long_type).unwrap()); + assert!(PyBool::type_object(py).is_subclass_of::().unwrap()); }); } } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 8dcdd9b6..cb49b474 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -110,14 +110,14 @@ fn is_subclass_and_is_instance() { let sub_ty = SubClass::type_object(py); let base_ty = BaseClass::type_object(py); - assert!(sub_ty.is_subclass::().unwrap()); - assert!(sub_ty.is_subclass_of(base_ty).unwrap()); + assert!(sub_ty.is_subclass_of::().unwrap()); + assert!(sub_ty.is_subclass(base_ty).unwrap()); let obj = PyCell::new(py, SubClass::new()).unwrap(); - assert!(obj.is_instance::().unwrap()); - assert!(obj.is_instance::().unwrap()); - assert!(obj.is_instance_of(sub_ty).unwrap()); - assert!(obj.is_instance_of(base_ty).unwrap()); + assert!(obj.is_instance_of::().unwrap()); + assert!(obj.is_instance_of::().unwrap()); + assert!(obj.is_instance(sub_ty).unwrap()); + assert!(obj.is_instance(base_ty).unwrap()); } #[pyclass(subclass)] diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index ad1ce58f..df9c0d01 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -97,7 +97,7 @@ fn test_getattr() { assert!(example_py .getattr("other_attr") .unwrap_err() - .is_instance::(py)); + .is_instance_of::(py)); }) } From b7419b5278e18ac9b99680ecb12fc109ddd56320 Mon Sep 17 00:00:00 2001 From: b05902132 Date: Wed, 17 Nov 2021 03:31:30 +0800 Subject: [PATCH 29/30] Refactor #[pyclass] and now it supports enum. There's no functionality since it does not generate __richcmp__. Also it only works on enums with only variants, and does not support C-like enums. --- pyo3-macros-backend/src/lib.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 724 +++++++++++++++++---------- pyo3-macros-backend/src/pyimpl.rs | 3 +- pyo3-macros/src/lib.rs | 50 +- tests/test_compile_error.rs | 2 + tests/test_enum.rs | 53 ++ tests/ui/invalid_pyclass_enum.rs | 15 + tests/ui/invalid_pyclass_enum.stderr | 11 + tests/ui/invalid_pyclass_item.rs | 6 + tests/ui/invalid_pyclass_item.stderr | 5 + 10 files changed, 596 insertions(+), 275 deletions(-) create mode 100644 tests/test_enum.rs create mode 100644 tests/ui/invalid_pyclass_enum.rs create mode 100644 tests/ui/invalid_pyclass_enum.stderr create mode 100644 tests/ui/invalid_pyclass_item.rs create mode 100644 tests/ui/invalid_pyclass_item.stderr diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index f42bd3b9..69fc24d2 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -25,7 +25,7 @@ mod pyproto; pub use from_pyobject::build_derive_from_pyobject; pub use module::{process_functions_in_module, py_init, PyModuleOptions}; -pub use pyclass::{build_py_class, PyClassArgs}; +pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; pub use pyproto::build_py_proto; diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index bafe9910..3e59a960 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2,7 +2,8 @@ use crate::attributes::{self, take_pyo3_options, NameAttribute, TextSignatureAttribute}; use crate::deprecations::Deprecations; -use crate::pyimpl::PyClassMethodsType; +use crate::konst::{ConstAttributes, ConstSpec}; +use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; use crate::utils::{self, unwrap_group, PythonDoc}; use proc_macro2::{Span, TokenStream}; @@ -10,7 +11,14 @@ use quote::quote; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{parse_quote, spanned::Spanned, Expr, Result, Token}; +use syn::{parse_quote, spanned::Spanned, Expr, Result, Token}; //unraw + +/// If the class is derived from a Rust `struct` or `enum`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum PyClassKind { + Struct, + Enum, +} /// The parsed arguments of the pyclass macro pub struct PyClassArgs { @@ -24,22 +32,28 @@ pub struct PyClassArgs { pub has_extends: bool, pub has_unsendable: bool, pub module: Option, + pub class_kind: PyClassKind, } -impl Parse for PyClassArgs { - fn parse(input: ParseStream) -> Result { - let mut slf = PyClassArgs::default(); - +impl PyClassArgs { + fn parse(input: ParseStream, kind: PyClassKind) -> Result { + let mut slf = PyClassArgs::new(kind); let vars = Punctuated::::parse_terminated(input)?; for expr in vars { slf.add_expr(&expr)?; } Ok(slf) } -} -impl Default for PyClassArgs { - fn default() -> Self { + pub fn parse_stuct_args(input: ParseStream) -> syn::Result { + Self::parse(input, PyClassKind::Struct) + } + + pub fn parse_enum_args(input: ParseStream) -> syn::Result { + Self::parse(input, PyClassKind::Enum) + } + + fn new(class_kind: PyClassKind) -> Self { PyClassArgs { freelist: None, name: None, @@ -51,11 +65,10 @@ impl Default for PyClassArgs { is_basetype: false, has_extends: false, has_unsendable: false, + class_kind, } } -} -impl PyClassArgs { /// Adda single expression from the comma separated list in the attribute, which is /// either a single word or an assignment expression fn add_expr(&mut self, expr: &Expr) -> Result<()> { @@ -113,6 +126,9 @@ impl PyClassArgs { }, "extends" => match unwrap_group(&**right) { syn::Expr::Path(exp) => { + if self.class_kind == PyClassKind::Enum { + bail_spanned!( assign.span() => "enums cannot extend from other classes" ); + } self.base = syn::TypePath { path: exp.path.clone(), qself: None, @@ -147,6 +163,9 @@ impl PyClassArgs { self.has_weaklist = true; } "subclass" => { + if self.class_kind == PyClassKind::Enum { + bail_spanned!(exp.span() => "enums can't be inherited by other classes"); + } self.is_basetype = true; } "dict" => { @@ -328,41 +347,6 @@ impl FieldPyO3Options { } } -/// To allow multiple #[pymethods] block, we define inventory types. -fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream { - // Try to build a unique type for better error messages - let name = format!("Pyo3MethodsInventoryFor{}", cls.unraw()); - let inventory_cls = syn::Ident::new(&name, Span::call_site()); - - quote! { - #[doc(hidden)] - pub struct #inventory_cls { - methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>, - slots: ::std::vec::Vec<::pyo3::ffi::PyType_Slot>, - } - impl ::pyo3::class::impl_::PyMethodsInventory for #inventory_cls { - fn new( - methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>, - slots: ::std::vec::Vec<::pyo3::ffi::PyType_Slot>, - ) -> Self { - Self { methods, slots } - } - fn methods(&'static self) -> &'static [::pyo3::class::PyMethodDefType] { - &self.methods - } - fn slots(&'static self) -> &'static [::pyo3::ffi::PyType_Slot] { - &self.slots - } - } - - impl ::pyo3::class::impl_::HasMethodsInventory for #cls { - type Methods = #inventory_cls; - } - - ::pyo3::inventory::collect!(#inventory_cls); - } -} - fn get_class_python_name<'a>(cls: &'a syn::Ident, attr: &'a PyClassArgs) -> &'a syn::Ident { attr.name.as_ref().unwrap_or(cls) } @@ -375,243 +359,121 @@ fn impl_class( methods_type: PyClassMethodsType, deprecations: Deprecations, ) -> syn::Result { - let cls_name = get_class_python_name(cls, attr).to_string(); + let pytypeinfo_impl = impl_pytypeinfo(cls, attr, Some(&deprecations)); - let alloc = attr.freelist.as_ref().map(|freelist| { - quote! { - impl ::pyo3::class::impl_::PyClassWithFreeList for #cls { - #[inline] - fn get_free_list(_py: ::pyo3::Python<'_>) -> &mut ::pyo3::impl_::freelist::FreeList<*mut ::pyo3::ffi::PyObject> { - static mut FREELIST: *mut ::pyo3::impl_::freelist::FreeList<*mut ::pyo3::ffi::PyObject> = 0 as *mut _; - unsafe { - if FREELIST.is_null() { - FREELIST = ::std::boxed::Box::into_raw(::std::boxed::Box::new( - ::pyo3::impl_::freelist::FreeList::with_capacity(#freelist))); - } - &mut *FREELIST - } - } - } - - impl ::pyo3::class::impl_::PyClassAllocImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { - #[inline] - fn alloc_impl(self) -> ::std::option::Option<::pyo3::ffi::allocfunc> { - ::std::option::Option::Some(::pyo3::class::impl_::alloc_with_freelist::<#cls>) - } - } - - impl ::pyo3::class::impl_::PyClassFreeImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { - #[inline] - fn free_impl(self) -> ::std::option::Option<::pyo3::ffi::freefunc> { - ::std::option::Option::Some(::pyo3::class::impl_::free_with_freelist::<#cls>) - } - } - } - }); + let py_class_impl = PyClassImplsBuilder::new(cls, attr, methods_type) + .doc(doc) + .impl_all(); let descriptors = impl_descriptors(cls, field_options)?; - // insert space for weak ref - let weakref = if attr.has_weaklist { - quote! { ::pyo3::pyclass_slots::PyClassWeakRefSlot } - } else if attr.has_extends { - quote! { ::WeakRef } - } else { - quote! { ::pyo3::pyclass_slots::PyClassDummySlot } - }; - let dict = if attr.has_dict { - quote! { ::pyo3::pyclass_slots::PyClassDictSlot } - } else if attr.has_extends { - quote! { ::Dict } - } else { - quote! { ::pyo3::pyclass_slots::PyClassDummySlot } - }; - let module = if let Some(m) = &attr.module { - quote! { ::std::option::Option::Some(#m) } - } else { - quote! { ::std::option::Option::None } - }; + Ok(quote! { + #pytypeinfo_impl - // Enforce at compile time that PyGCProtocol is implemented - let gc_impl = if attr.is_gc { - let closure_name = format!("__assertion_closure_{}", cls); - let closure_token = syn::Ident::new(&closure_name, Span::call_site()); - quote! { - fn #closure_token() { - use ::pyo3::class; + #py_class_impl - fn _assert_implements_protocol<'p, T: ::pyo3::class::PyGCProtocol<'p>>() {} - _assert_implements_protocol::<#cls>(); - } - } - } else { - quote! {} - }; + #descriptors + }) +} - let (impl_inventory, for_each_py_method) = match methods_type { - PyClassMethodsType::Specialization => (None, quote! { visitor(collector.py_methods()); }), - PyClassMethodsType::Inventory => ( - Some(impl_methods_inventory(cls)), - quote! { - for inventory in ::pyo3::inventory::iter::<::Methods>() { - visitor(::pyo3::class::impl_::PyMethodsInventory::methods(inventory)); - } - }, - ), - }; +struct PyClassEnumVariant<'a> { + ident: &'a syn::Ident, + /* currently have no more options */ +} - let methods_protos = match methods_type { - PyClassMethodsType::Specialization => { - quote! { visitor(collector.methods_protocol_slots()); } - } - PyClassMethodsType::Inventory => { - quote! { - for inventory in ::pyo3::inventory::iter::<::Methods>() { - visitor(::pyo3::class::impl_::PyMethodsInventory::slots(inventory)); - } - } - } - }; +pub fn build_py_enum( + enum_: &syn::ItemEnum, + args: PyClassArgs, + method_type: PyClassMethodsType, +) -> syn::Result { + let variants: Vec = enum_ + .variants + .iter() + .map(|v| extract_variant_data(v)) + .collect::>()?; + impl_enum(enum_, args, variants, method_type) +} - let base = &attr.base; - let base_nativetype = if attr.has_extends { - quote! { ::BaseNativeType } - } else { - quote! { ::pyo3::PyAny } - }; - - // If #cls is not extended type, we allow Self->PyObject conversion - let into_pyobject = if !attr.has_extends { - quote! { - impl ::pyo3::IntoPy<::pyo3::PyObject> for #cls { - fn into_py(self, py: ::pyo3::Python) -> ::pyo3::PyObject { - ::pyo3::IntoPy::into_py(::pyo3::Py::new(py, self).unwrap(), py) - } - } - } - } else { - quote! {} - }; - - let thread_checker = if attr.has_unsendable { - quote! { ::pyo3::class::impl_::ThreadCheckerImpl<#cls> } - } else if attr.has_extends { - quote! { - ::pyo3::class::impl_::ThreadCheckerInherited<#cls, <#cls as ::pyo3::class::impl_::PyClassImpl>::BaseType> - } - } else { - quote! { ::pyo3::class::impl_::ThreadCheckerStub<#cls> } - }; - - let is_gc = attr.is_gc; - let is_basetype = attr.is_basetype; - let is_subclass = attr.has_extends; +fn impl_enum( + enum_: &syn::ItemEnum, + attrs: PyClassArgs, + variants: Vec, + methods_type: PyClassMethodsType, +) -> syn::Result { + let enum_name = &enum_.ident; + let doc = utils::get_doc(&enum_.attrs, None); + let enum_cls = impl_enum_class(enum_name, &attrs, variants, doc, methods_type)?; Ok(quote! { - unsafe impl ::pyo3::type_object::PyTypeInfo for #cls { - type AsRefTarget = ::pyo3::PyCell; + #enum_cls + }) +} - const NAME: &'static str = #cls_name; - const MODULE: ::std::option::Option<&'static str> = #module; +fn impl_enum_class( + cls: &syn::Ident, + attr: &PyClassArgs, + variants: Vec, + doc: PythonDoc, + methods_type: PyClassMethodsType, +) -> syn::Result { + let pytypeinfo = impl_pytypeinfo(cls, attr, None); + let pyclass_impls = PyClassImplsBuilder::new(cls, attr, methods_type) + .doc(doc) + .impl_all(); + let descriptors = unit_variants_as_descriptors(cls, variants.iter().map(|v| v.ident)); - #[inline] - fn type_object_raw(py: ::pyo3::Python<'_>) -> *mut ::pyo3::ffi::PyTypeObject { - #deprecations + Ok(quote! { - use ::pyo3::type_object::LazyStaticType; - static TYPE_OBJECT: LazyStaticType = LazyStaticType::new(); - TYPE_OBJECT.get_or_init::(py) - } - } + #pytypeinfo - impl ::pyo3::PyClass for #cls { - type Dict = #dict; - type WeakRef = #weakref; - type BaseNativeType = #base_nativetype; - } - - impl<'a> ::pyo3::derive_utils::ExtractExt<'a> for &'a #cls - { - type Target = ::pyo3::PyRef<'a, #cls>; - } - - impl<'a> ::pyo3::derive_utils::ExtractExt<'a> for &'a mut #cls - { - type Target = ::pyo3::PyRefMut<'a, #cls>; - } - - #into_pyobject - - #impl_inventory - - impl ::pyo3::class::impl_::PyClassImpl for #cls { - const DOC: &'static str = #doc; - const IS_GC: bool = #is_gc; - const IS_BASETYPE: bool = #is_basetype; - const IS_SUBCLASS: bool = #is_subclass; - - type Layout = ::pyo3::PyCell; - type BaseType = #base; - type ThreadChecker = #thread_checker; - - fn for_each_method_def(visitor: &mut dyn ::std::ops::FnMut(&[::pyo3::class::PyMethodDefType])) { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - #for_each_py_method; - visitor(collector.py_class_descriptors()); - visitor(collector.object_protocol_methods()); - visitor(collector.async_protocol_methods()); - visitor(collector.descr_protocol_methods()); - visitor(collector.mapping_protocol_methods()); - visitor(collector.number_protocol_methods()); - } - fn get_new() -> ::std::option::Option<::pyo3::ffi::newfunc> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.new_impl() - } - fn get_alloc() -> ::std::option::Option<::pyo3::ffi::allocfunc> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.alloc_impl() - } - fn get_free() -> ::std::option::Option<::pyo3::ffi::freefunc> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.free_impl() - } - - fn for_each_proto_slot(visitor: &mut dyn ::std::ops::FnMut(&[::pyo3::ffi::PyType_Slot])) { - // Implementation which uses dtolnay specialization to load all slots. - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - visitor(collector.object_protocol_slots()); - visitor(collector.number_protocol_slots()); - visitor(collector.iter_protocol_slots()); - visitor(collector.gc_protocol_slots()); - visitor(collector.descr_protocol_slots()); - visitor(collector.mapping_protocol_slots()); - visitor(collector.sequence_protocol_slots()); - visitor(collector.async_protocol_slots()); - visitor(collector.buffer_protocol_slots()); - #methods_protos - } - - fn get_buffer() -> ::std::option::Option<&'static ::pyo3::class::impl_::PyBufferProcs> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.buffer_procs() - } - } - - #alloc + #pyclass_impls #descriptors - #gc_impl }) } +fn unit_variants_as_descriptors<'a>( + cls: &'a syn::Ident, + variant_names: impl IntoIterator, +) -> TokenStream { + let cls_type = syn::parse_quote!(#cls); + let variant_to_attribute = |ident: &syn::Ident| ConstSpec { + rust_ident: ident.clone(), + attributes: ConstAttributes { + is_class_attr: true, + name: Some(NameAttribute(ident.clone())), + deprecations: Default::default(), + }, + }; + let py_methods = variant_names + .into_iter() + .map(|var| gen_py_const(&cls_type, &variant_to_attribute(var))); + + quote! { + impl ::pyo3::class::impl_::PyClassDescriptors<#cls> + for ::pyo3::class::impl_::PyClassImplCollector<#cls> + { + fn py_class_descriptors(self) -> &'static [::pyo3::class::methods::PyMethodDefType] { + static METHODS: &[::pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*]; + METHODS + } + } + } +} + +fn extract_variant_data(variant: &syn::Variant) -> syn::Result { + use syn::Fields; + let ident = match variant.fields { + Fields::Unit => &variant.ident, + _ => bail_spanned!(variant.span() => "Currently only support unit variants."), + }; + if let Some(discriminant) = variant.discriminant.as_ref() { + bail_spanned!(discriminant.0.span() => "Currently does not support discriminats.") + }; + Ok(PyClassEnumVariant { ident }) +} + fn impl_descriptors( cls: &syn::Ident, field_options: Vec<(&syn::Field, FieldPyO3Options)>, @@ -662,3 +524,341 @@ fn impl_descriptors( } }) } + +fn impl_pytypeinfo( + cls: &syn::Ident, + attr: &PyClassArgs, + deprecations: Option<&Deprecations>, +) -> TokenStream { + let cls_name = get_class_python_name(cls, attr).to_string(); + + let module = if let Some(m) = &attr.module { + quote! { ::core::option::Option::Some(#m) } + } else { + quote! { ::core::option::Option::None } + }; + + quote! { + unsafe impl ::pyo3::type_object::PyTypeInfo for #cls { + type AsRefTarget = ::pyo3::PyCell; + + const NAME: &'static str = #cls_name; + const MODULE: ::std::option::Option<&'static str> = #module; + + #[inline] + fn type_object_raw(py: ::pyo3::Python<'_>) -> *mut ::pyo3::ffi::PyTypeObject { + #deprecations + + use ::pyo3::type_object::LazyStaticType; + static TYPE_OBJECT: LazyStaticType = LazyStaticType::new(); + TYPE_OBJECT.get_or_init::(py) + } + } + } +} + +/// Implements most traits used by `#[pyclass]`. +/// +/// Specifically, it implements traits that only depend on class name, +/// and attributes of `#[pyclass]`, and docstrings. +/// Therefore it doesn't implement traits that depends on struct fields and enum variants. +struct PyClassImplsBuilder<'a> { + cls: &'a syn::Ident, + attr: &'a PyClassArgs, + methods_type: PyClassMethodsType, + doc: Option, +} + +impl<'a> PyClassImplsBuilder<'a> { + fn new(cls: &'a syn::Ident, attr: &'a PyClassArgs, methods_type: PyClassMethodsType) -> Self { + Self { + cls, + attr, + methods_type, + doc: None, + } + } + + fn doc(self, doc: PythonDoc) -> Self { + Self { + doc: Some(doc), + ..self + } + } + + fn impl_all(&self) -> TokenStream { + vec![ + self.impl_pyclass(), + self.impl_extractext(), + self.impl_into_py(), + self.impl_methods_inventory(), + self.impl_pyclassimpl(), + self.impl_freelist(), + self.impl_gc(), + ] + .into_iter() + .collect() + } + + fn impl_pyclass(&self) -> TokenStream { + let cls = self.cls; + let attr = self.attr; + let dict = if attr.has_dict { + quote! { ::pyo3::pyclass_slots::PyClassDictSlot } + } else if attr.has_extends { + quote! { ::Dict } + } else { + quote! { ::pyo3::pyclass_slots::PyClassDummySlot } + }; + + // insert space for weak ref + let weakref = if attr.has_weaklist { + quote! { ::pyo3::pyclass_slots::PyClassWeakRefSlot } + } else if attr.has_extends { + quote! { ::WeakRef } + } else { + quote! { ::pyo3::pyclass_slots::PyClassDummySlot } + }; + + let base_nativetype = if attr.has_extends { + quote! { ::BaseNativeType } + } else { + quote! { ::pyo3::PyAny } + }; + quote! { + impl ::pyo3::PyClass for #cls { + type Dict = #dict; + type WeakRef = #weakref; + type BaseNativeType = #base_nativetype; + } + } + } + fn impl_extractext(&self) -> TokenStream { + let cls = self.cls; + quote! { + impl<'a> ::pyo3::derive_utils::ExtractExt<'a> for &'a #cls + { + type Target = ::pyo3::PyRef<'a, #cls>; + } + + impl<'a> ::pyo3::derive_utils::ExtractExt<'a> for &'a mut #cls + { + type Target = ::pyo3::PyRefMut<'a, #cls>; + } + } + } + + fn impl_into_py(&self) -> TokenStream { + let cls = self.cls; + let attr = self.attr; + // If #cls is not extended type, we allow Self->PyObject conversion + if !attr.has_extends { + quote! { + impl ::pyo3::IntoPy<::pyo3::PyObject> for #cls { + fn into_py(self, py: ::pyo3::Python) -> ::pyo3::PyObject { + ::pyo3::IntoPy::into_py(::pyo3::Py::new(py, self).unwrap(), py) + } + } + } + } else { + quote! {} + } + } + + /// To allow multiple #[pymethods] block, we define inventory types. + fn impl_methods_inventory(&self) -> TokenStream { + let cls = self.cls; + let methods_type = self.methods_type; + match methods_type { + PyClassMethodsType::Specialization => quote! {}, + PyClassMethodsType::Inventory => { + // Try to build a unique type for better error messages + let name = format!("Pyo3MethodsInventoryFor{}", cls.unraw()); + let inventory_cls = syn::Ident::new(&name, Span::call_site()); + + quote! { + #[doc(hidden)] + pub struct #inventory_cls { + methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>, + slots: ::std::vec::Vec<::pyo3::ffi::PyType_Slot>, + } + impl ::pyo3::class::impl_::PyMethodsInventory for #inventory_cls { + fn new( + methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>, + slots: ::std::vec::Vec<::pyo3::ffi::PyType_Slot>, + ) -> Self { + Self { methods, slots } + } + fn methods(&'static self) -> &'static [::pyo3::class::PyMethodDefType] { + &self.methods + } + fn slots(&'static self) -> &'static [::pyo3::ffi::PyType_Slot] { + &self.slots + } + } + + impl ::pyo3::class::impl_::HasMethodsInventory for #cls { + type Methods = #inventory_cls; + } + + ::pyo3::inventory::collect!(#inventory_cls); + } + } + } + } + fn impl_pyclassimpl(&self) -> TokenStream { + let cls = self.cls; + let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc}); + let is_gc = self.attr.is_gc; + let is_basetype = self.attr.is_basetype; + let base = &self.attr.base; + let is_subclass = self.attr.has_extends; + + let thread_checker = if self.attr.has_unsendable { + quote! { ::pyo3::class::impl_::ThreadCheckerImpl<#cls> } + } else if self.attr.has_extends { + quote! { + ::pyo3::class::impl_::ThreadCheckerInherited<#cls, <#cls as ::pyo3::class::impl_::PyClassImpl>::BaseType> + } + } else { + quote! { ::pyo3::class::impl_::ThreadCheckerStub<#cls> } + }; + + let methods_protos = match self.methods_type { + PyClassMethodsType::Specialization => { + quote! { visitor(collector.methods_protocol_slots()); } + } + PyClassMethodsType::Inventory => { + quote! { + for inventory in ::pyo3::inventory::iter::<::Methods>() { + visitor(::pyo3::class::impl_::PyMethodsInventory::slots(inventory)); + } + } + } + }; + let for_each_py_method = match self.methods_type { + PyClassMethodsType::Specialization => quote! { visitor(collector.py_methods()); }, + PyClassMethodsType::Inventory => quote! { + for inventory in ::pyo3::inventory::iter::<::Methods>() { + visitor(::pyo3::class::impl_::PyMethodsInventory::methods(inventory)); + } + }, + }; + quote! { + impl ::pyo3::class::impl_::PyClassImpl for #cls { + const DOC: &'static str = #doc; + const IS_GC: bool = #is_gc; + const IS_BASETYPE: bool = #is_basetype; + const IS_SUBCLASS: bool = #is_subclass; + + type Layout = ::pyo3::PyCell; + type BaseType = #base; + type ThreadChecker = #thread_checker; + + fn for_each_method_def(visitor: &mut dyn ::std::ops::FnMut(&[::pyo3::class::PyMethodDefType])) { + use ::pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + #for_each_py_method; + visitor(collector.py_class_descriptors()); + visitor(collector.object_protocol_methods()); + visitor(collector.async_protocol_methods()); + visitor(collector.descr_protocol_methods()); + visitor(collector.mapping_protocol_methods()); + visitor(collector.number_protocol_methods()); + } + fn get_new() -> ::std::option::Option<::pyo3::ffi::newfunc> { + use ::pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + collector.new_impl() + } + fn get_alloc() -> ::std::option::Option<::pyo3::ffi::allocfunc> { + use ::pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + collector.alloc_impl() + } + fn get_free() -> ::std::option::Option<::pyo3::ffi::freefunc> { + use ::pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + collector.free_impl() + } + + fn for_each_proto_slot(visitor: &mut dyn ::std::ops::FnMut(&[::pyo3::ffi::PyType_Slot])) { + // Implementation which uses dtolnay specialization to load all slots. + use ::pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + visitor(collector.object_protocol_slots()); + visitor(collector.number_protocol_slots()); + visitor(collector.iter_protocol_slots()); + visitor(collector.gc_protocol_slots()); + visitor(collector.descr_protocol_slots()); + visitor(collector.mapping_protocol_slots()); + visitor(collector.sequence_protocol_slots()); + visitor(collector.async_protocol_slots()); + visitor(collector.buffer_protocol_slots()); + #methods_protos + } + + fn get_buffer() -> ::std::option::Option<&'static ::pyo3::class::impl_::PyBufferProcs> { + use ::pyo3::class::impl_::*; + let collector = PyClassImplCollector::::new(); + collector.buffer_procs() + } + } + } + } + + fn impl_freelist(&self) -> TokenStream { + let cls = self.cls; + + self.attr.freelist.as_ref().map_or(quote!{}, |freelist| { + quote! { + impl ::pyo3::class::impl_::PyClassWithFreeList for #cls { + #[inline] + fn get_free_list(_py: ::pyo3::Python<'_>) -> &mut ::pyo3::impl_::freelist::FreeList<*mut ::pyo3::ffi::PyObject> { + static mut FREELIST: *mut ::pyo3::impl_::freelist::FreeList<*mut ::pyo3::ffi::PyObject> = 0 as *mut _; + unsafe { + if FREELIST.is_null() { + FREELIST = ::std::boxed::Box::into_raw(::std::boxed::Box::new( + ::pyo3::impl_::freelist::FreeList::with_capacity(#freelist))); + } + &mut *FREELIST + } + } + } + + impl ::pyo3::class::impl_::PyClassAllocImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { + #[inline] + fn alloc_impl(self) -> ::std::option::Option<::pyo3::ffi::allocfunc> { + ::std::option::Option::Some(::pyo3::class::impl_::alloc_with_freelist::<#cls>) + } + } + + impl ::pyo3::class::impl_::PyClassFreeImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { + #[inline] + fn free_impl(self) -> ::std::option::Option<::pyo3::ffi::freefunc> { + ::std::option::Option::Some(::pyo3::class::impl_::free_with_freelist::<#cls>) + } + } + } + }) + } + /// Enforce at compile time that PyGCProtocol is implemented + fn impl_gc(&self) -> TokenStream { + let cls = self.cls; + let attr = self.attr; + if attr.is_gc { + let closure_name = format!("__assertion_closure_{}", cls); + let closure_token = syn::Ident::new(&closure_name, Span::call_site()); + quote! { + fn #closure_token() { + use ::pyo3::class; + + fn _assert_implements_protocol<'p, T: ::pyo3::class::PyGCProtocol<'p>>() {} + _assert_implements_protocol::<#cls>(); + } + } + } else { + quote! {} + } + } +} diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 54e248c9..e2fbf8d8 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -13,6 +13,7 @@ use quote::quote; use syn::spanned::Spanned; /// The mechanism used to collect `#[pymethods]` into the type object +#[derive(Copy, Clone)] pub enum PyClassMethodsType { Specialization, Inventory, @@ -118,7 +119,7 @@ pub fn impl_methods( }) } -fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> TokenStream { +pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> TokenStream { let member = &spec.rust_ident; let deprecations = &spec.attributes.deprecations; let python_name = &spec.null_terminated_python_name(); diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index a6612d19..f08317d1 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -7,7 +7,7 @@ extern crate proc_macro; use proc_macro::TokenStream; use pyo3_macros_backend::{ - build_derive_from_pyobject, build_py_class, build_py_function, build_py_methods, + build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, build_py_proto, get_doc, process_functions_in_module, py_init, PyClassArgs, PyClassMethodsType, PyFunctionOptions, PyModuleOptions, }; @@ -107,12 +107,17 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream { /// [10]: https://en.wikipedia.org/wiki/Free_list #[proc_macro_attribute] pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { - let methods_type = if cfg!(feature = "multiple-pymethods") { - PyClassMethodsType::Inventory - } else { - PyClassMethodsType::Specialization - }; - pyclass_impl(attr, input, methods_type) + use syn::Item; + let item = parse_macro_input!(input as Item); + match item { + Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()), + Item::Enum(enum_) => pyclass_enum_impl(attr, enum_, methods_type()), + unsupported => { + syn::Error::new_spanned(unsupported, "#[pyclass] only supports structs and enums.") + .to_compile_error() + .into() + } + } } /// A proc macro used to expose methods to Python. @@ -195,12 +200,11 @@ pub fn derive_from_py_object(item: TokenStream) -> TokenStream { } fn pyclass_impl( - attr: TokenStream, - input: TokenStream, + attrs: TokenStream, + mut ast: syn::ItemStruct, methods_type: PyClassMethodsType, ) -> TokenStream { - let mut ast = parse_macro_input!(input as syn::ItemStruct); - let args = parse_macro_input!(attr as PyClassArgs); + let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args); let expanded = build_py_class(&mut ast, &args, methods_type).unwrap_or_else(|e| e.to_compile_error()); @@ -211,6 +215,22 @@ fn pyclass_impl( .into() } +fn pyclass_enum_impl( + attr: TokenStream, + enum_: syn::ItemEnum, + methods_type: PyClassMethodsType, +) -> TokenStream { + let args = parse_macro_input!(attr with PyClassArgs::parse_enum_args); + let expanded = + build_py_enum(&enum_, args, methods_type).unwrap_or_else(|e| e.into_compile_error()); + + quote!( + #enum_ + #expanded + ) + .into() +} + fn pymethods_impl(input: TokenStream, methods_type: PyClassMethodsType) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemImpl); let expanded = @@ -222,3 +242,11 @@ fn pymethods_impl(input: TokenStream, methods_type: PyClassMethodsType) -> Token ) .into() } + +fn methods_type() -> PyClassMethodsType { + if cfg!(feature = "multiple-pymethods") { + PyClassMethodsType::Inventory + } else { + PyClassMethodsType::Specialization + } +} diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 99decdc4..9631b2a1 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -19,6 +19,8 @@ fn _test_compile_errors() { t.compile_fail("tests/ui/invalid_need_module_arg_position.rs"); t.compile_fail("tests/ui/invalid_property_args.rs"); t.compile_fail("tests/ui/invalid_pyclass_args.rs"); + t.compile_fail("tests/ui/invalid_pyclass_enum.rs"); + t.compile_fail("tests/ui/invalid_pyclass_item.rs"); t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); diff --git a/tests/test_enum.rs b/tests/test_enum.rs new file mode 100644 index 00000000..67ed9278 --- /dev/null +++ b/tests/test_enum.rs @@ -0,0 +1,53 @@ +use pyo3::prelude::*; +use pyo3::{py_run, wrap_pyfunction}; + +mod common; + +#[pyclass] +#[derive(Debug, PartialEq, Clone)] +pub enum MyEnum { + Variant, + OtherVariant, +} + +#[test] +fn test_enum_class_attr() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let my_enum = py.get_type::(); + py_assert!(py, my_enum, "getattr(my_enum, 'Variant', None) is not None"); + py_assert!(py, my_enum, "getattr(my_enum, 'foobar', None) is None"); + py_run!(py, my_enum, "my_enum.Variant = None"); +} + +#[pyfunction] +fn return_enum() -> MyEnum { + MyEnum::Variant +} + +#[test] +#[ignore] // need to implement __eq__ +fn test_return_enum() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let f = wrap_pyfunction!(return_enum)(py).unwrap(); + let mynum = py.get_type::(); + + py_run!(py, f mynum, "assert f() == mynum.Variant") +} + +#[pyfunction] +fn enum_arg(e: MyEnum) { + assert_eq!(MyEnum::OtherVariant, e) +} + +#[test] +#[ignore] // need to implement __eq__ +fn test_enum_arg() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let f = wrap_pyfunction!(enum_arg)(py).unwrap(); + let mynum = py.get_type::(); + + py_run!(py, f mynum, "f(mynum.Variant)") +} diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs new file mode 100644 index 00000000..62f2a3d6 --- /dev/null +++ b/tests/ui/invalid_pyclass_enum.rs @@ -0,0 +1,15 @@ +use pyo3::prelude::*; + +#[pyclass(subclass)] +enum NotBaseClass { + x, + y, +} + +#[pyclass(extends = PyList)] +enum NotDrivedClass { + x, + y, +} + +fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr new file mode 100644 index 00000000..2dd0e737 --- /dev/null +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -0,0 +1,11 @@ +error: enums can't be inherited by other classes + --> tests/ui/invalid_pyclass_enum.rs:3:11 + | +3 | #[pyclass(subclass)] + | ^^^^^^^^ + +error: enums cannot extend from other classes + --> tests/ui/invalid_pyclass_enum.rs:9:11 + | +9 | #[pyclass(extends = PyList)] + | ^^^^^^^ diff --git a/tests/ui/invalid_pyclass_item.rs b/tests/ui/invalid_pyclass_item.rs new file mode 100644 index 00000000..4b348540 --- /dev/null +++ b/tests/ui/invalid_pyclass_item.rs @@ -0,0 +1,6 @@ +use pyo3::prelude::*; + +#[pyclass] +fn foo() {} + +fn main() {} diff --git a/tests/ui/invalid_pyclass_item.stderr b/tests/ui/invalid_pyclass_item.stderr new file mode 100644 index 00000000..f29756df --- /dev/null +++ b/tests/ui/invalid_pyclass_item.stderr @@ -0,0 +1,5 @@ +error: #[pyclass] only supports structs and enums. + --> tests/ui/invalid_pyclass_item.rs:4:1 + | +4 | fn foo() {} + | ^^^^^^^^^^^ From 8495d77722f780cdea6c1ca8e27cfe5e70476f1d Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 21 Nov 2021 20:39:52 +0000 Subject: [PATCH 30/30] examples: use setuptools-rust 1.0.0 --- examples/pyo3-benchmarks/requirements-dev.txt | 2 +- examples/setuptools-rust-starter/requirements-dev.txt | 2 +- examples/word-count/requirements-dev.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/pyo3-benchmarks/requirements-dev.txt b/examples/pyo3-benchmarks/requirements-dev.txt index 8f7900ee..8005ca5e 100644 --- a/examples/pyo3-benchmarks/requirements-dev.txt +++ b/examples/pyo3-benchmarks/requirements-dev.txt @@ -1,4 +1,4 @@ pytest>=3.5.0 -setuptools-rust>=0.10.2 +setuptools_rust~=1.0.0 pytest-benchmark~=3.2 pip>=21.3 diff --git a/examples/setuptools-rust-starter/requirements-dev.txt b/examples/setuptools-rust-starter/requirements-dev.txt index 678a0650..c52fb607 100644 --- a/examples/setuptools-rust-starter/requirements-dev.txt +++ b/examples/setuptools-rust-starter/requirements-dev.txt @@ -1,3 +1,3 @@ pytest>=3.5.0 -setuptools_rust~=0.11.0 +setuptools_rust~=1.0.0 pip>=21.3 diff --git a/examples/word-count/requirements-dev.txt b/examples/word-count/requirements-dev.txt index f55c7ca8..dbc0f652 100644 --- a/examples/word-count/requirements-dev.txt +++ b/examples/word-count/requirements-dev.txt @@ -1,3 +1,3 @@ pytest>=3.5.0 -setuptools-rust>=0.10.2 +setuptools_rust~=1.0.0 pytest-benchmark>=3.1.1