Merge branch 'main' into export-conf

This commit is contained in:
Ashley Anderson 2022-03-22 12:59:54 -04:00 committed by GitHub
commit 272d2bc0e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 994 additions and 570 deletions

View file

@ -1,11 +1,5 @@
[alias] [alias]
xtask = "run --package xtask --" xtask = "run --package xtask --"
pyo3_doc = "doc --lib --no-default-features --features=full --no-deps --workspace --open --exclude pyo3-macros --exclude pyo3-macros-backend"
pyo3_doc_scrape = "doc --lib --no-default-features --features=full --no-deps --workspace --open --exclude pyo3-macros --exclude pyo3-macros-backend -Z unstable-options -Z rustdoc-scrape-examples=examples"
pyo3_doc_internal = "doc --lib --no-default-features --features=full --no-deps --workspace --open --document-private-items -Z unstable-options -Z rustdoc-scrape-examples=examples"
[build]
rustdocflags = ["--cfg", "docsrs"]
[target.'cfg(feature = "cargo-clippy")'] [target.'cfg(feature = "cargo-clippy")']
rustflags = [ rustflags = [
@ -21,4 +15,4 @@ rustflags = [
"-Dclippy::todo", "-Dclippy::todo",
"-Dclippy::unnecessary_wraps", "-Dclippy::unnecessary_wraps",
"-Dclippy::useless_transmute", "-Dclippy::useless_transmute",
] ]

View file

@ -5,9 +5,6 @@ Please consider adding the following to your pull request:
- docs to all new functions and / or detail in the guide - docs to all new functions and / or detail in the guide
- tests for all new or changed functions - tests for all new or changed functions
Be aware the CI pipeline will check your pull request for the following. This is done using `nox` (you can install with `pip install nox`): PyO3's CI pipeline will check your pull request. To run its tests
- Rust tests (`cargo test` or `nox -s test-rust`) locally, you can run ```cargo xtask ci```. See its documentation
- Examples (`nox -s test-py`) [here](https://github.com/PyO3/pyo3/tree/main/xtask#readme).
- Rust lints (`nox -s clippy`)
- Rust formatting (`nox -s fmt-rust`)
- Python formatting (`nox -s fmt-py`)

View file

@ -175,8 +175,13 @@ jobs:
- if: matrix.msrv == 'MSRV' - if: matrix.msrv == 'MSRV'
name: Prepare minimal package versions (MSRV only) name: Prepare minimal package versions (MSRV only)
run: | run: |
set -x
cargo update -p indexmap --precise 1.6.2 cargo update -p indexmap --precise 1.6.2
cargo update -p hashbrown:0.12.0 --precise 0.9.1 cargo update -p hashbrown:0.12.0 --precise 0.9.1
PROJECTS=("." "examples/decorator" "examples/maturin-starter" "examples/setuptools-rust-starter" "examples/word-count")
for PROJ in ${PROJECTS[@]}; do
cargo update --manifest-path "$PROJ/Cargo.toml" -p parking_lot --precise 0.11.0
done
- name: Build docs - name: Build docs
run: cargo doc --no-deps --no-default-features --features "full ${{ matrix.extra_features }}" run: cargo doc --no-deps --no-default-features --features "full ${{ matrix.extra_features }}"

View file

@ -45,7 +45,7 @@ jobs:
mkdir target mkdir target
mkdir -p gh-pages-build/internal mkdir -p gh-pages-build/internal
echo "<div class='internal-banner' style='position:fixed; z-index: 99999; color:red;border:3px solid red;margin-left: auto; margin-right: auto; width: 430px;left:0;right: 0;'><div style='display: flex; align-items: center; justify-content: center;'> ⚠️ Internal Docs ⚠️ Not Public API 👉 <a href='https://pyo3.rs/main/doc/pyo3/index.html' style='color:red;text-decoration:underline;'>Official Docs Here</a></div></div>" > target/banner.html echo "<div class='internal-banner' style='position:fixed; z-index: 99999; color:red;border:3px solid red;margin-left: auto; margin-right: auto; width: 430px;left:0;right: 0;'><div style='display: flex; align-items: center; justify-content: center;'> ⚠️ Internal Docs ⚠️ Not Public API 👉 <a href='https://pyo3.rs/main/doc/pyo3/index.html' style='color:red;text-decoration:underline;'>Official Docs Here</a></div></div>" > target/banner.html
cargo +nightly pyo3_doc_internal cargo xtask doc --internal
cp -r target/doc gh-pages-build/internal cp -r target/doc gh-pages-build/internal
env: env:
RUSTDOCFLAGS: "--cfg docsrs --Z unstable-options --document-hidden-items --html-before-content target/banner.html" RUSTDOCFLAGS: "--cfg docsrs --Z unstable-options --document-hidden-items --html-before-content target/banner.html"
@ -71,7 +71,7 @@ jobs:
# This adds the docs to gh-pages-build/doc # This adds the docs to gh-pages-build/doc
- name: Build the doc - name: Build the doc
run: | run: |
cargo +nightly pyo3_doc_scrape cargo xtask doc
cp -r target/doc gh-pages-build/doc cp -r target/doc gh-pages-build/doc
echo "<meta http-equiv=refresh content=0;url=pyo3/index.html>" > gh-pages-build/doc/index.html echo "<meta http-equiv=refresh content=0;url=pyo3/index.html>" > gh-pages-build/doc/index.html

1
.gitignore vendored
View file

@ -20,3 +20,4 @@ guide/book/
extensions/stamps/ extensions/stamps/
pip-wheel-metadata pip-wheel-metadata
valgrind-python.supp valgrind-python.supp
*.pyd

View file

@ -13,11 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow dependent crates to access config values from `pyo3-build-config` via cargo link dep env vars. [#2092](https://github.com/PyO3/pyo3/pull/2092) - Allow dependent crates to access config values from `pyo3-build-config` via cargo link dep env vars. [#2092](https://github.com/PyO3/pyo3/pull/2092)
- Added methods on `InterpreterConfig` to run Python scripts using the configured executable. [#2092](https://github.com/PyO3/pyo3/pull/2092) - Added methods on `InterpreterConfig` to run Python scripts using the configured executable. [#2092](https://github.com/PyO3/pyo3/pull/2092)
### Changed
- Allow `#[pyo3(crate = "...", text_signature = "...")]` options to be used directly in `#[pyclass(crate = "...", text_signature = "...")]`. [#2234](https://github.com/PyO3/pyo3/pull/2234)
### Fixed ### Fixed
- Considered `PYTHONFRAMEWORK` when cross compiling in order that on macos cross compiling against a [Framework bundle](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html) is considered shared. [#2233](https://github.com/PyO3/pyo3/pull/2233) - Considered `PYTHONFRAMEWORK` when cross compiling in order that on macos cross compiling against a [Framework bundle](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/FrameworkAnatomy.html) is considered shared. [#2233](https://github.com/PyO3/pyo3/pull/2233)
- Panic during compilation when `PYO3_CROSS_LIB_DIR` is set for some host/target combinations. [#2232](https://github.com/PyO3/pyo3/pull/2232) - Panic during compilation when `PYO3_CROSS_LIB_DIR` is set for some host/target combinations. [#2232](https://github.com/PyO3/pyo3/pull/2232)
- Correct dependency version for `syn` to require correct minimal patch version 1.0.56. [#2240](https://github.com/PyO3/pyo3/pull/2240)
### Added
- Added `as_bytes` on `Py<PyBytes>`. [#2235](https://github.com/PyO3/pyo3/pull/2235)
### Packaging
- Extend `parking_lot` dependency supported versions to include 0.12. [#2239](https://github.com/PyO3/pyo3/pull/2239)
## [0.16.2] - 2022-03-15 ## [0.16.2] - 2022-03-15

View file

@ -16,7 +16,7 @@ edition = "2018"
[dependencies] [dependencies]
cfg-if = "1.0" cfg-if = "1.0"
libc = "0.2.62" libc = "0.2.62"
parking_lot = "0.11.0" parking_lot = ">= 0.11, < 0.13"
# ffi bindings to the python interpreter, split into a seperate crate so they can be used independently # ffi bindings to the python interpreter, split into a seperate crate so they can be used independently
pyo3-ffi = { path = "pyo3-ffi", version = "=0.16.2" } pyo3-ffi = { path = "pyo3-ffi", version = "=0.16.2" }

View file

@ -48,7 +48,7 @@ There are some specific areas of focus where help is currently needed for the do
- Not all APIs had docs or examples when they were made. The goal is to have documentation on all PyO3 APIs ([#306](https://github.com/PyO3/pyo3/issues/306)). If you see an API lacking a doc, please write one and open a PR! - Not all APIs had docs or examples when they were made. The goal is to have documentation on all PyO3 APIs ([#306](https://github.com/PyO3/pyo3/issues/306)). If you see an API lacking a doc, please write one and open a PR!
You can build the docs (including all features) with You can build the docs (including all features) with
```cargo +nightly pyo3_doc_scrape``` ```cargo xtask doc --open```
#### Doctests #### Doctests
@ -87,6 +87,10 @@ Tests run with all supported Python versions with the latest stable Rust compile
If you are adding a new feature, you should add it to the `full` feature in our *Cargo.toml** so that it is tested in CI. If you are adding a new feature, you should add it to the `full` feature in our *Cargo.toml** so that it is tested in CI.
You can run these tests yourself with
```cargo xtask ci```
See [it's documentation](https://github.com/PyO3/pyo3/tree/main/xtask#readme)for more commands you can run.
## Python and Rust version support policy ## Python and Rust version support policy
PyO3 aims to keep sufficient compatibility to make packaging Python extensions built with PyO3 feasible on most common package managers. PyO3 aims to keep sufficient compatibility to make packaging Python extensions built with PyO3 feasible on most common package managers.

View file

@ -195,22 +195,16 @@ Python::with_gil(|py|{
## Customizing the class ## Customizing the class
The `#[pyclass]` macro accepts the following parameters: {{#include ../../pyo3-macros/docs/pyclass_parameters.md}}
* `name="XXX"` - Set the class name shown in Python code. By default, the struct name is used as the class name. [params-1]: {{#PYO3_DOCS_URL}}/pyo3/prelude/struct.PyAny.html
* `freelist=XXX` - The `freelist` parameter adds support of free allocation list to custom class. [params-2]: https://en.wikipedia.org/wiki/Free_list
The performance improvement applies to types that are often created and deleted in a row, [params-3]: https://doc.rust-lang.org/stable/std/marker/trait.Send.html
so that they can benefit from a freelist. `XXX` is a number of items for the free list. [params-4]: https://doc.rust-lang.org/stable/std/rc/struct.Rc.html
* `gc` - Classes with the `gc` parameter participate in Python garbage collection. [params-5]: https://doc.rust-lang.org/stable/std/sync/struct.Rc.html
If a custom class contains references to other Python objects that can be collected, the [`PyGCProtocol`]({{#PYO3_DOCS_URL}}/pyo3/class/gc/trait.PyGCProtocol.html) trait has to be implemented. [params-6]: https://docs.python.org/3/library/weakref.html
* `weakref` - Adds support for Python weak references.
* `extends=BaseType` - Use a custom base class. The base `BaseType` must implement `PyTypeInfo`. `enum` pyclasses can't use a custom base class. These parameters are covered in various sections of this guide.
* `subclass` - Allows Python classes to inherit from this class. `enum` pyclasses can't be inherited from.
* `dict` - Adds `__dict__` support, so that the instances of this type have a dictionary containing arbitrary instance variables.
* `unsendable` - Making it safe to expose `!Send` structs to Python, where all object can be accessed
by multiple threads. A class marked with `unsendable` panics when accessed by another thread.
* `module="XXX"` - Set the name of the module the class will be shown as defined in. If not given, the class
will be a virtual member of the `builtins` module.
### Return type ### Return type
@ -716,7 +710,7 @@ num=-1
## Making class method signatures available to Python ## Making class method signatures available to Python
The [`#[pyo3(text_signature = "...")]`](./function.md#text_signature) option for `#[pyfunction]` also works for classes and methods: The [`text_signature = "..."`](./function.md#text_signature) option for `#[pyfunction]` also works for classes and methods:
```rust ```rust
# #![allow(dead_code)] # #![allow(dead_code)]
@ -724,8 +718,7 @@ use pyo3::prelude::*;
use pyo3::types::PyType; use pyo3::types::PyType;
// it works even if the item is not documented: // it works even if the item is not documented:
#[pyclass] #[pyclass(text_signature = "(c, d, /)")]
#[pyo3(text_signature = "(c, d, /)")]
struct MyClass {} struct MyClass {}
#[pymethods] #[pymethods]

View file

@ -9,7 +9,7 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md).
PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. This enables use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project. PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. This enables use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project.
### `#[pyproto]` has been deprecated ### `#[pyproto]` has been deprecated
In PyO3 0.15, the `#[pymethods]` attribute macro gained support for implementing "magic methods" such as `__str__` (aka "dunder" methods). This implementation was not quite finalized at the time, with a few edge cases to be decided upon. The existing `#[pyproto]` attribute macro was left untouched, because it covered these edge cases. In PyO3 0.15, the `#[pymethods]` attribute macro gained support for implementing "magic methods" such as `__str__` (aka "dunder" methods). This implementation was not quite finalized at the time, with a few edge cases to be decided upon. The existing `#[pyproto]` attribute macro was left untouched, because it covered these edge cases.

View file

@ -19,7 +19,7 @@ proc-macro2 = { version = "1", default-features = false }
pyo3-build-config = { path = "../pyo3-build-config", version = "0.16.2", features = ["resolve-config"] } pyo3-build-config = { path = "../pyo3-build-config", version = "0.16.2", features = ["resolve-config"] }
[dependencies.syn] [dependencies.syn]
version = "1" version = "1.0.56"
default-features = false default-features = false
features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"]

View file

@ -1,77 +1,107 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{ use syn::{
parse::{Parse, ParseStream}, parse::{Parse, ParseStream},
punctuated::Punctuated, punctuated::Punctuated,
spanned::Spanned,
token::Comma, token::Comma,
Attribute, ExprPath, Ident, LitStr, Path, Result, Token, Attribute, Expr, ExprPath, Ident, LitStr, Path, Result, Token,
}; };
pub mod kw { pub mod kw {
syn::custom_keyword!(annotation); syn::custom_keyword!(annotation);
syn::custom_keyword!(attribute); syn::custom_keyword!(attribute);
syn::custom_keyword!(dict);
syn::custom_keyword!(extends);
syn::custom_keyword!(freelist);
syn::custom_keyword!(from_py_with); syn::custom_keyword!(from_py_with);
syn::custom_keyword!(gc);
syn::custom_keyword!(get); syn::custom_keyword!(get);
syn::custom_keyword!(item); syn::custom_keyword!(item);
syn::custom_keyword!(pass_module); syn::custom_keyword!(module);
syn::custom_keyword!(name); syn::custom_keyword!(name);
syn::custom_keyword!(pass_module);
syn::custom_keyword!(set); syn::custom_keyword!(set);
syn::custom_keyword!(signature); syn::custom_keyword!(signature);
syn::custom_keyword!(subclass);
syn::custom_keyword!(text_signature); syn::custom_keyword!(text_signature);
syn::custom_keyword!(transparent); syn::custom_keyword!(transparent);
syn::custom_keyword!(unsendable);
syn::custom_keyword!(weakref);
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug)]
pub struct FromPyWithAttribute(pub ExprPath); pub struct KeywordAttribute<K, V> {
pub kw: K,
pub value: V,
}
impl Parse for FromPyWithAttribute { /// A helper type which parses the inner type via a literal string
/// e.g. LitStrValue<Path> -> parses "some::path" in quotes.
#[derive(Clone, Debug, PartialEq)]
pub struct LitStrValue<T>(pub T);
impl<T: Parse> Parse for LitStrValue<T> {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
let _: kw::from_py_with = input.parse()?; let lit_str: LitStr = input.parse()?;
let _: Token![=] = input.parse()?; lit_str.parse().map(LitStrValue)
let string_literal: LitStr = input.parse()?;
string_literal.parse().map(FromPyWithAttribute)
} }
} }
#[derive(Clone, Debug, PartialEq)] impl<T: ToTokens> ToTokens for LitStrValue<T> {
pub struct NameAttribute(pub Ident); fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens)
impl Parse for NameAttribute {
fn parse(input: ParseStream) -> Result<Self> {
let _: kw::name = input.parse()?;
let _: Token![=] = input.parse()?;
let string_literal: LitStr = input.parse()?;
string_literal.parse().map(NameAttribute)
} }
} }
/// A helper type which parses a name via a literal string
#[derive(Clone, Debug, PartialEq)]
pub struct NameLitStr(pub Ident);
impl Parse for NameLitStr {
fn parse(input: ParseStream) -> Result<Self> {
let string_literal: LitStr = input.parse()?;
if let Ok(ident) = string_literal.parse() {
Ok(NameLitStr(ident))
} else {
bail_spanned!(string_literal.span() => "expected a single identifier in double quotes")
}
}
}
impl ToTokens for NameLitStr {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens)
}
}
pub type ExtendsAttribute = KeywordAttribute<kw::extends, Path>;
pub type FreelistAttribute = KeywordAttribute<kw::freelist, Box<Expr>>;
pub type ModuleAttribute = KeywordAttribute<kw::module, LitStr>;
pub type NameAttribute = KeywordAttribute<kw::name, NameLitStr>;
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, LitStr>;
impl<K: Parse + std::fmt::Debug, V: Parse> Parse for KeywordAttribute<K, V> {
fn parse(input: ParseStream) -> Result<Self> {
let kw: K = input.parse()?;
let _: Token![=] = input.parse()?;
let value = input.parse()?;
Ok(KeywordAttribute { kw, value })
}
}
impl<K: ToTokens, V: ToTokens> ToTokens for KeywordAttribute<K, V> {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.kw.to_tokens(tokens);
Token![=](self.kw.span()).to_tokens(tokens);
self.value.to_tokens(tokens);
}
}
pub type FromPyWithAttribute = KeywordAttribute<kw::from_py_with, LitStrValue<ExprPath>>;
/// For specifying the path to the pyo3 crate. /// For specifying the path to the pyo3 crate.
#[derive(Clone, Debug, PartialEq)] pub type CrateAttribute = KeywordAttribute<Token![crate], LitStrValue<Path>>;
pub struct CrateAttribute(pub Path);
impl Parse for CrateAttribute {
fn parse(input: ParseStream) -> Result<Self> {
let _: Token![crate] = input.parse()?;
let _: Token![=] = input.parse()?;
let string_literal: LitStr = input.parse()?;
string_literal.parse().map(CrateAttribute)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextSignatureAttribute {
pub kw: kw::text_signature,
pub eq_token: Token![=],
pub lit: LitStr,
}
impl Parse for TextSignatureAttribute {
fn parse(input: ParseStream) -> Result<Self> {
Ok(TextSignatureAttribute {
kw: input.parse()?,
eq_token: input.parse()?,
lit: input.parse()?,
})
}
}
pub fn get_pyo3_options<T: Parse>(attr: &syn::Attribute) -> Result<Option<Punctuated<T, Comma>>> { pub fn get_pyo3_options<T: Parse>(attr: &syn::Attribute) -> Result<Option<Punctuated<T, Comma>>> {
if is_attribute_ident(attr, "pyo3") { if is_attribute_ident(attr, "pyo3") {

View file

@ -252,7 +252,9 @@ impl<'a> Container<'a> {
None => quote!( None => quote!(
obj.get_item(#index)?.extract() obj.get_item(#index)?.extract()
), ),
Some(FromPyWithAttribute(expr_path)) => quote! ( Some(FromPyWithAttribute {
value: expr_path, ..
}) => quote! (
#expr_path(obj.get_item(#index)?) #expr_path(obj.get_item(#index)?)
), ),
}; };
@ -308,7 +310,9 @@ impl<'a> Container<'a> {
new_err.set_cause(py, ::std::option::Option::Some(inner)); new_err.set_cause(py, ::std::option::Option::Some(inner));
new_err new_err
})?), })?),
Some(FromPyWithAttribute(expr_path)) => quote! ( Some(FromPyWithAttribute {
value: expr_path, ..
}) => quote! (
#expr_path(#get_field).map_err(|inner| { #expr_path(#get_field).map_err(|inner| {
let py = _pyo3::PyNativeType::py(obj); let py = _pyo3::PyNativeType::py(obj);
let new_err = _pyo3::exceptions::PyTypeError::new_err(#conversion_error_msg); let new_err = _pyo3::exceptions::PyTypeError::new_err(#conversion_error_msg);
@ -388,7 +392,7 @@ impl ContainerOptions {
ContainerPyO3Attribute::Crate(path) => { ContainerPyO3Attribute::Crate(path) => {
ensure_spanned!( ensure_spanned!(
options.krate.is_none(), options.krate.is_none(),
path.0.span() => "`crate` may only be provided once" path.span() => "`crate` may only be provided once"
); );
options.krate = Some(path); options.krate = Some(path);
} }

View file

@ -21,7 +21,7 @@ pub struct ConstSpec {
impl ConstSpec { impl ConstSpec {
pub fn python_name(&self) -> Cow<Ident> { pub fn python_name(&self) -> Cow<Ident> {
if let Some(name) = &self.attributes.name { if let Some(name) = &self.attributes.name {
Cow::Borrowed(&name.0) Cow::Borrowed(&name.value.0)
} else { } else {
Cow::Owned(self.rust_ident.unraw()) Cow::Owned(self.rust_ident.unraw())
} }
@ -89,7 +89,7 @@ impl ConstAttributes {
fn set_name(&mut self, name: NameAttribute) -> Result<()> { fn set_name(&mut self, name: NameAttribute) -> Result<()> {
ensure_spanned!( ensure_spanned!(
self.name.is_none(), self.name.is_none(),
name.0.span() => "`name` may only be specified once" name.span() => "`name` may only be specified once"
); );
self.name = Some(name); self.name = Some(name);
Ok(()) Ok(())

View file

@ -14,7 +14,7 @@ use syn::ext::IdentExt;
use syn::spanned::Spanned; use syn::spanned::Spanned;
use syn::Result; use syn::Result;
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, Debug)]
pub struct FnArg<'a> { pub struct FnArg<'a> {
pub name: &'a syn::Ident, pub name: &'a syn::Ident,
pub by_ref: &'a Option<syn::token::Ref>, pub by_ref: &'a Option<syn::token::Ref>,
@ -273,7 +273,7 @@ impl<'a> FnSpec<'a> {
ty: fn_type_attr, ty: fn_type_attr,
args: fn_attrs, args: fn_attrs,
mut python_name, mut python_name,
} = parse_method_attributes(meth_attrs, name.map(|name| name.0), &mut deprecations)?; } = parse_method_attributes(meth_attrs, name.map(|name| name.value.0), &mut deprecations)?;
let (fn_type, skip_first_arg, fixed_convention) = let (fn_type, skip_first_arg, fixed_convention) =
Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?; Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?;

View file

@ -31,7 +31,7 @@ impl PyModuleOptions {
for option in take_pyo3_options(attrs)? { for option in take_pyo3_options(attrs)? {
match option { match option {
PyModulePyO3Option::Name(name) => options.set_name(name.0)?, PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?,
PyModulePyO3Option::Crate(path) => options.set_crate(path)?, PyModulePyO3Option::Crate(path) => options.set_crate(path)?,
} }
} }
@ -52,7 +52,7 @@ impl PyModuleOptions {
fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { fn set_crate(&mut self, path: CrateAttribute) -> Result<()> {
ensure_spanned!( ensure_spanned!(
self.krate.is_none(), self.krate.is_none(),
path.0.span() => "`crate` may only be specified once" path.span() => "`crate` may only be specified once"
); );
self.krate = Some(path); self.krate = Some(path);

View file

@ -231,7 +231,9 @@ fn impl_arg_param(
let arg_value = quote_arg_span!(#args_array[#option_pos]); let arg_value = quote_arg_span!(#args_array[#option_pos]);
*option_pos += 1; *option_pos += 1;
let arg_value_or_default = if let Some(FromPyWithAttribute(expr_path)) = &arg.attrs.from_py_with let arg_value_or_default = if let Some(FromPyWithAttribute {
value: expr_path, ..
}) = &arg.attrs.from_py_with
{ {
match (spec.default_value(name), arg.optional.is_some()) { match (spec.default_value(name), arg.optional.is_some()) {
(Some(default), true) if default.to_string() != "None" => { (Some(default), true) if default.to_string() != "None" => {

View file

@ -1,19 +1,20 @@
// Copyright (c) 2017-present PyO3 Project and Contributors // Copyright (c) 2017-present PyO3 Project and Contributors
use crate::attributes::{ use crate::attributes::{
self, take_pyo3_options, CrateAttribute, NameAttribute, TextSignatureAttribute, self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute,
ModuleAttribute, NameAttribute, NameLitStr, TextSignatureAttribute,
}; };
use crate::deprecations::{Deprecation, Deprecations}; use crate::deprecations::{Deprecation, Deprecations};
use crate::konst::{ConstAttributes, ConstSpec}; use crate::konst::{ConstAttributes, ConstSpec};
use crate::pyimpl::{gen_default_items, gen_py_const, PyClassMethodsType}; use crate::pyimpl::{gen_default_items, gen_py_const, PyClassMethodsType};
use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType};
use crate::utils::{self, get_pyo3_crate, unwrap_group, PythonDoc}; use crate::utils::{self, get_pyo3_crate, PythonDoc};
use proc_macro2::{Span, TokenStream}; use proc_macro2::{Span, TokenStream};
use quote::quote; use quote::quote;
use syn::ext::IdentExt; use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream}; use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated; use syn::punctuated::Punctuated;
use syn::{parse_quote, spanned::Spanned, Expr, Result, Token}; //unraw use syn::{parse_quote, spanned::Spanned, Result, Token};
/// If the class is derived from a Rust `struct` or `enum`. /// If the class is derived from a Rust `struct` or `enum`.
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@ -24,27 +25,18 @@ pub enum PyClassKind {
/// The parsed arguments of the pyclass macro /// The parsed arguments of the pyclass macro
pub struct PyClassArgs { pub struct PyClassArgs {
pub freelist: Option<syn::Expr>,
pub name: Option<syn::Ident>,
pub base: syn::TypePath,
pub has_dict: bool,
pub has_weaklist: bool,
pub is_basetype: bool,
pub has_extends: bool,
pub has_unsendable: bool,
pub module: Option<syn::LitStr>,
pub class_kind: PyClassKind, pub class_kind: PyClassKind,
pub options: PyClassPyO3Options,
pub deprecations: Deprecations, pub deprecations: Deprecations,
} }
impl PyClassArgs { impl PyClassArgs {
fn parse(input: ParseStream, kind: PyClassKind) -> Result<Self> { fn parse(input: ParseStream, kind: PyClassKind) -> Result<Self> {
let mut slf = PyClassArgs::new(kind); Ok(PyClassArgs {
let vars = Punctuated::<Expr, Token![,]>::parse_terminated(input)?; class_kind: kind,
for expr in vars { options: PyClassPyO3Options::parse(input)?,
slf.add_expr(&expr)?; deprecations: Deprecations::new(),
} })
Ok(slf)
} }
pub fn parse_stuct_args(input: ParseStream) -> syn::Result<Self> { pub fn parse_stuct_args(input: ParseStream) -> syn::Result<Self> {
@ -54,155 +46,64 @@ impl PyClassArgs {
pub fn parse_enum_args(input: ParseStream) -> syn::Result<Self> { pub fn parse_enum_args(input: ParseStream) -> syn::Result<Self> {
Self::parse(input, PyClassKind::Enum) Self::parse(input, PyClassKind::Enum)
} }
fn new(class_kind: PyClassKind) -> Self {
PyClassArgs {
freelist: None,
name: None,
module: None,
base: parse_quote! { _pyo3::PyAny },
has_dict: false,
has_weaklist: false,
is_basetype: false,
has_extends: false,
has_unsendable: false,
class_kind,
deprecations: Deprecations::new(),
}
}
/// Add a 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<()> {
match expr {
syn::Expr::Path(exp) if exp.path.segments.len() == 1 => self.add_path(exp),
syn::Expr::Assign(assign) => self.add_assign(assign),
_ => bail_spanned!(expr.span() => "failed to parse arguments"),
}
}
/// Match a key/value flag
fn add_assign(&mut self, assign: &syn::ExprAssign) -> syn::Result<()> {
let syn::ExprAssign { left, right, .. } = assign;
let key = match &**left {
syn::Expr::Path(exp) if exp.path.segments.len() == 1 => {
exp.path.segments.first().unwrap().ident.to_string()
}
_ => bail_spanned!(assign.span() => "failed to parse arguments"),
};
macro_rules! expected {
($expected: literal) => {
expected!($expected, right.span())
};
($expected: literal, $span: expr) => {
bail_spanned!($span => concat!("expected ", $expected))
};
}
match key.as_str() {
"freelist" => {
// We allow arbitrary expressions here so you can e.g. use `8*64`
self.freelist = Some(syn::Expr::clone(right));
}
"name" => match unwrap_group(&**right) {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) => {
self.name = Some(lit.parse().map_err(|_| {
err_spanned!(
lit.span() => "expected a single identifier in double-quotes")
})?);
}
syn::Expr::Path(exp) if exp.path.segments.len() == 1 => {
bail_spanned!(
exp.span() => format!(
"since PyO3 0.13 a pyclass name should be in double-quotes, \
e.g. \"{}\"",
exp.path.get_ident().expect("path has 1 segment")
)
);
}
_ => expected!("type name (e.g. \"Name\")"),
},
"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,
};
self.has_extends = true;
}
_ => expected!("type path (e.g., my_mod::BaseClass)"),
},
"module" => match unwrap_group(&**right) {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) => {
self.module = Some(lit.clone());
}
_ => expected!(r#"string literal (e.g., "my_mod")"#),
},
_ => expected!("one of freelist/name/extends/module", left.span()),
};
Ok(())
}
/// Match a single flag
fn add_path(&mut self, exp: &syn::ExprPath) -> syn::Result<()> {
let flag = exp.path.segments.first().unwrap().ident.to_string();
match flag.as_str() {
"gc" => self
.deprecations
.push(Deprecation::PyClassGcOption, exp.span()),
"weakref" => {
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" => {
self.has_dict = true;
}
"unsendable" => {
self.has_unsendable = true;
}
_ => bail_spanned!(
exp.path.span() => "expected one of gc/weakref/subclass/dict/unsendable"
),
};
Ok(())
}
} }
#[derive(Default)] #[derive(Default)]
pub struct PyClassPyO3Options { pub struct PyClassPyO3Options {
pub text_signature: Option<TextSignatureAttribute>,
pub deprecations: Deprecations,
pub krate: Option<CrateAttribute>, pub krate: Option<CrateAttribute>,
pub dict: Option<kw::dict>,
pub extends: Option<ExtendsAttribute>,
pub freelist: Option<FreelistAttribute>,
pub module: Option<ModuleAttribute>,
pub name: Option<NameAttribute>,
pub subclass: Option<kw::subclass>,
pub text_signature: Option<TextSignatureAttribute>,
pub unsendable: Option<kw::unsendable>,
pub weakref: Option<kw::weakref>,
pub deprecations: Deprecations,
} }
enum PyClassPyO3Option { enum PyClassPyO3Option {
TextSignature(TextSignatureAttribute),
Crate(CrateAttribute), Crate(CrateAttribute),
Dict(kw::dict),
Extends(ExtendsAttribute),
Freelist(FreelistAttribute),
Module(ModuleAttribute),
Name(NameAttribute),
Subclass(kw::subclass),
TextSignature(TextSignatureAttribute),
Unsendable(kw::unsendable),
Weakref(kw::weakref),
DeprecatedGC(kw::gc),
} }
impl Parse for PyClassPyO3Option { impl Parse for PyClassPyO3Option {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
let lookahead = input.lookahead1(); let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::text_signature) { if lookahead.peek(Token![crate]) {
input.parse().map(PyClassPyO3Option::TextSignature)
} else if lookahead.peek(Token![crate]) {
input.parse().map(PyClassPyO3Option::Crate) input.parse().map(PyClassPyO3Option::Crate)
} else if lookahead.peek(kw::dict) {
input.parse().map(PyClassPyO3Option::Dict)
} else if lookahead.peek(kw::extends) {
input.parse().map(PyClassPyO3Option::Extends)
} else if lookahead.peek(attributes::kw::freelist) {
input.parse().map(PyClassPyO3Option::Freelist)
} else if lookahead.peek(attributes::kw::module) {
input.parse().map(PyClassPyO3Option::Module)
} else if lookahead.peek(kw::name) {
input.parse().map(PyClassPyO3Option::Name)
} else if lookahead.peek(attributes::kw::subclass) {
input.parse().map(PyClassPyO3Option::Subclass)
} else if lookahead.peek(attributes::kw::text_signature) {
input.parse().map(PyClassPyO3Option::TextSignature)
} else if lookahead.peek(attributes::kw::unsendable) {
input.parse().map(PyClassPyO3Option::Unsendable)
} else if lookahead.peek(attributes::kw::weakref) {
input.parse().map(PyClassPyO3Option::Weakref)
} else if lookahead.peek(attributes::kw::gc) {
input.parse().map(PyClassPyO3Option::DeprecatedGC)
} else { } else {
Err(lookahead.error()) Err(lookahead.error())
} }
@ -210,57 +111,69 @@ impl Parse for PyClassPyO3Option {
} }
impl PyClassPyO3Options { impl PyClassPyO3Options {
pub fn take_pyo3_options(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> { fn parse(input: ParseStream) -> syn::Result<Self> {
let mut options: PyClassPyO3Options = Default::default(); let mut options: PyClassPyO3Options = Default::default();
for option in take_pyo3_options(attrs)? {
match option { for option in Punctuated::<PyClassPyO3Option, syn::Token![,]>::parse_terminated(input)? {
PyClassPyO3Option::TextSignature(text_signature) => { options.set_option(option)?;
options.set_text_signature(text_signature)?;
}
PyClassPyO3Option::Crate(path) => {
options.set_crate(path)?;
}
}
} }
Ok(options) Ok(options)
} }
pub fn set_text_signature( pub fn take_pyo3_options(&mut self, attrs: &mut Vec<syn::Attribute>) -> syn::Result<()> {
&mut self, take_pyo3_options(attrs)?
text_signature: TextSignatureAttribute, .into_iter()
) -> syn::Result<()> { .try_for_each(|option| self.set_option(option))
ensure_spanned!(
self.text_signature.is_none(),
text_signature.kw.span() => "`text_signature` may only be specified once"
);
self.text_signature = Some(text_signature);
Ok(())
} }
pub fn set_crate(&mut self, path: CrateAttribute) -> syn::Result<()> { fn set_option(&mut self, option: PyClassPyO3Option) -> syn::Result<()> {
ensure_spanned!( macro_rules! set_option {
self.krate.is_none(), ($key:ident) => {
path.0.span() => "`text_signature` may only be specified once" {
); ensure_spanned!(
self.krate = Some(path); self.$key.is_none(),
$key.span() => concat!("`", stringify!($key), "` may only be specified once")
);
self.$key = Some($key);
}
};
}
match option {
PyClassPyO3Option::Crate(krate) => set_option!(krate),
PyClassPyO3Option::Dict(dict) => set_option!(dict),
PyClassPyO3Option::Extends(extends) => set_option!(extends),
PyClassPyO3Option::Freelist(freelist) => set_option!(freelist),
PyClassPyO3Option::Module(module) => set_option!(module),
PyClassPyO3Option::Name(name) => set_option!(name),
PyClassPyO3Option::Subclass(subclass) => set_option!(subclass),
PyClassPyO3Option::TextSignature(text_signature) => set_option!(text_signature),
PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable),
PyClassPyO3Option::Weakref(weakref) => set_option!(weakref),
PyClassPyO3Option::DeprecatedGC(gc) => self
.deprecations
.push(Deprecation::PyClassGcOption, gc.span()),
}
Ok(()) Ok(())
} }
} }
pub fn build_py_class( pub fn build_py_class(
class: &mut syn::ItemStruct, class: &mut syn::ItemStruct,
args: &PyClassArgs, mut args: PyClassArgs,
methods_type: PyClassMethodsType, methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> { ) -> syn::Result<TokenStream> {
let options = PyClassPyO3Options::take_pyo3_options(&mut class.attrs)?; args.options.take_pyo3_options(&mut class.attrs)?;
let doc = utils::get_doc( let doc = utils::get_doc(
&class.attrs, &class.attrs,
options args.options
.text_signature .text_signature
.as_ref() .as_ref()
.map(|attr| (get_class_python_name(&class.ident, args), attr)), .map(|attr| (get_class_python_name(&class.ident, &args), attr)),
); );
let krate = get_pyo3_crate(&options.krate); let krate = get_pyo3_crate(&args.options.krate);
ensure_spanned!( ensure_spanned!(
class.generics.params.is_empty(), class.generics.params.is_empty(),
@ -290,15 +203,7 @@ pub fn build_py_class(
} }
}; };
impl_class( impl_class(&class.ident, &args, doc, field_options, methods_type, krate)
&class.ident,
args,
doc,
field_options,
methods_type,
options.deprecations,
krate,
)
} }
/// `#[pyo3()]` options for pyclass fields /// `#[pyo3()]` options for pyclass fields
@ -356,7 +261,7 @@ impl FieldPyO3Options {
FieldPyO3Option::Name(name) => { FieldPyO3Option::Name(name) => {
ensure_spanned!( ensure_spanned!(
options.name.is_none(), options.name.is_none(),
name.0.span() => "`name` may only be specified once" name.span() => "`name` may only be specified once"
); );
options.name = Some(name); options.name = Some(name);
} }
@ -367,24 +272,27 @@ impl FieldPyO3Options {
} }
} }
fn get_class_python_name<'a>(cls: &'a syn::Ident, attr: &'a PyClassArgs) -> &'a syn::Ident { fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> &'a syn::Ident {
attr.name.as_ref().unwrap_or(cls) args.options
.name
.as_ref()
.map(|name_attr| &name_attr.value.0)
.unwrap_or(cls)
} }
fn impl_class( fn impl_class(
cls: &syn::Ident, cls: &syn::Ident,
attr: &PyClassArgs, args: &PyClassArgs,
doc: PythonDoc, doc: PythonDoc,
field_options: Vec<(&syn::Field, FieldPyO3Options)>, field_options: Vec<(&syn::Field, FieldPyO3Options)>,
methods_type: PyClassMethodsType, methods_type: PyClassMethodsType,
deprecations: Deprecations,
krate: syn::Path, krate: syn::Path,
) -> syn::Result<TokenStream> { ) -> syn::Result<TokenStream> {
let pytypeinfo_impl = impl_pytypeinfo(cls, attr, Some(&deprecations)); let pytypeinfo_impl = impl_pytypeinfo(cls, args, Some(&args.options.deprecations));
let py_class_impl = PyClassImplsBuilder::new( let py_class_impl = PyClassImplsBuilder::new(
cls, cls,
attr, args,
methods_type, methods_type,
descriptors_to_items(cls, field_options)?, descriptors_to_items(cls, field_options)?,
vec![], vec![],
@ -458,23 +366,28 @@ impl<'a> PyClassEnum<'a> {
pub fn build_py_enum( pub fn build_py_enum(
enum_: &mut syn::ItemEnum, enum_: &mut syn::ItemEnum,
args: &PyClassArgs, mut args: PyClassArgs,
method_type: PyClassMethodsType, method_type: PyClassMethodsType,
) -> syn::Result<TokenStream> { ) -> syn::Result<TokenStream> {
let options = PyClassPyO3Options::take_pyo3_options(&mut enum_.attrs)?; args.options.take_pyo3_options(&mut enum_.attrs)?;
if enum_.variants.is_empty() { if let Some(extends) = &args.options.extends {
bail_spanned!(enum_.brace_token.span => "Empty enums can't be #[pyclass]."); bail_spanned!(extends.span() => "enums can't extend from other classes");
} else if let Some(subclass) = &args.options.subclass {
bail_spanned!(subclass.span() => "enums can't be inherited by other classes");
} else if enum_.variants.is_empty() {
bail_spanned!(enum_.brace_token.span => "#[pyclass] can't be used on enums without any variants");
} }
let doc = utils::get_doc( let doc = utils::get_doc(
&enum_.attrs, &enum_.attrs,
options args.options
.text_signature .text_signature
.as_ref() .as_ref()
.map(|attr| (get_class_python_name(&enum_.ident, args), attr)), .map(|attr| (get_class_python_name(&enum_.ident, &args), attr)),
); );
let enum_ = PyClassEnum::new(enum_)?; let enum_ = PyClassEnum::new(enum_)?;
Ok(impl_enum(enum_, args, doc, method_type, options)) Ok(impl_enum(enum_, &args, doc, method_type))
} }
fn impl_enum( fn impl_enum(
@ -482,9 +395,8 @@ fn impl_enum(
args: &PyClassArgs, args: &PyClassArgs,
doc: PythonDoc, doc: PythonDoc,
methods_type: PyClassMethodsType, methods_type: PyClassMethodsType,
options: PyClassPyO3Options,
) -> TokenStream { ) -> TokenStream {
let krate = get_pyo3_crate(&options.krate); let krate = get_pyo3_crate(&args.options.krate);
impl_enum_class(enum_, args, doc, methods_type, krate) impl_enum_class(enum_, args, doc, methods_type, krate)
} }
@ -613,7 +525,10 @@ fn enum_default_methods<'a>(
rust_ident: ident.clone(), rust_ident: ident.clone(),
attributes: ConstAttributes { attributes: ConstAttributes {
is_class_attr: true, is_class_attr: true,
name: Some(NameAttribute(ident.clone())), name: Some(NameAttribute {
kw: syn::parse_quote! { name },
value: NameLitStr(ident.clone()),
}),
deprecations: Default::default(), deprecations: Default::default(),
}, },
}; };
@ -649,7 +564,7 @@ fn descriptors_to_items(
.enumerate() .enumerate()
.flat_map(|(field_index, (field, options))| { .flat_map(|(field_index, (field, options))| {
let name_err = if options.name.is_some() && !options.get && !options.set { let name_err = if options.name.is_some() && !options.get && !options.set {
Some(Err(err_spanned!(options.name.as_ref().unwrap().0.span() => "`name` is useless without `get` or `set`"))) Some(Err(err_spanned!(options.name.as_ref().unwrap().span() => "`name` is useless without `get` or `set`")))
} else { } else {
None None
}; };
@ -686,8 +601,8 @@ fn impl_pytypeinfo(
) -> TokenStream { ) -> TokenStream {
let cls_name = get_class_python_name(cls, attr).to_string(); let cls_name = get_class_python_name(cls, attr).to_string();
let module = if let Some(m) = &attr.module { let module = if let Some(ModuleAttribute { value, .. }) = &attr.options.module {
quote! { ::core::option::Option::Some(#m) } quote! { ::core::option::Option::Some(#value) }
} else { } else {
quote! { ::core::option::Option::None } quote! { ::core::option::Option::None }
}; };
@ -765,20 +680,20 @@ impl<'a> PyClassImplsBuilder<'a> {
fn impl_pyclass(&self) -> TokenStream { fn impl_pyclass(&self) -> TokenStream {
let cls = self.cls; let cls = self.cls;
let attr = self.attr; let attr = self.attr;
let dict = if attr.has_dict { let dict = if attr.options.dict.is_some() {
quote! { _pyo3::impl_::pyclass::PyClassDictSlot } quote! { _pyo3::impl_::pyclass::PyClassDictSlot }
} else { } else {
quote! { _pyo3::impl_::pyclass::PyClassDummySlot } quote! { _pyo3::impl_::pyclass::PyClassDummySlot }
}; };
// insert space for weak ref // insert space for weak ref
let weakref = if attr.has_weaklist { let weakref = if attr.options.weakref.is_some() {
quote! { _pyo3::impl_::pyclass::PyClassWeakRefSlot } quote! { _pyo3::impl_::pyclass::PyClassWeakRefSlot }
} else { } else {
quote! { _pyo3::impl_::pyclass::PyClassDummySlot } quote! { _pyo3::impl_::pyclass::PyClassDummySlot }
}; };
let base_nativetype = if attr.has_extends { let base_nativetype = if attr.options.extends.is_some() {
quote! { <Self::BaseType as _pyo3::impl_::pyclass::PyClassBaseType>::BaseNativeType } quote! { <Self::BaseType as _pyo3::impl_::pyclass::PyClassBaseType>::BaseNativeType }
} else { } else {
quote! { _pyo3::PyAny } quote! { _pyo3::PyAny }
@ -810,7 +725,7 @@ impl<'a> PyClassImplsBuilder<'a> {
let cls = self.cls; let cls = self.cls;
let attr = self.attr; let attr = self.attr;
// If #cls is not extended type, we allow Self->PyObject conversion // If #cls is not extended type, we allow Self->PyObject conversion
if !attr.has_extends { if attr.options.extends.is_none() {
quote! { quote! {
impl _pyo3::IntoPy<_pyo3::PyObject> for #cls { impl _pyo3::IntoPy<_pyo3::PyObject> for #cls {
fn into_py(self, py: _pyo3::Python) -> _pyo3::PyObject { fn into_py(self, py: _pyo3::Python) -> _pyo3::PyObject {
@ -825,11 +740,17 @@ impl<'a> PyClassImplsBuilder<'a> {
fn impl_pyclassimpl(&self) -> TokenStream { fn impl_pyclassimpl(&self) -> TokenStream {
let cls = self.cls; let cls = self.cls;
let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc}); let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc});
let is_basetype = self.attr.is_basetype; let is_basetype = self.attr.options.subclass.is_some();
let base = &self.attr.base; let base = self
let is_subclass = self.attr.has_extends; .attr
.options
.extends
.as_ref()
.map(|extends_attr| extends_attr.value.clone())
.unwrap_or_else(|| parse_quote! { _pyo3::PyAny });
let is_subclass = self.attr.options.extends.is_some();
let dict_offset = if self.attr.has_dict { let dict_offset = if self.attr.options.dict.is_some() {
quote! { quote! {
fn dict_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> { fn dict_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> {
::std::option::Option::Some(_pyo3::impl_::pyclass::dict_offset::<Self>()) ::std::option::Option::Some(_pyo3::impl_::pyclass::dict_offset::<Self>())
@ -840,7 +761,7 @@ impl<'a> PyClassImplsBuilder<'a> {
}; };
// insert space for weak ref // insert space for weak ref
let weaklist_offset = if self.attr.has_weaklist { let weaklist_offset = if self.attr.options.weakref.is_some() {
quote! { quote! {
fn weaklist_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> { fn weaklist_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> {
::std::option::Option::Some(_pyo3::impl_::pyclass::weaklist_offset::<Self>()) ::std::option::Option::Some(_pyo3::impl_::pyclass::weaklist_offset::<Self>())
@ -850,9 +771,9 @@ impl<'a> PyClassImplsBuilder<'a> {
TokenStream::new() TokenStream::new()
}; };
let thread_checker = if self.attr.has_unsendable { let thread_checker = if self.attr.options.unsendable.is_some() {
quote! { _pyo3::impl_::pyclass::ThreadCheckerImpl<#cls> } quote! { _pyo3::impl_::pyclass::ThreadCheckerImpl<#cls> }
} else if self.attr.has_extends { } else if self.attr.options.extends.is_some() {
quote! { quote! {
_pyo3::impl_::pyclass::ThreadCheckerInherited<#cls, <#cls as _pyo3::impl_::pyclass::PyClassImpl>::BaseType> _pyo3::impl_::pyclass::ThreadCheckerInherited<#cls, <#cls as _pyo3::impl_::pyclass::PyClassImpl>::BaseType>
} }
@ -940,7 +861,8 @@ impl<'a> PyClassImplsBuilder<'a> {
fn impl_freelist(&self) -> TokenStream { fn impl_freelist(&self) -> TokenStream {
let cls = self.cls; let cls = self.cls;
self.attr.freelist.as_ref().map_or(quote!{}, |freelist| { self.attr.options.freelist.as_ref().map_or(quote!{}, |freelist| {
let freelist = &freelist.value;
quote! { quote! {
impl _pyo3::impl_::pyclass::PyClassWithFreeList for #cls { impl _pyo3::impl_::pyclass::PyClassWithFreeList for #cls {
#[inline] #[inline]
@ -962,7 +884,7 @@ impl<'a> PyClassImplsBuilder<'a> {
fn freelist_slots(&self) -> Vec<TokenStream> { fn freelist_slots(&self) -> Vec<TokenStream> {
let cls = self.cls; let cls = self.cls;
if self.attr.freelist.is_some() { if self.attr.options.freelist.is_some() {
vec![ vec![
quote! { quote! {
_pyo3::ffi::PyType_Slot { _pyo3::ffi::PyType_Slot {

View file

@ -40,7 +40,7 @@ pub struct PyFunctionSignature {
has_kwargs: bool, has_kwargs: bool,
} }
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, Debug)]
pub struct PyFunctionArgPyO3Attributes { pub struct PyFunctionArgPyO3Attributes {
pub from_py_with: Option<FromPyWithAttribute>, pub from_py_with: Option<FromPyWithAttribute>,
} }
@ -71,7 +71,7 @@ impl PyFunctionArgPyO3Attributes {
PyFunctionArgPyO3Attribute::FromPyWith(from_py_with) => { PyFunctionArgPyO3Attribute::FromPyWith(from_py_with) => {
ensure_spanned!( ensure_spanned!(
attributes.from_py_with.is_none(), attributes.from_py_with.is_none(),
from_py_with.0.span() => "`from_py_with` may only be specified once per argument" from_py_with.span() => "`from_py_with` may only be specified once per argument"
); );
attributes.from_py_with = Some(from_py_with); attributes.from_py_with = Some(from_py_with);
} }
@ -339,7 +339,7 @@ impl PyFunctionOptions {
PyFunctionOption::Crate(path) => { PyFunctionOption::Crate(path) => {
ensure_spanned!( ensure_spanned!(
self.krate.is_none(), self.krate.is_none(),
path.0.span() => "`crate` may only be specified once" path.span() => "`crate` may only be specified once"
); );
self.krate = Some(path); self.krate = Some(path);
} }
@ -351,7 +351,7 @@ impl PyFunctionOptions {
pub fn set_name(&mut self, name: NameAttribute) -> Result<()> { pub fn set_name(&mut self, name: NameAttribute) -> Result<()> {
ensure_spanned!( ensure_spanned!(
self.name.is_none(), self.name.is_none(),
name.0.span() => "`name` may only be specified once" name.span() => "`name` may only be specified once"
); );
self.name = Some(name); self.name = Some(name);
Ok(()) Ok(())
@ -377,7 +377,7 @@ pub fn impl_wrap_pyfunction(
let python_name = options let python_name = options
.name .name
.map_or_else(|| func.sig.ident.unraw(), |name| name.0); .map_or_else(|| func.sig.ident.unraw(), |name| name.value.0);
let signature = options.signature.unwrap_or_default(); let signature = options.signature.unwrap_or_default();

View file

@ -61,7 +61,7 @@ impl PyImplOptions {
fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { fn set_crate(&mut self, path: CrateAttribute) -> Result<()> {
ensure_spanned!( ensure_spanned!(
self.krate.is_none(), self.krate.is_none(),
path.0.span() => "`crate` may only be specified once" path.span() => "`crate` may only be specified once"
); );
self.krate = Some(path); self.krate = Some(path);

View file

@ -544,7 +544,7 @@ impl PropertyType<'_> {
field, python_name, .. field, python_name, ..
} => { } => {
let name = match (python_name, &field.ident) { let name = match (python_name, &field.ident) {
(Some(name), _) => name.0.to_string(), (Some(name), _) => name.value.0.to_string(),
(None, Some(field_name)) => format!("{}\0", field_name.unraw()), (None, Some(field_name)) => format!("{}\0", field_name.unraw()),
(None, None) => { (None, None) => {
bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`");

View file

@ -77,7 +77,8 @@ pub fn get_doc(
syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| { syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| {
if let Some((python_name, text_signature)) = text_signature { if let Some((python_name, text_signature)) = text_signature {
// create special doc string lines to set `__text_signature__` // create special doc string lines to set `__text_signature__`
let signature_lines = format!("{}{}\n--\n\n", python_name, text_signature.lit.value()); let signature_lines =
format!("{}{}\n--\n\n", python_name, text_signature.value.value());
signature_lines.to_tokens(tokens); signature_lines.to_tokens(tokens);
comma.to_tokens(tokens); comma.to_tokens(tokens);
} }
@ -154,13 +155,6 @@ pub fn ensure_not_async_fn(sig: &syn::Signature) -> syn::Result<()> {
Ok(()) Ok(())
} }
pub fn unwrap_group(mut expr: &syn::Expr) -> &syn::Expr {
while let syn::Expr::Group(g) = expr {
expr = &*g.expr;
}
expr
}
pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type {
while let syn::Type::Group(g) = ty { while let syn::Type::Group(g) = ty {
ty = &*g.elem; ty = &*g.elem;
@ -193,6 +187,6 @@ pub(crate) fn replace_self(ty: &mut syn::Type, cls: &syn::Type) {
/// Extract the path to the pyo3 crate, or use the default (`::pyo3`). /// Extract the path to the pyo3 crate, or use the default (`::pyo3`).
pub(crate) fn get_pyo3_crate(attr: &Option<CrateAttribute>) -> syn::Path { pub(crate) fn get_pyo3_crate(attr: &Option<CrateAttribute>) -> syn::Path {
attr.as_ref() attr.as_ref()
.map(|p| p.0.clone()) .map(|p| p.value.0.clone())
.unwrap_or_else(|| syn::parse_str("::pyo3").unwrap()) .unwrap_or_else(|| syn::parse_str("::pyo3").unwrap())
} }

View file

@ -21,5 +21,5 @@ pyproto = ["pyo3-macros-backend/pyproto"]
[dependencies] [dependencies]
proc-macro2 = { version = "1", default-features = false } proc-macro2 = { version = "1", default-features = false }
quote = "1" quote = "1"
syn = { version = "1", features = ["full", "extra-traits"] } syn = { version = "1.0.56", features = ["full", "extra-traits"] }
pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.16.2" } pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.16.2" }

View file

@ -0,0 +1,28 @@
`#[pyclass]` can be used with the following parameters:
| Parameter | Description |
| :- | :- |
| <span style="white-space: pre">`crate = "some::path"`</span> | Path to import the `pyo3` crate, if it's not accessible at `::pyo3`. |
| `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. |
| <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][params-1] |
| <span style="white-space: pre">`freelist = N`</span> | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
| <span style="white-space: pre">`module = "module_name"`</span> | Python code will see the class as being defined in this module. Defaults to `builtins`. |
| <span style="white-space: pre">`name = "python_name"`</span> | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. |
| <span style="white-space: pre">`text_signature = "(arg1, arg2, ...)"`</span> | Sets the text signature for the Python class' `__new__` method. |
| `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. |
| `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread.|
| `weakref` | Allows this class to be [weakly referenceable][params-6]. |
All of these parameters can either be passed directly on the `#[pyclass(...)]` annotation, or as one or
more accompanying `#[pyo3(...)]` annotations, e.g.:
```rust,ignore
// Argument supplied directly to the `#[pyclass]` annotation.
#[pyclass(name = "SomeName", subclass)]
struct MyClass { }
// Argument supplied as a separate annotation.
#[pyclass]
#[pyo3(name = "SomeName", subclass)]
struct MyClass { }
```

View file

@ -38,11 +38,11 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as syn::ItemFn); let mut ast = parse_macro_input!(input as syn::ItemFn);
let options = match PyModuleOptions::from_attrs(&mut ast.attrs) { let options = match PyModuleOptions::from_attrs(&mut ast.attrs) {
Ok(options) => options, Ok(options) => options,
Err(e) => return e.to_compile_error().into(), Err(e) => return e.into_compile_error().into(),
}; };
if let Err(err) = process_functions_in_module(&mut ast) { if let Err(err) = process_functions_in_module(&mut ast) {
return err.to_compile_error().into(); return err.into_compile_error().into();
} }
let doc = get_doc(&ast.attrs, None); let doc = get_doc(&ast.attrs, None);
@ -81,30 +81,18 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream {
/// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// A proc macro used to expose Rust structs and fieldless enums as Python objects.
/// ///
/// `#[pyclass]` accepts the following [parameters][2]: #[cfg_attr(docsrs, cfg_attr(docsrs, doc = include_str!("../docs/pyclass_parameters.md")))]
///
/// | Parameter | Description |
/// | :- | :- |
/// | <span style="white-space: pre">`name = "python_name"`</span> | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. |
/// | <span style="white-space: pre">`freelist = N`</span> | Implements a [free list][9] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. |
/// | `weakref` | Allows this class to be [weakly referenceable][6]. |
/// | <span style="white-space: pre">`extends = BaseType`</span> | Use a custom baseclass. Defaults to [`PyAny`][4] |
/// | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. |
/// | `unsendable` | Required if your struct is not [`Send`][3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][7] with [`Arc`][8]. By using `unsendable`, your class will panic when accessed by another thread.|
/// | <span style="white-space: pre">`module = "module_name"`</span> | Python code will see the class as being defined in this module. Defaults to `builtins`. |
/// ///
/// For more on creating Python classes, /// For more on creating Python classes,
/// see the [class section of the guide][1]. /// see the [class section of the guide][1].
/// ///
/// [1]: https://pyo3.rs/latest/class.html /// [1]: https://pyo3.rs/latest/class.html
/// [2]: https://pyo3.rs/latest/class.html#customizing-the-class /// [params-1]: ../prelude/struct.PyAny.html
/// [3]: std::marker::Send /// [params-2]: https://en.wikipedia.org/wiki/Free_list
/// [4]: ../prelude/struct.PyAny.html /// [params-3]: std::marker::Send
/// [5]: https://pyo3.rs/latest/class/protocols.html#garbage-collector-integration /// [params-4]: std::rc::Rc
/// [6]: https://docs.python.org/3/library/weakref.html /// [params-5]: std::sync::Arc
/// [7]: std::rc::Rc /// [params-6]: https://docs.python.org/3/library/weakref.html
/// [8]: std::sync::Arc
/// [9]: https://en.wikipedia.org/wiki/Free_list
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
use syn::Item; use syn::Item;
@ -114,7 +102,7 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
Item::Enum(enum_) => pyclass_enum_impl(attr, enum_, methods_type()), Item::Enum(enum_) => pyclass_enum_impl(attr, enum_, methods_type()),
unsupported => { unsupported => {
syn::Error::new_spanned(unsupported, "#[pyclass] only supports structs and enums.") syn::Error::new_spanned(unsupported, "#[pyclass] only supports structs and enums.")
.to_compile_error() .into_compile_error()
.into() .into()
} }
} }
@ -230,7 +218,7 @@ fn pyclass_impl(
methods_type: PyClassMethodsType, methods_type: PyClassMethodsType,
) -> TokenStream { ) -> TokenStream {
let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args); let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args);
let expanded = build_py_class(&mut ast, &args, methods_type).unwrap_or_compile_error(); let expanded = build_py_class(&mut ast, args, methods_type).unwrap_or_compile_error();
quote!( quote!(
#ast #ast
@ -245,7 +233,7 @@ fn pyclass_enum_impl(
methods_type: PyClassMethodsType, methods_type: PyClassMethodsType,
) -> TokenStream { ) -> TokenStream {
let args = parse_macro_input!(attrs with PyClassArgs::parse_enum_args); let args = parse_macro_input!(attrs with PyClassArgs::parse_enum_args);
let expanded = build_py_enum(&mut ast, &args, methods_type).unwrap_or_compile_error(); let expanded = build_py_enum(&mut ast, args, methods_type).unwrap_or_compile_error();
quote!( quote!(
#ast #ast

View file

@ -54,8 +54,9 @@ elif _pointer_size == 4:
else: else:
raise RuntimeError("unexpected pointer size: " + repr(_pointer_size)) raise RuntimeError("unexpected pointer size: " + repr(_pointer_size))
IS_WINDOWS = sys.platform == "win32" IS_WINDOWS = sys.platform == "win32"
if IS_WINDOWS: if IS_WINDOWS:
MIN_DATETIME = pdt.datetime(1970, 1, 2, 0, 0) MIN_DATETIME = pdt.datetime(1971, 1, 2, 0, 0)
if IS_32_BIT: if IS_32_BIT:
MAX_DATETIME = pdt.datetime(3001, 1, 19, 4, 59, 59) MAX_DATETIME = pdt.datetime(3001, 1, 19, 4, 59, 59)
else: else:
@ -227,7 +228,7 @@ def test_datetime_typeerror():
@given(dt=st.datetimes(MIN_DATETIME, MAX_DATETIME)) @given(dt=st.datetimes(MIN_DATETIME, MAX_DATETIME))
@example(dt=pdt.datetime(1970, 1, 2, 0, 0)) @example(dt=pdt.datetime(1971, 1, 2, 0, 0))
def test_datetime_from_timestamp(dt): def test_datetime_from_timestamp(dt):
if PYPY and dt < pdt.datetime(1900, 1, 1): if PYPY and dt < pdt.datetime(1900, 1, 1):
pytest.xfail("pdt.datetime.timestamp will raise on PyPy with dates before 1900") pytest.xfail("pdt.datetime.timestamp will raise on PyPy with dates before 1900")

View file

@ -97,6 +97,24 @@ impl PyBytes {
} }
} }
impl Py<PyBytes> {
/// Gets the Python bytes as a byte slice. Because Python bytes are
/// immutable, the result may be used for as long as the reference to
/// `self` is held, including when the GIL is released.
pub fn as_bytes<'a>(&'a self, _py: Python<'_>) -> &'a [u8] {
// py is required here because `PyBytes_AsString` and `PyBytes_Size`
// can both technically raise exceptions which require the GIL to be
// held. The only circumstance in which they raise is if the value
// isn't really a `PyBytes`, but better safe than sorry.
unsafe {
let buffer = ffi::PyBytes_AsString(self.as_ptr()) as *const u8;
let length = ffi::PyBytes_Size(self.as_ptr()) as usize;
debug_assert!(!buffer.is_null());
std::slice::from_raw_parts(buffer, length)
}
}
}
/// This is the same way [Vec] is indexed. /// This is the same way [Vec] is indexed.
impl<I: SliceIndex<[u8]>> Index<I> for PyBytes { impl<I: SliceIndex<[u8]>> Index<I> for PyBytes {
type Output = I::Output; type Output = I::Output;

View file

@ -41,3 +41,18 @@ fn test_bytearray_vec_conversion() {
let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap(); let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap();
py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'"); py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'");
} }
#[test]
fn test_py_as_bytes() {
let pyobj: pyo3::Py<pyo3::types::PyBytes>;
let data: &[u8];
{
let gil = Python::acquire_gil();
let py = gil.python();
pyobj = pyo3::types::PyBytes::new(py, b"abc").into_py(py);
data = pyobj.as_bytes(py);
}
assert_eq!(data, b"abc");
}

View file

@ -35,13 +35,13 @@ error: `set` may only be specified once
| ^^^ | ^^^
error: `name` may only be specified once error: `name` may only be specified once
--> tests/ui/invalid_property_args.rs:37:49 --> tests/ui/invalid_property_args.rs:37:42
| |
37 | struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32); 37 | struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32);
| ^^^^^ | ^^^^
error: `name` is useless without `get` or `set` error: `name` is useless without `get` or `set`
--> tests/ui/invalid_property_args.rs:40:40 --> tests/ui/invalid_property_args.rs:40:33
| |
40 | struct NameWithoutGetSet(#[pyo3(name = "value")] i32); 40 | struct NameWithoutGetSet(#[pyo3(name = "value")] i32);
| ^^^^^^^ | ^^^^

View file

@ -1,40 +1,40 @@
error: expected one of freelist/name/extends/module error: expected one of: `crate`, `dict`, `extends`, `freelist`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
--> tests/ui/invalid_pyclass_args.rs:3:11 --> tests/ui/invalid_pyclass_args.rs:3:11
| |
3 | #[pyclass(extend=pyo3::types::PyDict)] 3 | #[pyclass(extend=pyo3::types::PyDict)]
| ^^^^^^ | ^^^^^^
error: expected type path (e.g., my_mod::BaseClass) error: expected identifier
--> tests/ui/invalid_pyclass_args.rs:6:21 --> tests/ui/invalid_pyclass_args.rs:6:21
| |
6 | #[pyclass(extends = "PyDict")] 6 | #[pyclass(extends = "PyDict")]
| ^^^^^^^^ | ^^^^^^^^
error: expected type name (e.g. "Name") error: expected string literal
--> tests/ui/invalid_pyclass_args.rs:9:18 --> tests/ui/invalid_pyclass_args.rs:9:18
| |
9 | #[pyclass(name = m::MyClass)] 9 | #[pyclass(name = m::MyClass)]
| ^ | ^
error: expected a single identifier in double-quotes error: expected a single identifier in double quotes
--> tests/ui/invalid_pyclass_args.rs:12:18 --> tests/ui/invalid_pyclass_args.rs:12:18
| |
12 | #[pyclass(name = "Custom Name")] 12 | #[pyclass(name = "Custom Name")]
| ^^^^^^^^^^^^^ | ^^^^^^^^^^^^^
error: since PyO3 0.13 a pyclass name should be in double-quotes, e.g. "CustomName" error: expected string literal
--> tests/ui/invalid_pyclass_args.rs:15:18 --> tests/ui/invalid_pyclass_args.rs:15:18
| |
15 | #[pyclass(name = CustomName)] 15 | #[pyclass(name = CustomName)]
| ^^^^^^^^^^ | ^^^^^^^^^^
error: expected string literal (e.g., "my_mod") error: expected string literal
--> tests/ui/invalid_pyclass_args.rs:18:20 --> tests/ui/invalid_pyclass_args.rs:18:20
| |
18 | #[pyclass(module = my_module)] 18 | #[pyclass(module = my_module)]
| ^^^^^^^^^ | ^^^^^^^^^
error: expected one of gc/weakref/subclass/dict/unsendable error: expected one of: `crate`, `dict`, `extends`, `freelist`, `module`, `name`, `subclass`, `text_signature`, `unsendable`, `weakref`, `gc`
--> tests/ui/invalid_pyclass_args.rs:21:11 --> tests/ui/invalid_pyclass_args.rs:21:11
| |
21 | #[pyclass(weakrev)] 21 | #[pyclass(weakrev)]

View file

@ -2,14 +2,14 @@ use pyo3::prelude::*;
#[pyclass(subclass)] #[pyclass(subclass)]
enum NotBaseClass { enum NotBaseClass {
x, X,
y, Y,
} }
#[pyclass(extends = PyList)] #[pyclass(extends = PyList)]
enum NotDrivedClass { enum NotDrivedClass {
x, X,
y, Y,
} }
#[pyclass] #[pyclass]

View file

@ -4,13 +4,13 @@ error: enums can't be inherited by other classes
3 | #[pyclass(subclass)] 3 | #[pyclass(subclass)]
| ^^^^^^^^ | ^^^^^^^^
error: enums cannot extend from other classes error: enums can't extend from other classes
--> tests/ui/invalid_pyclass_enum.rs:9:11 --> tests/ui/invalid_pyclass_enum.rs:9:11
| |
9 | #[pyclass(extends = PyList)] 9 | #[pyclass(extends = PyList)]
| ^^^^^^^ | ^^^^^^^
error: Empty enums can't be #[pyclass]. error: #[pyclass] can't be used on enums without any variants
--> tests/ui/invalid_pyclass_enum.rs:16:18 --> tests/ui/invalid_pyclass_enum.rs:16:18
| |
16 | enum NoEmptyEnum {} 16 | enum NoEmptyEnum {}

View file

@ -5,10 +5,10 @@ error: `name` may only be specified once
| ^^^^^ | ^^^^^
error: `name` may only be specified once error: `name` may only be specified once
--> tests/ui/invalid_pymethod_names.rs:18:19 --> tests/ui/invalid_pymethod_names.rs:18:12
| |
18 | #[pyo3(name = "bar")] 18 | #[pyo3(name = "bar")]
| ^^^^^ | ^^^^
error: `name` not allowed with `#[new]` error: `name` not allowed with `#[new]`
--> tests/ui/invalid_pymethod_names.rs:24:19 --> tests/ui/invalid_pymethod_names.rs:24:19

View file

@ -3,9 +3,13 @@ name = "xtask"
version = "0.1.0" version = "0.1.0"
edition = "2018" edition = "2018"
[[bin]]
name = "xtask"
[dependencies] [dependencies]
anyhow = "1.0.51" anyhow = "1.0.51"
# Clap 3 requires MSRV 1.54 # Clap 3 requires MSRV 1.54
rustversion = "1.0" rustversion = "1.0"
structopt = { version = "0.3", default-features = false } structopt = { version = "0.3", default-features = false }
clap = { version = "2" }

23
xtask/README.md Normal file
View file

@ -0,0 +1,23 @@
## Commands to test PyO3.
To run these commands, you should be in PyO3's root directory, and run (for example) `cargo xtask ci`.
```
USAGE:
xtask.exe <SUBCOMMAND>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
SUBCOMMANDS:
ci Runs everything
clippy Runs `clippy`, denying all warnings
coverage Runs `cargo llvm-cov` for the PyO3 codebase
default Only runs the fast things (this is used if no command is specified)
doc Attempts to render the documentation
fmt Checks Rust and Python code formatting with `rustfmt` and `black`
help Prints this message or the help of the given subcommand(s)
test Runs various variations on `cargo test`
test-py Runs the tests in examples/ and pytests/
```

201
xtask/src/cli.rs Normal file
View file

@ -0,0 +1,201 @@
use crate::utils::*;
use anyhow::{ensure, Result};
use std::io;
use std::process::{Command, Stdio};
use std::time::Instant;
use structopt::StructOpt;
pub const MSRV: &str = "1.48";
#[derive(StructOpt)]
pub enum Subcommand {
/// Only runs the fast things (this is used if no command is specified)
Default,
/// Runs everything
Ci,
/// Checks Rust and Python code formatting with `rustfmt` and `black`
Fmt,
/// Runs `clippy`, denying all warnings.
Clippy,
/// Runs `cargo llvm-cov` for the PyO3 codebase.
Coverage(CoverageOpts),
/// Attempts to render the documentation.
Doc(DocOpts),
/// Runs various variations on `cargo test`
Test,
/// Runs the tests in examples/ and pytests/
TestPy,
}
impl Default for Subcommand {
fn default() -> Self {
Self::Default
}
}
#[derive(StructOpt, Default)]
pub struct CoverageOpts {
/// Creates an lcov output instead of printing to the terminal.
#[structopt(long)]
pub output_lcov: Option<String>,
}
#[derive(StructOpt)]
pub struct DocOpts {
/// Whether to run the docs using nightly rustdoc
#[structopt(long)]
pub stable: bool,
/// Whether to open the docs after rendering.
#[structopt(long)]
pub open: bool,
/// Whether to show the private and hidden API.
#[structopt(long)]
pub internal: bool,
}
impl Default for DocOpts {
fn default() -> Self {
Self {
stable: true,
open: false,
internal: false,
}
}
}
impl Subcommand {
pub fn execute(self) -> Result<()> {
print_metadata()?;
let start = Instant::now();
match self {
Subcommand::Default => {
crate::fmt::rust::run()?;
crate::clippy::run()?;
crate::test::run()?;
crate::doc::run(DocOpts::default())?;
}
Subcommand::Ci => {
let installed = Installed::new()?;
crate::fmt::rust::run()?;
if installed.black {
crate::fmt::python::run()?;
} else {
Installed::warn_black()
};
crate::clippy::run()?;
crate::test::run()?;
crate::doc::run(DocOpts::default())?;
if installed.nox {
crate::pytests::run(None)?;
} else {
Installed::warn_nox()
};
crate::llvm_cov::run(CoverageOpts::default())?;
installed.assert()?
}
Subcommand::Doc(opts) => crate::doc::run(opts)?,
Subcommand::Fmt => {
crate::fmt::rust::run()?;
crate::fmt::python::run()?;
}
Subcommand::Clippy => crate::clippy::run()?,
Subcommand::Coverage(opts) => crate::llvm_cov::run(opts)?,
Subcommand::TestPy => crate::pytests::run(None)?,
Subcommand::Test => crate::test::run()?,
};
let dt = start.elapsed().as_secs();
let minutes = dt / 60;
let seconds = dt % 60;
println!("\nxtask finished in {}m {}s.", minutes, seconds);
Ok(())
}
}
pub fn run(command: &mut Command) -> Result<()> {
println!("Running: {}", format_command(command));
let output = command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?
.wait_with_output()?;
ensure! {
output.status.success(),
"process did not run successfully ({exit}): {command}/n {out} {err}",
exit = match output.status.code() {
Some(code) => format!("exit code {}", code),
None => "terminated by signal".into(),
},
command = format_command(command),
out = String::from_utf8_lossy(&output.stdout),
err = String::from_utf8_lossy(&output.stderr)
};
Ok(())
}
#[derive(Copy, Clone, Debug)]
pub struct Installed {
pub nox: bool,
pub black: bool,
}
impl Installed {
pub fn new() -> anyhow::Result<Self> {
Ok(Self {
nox: Self::nox()?,
black: Self::black()?,
})
}
pub fn nox() -> anyhow::Result<bool> {
let output = std::process::Command::new("nox").arg("--version").output();
match output {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(other) => Err(other.into()),
}
}
pub fn warn_nox() {
eprintln!("Skipping: formatting Python code, because `nox` was not found");
}
pub fn black() -> anyhow::Result<bool> {
let output = std::process::Command::new("black")
.arg("--version")
.output();
match output {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(other) => Err(other.into()),
}
}
pub fn warn_black() {
eprintln!("Skipping: Python code formatting, because `black` was not found.");
}
pub fn assert(&self) -> anyhow::Result<()> {
if self.nox && self.black {
Ok(())
} else {
let mut err =
String::from("\n\nxtask was unable to run all tests due to some missing programs:");
if !self.black {
err.push_str("\n`black` was not installed. (`pip install black`)");
}
if !self.nox {
err.push_str("\n`nox` was not installed. (`pip install nox`)");
}
Err(anyhow::anyhow!(err))
}
}
}

25
xtask/src/clippy.rs Normal file
View file

@ -0,0 +1,25 @@
use crate::cli;
use std::process::Command;
pub fn run() -> anyhow::Result<()> {
cli::run(
Command::new("cargo")
.arg("clippy")
.arg("--features=full")
.arg("--all-targets")
.arg("--workspace")
.arg("--")
.arg("-Dwarnings"),
)?;
cli::run(
Command::new("cargo")
.arg("clippy")
.arg("--all-targets")
.arg("--workspace")
.arg("--features=abi3,full")
.arg("--")
.arg("-Dwarnings"),
)?;
Ok(())
}

47
xtask/src/doc.rs Normal file
View file

@ -0,0 +1,47 @@
use crate::cli;
use crate::cli::DocOpts;
use std::process::Command;
//--cfg docsrs --Z unstable-options --document-hidden-items
pub fn run(opts: DocOpts) -> anyhow::Result<()> {
let mut flags = Vec::new();
if !opts.stable {
flags.push("--cfg docsrs");
}
if opts.internal {
flags.push("--Z unstable-options");
flags.push("--document-hidden-items");
}
flags.push("-Dwarnings");
std::env::set_var("RUSTDOCFLAGS", flags.join(" "));
cli::run(
Command::new("cargo")
.args(if opts.stable { None } else { Some("+nightly") })
.arg("doc")
.arg("--lib")
.arg("--no-default-features")
.arg("--features=full")
.arg("--no-deps")
.arg("--workspace")
.args(if opts.internal {
&["--document-private-items"][..]
} else {
&["--exclude=pyo3-macros", "--exclude=pyo3-macros-backend"][..]
})
.args(if opts.stable {
&[][..]
} else {
&[
"-Z",
"unstable-options",
"-Z",
"rustdoc-scrape-examples=examples",
]
})
.args(if opts.open { Some("--open") } else { None }),
)?;
Ok(())
}

23
xtask/src/fmt.rs Normal file
View file

@ -0,0 +1,23 @@
pub mod rust {
use crate::cli;
use std::process::Command;
pub fn run() -> anyhow::Result<()> {
cli::run(
Command::new("cargo")
.arg("fmt")
.arg("--all")
.arg("--")
.arg("--check"),
)?;
Ok(())
}
}
pub mod python {
use crate::cli;
use std::process::Command;
pub fn run() -> anyhow::Result<()> {
cli::run(Command::new("black").arg(".").arg("--check"))?;
Ok(())
}
}

100
xtask/src/llvm_cov.rs Normal file
View file

@ -0,0 +1,100 @@
use crate::cli;
use crate::cli::CoverageOpts;
use crate::utils::*;
use anyhow::{Context, Result};
use std::{collections::HashMap, process::Command};
/// Runs `cargo llvm-cov` for the PyO3 codebase.
pub fn run(opts: CoverageOpts) -> Result<()> {
let env = get_coverage_env()?;
cli::run(llvm_cov_command(&["clean", "--workspace"]).envs(&env))?;
cli::run(
Command::new("cargo")
.args(&["test", "--manifest-path", "pyo3-build-config/Cargo.toml"])
.envs(&env),
)?;
cli::run(
Command::new("cargo")
.args(&["test", "--manifest-path", "pyo3-macros-backend/Cargo.toml"])
.envs(&env),
)?;
cli::run(
Command::new("cargo")
.args(&["test", "--manifest-path", "pyo3-macros/Cargo.toml"])
.envs(&env),
)?;
cli::run(Command::new("cargo").arg("test").envs(&env))?;
cli::run(
Command::new("cargo")
.args(&["test", "--features", "abi3"])
.envs(&env),
)?;
cli::run(
Command::new("cargo")
.args(&["test", "--features", "full"])
.envs(&env),
)?;
cli::run(
Command::new("cargo")
.args(&["test", "--features", "abi3 full"])
.envs(&env),
)?;
crate::pytests::run(&env)?;
match opts.output_lcov {
Some(path) => {
cli::run(llvm_cov_command(&["--no-run", "--lcov", "--output-path", &path]).envs(&env))?
}
None => cli::run(llvm_cov_command(&["--no-run", "--summary-only"]).envs(&env))?,
}
Ok(())
}
fn llvm_cov_command(args: &[&str]) -> Command {
let mut command = Command::new("cargo");
command
.args(&[
"llvm-cov",
"--package=pyo3",
"--package=pyo3-build-config",
"--package=pyo3-macros-backend",
"--package=pyo3-macros",
"--package=pyo3-ffi",
])
.args(args);
command
}
fn get_coverage_env() -> Result<HashMap<String, String>> {
let mut env = HashMap::new();
let output = String::from_utf8(llvm_cov_command(&["show-env"]).output()?.stdout)?;
for line in output.trim().split('\n') {
let (key, value) = split_once(line, '=')
.context("expected '=' in each line of output from llvm-cov show-env")?;
env.insert(key.to_owned(), value.trim_matches('"').to_owned());
}
// Ensure that examples/ and pytests/ all build to the correct target directory to collect
// coverage artifacts.
env.insert(
"CARGO_TARGET_DIR".to_owned(),
env.get("CARGO_LLVM_COV_TARGET_DIR").unwrap().to_owned(),
);
// Coverage only works on nightly.
let rustc_version =
String::from_utf8(get_output(Command::new("rustc").arg("--version"))?.stdout)
.context("failed to parse rust version as utf8")?;
if !rustc_version.contains("nightly") {
env.insert("RUSTUP_TOOLCHAIN".to_owned(), "nightly".to_owned());
}
Ok(env)
}

View file

@ -1,196 +1,24 @@
use anyhow::{ensure, Context, Result}; use clap::ErrorKind::MissingArgumentOrSubcommand;
use std::{collections::HashMap, path::Path, process::Command};
use structopt::StructOpt; use structopt::StructOpt;
#[derive(StructOpt)] pub mod cli;
enum Subcommand { pub mod clippy;
/// Runs `cargo llvm-cov` for the PyO3 codebase. pub mod doc;
Coverage(CoverageOpts), pub mod fmt;
/// Runs tests in examples/ and pytests/ pub mod llvm_cov;
TestPy, pub mod pytests;
} pub mod test;
pub mod utils;
#[derive(StructOpt)] fn main() -> anyhow::Result<()> {
struct CoverageOpts { // Avoid spewing backtraces all over the command line
/// Creates an lcov output instead of printing to the terminal. // For some reason this is automatically enabled on nightly compilers...
#[structopt(long)] std::env::set_var("RUST_LIB_BACKTRACE", "0");
output_lcov: Option<String>,
}
impl Subcommand { match cli::Subcommand::from_args_safe() {
fn execute(self) -> Result<()> { Ok(c) => c.execute()?,
match self { Err(e) if e.kind == MissingArgumentOrSubcommand => cli::Subcommand::default().execute()?,
Subcommand::Coverage(opts) => subcommand_coverage(opts), Err(e) => return Err(e.into()),
Subcommand::TestPy => run_python_tests(None),
}
}
}
fn main() -> Result<()> {
Subcommand::from_args().execute()
}
/// Runs `cargo llvm-cov` for the PyO3 codebase.
fn subcommand_coverage(opts: CoverageOpts) -> Result<()> {
let env = get_coverage_env()?;
run(llvm_cov_command(&["clean", "--workspace"]).envs(&env))?;
run(Command::new("cargo")
.args(&["test", "--manifest-path", "pyo3-build-config/Cargo.toml"])
.envs(&env))?;
run(Command::new("cargo")
.args(&["test", "--manifest-path", "pyo3-macros-backend/Cargo.toml"])
.envs(&env))?;
run(Command::new("cargo")
.args(&["test", "--manifest-path", "pyo3-macros/Cargo.toml"])
.envs(&env))?;
run(Command::new("cargo").arg("test").envs(&env))?;
run(Command::new("cargo")
.args(&["test", "--features", "abi3"])
.envs(&env))?;
run(Command::new("cargo")
.args(&["test", "--features", "full"])
.envs(&env))?;
run(Command::new("cargo")
.args(&["test", "--features", "abi3 full"])
.envs(&env))?;
run_python_tests(&env)?;
match opts.output_lcov {
Some(path) => {
run(llvm_cov_command(&["--no-run", "--lcov", "--output-path", &path]).envs(&env))?
}
None => run(llvm_cov_command(&["--no-run", "--summary-only"]).envs(&env))?,
}
Ok(())
}
fn run(command: &mut Command) -> Result<()> {
println!("running: {}", format_command(command));
let status = command.spawn()?.wait()?;
ensure! {
status.success(),
"process did not run successfully ({exit}): {command}",
exit = match status.code() {
Some(code) => format!("exit code {}", code),
None => "terminated by signal".into(),
},
command = format_command(command),
};
Ok(())
}
fn get_output(command: &mut Command) -> Result<std::process::Output> {
let output = command.output()?;
ensure! {
output.status.success(),
"process did not run successfully ({exit}): {command}",
exit = match output.status.code() {
Some(code) => format!("exit code {}", code),
None => "terminated by signal".into(),
},
command = format_command(command),
};
Ok(output)
}
fn llvm_cov_command(args: &[&str]) -> Command {
let mut command = Command::new("cargo");
command
.args(&[
"llvm-cov",
"--package=pyo3",
"--package=pyo3-build-config",
"--package=pyo3-macros-backend",
"--package=pyo3-macros",
"--package=pyo3-ffi",
])
.args(args);
command
}
fn run_python_tests<'a>(
env: impl IntoIterator<Item = (&'a String, &'a String)> + Copy,
) -> Result<()> {
run(Command::new("nox")
.arg("--non-interactive")
.arg("-f")
.arg(Path::new("pytests").join("noxfile.py"))
.envs(env))?;
for entry in std::fs::read_dir("examples")? {
let path = entry?.path();
if path.is_dir() && path.join("noxfile.py").exists() {
run(Command::new("nox")
.arg("--non-interactive")
.arg("-f")
.arg(path.join("noxfile.py"))
.envs(env))?;
}
} }
Ok(()) Ok(())
} }
fn get_coverage_env() -> Result<HashMap<String, String>> {
let mut env = HashMap::new();
let output = String::from_utf8(llvm_cov_command(&["show-env"]).output()?.stdout)?;
for line in output.trim().split('\n') {
let (key, value) = split_once(line, '=')
.context("expected '=' in each line of output from llvm-cov show-env")?;
env.insert(key.to_owned(), value.trim_matches('"').to_owned());
}
// Ensure that examples/ and pytests/ all build to the correct target directory to collect
// coverage artifacts.
env.insert(
"CARGO_TARGET_DIR".to_owned(),
env.get("CARGO_LLVM_COV_TARGET_DIR").unwrap().to_owned(),
);
// Coverage only works on nightly.
let rustc_version =
String::from_utf8(get_output(Command::new("rustc").arg("--version"))?.stdout)
.context("failed to parse rust version as utf8")?;
if !rustc_version.contains("nightly") {
env.insert("RUSTUP_TOOLCHAIN".to_owned(), "nightly".to_owned());
}
Ok(env)
}
// Replacement for str.split_once() on Rust older than 1.52
#[rustversion::before(1.52)]
fn split_once(s: &str, pat: char) -> Option<(&str, &str)> {
let mut iter = s.splitn(2, pat);
Some((iter.next()?, iter.next()?))
}
#[rustversion::since(1.52)]
fn split_once(s: &str, pat: char) -> Option<(&str, &str)> {
s.split_once(pat)
}
#[rustversion::since(1.57)]
fn format_command(command: &Command) -> String {
let mut buf = String::new();
buf.push('`');
buf.push_str(&command.get_program().to_string_lossy());
for arg in command.get_args() {
buf.push(' ');
buf.push_str(&arg.to_string_lossy());
}
buf.push('`');
buf
}
#[rustversion::before(1.57)]
fn format_command(command: &Command) -> String {
// Debug impl isn't as nice as the above, but will do on < 1.57
format!("{:?}", command)
}

27
xtask/src/pytests.rs Normal file
View file

@ -0,0 +1,27 @@
use crate::cli;
use anyhow::Result;
use std::{path::Path, process::Command};
pub fn run<'a>(env: impl IntoIterator<Item = (&'a String, &'a String)> + Copy) -> Result<()> {
cli::run(
Command::new("nox")
.arg("--non-interactive")
.arg("-f")
.arg(Path::new("pytests").join("noxfile.py"))
.envs(env),
)?;
for entry in std::fs::read_dir("examples")? {
let path = entry?.path();
if path.is_dir() && path.join("noxfile.py").exists() {
cli::run(
Command::new("nox")
.arg("--non-interactive")
.arg("-f")
.arg(path.join("noxfile.py"))
.envs(env),
)?;
}
}
Ok(())
}

73
xtask/src/test.rs Normal file
View file

@ -0,0 +1,73 @@
use crate::cli::{self, MSRV};
use std::process::Command;
pub fn run() -> anyhow::Result<()> {
cli::run(
Command::new("cargo")
.arg("test")
.arg("--lib")
.arg("--no-default-features")
.arg("--tests")
.arg("--quiet"),
)?;
cli::run(
Command::new("cargo")
.arg("test")
.arg("--no-default-features")
.arg("--features=full")
.arg("--quiet"),
)?;
cli::run(
Command::new("cargo")
.arg("test")
.arg("--no-default-features")
.arg("--features=abi3,full")
.arg("--quiet"),
)?;
// If the MSRV toolchain is not installed, this will install it
cli::run(
Command::new("rustup")
.arg("toolchain")
.arg("install")
.arg(MSRV),
)?;
// Test MSRV
cli::run(
Command::new("cargo")
.arg(format!("+{}", MSRV))
.arg("test")
.arg("--no-default-features")
.arg("--features=full,auto-initialize")
.arg("--quiet"),
)?;
cli::run(
Command::new("cargo")
.arg("+nightly")
.arg("test")
.arg("--no-default-features")
.arg("--features=full,nightly")
.arg("--quiet"),
)?;
cli::run(
Command::new("cargo")
.arg("test")
.arg("--manifest-path=pyo3-ffi/Cargo.toml")
.arg("--quiet"),
)?;
cli::run(
Command::new("cargo")
.arg("test")
.arg("--no-default-features")
.arg("--manifest-path=pyo3-build-config/Cargo.toml")
.arg("--quiet"),
)?;
Ok(())
}

65
xtask/src/utils.rs Normal file
View file

@ -0,0 +1,65 @@
use anyhow::ensure;
use std::process::Command;
// Replacement for str.split_once() on Rust older than 1.52
#[rustversion::before(1.52)]
pub fn split_once(s: &str, pat: char) -> Option<(&str, &str)> {
let mut iter = s.splitn(2, pat);
Some((iter.next()?, iter.next()?))
}
#[rustversion::since(1.52)]
pub fn split_once(s: &str, pat: char) -> Option<(&str, &str)> {
s.split_once(pat)
}
#[rustversion::since(1.57)]
pub fn format_command(command: &Command) -> String {
let mut buf = String::new();
buf.push('`');
buf.push_str(&command.get_program().to_string_lossy());
for arg in command.get_args() {
buf.push(' ');
buf.push_str(&arg.to_string_lossy());
}
buf.push('`');
buf
}
#[rustversion::before(1.57)]
pub fn format_command(command: &Command) -> String {
// Debug impl isn't as nice as the above, but will do on < 1.57
format!("{:?}", command)
}
pub fn get_output(command: &mut Command) -> anyhow::Result<std::process::Output> {
let output = command.output()?;
ensure! {
output.status.success(),
"process did not run successfully ({exit}): {command}",
exit = match output.status.code() {
Some(code) => format!("exit code {}", code),
None => "terminated by signal".into(),
},
command = format_command(command),
};
Ok(output)
}
pub fn print_metadata() -> anyhow::Result<()> {
let rustc_output = std::process::Command::new("rustc")
.arg("--version")
.arg("--verbose")
.output()?;
let rustc_version = core::str::from_utf8(&rustc_output.stdout).unwrap();
println!("Metadata: \n\n{}", rustc_version);
let py_output = std::process::Command::new("python")
.arg("--version")
.arg("-V")
.output()?;
let py_version = core::str::from_utf8(&py_output.stdout).unwrap();
println!("{}", py_version);
Ok(())
}