Vendor deps.

This commit is contained in:
jimmy committed 2026-09-16 20:10:35 -04:00
1 parent 294527085e
commit 91e18fde3a
1714 files changed
+556667 -7

No files matched your search

+1
View File
@@ -0,0 +1 @@
{"files":{".cargo_vcs_info.json":"0e1142e078c836d3fc899bac603093f62ed68865ca83470570104176e5351d4c","Cargo.lock":"71cc767bf9534b42c58032ab23525bb98f2e7fd7d733a4ee38ed5fa3eb7e5e8f","Cargo.toml":"a965cadd857e44658af0673a7e7402a266de9fbbfdbc12beff0fcf58c40375ef","Cargo.toml.orig":"2d85e4cf389597ee7e3d0661c5d3a4dbec08fef4f9327ac841f75a5683575833","LICENSE-APACHE":"32c76dbe0e73d79100d5ece77c158399f2e2541bc5c78548a4ba45c1cb53c5c9","LICENSE-MIT":"afcbe3b2e6b37172b5a9ca869ee4c0b8cdc09316e5d4384864154482c33e5af6","src/attributes.rs":"153b8b3c48279c4ce58aef6f7a75fe0bc404721b183026df2ec87020f8845b0d","src/combine_errors.rs":"e4fd8dc4ad4ce70810012c88379ca31eb064c713ef1a0c7705d1c16699b38b74","src/derive_attributes.rs":"07ae0ba3230060c5dd82628d514bc03d6e9b5f9e804127553d1e3d9b856eb9b9","src/frompyobject.rs":"3c76c8388e9a82382859d7653977eb98ec4ff453efaec323eead3812a7a9d9c4","src/intopyobject.rs":"9a370047b655d21033cbaaeb8961d28708744b5dd74dc29f3a43bbabf6f5b7cc","src/introspection.rs":"3d4903dcd69dc3b98c64714a7ca1ecc4870b354d9a30b512e3cfc17c2af1daa2","src/konst.rs":"00ff9f22898cd65b20d9069fb4454a246ee7f2fa458a6b967f33e03942ca5d10","src/lib.rs":"1aaec987df980bbba136ed55cefcb0d9035155c0b819ca02f695915c56456e9f","src/method.rs":"efb72df7fb00e34af90a64fa2d5f3f74b2ecb795209935cb8ba5c12ea0b62575","src/module.rs":"8717fd425d3ef6e3844d2f67c3eab3d265fab88c99b921615da381cafa959894","src/params.rs":"cab1d681390152ded6c5871e47fc54eac7bf457cd6a5772e271cb745664b00f4","src/py_expr.rs":"f49abc226659d650cb717c640222d85d7166c54c48373ed85d3c280b6a3d254e","src/pyclass.rs":"75ac0fe8947af3702a6e9c76c1b33725721a7ebc802a6b53dfd0b770a3776250","src/pyfunction.rs":"b5b808017500010fc3f2d751c482ae3fa8244559ad2a6518c9cd9f3cb96f0a92","src/pyfunction/signature.rs":"6f353506a230df30448ba5c288dd4abd83b2c8c96edbbd727c225c678702185f","src/pyimpl.rs":"5f41adfa9b291444dfe4c21c35db97f9a7c4853d1c8a68fa94a033e05216934b","src/pymethod.rs":"9ac954eb95ba17d64615a760cae1329fb0b4468813f721f8eee8890b8759ae2c","src/quotes.rs":"95d11e4a4e4742be4caefa8a69762080b53a9f10d8255cfedab23bd5912cf194","src/utils.rs":"b3ab1888bc6e2169176ef4f3cc1fee38ad51a232f8c9071f9a2090622ceb70e6"},"package":"571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "a70d17f898df41b3d1632ee987f624f5222d8d39"
},
"path_in_vcs": "pyo3-macros-backend"
}
+54
View File
@@ -0,0 +1,54 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.29.2"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+111
View File
@@ -0,0 +1,111 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.83"
name = "pyo3-macros-backend"
version = "0.29.2"
authors = ["PyO3 Project and Contributors <https://github.com/PyO3>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Code generation for PyO3 package"
homepage = "https://github.com/pyo3/pyo3"
readme = false
keywords = [
"pyo3",
"python",
"cpython",
"ffi",
]
categories = [
"api-bindings",
"development-tools::ffi",
]
license = "MIT OR Apache-2.0"
repository = "https://github.com/pyo3/pyo3"
[features]
experimental-async = []
experimental-inspect = []
[lib]
name = "pyo3_macros_backend"
path = "src/lib.rs"
[dependencies.heck]
version = "0.5"
[dependencies.proc-macro2]
version = "1.0.60"
default-features = false
[dependencies.quote]
version = "1.0.37"
default-features = false
[dependencies.syn]
version = "2.0.59"
features = [
"derive",
"parsing",
"printing",
"clone-impls",
"full",
"extra-traits",
"visit-mut",
]
default-features = false
[lints.clippy]
checked_conversions = "warn"
dbg_macro = "warn"
explicit_into_iter_loop = "warn"
explicit_iter_loop = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
let_unit_value = "warn"
manual_assert = "warn"
manual_ok_or = "warn"
todo = "warn"
undocumented_unsafe_blocks = "warn"
unnecessary_wraps = "warn"
used_underscore_binding = "warn"
useless_transmute = "warn"
[lints.rust]
elided_lifetimes_in_paths = "warn"
invalid_doc_attributes = "warn"
rust_2021_prelude_collisions = "warn"
unsafe_op_in_unsafe_fn = "warn"
unused_lifetimes = "warn"
[lints.rust.rust_2018_idioms]
level = "warn"
priority = -1
[lints.rust.unexpected_cfgs]
level = "warn"
priority = 0
check-cfg = [
"cfg(pyo3_disable_reference_pool)",
"cfg(pyo3_leak_on_drop_without_reference_pool)",
"cfg(pyo3_use_raw_dylib)",
'cfg(pyo3_dll, values("python3", "python3_d", "python3t", "python3t_d", "python38", "python38_d", "python39", "python39_d", "python310", "python310_d", "python311", "python311_d", "python312", "python312_d", "python313", "python313_d", "python313t", "python313t_d", "python314", "python314_d", "python314t", "python314t_d", "python315", "python315_d", "python315t", "python315t_d", "python316", "python316_d", "python316t", "python316t_d", "libpypy3.11-c"))',
]
[lints.rustdoc]
bare_urls = "warn"
broken_intra_doc_links = "warn"
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "pyo3-macros-backend"
version = "0.29.2"
description = "Code generation for PyO3 package"
authors = ["PyO3 Project and Contributors <https://github.com/PyO3>"]
keywords = ["pyo3", "python", "cpython", "ffi"]
homepage = "https://github.com/pyo3/pyo3"
repository = "https://github.com/pyo3/pyo3"
categories = ["api-bindings", "development-tools::ffi"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version.workspace = true
# Note: we use default-features = false for proc-macro related crates
# not to depend on proc-macro itself.
# See https://github.com/PyO3/pyo3/pull/810 for more.
[dependencies]
heck = "0.5"
proc-macro2 = { version = "1.0.60", default-features = false }
quote = { version = "1.0.37", default-features = false }
[dependencies.syn]
# 2.0.59 for `LitCStr`
version = "2.0.59"
default-features = false
features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits", "visit-mut"]
[lints]
workspace = true
[features]
experimental-async = []
experimental-inspect = []
+178
View File
@@ -0,0 +1,178 @@
Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2023-present PyO3 Project and Contributors. https://github.com/PyO3
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+450
View File
@@ -0,0 +1,450 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::parse::Parser;
use syn::{
ext::IdentExt,
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
token::Comma,
Attribute, Expr, ExprPath, Ident, Index, LitBool, LitStr, Member, Path, Result, Token,
};
use crate::combine_errors::CombineErrors;
pub mod kw {
syn::custom_keyword!(annotation);
syn::custom_keyword!(attribute);
syn::custom_keyword!(cancel_handle);
syn::custom_keyword!(constructor);
syn::custom_keyword!(dict);
syn::custom_keyword!(eq);
syn::custom_keyword!(eq_int);
syn::custom_keyword!(extends);
syn::custom_keyword!(freelist);
syn::custom_keyword!(from_py_with);
syn::custom_keyword!(frozen);
syn::custom_keyword!(get);
syn::custom_keyword!(get_all);
syn::custom_keyword!(hash);
syn::custom_keyword!(into_py_with);
syn::custom_keyword!(item);
syn::custom_keyword!(immutable_type);
syn::custom_keyword!(from_item_all);
syn::custom_keyword!(mapping);
syn::custom_keyword!(module);
syn::custom_keyword!(name);
syn::custom_keyword!(ord);
syn::custom_keyword!(pass_module);
syn::custom_keyword!(rename_all);
syn::custom_keyword!(sequence);
syn::custom_keyword!(set);
syn::custom_keyword!(set_all);
syn::custom_keyword!(new);
syn::custom_keyword!(signature);
syn::custom_keyword!(str);
syn::custom_keyword!(subclass);
syn::custom_keyword!(submodule);
syn::custom_keyword!(text_signature);
syn::custom_keyword!(transparent);
syn::custom_keyword!(unsendable);
syn::custom_keyword!(weakref);
syn::custom_keyword!(generic);
syn::custom_keyword!(gil_used);
syn::custom_keyword!(warn);
syn::custom_keyword!(message);
syn::custom_keyword!(category);
syn::custom_keyword!(from_py_object);
syn::custom_keyword!(skip_from_py_object);
}
fn take_int(read: &mut &str, tracker: &mut usize) -> String {
let mut int = String::new();
for (i, ch) in read.char_indices() {
match ch {
'0'..='9' => {
*tracker += 1;
int.push(ch)
}
_ => {
*read = &read[i..];
break;
}
}
}
int
}
fn take_ident(read: &mut &str, tracker: &mut usize) -> Ident {
let mut ident = String::new();
if read.starts_with("r#") {
ident.push_str("r#");
*tracker += 2;
*read = &read[2..];
}
for (i, ch) in read.char_indices() {
match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => {
*tracker += 1;
ident.push(ch)
}
_ => {
*read = &read[i..];
break;
}
}
}
Ident::parse_any.parse_str(&ident).unwrap()
}
// shorthand parsing logic inspiration taken from https://github.com/dtolnay/thiserror/blob/master/impl/src/fmt.rs
fn parse_shorthand_format(fmt: LitStr) -> Result<(LitStr, Vec<Member>)> {
let span = fmt.span();
let token = fmt.token();
let value = fmt.value();
let mut read = value.as_str();
let mut out = String::new();
let mut members = Vec::new();
let mut tracker = 1;
while let Some(brace) = read.find('{') {
tracker += brace;
out += &read[..brace + 1];
read = &read[brace + 1..];
if read.starts_with('{') {
out.push('{');
read = &read[1..];
tracker += 2;
continue;
}
let next = match read.chars().next() {
Some(next) => next,
None => break,
};
tracker += 1;
let member = match next {
'0'..='9' => {
let start = tracker;
let index = take_int(&mut read, &mut tracker).parse::<u32>().unwrap();
let end = tracker;
let subspan = token.subspan(start..end).unwrap_or(span);
let idx = Index {
index,
span: subspan,
};
Member::Unnamed(idx)
}
'a'..='z' | 'A'..='Z' | '_' => {
let start = tracker;
let mut ident = take_ident(&mut read, &mut tracker);
let end = tracker;
let subspan = token.subspan(start..end).unwrap_or(span);
ident.set_span(subspan);
Member::Named(ident)
}
'}' | ':' => {
let start = tracker;
tracker += 1;
let end = tracker;
let subspan = token.subspan(start..end).unwrap_or(span);
// we found a closing bracket or formatting ':' without finding a member, we assume the user wants the instance formatted here
bail_spanned!(subspan.span() => "No member found, you must provide a named or positionally specified member.")
}
_ => continue,
};
members.push(member);
}
out += read;
Ok((LitStr::new(&out, span), members))
}
#[derive(Clone, Debug)]
pub struct StringFormatter {
pub fmt: LitStr,
pub args: Vec<Member>,
}
impl Parse for crate::attributes::StringFormatter {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let (fmt, args) = parse_shorthand_format(input.parse()?)?;
Ok(Self { fmt, args })
}
}
impl ToTokens for crate::attributes::StringFormatter {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.fmt.to_tokens(tokens);
tokens.extend(quote! {self.args})
}
}
#[derive(Clone, Debug)]
pub struct KeywordAttribute<K, V> {
pub kw: K,
pub value: V,
}
#[derive(Clone, Debug)]
pub struct OptionalKeywordAttribute<K, V> {
pub kw: K,
pub value: Option<V>,
}
/// 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, Eq)]
pub struct LitStrValue<T>(pub T);
impl<T: Parse> Parse for LitStrValue<T> {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let lit_str: LitStr = input.parse()?;
lit_str.parse().map(LitStrValue)
}
}
impl<T: ToTokens> ToTokens for LitStrValue<T> {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens)
}
}
/// A helper type which parses a name via a literal string
#[derive(Clone, Debug, PartialEq, Eq)]
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_with(Ident::parse_any) {
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)
}
}
/// Available renaming rules
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RenamingRule {
CamelCase,
KebabCase,
Lowercase,
PascalCase,
ScreamingKebabCase,
ScreamingSnakeCase,
SnakeCase,
Uppercase,
}
/// A helper type which parses a renaming rule via a literal string
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RenamingRuleLitStr {
pub lit: LitStr,
pub rule: RenamingRule,
}
impl Parse for RenamingRuleLitStr {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let string_literal: LitStr = input.parse()?;
let rule = match string_literal.value().as_ref() {
"camelCase" => RenamingRule::CamelCase,
"kebab-case" => RenamingRule::KebabCase,
"lowercase" => RenamingRule::Lowercase,
"PascalCase" => RenamingRule::PascalCase,
"SCREAMING-KEBAB-CASE" => RenamingRule::ScreamingKebabCase,
"SCREAMING_SNAKE_CASE" => RenamingRule::ScreamingSnakeCase,
"snake_case" => RenamingRule::SnakeCase,
"UPPERCASE" => RenamingRule::Uppercase,
_ => {
bail_spanned!(string_literal.span() => "expected a valid renaming rule, possible values are: \"camelCase\", \"kebab-case\", \"lowercase\", \"PascalCase\", \"SCREAMING-KEBAB-CASE\", \"SCREAMING_SNAKE_CASE\", \"snake_case\", \"UPPERCASE\"")
}
};
Ok(Self {
lit: string_literal,
rule,
})
}
}
impl ToTokens for RenamingRuleLitStr {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.lit.to_tokens(tokens)
}
}
/// Text signature can be either a literal string or opt-in/out
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TextSignatureAttributeValue {
Str(LitStr),
// `None` ident to disable automatic text signature generation
Disabled(Ident),
}
impl Parse for TextSignatureAttributeValue {
fn parse(input: ParseStream<'_>) -> Result<Self> {
if let Ok(lit_str) = input.parse::<LitStr>() {
return Ok(TextSignatureAttributeValue::Str(lit_str));
}
let err_span = match input.parse::<Ident>() {
Ok(ident) if ident == "None" => {
return Ok(TextSignatureAttributeValue::Disabled(ident));
}
Ok(other_ident) => other_ident.span(),
Err(e) => e.span(),
};
Err(err_spanned!(err_span => "expected a string literal or `None`"))
}
}
impl ToTokens for TextSignatureAttributeValue {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
TextSignatureAttributeValue::Str(s) => s.to_tokens(tokens),
TextSignatureAttributeValue::Disabled(b) => b.to_tokens(tokens),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NewImplTypeAttributeValue {
FromFields,
// Future variant for 'default' should go here
}
impl Parse for NewImplTypeAttributeValue {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let string_literal: LitStr = input.parse()?;
if string_literal.value().as_str() == "from_fields" {
Ok(NewImplTypeAttributeValue::FromFields)
} else {
bail_spanned!(string_literal.span() => "expected \"from_fields\"")
}
}
}
impl ToTokens for NewImplTypeAttributeValue {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
NewImplTypeAttributeValue::FromFields => {
tokens.extend(quote! { "from_fields" });
}
}
}
}
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 RenameAllAttribute = KeywordAttribute<kw::rename_all, RenamingRuleLitStr>;
pub type StrFormatterAttribute = OptionalKeywordAttribute<kw::str, StringFormatter>;
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;
pub type NewImplTypeAttribute = KeywordAttribute<kw::new, NewImplTypeAttributeValue>;
pub type SubmoduleAttribute = kw::submodule;
pub type GILUsedAttribute = KeywordAttribute<kw::gil_used, LitBool>;
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);
}
}
impl<K: Parse + std::fmt::Debug, V: Parse> Parse for OptionalKeywordAttribute<K, V> {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let kw: K = input.parse()?;
let value = match input.parse::<Token![=]>() {
Ok(_) => Some(input.parse()?),
Err(_) => None,
};
Ok(OptionalKeywordAttribute { kw, value })
}
}
impl<K: ToTokens, V: ToTokens> ToTokens for OptionalKeywordAttribute<K, V> {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.kw.to_tokens(tokens);
if self.value.is_some() {
Token![=](self.kw.span()).to_tokens(tokens);
self.value.to_tokens(tokens);
}
}
}
pub type FromPyWithAttribute = KeywordAttribute<kw::from_py_with, ExprPath>;
pub type IntoPyWithAttribute = KeywordAttribute<kw::into_py_with, ExprPath>;
pub type DefaultAttribute = OptionalKeywordAttribute<Token![default], Expr>;
/// For specifying the path to the pyo3 crate.
pub type CrateAttribute = KeywordAttribute<Token![crate], LitStrValue<Path>>;
pub fn get_pyo3_options<T: Parse>(attr: &syn::Attribute) -> Result<Option<Punctuated<T, Comma>>> {
if attr.path().is_ident("pyo3") {
attr.parse_args_with(Punctuated::parse_terminated).map(Some)
} else {
Ok(None)
}
}
/// Takes attributes from an attribute vector.
///
/// For each attribute in `attrs`, `extractor` is called. If `extractor` returns `Ok(true)`, then
/// the attribute will be removed from the vector.
///
/// This is similar to `Vec::retain` except the closure is fallible and the condition is reversed.
/// (In `retain`, returning `true` keeps the element, here it removes it.)
pub fn take_attributes(
attrs: &mut Vec<Attribute>,
mut extractor: impl FnMut(&Attribute) -> Result<bool>,
) -> Result<()> {
*attrs = attrs
.drain(..)
.filter_map(|attr| {
extractor(&attr)
.map(move |attribute_handled| if attribute_handled { None } else { Some(attr) })
.transpose()
})
.collect::<Result<_>>()?;
Ok(())
}
pub fn take_pyo3_options<T: Parse>(attrs: &mut Vec<syn::Attribute>) -> Result<Vec<T>> {
let mut out = Vec::new();
take_attributes(attrs, |attr| match get_pyo3_options(attr) {
Ok(result) => {
if let Some(options) = result {
out.extend(options.into_iter().map(|a| Ok(a)));
Ok(true)
} else {
Ok(false)
}
}
Err(err) => {
out.push(Err(err));
Ok(true)
}
})?;
let out: Vec<T> = out.into_iter().try_combine_syn_errors()?;
Ok(out)
}
+36
View File
@@ -0,0 +1,36 @@
pub(crate) trait CombineErrors: Iterator {
type Ok;
fn try_combine_syn_errors(self) -> syn::Result<Vec<Self::Ok>>;
}
impl<I, T> CombineErrors for I
where
I: Iterator<Item = syn::Result<T>>,
{
type Ok = T;
fn try_combine_syn_errors(self) -> syn::Result<Vec<Self::Ok>> {
let mut oks: Vec<Self::Ok> = Vec::new();
let mut errors: Vec<syn::Error> = Vec::new();
for res in self {
match res {
Ok(val) => oks.push(val),
Err(e) => errors.push(e),
}
}
let mut err_iter = errors.into_iter();
let mut err = match err_iter.next() {
// There are no errors
None => return Ok(oks),
Some(e) => e,
};
for e in err_iter {
err.combine(e);
}
Err(err)
}
}
+217
View File
@@ -0,0 +1,217 @@
use crate::attributes::{
self, get_pyo3_options, CrateAttribute, DefaultAttribute, FromPyWithAttribute,
IntoPyWithAttribute, RenameAllAttribute,
};
use proc_macro2::Span;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{parenthesized, Attribute, LitStr, Result, Token};
/// Attributes for deriving `FromPyObject`/`IntoPyObject` scoped on containers.
pub enum ContainerAttribute {
/// Treat the Container as a Wrapper, operate directly on its field
Transparent(attributes::kw::transparent),
/// Force every field to be extracted from item of source Python object.
ItemAll(attributes::kw::from_item_all),
/// Change the name of an enum variant in the generated error message.
ErrorAnnotation(LitStr),
/// Change the path for the pyo3 crate
Crate(CrateAttribute),
/// Converts the field idents according to the [RenamingRule](attributes::RenamingRule) before extraction
RenameAll(RenameAllAttribute),
}
impl Parse for ContainerAttribute {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::transparent) {
let kw: attributes::kw::transparent = input.parse()?;
Ok(ContainerAttribute::Transparent(kw))
} else if lookahead.peek(attributes::kw::from_item_all) {
let kw: attributes::kw::from_item_all = input.parse()?;
Ok(ContainerAttribute::ItemAll(kw))
} else if lookahead.peek(attributes::kw::annotation) {
let _: attributes::kw::annotation = input.parse()?;
let _: Token![=] = input.parse()?;
input.parse().map(ContainerAttribute::ErrorAnnotation)
} else if lookahead.peek(Token![crate]) {
input.parse().map(ContainerAttribute::Crate)
} else if lookahead.peek(attributes::kw::rename_all) {
input.parse().map(ContainerAttribute::RenameAll)
} else {
Err(lookahead.error())
}
}
}
#[derive(Default, Clone)]
pub struct ContainerAttributes {
/// Treat the Container as a Wrapper, operate directly on its field
pub transparent: Option<attributes::kw::transparent>,
/// Force every field to be extracted from item of source Python object.
pub from_item_all: Option<attributes::kw::from_item_all>,
/// Change the name of an enum variant in the generated error message.
pub annotation: Option<syn::LitStr>,
/// Change the path for the pyo3 crate
pub krate: Option<CrateAttribute>,
/// Converts the field idents according to the [RenamingRule](attributes::RenamingRule) before extraction
pub rename_all: Option<RenameAllAttribute>,
}
impl ContainerAttributes {
pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
let mut options = ContainerAttributes::default();
for attr in attrs {
if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
pyo3_attrs
.into_iter()
.try_for_each(|opt| options.set_option(opt))?;
}
}
Ok(options)
}
fn set_option(&mut self, option: ContainerAttribute) -> syn::Result<()> {
macro_rules! set_option {
($key:ident) => {
{
ensure_spanned!(
self.$key.is_none(),
$key.span() => concat!("`", stringify!($key), "` may only be specified once")
);
self.$key = Some($key);
}
};
}
match option {
ContainerAttribute::Transparent(transparent) => set_option!(transparent),
ContainerAttribute::ItemAll(from_item_all) => set_option!(from_item_all),
ContainerAttribute::ErrorAnnotation(annotation) => set_option!(annotation),
ContainerAttribute::Crate(krate) => set_option!(krate),
ContainerAttribute::RenameAll(rename_all) => set_option!(rename_all),
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub enum FieldGetter {
GetItem(attributes::kw::item, Option<syn::Lit>),
GetAttr(attributes::kw::attribute, Option<syn::LitStr>),
}
impl FieldGetter {
pub fn span(&self) -> Span {
match self {
FieldGetter::GetItem(item, _) => item.span,
FieldGetter::GetAttr(attribute, _) => attribute.span,
}
}
}
pub enum FieldAttribute {
Getter(FieldGetter),
FromPyWith(FromPyWithAttribute),
IntoPyWith(IntoPyWithAttribute),
Default(DefaultAttribute),
}
impl Parse for FieldAttribute {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::attribute) {
let attr_kw: attributes::kw::attribute = input.parse()?;
if input.peek(syn::token::Paren) {
let content;
let _ = parenthesized!(content in input);
let attr_name: LitStr = content.parse()?;
if !content.is_empty() {
return Err(content.error(
"expected at most one argument: `attribute` or `attribute(\"name\")`",
));
}
ensure_spanned!(
!attr_name.value().is_empty(),
attr_name.span() => "attribute name cannot be empty"
);
Ok(Self::Getter(FieldGetter::GetAttr(attr_kw, Some(attr_name))))
} else {
Ok(Self::Getter(FieldGetter::GetAttr(attr_kw, None)))
}
} else if lookahead.peek(attributes::kw::item) {
let item_kw: attributes::kw::item = input.parse()?;
if input.peek(syn::token::Paren) {
let content;
let _ = parenthesized!(content in input);
let key = content.parse()?;
if !content.is_empty() {
return Err(
content.error("expected at most one argument: `item` or `item(key)`")
);
}
Ok(Self::Getter(FieldGetter::GetItem(item_kw, Some(key))))
} else {
Ok(Self::Getter(FieldGetter::GetItem(item_kw, None)))
}
} else if lookahead.peek(attributes::kw::from_py_with) {
input.parse().map(Self::FromPyWith)
} else if lookahead.peek(attributes::kw::into_py_with) {
input.parse().map(FieldAttribute::IntoPyWith)
} else if lookahead.peek(Token![default]) {
input.parse().map(Self::Default)
} else {
Err(lookahead.error())
}
}
}
#[derive(Clone, Debug, Default)]
pub struct FieldAttributes {
pub getter: Option<FieldGetter>,
pub from_py_with: Option<FromPyWithAttribute>,
pub into_py_with: Option<IntoPyWithAttribute>,
pub default: Option<DefaultAttribute>,
}
impl FieldAttributes {
/// Extract the field attributes.
pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
let mut options = FieldAttributes::default();
for attr in attrs {
if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
pyo3_attrs
.into_iter()
.try_for_each(|opt| options.set_option(opt))?;
}
}
Ok(options)
}
fn set_option(&mut self, option: FieldAttribute) -> syn::Result<()> {
macro_rules! set_option {
($key:ident) => {
set_option!($key, concat!("`", stringify!($key), "` may only be specified once"))
};
($key:ident, $msg: expr) => {{
ensure_spanned!(
self.$key.is_none(),
$key.span() => $msg
);
self.$key = Some($key);
}}
}
match option {
FieldAttribute::Getter(getter) => {
set_option!(getter, "only one of `attribute` or `item` can be provided")
}
FieldAttribute::FromPyWith(from_py_with) => set_option!(from_py_with),
FieldAttribute::IntoPyWith(into_py_with) => set_option!(into_py_with),
FieldAttribute::Default(default) => set_option!(default),
}
Ok(())
}
}
+598
View File
@@ -0,0 +1,598 @@
use crate::attributes::{DefaultAttribute, FromPyWithAttribute, RenamingRule};
use crate::derive_attributes::{ContainerAttributes, FieldAttributes, FieldGetter};
#[cfg(feature = "experimental-inspect")]
use crate::py_expr::PyExpr;
use crate::utils::{self, Ctx};
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::{
ext::IdentExt, parse_quote, punctuated::Punctuated, spanned::Spanned, DataEnum, DeriveInput,
Fields, Ident, Result, Token,
};
/// Describes derivation input of an enum.
struct Enum<'a> {
enum_ident: &'a Ident,
variants: Vec<Container<'a>>,
}
impl<'a> Enum<'a> {
/// Construct a new enum representation.
///
/// `data_enum` is the `syn` representation of the input enum, `ident` is the
/// `Identifier` of the enum.
fn new(
data_enum: &'a DataEnum,
ident: &'a Ident,
options: ContainerAttributes,
) -> Result<Self> {
ensure_spanned!(
!data_enum.variants.is_empty(),
ident.span() => "cannot derive FromPyObject for empty enum"
);
let variants = data_enum
.variants
.iter()
.map(|variant| {
let mut variant_options = ContainerAttributes::from_attrs(&variant.attrs)?;
if let Some(rename_all) = &options.rename_all {
ensure_spanned!(
variant_options.rename_all.is_none(),
variant_options.rename_all.span() => "Useless variant `rename_all` - enum is already annotated with `rename_all"
);
variant_options.rename_all = Some(rename_all.clone());
}
let var_ident = &variant.ident;
Container::new(
&variant.fields,
parse_quote!(#ident::#var_ident),
variant_options,
)
})
.collect::<Result<Vec<_>>>()?;
Ok(Enum {
enum_ident: ident,
variants,
})
}
/// Build derivation body for enums.
fn build(&self, ctx: &Ctx) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let mut var_extracts = Vec::new();
let mut variant_names = Vec::new();
let mut error_names = Vec::new();
for var in &self.variants {
let struct_derive = var.build(ctx);
let ext = quote!({
let maybe_ret = || -> #pyo3_path::PyResult<Self> {
#struct_derive
}();
match maybe_ret {
ok @ ::std::result::Result::Ok(_) => return ok,
::std::result::Result::Err(err) => err
}
});
var_extracts.push(ext);
variant_names.push(var.path.segments.last().unwrap().ident.to_string());
error_names.push(&var.err_name);
}
let ty_name = self.enum_ident.to_string();
quote!(
let errors = [
#(#var_extracts),*
];
::std::result::Result::Err(
#pyo3_path::impl_::frompyobject::failed_to_extract_enum(
obj.py(),
#ty_name,
&[#(#variant_names),*],
&[#(#error_names),*],
&errors
)
)
)
}
#[cfg(feature = "experimental-inspect")]
fn input_type(&self) -> PyExpr {
self.variants
.iter()
.map(|var| var.input_type())
.reduce(PyExpr::union)
.expect("Empty enum")
}
}
struct NamedStructField<'a> {
ident: &'a syn::Ident,
getter: Option<FieldGetter>,
from_py_with: Option<FromPyWithAttribute>,
default: Option<DefaultAttribute>,
ty: &'a syn::Type,
}
struct TupleStructField {
from_py_with: Option<FromPyWithAttribute>,
ty: syn::Type,
}
/// Container Style
///
/// Covers Structs, Tuplestructs and corresponding Newtypes.
enum ContainerType<'a> {
/// Struct Container, e.g. `struct Foo { a: String }`
///
/// Variant contains the list of field identifiers and the corresponding extraction call.
Struct(Vec<NamedStructField<'a>>),
/// Newtype struct container, e.g. `#[transparent] struct Foo { a: String }`
///
/// The field specified by the identifier is extracted directly from the object.
#[cfg_attr(not(feature = "experimental-inspect"), allow(unused))]
StructNewtype(&'a syn::Ident, Option<FromPyWithAttribute>, &'a syn::Type),
/// Tuple struct, e.g. `struct Foo(String)`.
///
/// Variant contains a list of conversion methods for each of the fields that are directly
/// extracted from the tuple.
Tuple(Vec<TupleStructField>),
/// Tuple newtype, e.g. `#[transparent] struct Foo(String)`
///
/// The wrapped field is directly extracted from the object.
#[cfg_attr(not(feature = "experimental-inspect"), allow(unused))]
TupleNewtype(Option<FromPyWithAttribute>, Box<syn::Type>),
}
/// Data container
///
/// Either describes a struct or an enum variant.
struct Container<'a> {
path: syn::Path,
ty: ContainerType<'a>,
err_name: String,
rename_rule: Option<RenamingRule>,
}
impl<'a> Container<'a> {
/// Construct a container based on fields, identifier and attributes.
///
/// Fails if the variant has no fields or incompatible attributes.
fn new(fields: &'a Fields, path: syn::Path, options: ContainerAttributes) -> Result<Self> {
let style = match fields {
Fields::Unnamed(unnamed) if !unnamed.unnamed.is_empty() => {
ensure_spanned!(
options.rename_all.is_none(),
options.rename_all.span() => "`rename_all` is useless on tuple structs and variants."
);
let mut tuple_fields = unnamed
.unnamed
.iter()
.map(|field| {
let attrs = FieldAttributes::from_attrs(&field.attrs)?;
ensure_spanned!(
attrs.getter.is_none(),
field.span() => "`getter` is not permitted on tuple struct elements."
);
ensure_spanned!(
attrs.default.is_none(),
field.span() => "`default` is not permitted on tuple struct elements."
);
Ok(TupleStructField {
from_py_with: attrs.from_py_with,
ty: field.ty.clone(),
})
})
.collect::<Result<Vec<_>>>()?;
if tuple_fields.len() == 1 {
// Always treat a 1-length tuple struct as "transparent", even without the
// explicit annotation.
let field = tuple_fields.pop().unwrap();
ContainerType::TupleNewtype(field.from_py_with, Box::new(field.ty))
} else if options.transparent.is_some() {
bail_spanned!(
fields.span() => "transparent structs and variants can only have 1 field"
);
} else {
ContainerType::Tuple(tuple_fields)
}
}
Fields::Named(named) if !named.named.is_empty() => {
let mut struct_fields = named
.named
.iter()
.map(|field| {
let ident = field
.ident
.as_ref()
.expect("Named fields should have identifiers");
let mut attrs = FieldAttributes::from_attrs(&field.attrs)?;
if let Some(ref from_item_all) = options.from_item_all {
if let Some(replaced) = attrs.getter.replace(FieldGetter::GetItem(parse_quote!(item), None))
{
match replaced {
FieldGetter::GetItem(item, Some(item_name)) => {
attrs.getter = Some(FieldGetter::GetItem(item, Some(item_name)));
}
FieldGetter::GetItem(_, None) => bail_spanned!(from_item_all.span() => "Useless `item` - the struct is already annotated with `from_item_all`"),
FieldGetter::GetAttr(_, _) => bail_spanned!(
from_item_all.span() => "The struct is already annotated with `from_item_all`, `attribute` is not allowed"
),
}
}
}
Ok(NamedStructField {
ident,
getter: attrs.getter,
from_py_with: attrs.from_py_with,
default: attrs.default,
ty: &field.ty,
})
})
.collect::<Result<Vec<_>>>()?;
if struct_fields.iter().all(|field| field.default.is_some()) {
bail_spanned!(
fields.span() => "cannot derive FromPyObject for structs and variants with only default values"
)
} else if options.transparent.is_some() {
ensure_spanned!(
struct_fields.len() == 1,
fields.span() => "transparent structs and variants can only have 1 field"
);
ensure_spanned!(
options.rename_all.is_none(),
options.rename_all.span() => "`rename_all` is not permitted on `transparent` structs and variants"
);
let field = struct_fields.pop().unwrap();
ensure_spanned!(
field.getter.is_none(),
field.ident.span() => "`transparent` structs may not have a `getter` for the inner field"
);
ContainerType::StructNewtype(field.ident, field.from_py_with, field.ty)
} else {
ContainerType::Struct(struct_fields)
}
}
_ => bail_spanned!(
fields.span() => "cannot derive FromPyObject for empty structs and variants"
),
};
let err_name = options.annotation.map_or_else(
|| path.segments.last().unwrap().ident.to_string(),
|lit_str| lit_str.value(),
);
let v = Container {
path,
ty: style,
err_name,
rename_rule: options.rename_all.map(|v| v.value.rule),
};
Ok(v)
}
fn name(&self) -> String {
let mut value = String::new();
for segment in &self.path.segments {
if !value.is_empty() {
value.push_str("::");
}
value.push_str(&segment.ident.to_string());
}
value
}
/// Build derivation body for a struct.
fn build(&self, ctx: &Ctx) -> TokenStream {
match &self.ty {
ContainerType::StructNewtype(ident, from_py_with, _) => {
self.build_newtype_struct(Some(ident), from_py_with, ctx)
}
ContainerType::TupleNewtype(from_py_with, _) => {
self.build_newtype_struct(None, from_py_with, ctx)
}
ContainerType::Tuple(tups) => self.build_tuple_struct(tups, ctx),
ContainerType::Struct(tups) => self.build_struct(tups, ctx),
}
}
fn build_newtype_struct(
&self,
field_ident: Option<&Ident>,
from_py_with: &Option<FromPyWithAttribute>,
ctx: &Ctx,
) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let self_ty = &self.path;
let struct_name = self.name();
if let Some(ident) = field_ident {
let field_name = ident.to_string();
if let Some(FromPyWithAttribute {
kw,
value: expr_path,
}) = from_py_with
{
let extractor = quote_spanned! { kw.span =>
{ let from_py_with: fn(_) -> _ = #expr_path; from_py_with }
};
quote! {
Ok(#self_ty {
#ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#extractor, obj, #struct_name, #field_name)?
})
}
} else {
quote! {
Ok(#self_ty {
#ident: #pyo3_path::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)?
})
}
}
} else if let Some(FromPyWithAttribute {
kw,
value: expr_path,
}) = from_py_with
{
let extractor = quote_spanned! { kw.span =>
{ let from_py_with: fn(_) -> _ = #expr_path; from_py_with }
};
quote! {
#pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#extractor, obj, #struct_name, 0).map(#self_ty)
}
} else {
quote! {
#pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty)
}
}
}
fn build_tuple_struct(&self, struct_fields: &[TupleStructField], ctx: &Ctx) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let self_ty = &self.path;
let struct_name = &self.name();
let field_idents: Vec<_> = (0..struct_fields.len())
.map(|i| format_ident!("arg{}", i))
.collect();
let fields = struct_fields.iter().zip(&field_idents).enumerate().map(|(index, (field, ident))| {
if let Some(FromPyWithAttribute {
kw,
value: expr_path, ..
}) = &field.from_py_with {
let extractor = quote_spanned! { kw.span =>
{ let from_py_with: fn(_) -> _ = #expr_path; from_py_with }
};
quote! {
#pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#extractor, &#ident, #struct_name, #index)?
}
} else {
quote!{
#pyo3_path::impl_::frompyobject::extract_tuple_struct_field(&#ident, #struct_name, #index)?
}}
});
quote!(
match #pyo3_path::types::PyAnyMethods::extract(obj) {
::std::result::Result::Ok((#(#field_idents),*)) => ::std::result::Result::Ok(#self_ty(#(#fields),*)),
::std::result::Result::Err(err) => ::std::result::Result::Err(err),
}
)
}
fn build_struct(&self, struct_fields: &[NamedStructField<'_>], ctx: &Ctx) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let self_ty = &self.path;
let struct_name = self.name();
let mut fields: Punctuated<TokenStream, Token![,]> = Punctuated::new();
for field in struct_fields {
let ident = field.ident;
let field_name = ident.unraw().to_string();
let getter = match field
.getter
.as_ref()
.unwrap_or(&FieldGetter::GetAttr(parse_quote!(attribute), None))
{
FieldGetter::GetAttr(_, Some(name)) => {
quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #name)))
}
FieldGetter::GetAttr(_, None) => {
let name = self
.rename_rule
.map(|rule| utils::apply_renaming_rule(rule, &field_name));
let name = name.as_deref().unwrap_or(&field_name);
quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #name)))
}
FieldGetter::GetItem(_, Some(syn::Lit::Str(key))) => {
quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #key)))
}
FieldGetter::GetItem(_, Some(key)) => {
quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #key))
}
FieldGetter::GetItem(_, None) => {
let name = self
.rename_rule
.map(|rule| utils::apply_renaming_rule(rule, &field_name));
let name = name.as_deref().unwrap_or(&field_name);
quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #name)))
}
};
let extractor = if let Some(FromPyWithAttribute {
kw,
value: expr_path,
}) = &field.from_py_with
{
let extractor = quote_spanned! { kw.span =>
{ let from_py_with: fn(_) -> _ = #expr_path; from_py_with }
};
quote! (#pyo3_path::impl_::frompyobject::extract_struct_field_with(#extractor, &#getter?, #struct_name, #field_name)?)
} else {
quote!(#pyo3_path::impl_::frompyobject::extract_struct_field(&value, #struct_name, #field_name)?)
};
let extracted = if let Some(default) = &field.default {
let default_expr = if let Some(default_expr) = &default.value {
default_expr.to_token_stream()
} else {
quote!(::std::default::Default::default())
};
quote!(if let ::std::result::Result::Ok(value) = #getter {
#extractor
} else {
#default_expr
})
} else {
quote!({
let value = #getter?;
#extractor
})
};
fields.push(quote!(#ident: #extracted));
}
quote!(::std::result::Result::Ok(#self_ty{#fields}))
}
#[cfg(feature = "experimental-inspect")]
fn input_type(&self) -> PyExpr {
match &self.ty {
ContainerType::StructNewtype(_, from_py_with, ty) => {
Self::field_input_type(from_py_with, ty)
}
ContainerType::TupleNewtype(from_py_with, ty) => {
Self::field_input_type(from_py_with, ty)
}
ContainerType::Tuple(tups) => PyExpr::subscript(
PyExpr::builtin("tuple"),
PyExpr::tuple(tups.iter().map(|TupleStructField { from_py_with, ty }| {
Self::field_input_type(from_py_with, ty)
})),
),
ContainerType::Struct(_) => {
// TODO: implement using a Protocol?
PyExpr::module_attr("_typeshed", "Incomplete")
}
}
}
#[cfg(feature = "experimental-inspect")]
fn field_input_type(from_py_with: &Option<FromPyWithAttribute>, ty: &syn::Type) -> PyExpr {
if from_py_with.is_some() {
// We don't know what from_py_with is doing
PyExpr::module_attr("_typeshed", "Incomplete")
} else {
PyExpr::from_from_py_object(ty.clone(), None)
}
}
}
fn verify_and_get_lifetime(generics: &syn::Generics) -> Result<Option<&syn::LifetimeParam>> {
let mut lifetimes = generics.lifetimes();
let lifetime = lifetimes.next();
ensure_spanned!(
lifetimes.next().is_none(),
generics.span() => "FromPyObject can be derived with at most one lifetime parameter"
);
Ok(lifetime)
}
/// Derive FromPyObject for enums and structs.
///
/// * Max 1 lifetime specifier, will be tied to `FromPyObject`'s specifier
/// * At least one field, in case of `#[transparent]`, exactly one field
/// * At least one variant for enums.
/// * Fields of input structs and enums must implement `FromPyObject` or be annotated with `from_py_with`
/// * Derivation for structs with generic fields like `struct<T> Foo(T)`
/// adds `T: FromPyObject` on the derived implementation.
pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result<TokenStream> {
let options = ContainerAttributes::from_attrs(&tokens.attrs)?;
let ctx = &Ctx::new(&options.krate, None);
let Ctx { pyo3_path, .. } = &ctx;
let (_, ty_generics, _) = tokens.generics.split_for_impl();
let mut trait_generics = tokens.generics.clone();
let lt_param = if let Some(lt) = verify_and_get_lifetime(&trait_generics)? {
lt.clone()
} else {
trait_generics.params.push(parse_quote!('py));
parse_quote!('py)
};
let (impl_generics, _, where_clause) = trait_generics.split_for_impl();
let mut where_clause = where_clause.cloned().unwrap_or_else(|| parse_quote!(where));
for param in trait_generics.type_params() {
let gen_ident = &param.ident;
where_clause
.predicates
.push(parse_quote!(#gen_ident: #pyo3_path::conversion::FromPyObjectOwned<#lt_param>))
}
let derives = match &tokens.data {
syn::Data::Enum(en) => {
if options.transparent.is_some() || options.annotation.is_some() {
bail_spanned!(tokens.span() => "`transparent` or `annotation` is not supported \
at top level for enums");
}
let en = Enum::new(en, &tokens.ident, options.clone())?;
en.build(ctx)
}
syn::Data::Struct(st) => {
if let Some(lit_str) = &options.annotation {
bail_spanned!(lit_str.span() => "`annotation` is unsupported for structs");
}
let ident = &tokens.ident;
let st = Container::new(&st.fields, parse_quote!(#ident), options.clone())?;
st.build(ctx)
}
syn::Data::Union(_) => bail_spanned!(
tokens.span() => "#[derive(FromPyObject)] is not supported for unions"
),
};
#[cfg(feature = "experimental-inspect")]
let input_type = {
let pyo3_crate_path = &ctx.pyo3_path;
let input_type = if tokens
.generics
.params
.iter()
.all(|p| matches!(p, syn::GenericParam::Lifetime(_)))
{
match &tokens.data {
syn::Data::Enum(en) => Enum::new(en, &tokens.ident, options)?.input_type(),
syn::Data::Struct(st) => {
let ident = &tokens.ident;
Container::new(&st.fields, parse_quote!(#ident), options.clone())?.input_type()
}
syn::Data::Union(_) => {
// Not supported at this point
PyExpr::module_attr("_typeshed", "Incomplete")
}
}
} else {
// We don't know how to deal with generic parameters
// Blocked by https://github.com/rust-lang/rust/issues/76560
PyExpr::module_attr("_typeshed", "Incomplete")
}
.to_introspection_token_stream(pyo3_crate_path);
quote! { const INPUT_TYPE: #pyo3_crate_path::inspect::PyStaticExpr = #input_type; }
};
#[cfg(not(feature = "experimental-inspect"))]
let input_type = quote! {};
let ident = &tokens.ident;
Ok(quote!(
#[automatically_derived]
impl #impl_generics #pyo3_path::FromPyObject<'_, #lt_param> for #ident #ty_generics #where_clause {
type Error = #pyo3_path::PyErr;
fn extract(obj: #pyo3_path::Borrowed<'_, #lt_param, #pyo3_path::PyAny>) -> ::std::result::Result<Self, #pyo3_path::PyErr> {
let obj: &#pyo3_path::Bound<'_, _> = &*obj;
#derives
}
#input_type
}
))
}
+622
View File
@@ -0,0 +1,622 @@
use crate::attributes::{IntoPyWithAttribute, RenamingRule};
use crate::derive_attributes::{ContainerAttributes, FieldAttributes};
#[cfg(feature = "experimental-inspect")]
use crate::py_expr::PyExpr;
use crate::utils::{self, Ctx};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::ext::IdentExt;
use syn::spanned::Spanned as _;
use syn::{parse_quote, DataEnum, DeriveInput, Fields, Ident, Index, Result};
struct ItemOption(Option<syn::Lit>);
enum IntoPyObjectTypes {
Transparent(syn::Type),
Opaque {
target: TokenStream,
output: TokenStream,
error: TokenStream,
},
}
struct IntoPyObjectImpl {
types: IntoPyObjectTypes,
body: TokenStream,
}
struct NamedStructField<'a> {
ident: &'a syn::Ident,
field: &'a syn::Field,
item: Option<ItemOption>,
into_py_with: Option<IntoPyWithAttribute>,
}
struct TupleStructField<'a> {
field: &'a syn::Field,
into_py_with: Option<IntoPyWithAttribute>,
}
/// Container Style
///
/// Covers Structs, Tuplestructs and corresponding Newtypes.
enum ContainerType<'a> {
/// Struct Container, e.g. `struct Foo { a: String }`
///
/// Variant contains the list of field identifiers and the corresponding extraction call.
Struct(Vec<NamedStructField<'a>>),
/// Newtype struct container, e.g. `#[transparent] struct Foo { a: String }`
///
/// The field specified by the identifier is extracted directly from the object.
StructNewtype(&'a syn::Field),
/// Tuple struct, e.g. `struct Foo(String)`.
///
/// Variant contains a list of conversion methods for each of the fields that are directly
/// extracted from the tuple.
Tuple(Vec<TupleStructField<'a>>),
/// Tuple newtype, e.g. `#[transparent] struct Foo(String)`
///
/// The wrapped field is directly extracted from the object.
TupleNewtype(&'a syn::Field),
}
/// Data container
///
/// Either describes a struct or an enum variant.
struct Container<'a, const REF: bool> {
path: syn::Path,
receiver: Option<Ident>,
ty: ContainerType<'a>,
rename_rule: Option<RenamingRule>,
}
/// Construct a container based on fields, identifier and attributes.
impl<'a, const REF: bool> Container<'a, REF> {
///
/// Fails if the variant has no fields or incompatible attributes.
fn new(
receiver: Option<Ident>,
fields: &'a Fields,
path: syn::Path,
options: ContainerAttributes,
) -> Result<Self> {
let style = match fields {
Fields::Unnamed(unnamed) if !unnamed.unnamed.is_empty() => {
ensure_spanned!(
options.rename_all.is_none(),
options.rename_all.span() => "`rename_all` is useless on tuple structs and variants."
);
let mut tuple_fields = unnamed
.unnamed
.iter()
.map(|field| {
let attrs = FieldAttributes::from_attrs(&field.attrs)?;
ensure_spanned!(
attrs.getter.is_none(),
attrs.getter.unwrap().span() => "`item` and `attribute` are not permitted on tuple struct elements."
);
Ok(TupleStructField {
field,
into_py_with: attrs.into_py_with,
})
})
.collect::<Result<Vec<_>>>()?;
if tuple_fields.len() == 1 {
// Always treat a 1-length tuple struct as "transparent", even without the
// explicit annotation.
let TupleStructField {
field,
into_py_with,
} = tuple_fields.pop().unwrap();
ensure_spanned!(
into_py_with.is_none(),
into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs"
);
ContainerType::TupleNewtype(field)
} else if options.transparent.is_some() {
bail_spanned!(
fields.span() => "transparent structs and variants can only have 1 field"
);
} else {
ContainerType::Tuple(tuple_fields)
}
}
Fields::Named(named) if !named.named.is_empty() => {
if options.transparent.is_some() {
ensure_spanned!(
named.named.iter().count() == 1,
fields.span() => "transparent structs and variants can only have 1 field"
);
let field = named.named.iter().next().unwrap();
let attrs = FieldAttributes::from_attrs(&field.attrs)?;
ensure_spanned!(
attrs.getter.is_none(),
attrs.getter.unwrap().span() => "`transparent` structs may not have `item` nor `attribute` for the inner field"
);
ensure_spanned!(
options.rename_all.is_none(),
options.rename_all.span() => "`rename_all` is not permitted on `transparent` structs and variants"
);
ensure_spanned!(
attrs.into_py_with.is_none(),
attrs.into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs or variants"
);
ContainerType::StructNewtype(field)
} else {
let struct_fields = named
.named
.iter()
.map(|field| {
let ident = field
.ident
.as_ref()
.expect("Named fields should have identifiers");
let attrs = FieldAttributes::from_attrs(&field.attrs)?;
Ok(NamedStructField {
ident,
field,
item: attrs.getter.and_then(|getter| match getter {
crate::derive_attributes::FieldGetter::GetItem(_, lit) => {
Some(ItemOption(lit))
}
crate::derive_attributes::FieldGetter::GetAttr(_, _) => None,
}),
into_py_with: attrs.into_py_with,
})
})
.collect::<Result<Vec<_>>>()?;
ContainerType::Struct(struct_fields)
}
}
_ => bail_spanned!(
fields.span() => "cannot derive `IntoPyObject` for empty structs"
),
};
let v = Container {
path,
receiver,
ty: style,
rename_rule: options.rename_all.map(|v| v.value.rule),
};
Ok(v)
}
fn match_pattern(&self) -> TokenStream {
let path = &self.path;
let pattern = match &self.ty {
ContainerType::Struct(fields) => fields
.iter()
.enumerate()
.map(|(i, f)| {
let ident = f.ident;
let new_ident = format_ident!("arg{i}");
quote! {#ident: #new_ident,}
})
.collect::<TokenStream>(),
ContainerType::StructNewtype(field) => {
let ident = field.ident.as_ref().unwrap();
quote!(#ident: arg0)
}
ContainerType::Tuple(fields) => {
let i = (0..fields.len()).map(Index::from);
let idents = (0..fields.len()).map(|i| format_ident!("arg{i}"));
quote! { #(#i: #idents,)* }
}
ContainerType::TupleNewtype(_) => quote!(0: arg0),
};
quote! { #path{ #pattern } }
}
/// Build derivation body for a struct.
fn build(&self, ctx: &Ctx) -> IntoPyObjectImpl {
match &self.ty {
ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => {
self.build_newtype_struct(field, ctx)
}
ContainerType::Tuple(fields) => self.build_tuple_struct(fields, ctx),
ContainerType::Struct(fields) => self.build_struct(fields, ctx),
}
}
fn build_newtype_struct(&self, field: &syn::Field, ctx: &Ctx) -> IntoPyObjectImpl {
let Ctx { pyo3_path, .. } = ctx;
let ty = &field.ty;
let unpack = self
.receiver
.as_ref()
.map(|i| {
let pattern = self.match_pattern();
quote! { let #pattern = #i;}
})
.unwrap_or_default();
IntoPyObjectImpl {
types: IntoPyObjectTypes::Transparent(ty.clone()),
body: quote_spanned! { ty.span() =>
#unpack
#pyo3_path::conversion::IntoPyObject::into_pyobject(arg0, py)
},
}
}
fn build_struct(&self, fields: &[NamedStructField<'_>], ctx: &Ctx) -> IntoPyObjectImpl {
let Ctx { pyo3_path, .. } = ctx;
let unpack = self
.receiver
.as_ref()
.map(|i| {
let pattern = self.match_pattern();
quote! { let #pattern = #i;}
})
.unwrap_or_default();
let setter = fields
.iter()
.enumerate()
.map(|(i, f)| {
let key = f
.item
.as_ref()
.and_then(|item| item.0.as_ref())
.map(|item| item.into_token_stream())
.unwrap_or_else(|| {
let name = f.ident.unraw().to_string();
self.rename_rule.map(|rule| utils::apply_renaming_rule(rule, &name)).unwrap_or(name).into_token_stream()
});
let value = Ident::new(&format!("arg{i}"), f.field.ty.span());
if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
let cow = if REF {
quote!(::std::borrow::Cow::Borrowed(#value))
} else {
quote!(::std::borrow::Cow::Owned(#value))
};
quote! {
let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path;
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, into_py_with(#cow, py)?)?;
}
} else {
quote! {
#pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?;
}
}
})
.collect::<TokenStream>();
IntoPyObjectImpl {
types: IntoPyObjectTypes::Opaque {
target: quote!(#pyo3_path::types::PyDict),
output: quote!(#pyo3_path::Bound<'py, Self::Target>),
error: quote!(#pyo3_path::PyErr),
},
body: quote! {
#unpack
let dict = #pyo3_path::types::PyDict::new(py);
#setter
::std::result::Result::Ok::<_, Self::Error>(dict)
},
}
}
fn build_tuple_struct(&self, fields: &[TupleStructField<'_>], ctx: &Ctx) -> IntoPyObjectImpl {
let Ctx { pyo3_path, .. } = ctx;
let unpack = self
.receiver
.as_ref()
.map(|i| {
let pattern = self.match_pattern();
quote! { let #pattern = #i;}
})
.unwrap_or_default();
let setter = fields
.iter()
.enumerate()
.map(|(i, f)| {
let ty = &f.field.ty;
let value = Ident::new(&format!("arg{i}"), f.field.ty.span());
if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) {
let cow = if REF {
quote!(::std::borrow::Cow::Borrowed(#value))
} else {
quote!(::std::borrow::Cow::Owned(#value))
};
quote_spanned! { ty.span() =>
{
let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path;
into_py_with(#cow, py)?
},
}
} else {
quote_spanned! { ty.span() =>
#pyo3_path::conversion::IntoPyObject::into_pyobject(#value, py)
.map(#pyo3_path::BoundObject::into_any)
.map(#pyo3_path::BoundObject::into_bound)?,
}
}
})
.collect::<TokenStream>();
IntoPyObjectImpl {
types: IntoPyObjectTypes::Opaque {
target: quote!(#pyo3_path::types::PyTuple),
output: quote!(#pyo3_path::Bound<'py, Self::Target>),
error: quote!(#pyo3_path::PyErr),
},
body: quote! {
#unpack
#pyo3_path::types::PyTuple::new(py, [#setter])
},
}
}
#[cfg(feature = "experimental-inspect")]
fn output_type(&self) -> PyExpr {
match &self.ty {
ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => {
Self::field_output_type(&None, &field.ty)
}
ContainerType::Tuple(tups) => PyExpr::subscript(
PyExpr::builtin("tuple"),
PyExpr::tuple(tups.iter().map(
|TupleStructField {
into_py_with,
field,
}| { Self::field_output_type(into_py_with, &field.ty) },
)),
),
ContainerType::Struct(_) => {
// TODO: implement using a Protocol?
PyExpr::module_attr("_typeshed", "Incomplete")
}
}
}
#[cfg(feature = "experimental-inspect")]
fn field_output_type(into_py_with: &Option<IntoPyWithAttribute>, ty: &syn::Type) -> PyExpr {
if into_py_with.is_some() {
// We don't know what into_py_with is doing
PyExpr::module_attr("_typeshed", "Incomplete")
} else {
PyExpr::from_into_py_object(ty.clone(), None)
}
}
}
/// Describes derivation input of an enum.
struct Enum<'a, const REF: bool> {
variants: Vec<Container<'a, REF>>,
}
impl<'a, const REF: bool> Enum<'a, REF> {
/// Construct a new enum representation.
///
/// `data_enum` is the `syn` representation of the input enum, `ident` is the
/// `Identifier` of the enum.
fn new(data_enum: &'a DataEnum, ident: &'a Ident) -> Result<Self> {
ensure_spanned!(
!data_enum.variants.is_empty(),
ident.span() => "cannot derive `IntoPyObject` for empty enum"
);
let variants = data_enum
.variants
.iter()
.map(|variant| {
let attrs = ContainerAttributes::from_attrs(&variant.attrs)?;
let var_ident = &variant.ident;
ensure_spanned!(
!variant.fields.is_empty(),
variant.ident.span() => "cannot derive `IntoPyObject` for empty variants"
);
Container::new(
None,
&variant.fields,
parse_quote!(#ident::#var_ident),
attrs,
)
})
.collect::<Result<Vec<_>>>()?;
Ok(Enum { variants })
}
/// Build derivation body for enums.
fn build(&self, ctx: &Ctx) -> IntoPyObjectImpl {
let Ctx { pyo3_path, .. } = ctx;
let variants = self
.variants
.iter()
.map(|v| {
let IntoPyObjectImpl { body, .. } = v.build(ctx);
let pattern = v.match_pattern();
quote! {
#pattern => {
{#body}
.map(#pyo3_path::BoundObject::into_any)
.map(#pyo3_path::BoundObject::into_bound)
.map_err(::std::convert::Into::<#pyo3_path::PyErr>::into)
}
}
})
.collect::<TokenStream>();
IntoPyObjectImpl {
types: IntoPyObjectTypes::Opaque {
target: quote!(#pyo3_path::types::PyAny),
output: quote!(#pyo3_path::Bound<'py, <Self as #pyo3_path::conversion::IntoPyObject<'py>>::Target>),
error: quote!(#pyo3_path::PyErr),
},
body: quote! {
match self {
#variants
}
},
}
}
#[cfg(feature = "experimental-inspect")]
fn output_type(&self) -> PyExpr {
self.variants
.iter()
.map(|var| var.output_type())
.reduce(PyExpr::union)
.expect("Empty enum")
}
}
// if there is a `'py` lifetime, we treat it as the `Python<'py>` lifetime
fn verify_and_get_lifetime(generics: &syn::Generics) -> Option<&syn::LifetimeParam> {
let mut lifetimes = generics.lifetimes();
lifetimes.find(|l| l.lifetime.ident == "py")
}
pub fn build_derive_into_pyobject<const REF: bool>(tokens: &DeriveInput) -> Result<TokenStream> {
let options = ContainerAttributes::from_attrs(&tokens.attrs)?;
let ctx = &Ctx::new(&options.krate, None);
let Ctx { pyo3_path, .. } = &ctx;
let (_, ty_generics, _) = tokens.generics.split_for_impl();
let mut trait_generics = tokens.generics.clone();
if REF {
trait_generics.params.push(parse_quote!('_a));
}
let lt_param = if let Some(lt) = verify_and_get_lifetime(&trait_generics) {
lt.clone()
} else {
trait_generics.params.push(parse_quote!('py));
parse_quote!('py)
};
let (impl_generics, _, where_clause) = trait_generics.split_for_impl();
let mut where_clause = where_clause.cloned().unwrap_or_else(|| parse_quote!(where));
for param in trait_generics.type_params() {
let gen_ident = &param.ident;
where_clause.predicates.push(if REF {
parse_quote!(&'_a #gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)
} else {
parse_quote!(#gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)
})
}
let IntoPyObjectImpl { types, body } = match &tokens.data {
syn::Data::Enum(en) => {
if options.transparent.is_some() {
bail_spanned!(tokens.span() => "`transparent` is not supported at top level for enums");
}
if let Some(rename_all) = options.rename_all {
bail_spanned!(rename_all.span() => "`rename_all` is not supported at top level for enums");
}
let en = Enum::<REF>::new(en, &tokens.ident)?;
en.build(ctx)
}
syn::Data::Struct(st) => {
let ident = &tokens.ident;
let st = Container::<REF>::new(
Some(Ident::new("self", Span::call_site())),
&st.fields,
parse_quote!(#ident),
options.clone(),
)?;
st.build(ctx)
}
syn::Data::Union(_) => bail_spanned!(
tokens.span() => "#[derive(`IntoPyObject`)] is not supported for unions"
),
};
let (target, output, error) = match types {
IntoPyObjectTypes::Transparent(ty) => {
if REF {
(
quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Target },
quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Output },
quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Error },
)
} else {
(
quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Target },
quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Output },
quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Error },
)
}
}
IntoPyObjectTypes::Opaque {
target,
output,
error,
} => (target, output, error),
};
let ident = &tokens.ident;
let ident = if REF {
quote! { &'_a #ident}
} else {
quote! { #ident }
};
#[cfg(feature = "experimental-inspect")]
let output_type = {
let pyo3_crate_path = &ctx.pyo3_path;
let output_type = if tokens
.generics
.params
.iter()
.all(|p| matches!(p, syn::GenericParam::Lifetime(_)))
{
match &tokens.data {
syn::Data::Enum(en) => Enum::<REF>::new(en, &tokens.ident)?.output_type(),
syn::Data::Struct(st) => {
let ident = &tokens.ident;
Container::<REF>::new(
Some(Ident::new("self", Span::call_site())),
&st.fields,
parse_quote!(#ident),
options,
)?
.output_type()
}
syn::Data::Union(_) => {
// Not supported at this point
PyExpr::module_attr("_typeshed", "Incomplete")
}
}
} else {
// We don't know how to deal with generic parameters
// Blocked by https://github.com/rust-lang/rust/issues/76560
PyExpr::module_attr("_typeshed", "Incomplete")
}
.to_introspection_token_stream(pyo3_crate_path);
quote! { const OUTPUT_TYPE: #pyo3_path::inspect::PyStaticExpr = #output_type; }
};
#[cfg(not(feature = "experimental-inspect"))]
let output_type = quote! {};
Ok(quote!(
#[automatically_derived]
impl #impl_generics #pyo3_path::conversion::IntoPyObject<#lt_param> for #ident #ty_generics #where_clause {
type Target = #target;
type Output = #output;
type Error = #error;
#output_type
fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result<
<Self as #pyo3_path::conversion::IntoPyObject<#lt_param>>::Output,
<Self as #pyo3_path::conversion::IntoPyObject<#lt_param>>::Error,
> {
#body
}
}
))
}
+641
View File
@@ -0,0 +1,641 @@
//! Generates introspection data i.e. JSON strings in the .pyo3i0 section.
//!
//! There is a JSON per PyO3 proc macro (pyclass, pymodule, pyfunction...).
//!
//! These JSON blobs can refer to each others via the _PYO3_INTROSPECTION_ID constants
//! providing unique ids for each element.
//!
//! The JSON blobs format must be synchronized with the `pyo3_introspection::introspection.rs::Chunk`
//! type that is used to parse them.
use crate::method::{FnArg, RegularArg};
use crate::py_expr::PyExpr;
use crate::pyfunction::FunctionSignature;
use crate::utils::{PyO3CratePath, PythonDoc, StrOrExpr};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::collections::BTreeMap;
use std::fmt::Write;
use std::hash::{Hash, Hasher};
use std::mem::take;
use std::sync::atomic::{AtomicUsize, Ordering};
use syn::{Attribute, Ident, Type, TypePath};
static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0);
pub fn module_introspection_code<'a>(
pyo3_crate_path: &PyO3CratePath,
name: &str,
members: impl IntoIterator<Item = &'a Ident>,
members_cfg_attrs: impl IntoIterator<Item = &'a Vec<Attribute>>,
doc: Option<&PythonDoc>,
incomplete: bool,
) -> TokenStream {
let mut desc = BTreeMap::from([
("type", IntrospectionNode::String("module".into())),
("id", IntrospectionNode::IntrospectionId(None)),
("name", IntrospectionNode::String(name.into())),
(
"members",
IntrospectionNode::List(
members
.into_iter()
.zip(members_cfg_attrs)
.map(|(member, attributes)| AttributedIntrospectionNode {
node: IntrospectionNode::IntrospectionId(Some(ident_to_type(member))),
attributes,
})
.collect(),
),
),
("incomplete", IntrospectionNode::Bool(incomplete)),
]);
if let Some(doc) = doc {
desc.insert("doc", IntrospectionNode::Doc(doc));
}
IntrospectionNode::Map(desc).emit(pyo3_crate_path)
}
pub fn class_introspection_code(
pyo3_crate_path: &PyO3CratePath,
ident: &Ident,
name: &str,
extends: Option<PyExpr>,
is_final: bool,
parent: Option<&Type>,
doc: Option<&PythonDoc>,
) -> TokenStream {
let mut desc = BTreeMap::from([
("type", IntrospectionNode::String("class".into())),
(
"id",
IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))),
),
("name", IntrospectionNode::String(name.into())),
]);
if let Some(extends) = extends {
desc.insert("bases", IntrospectionNode::List(vec![extends.into()]));
}
if is_final {
desc.insert(
"decorators",
IntrospectionNode::List(vec![PyExpr::module_attr("typing", "final").into()]),
);
}
if let Some(parent) = parent {
desc.insert(
"parent",
IntrospectionNode::IntrospectionId(Some(Cow::Borrowed(parent))),
);
}
if let Some(doc) = &doc {
desc.insert("doc", IntrospectionNode::Doc(doc));
}
IntrospectionNode::Map(desc).emit(pyo3_crate_path)
}
#[expect(clippy::too_many_arguments)]
pub fn function_introspection_code(
pyo3_crate_path: &PyO3CratePath,
ident: Option<&Ident>,
name: &str,
signature: &FunctionSignature<'_>,
first_argument: Option<&'static str>,
returns: PyExpr,
decorators: impl IntoIterator<Item = PyExpr>,
is_async: bool,
is_returning_not_implemented_on_extraction_error: bool,
doc: Option<&PythonDoc>,
parent: Option<&Type>,
) -> TokenStream {
let mut desc = BTreeMap::from([
("type", IntrospectionNode::String("function".into())),
("name", IntrospectionNode::String(name.into())),
(
"arguments",
arguments_introspection_data(
signature,
first_argument,
is_returning_not_implemented_on_extraction_error,
parent,
),
),
(
"returns",
if let Some((_, returns)) = signature
.attribute
.as_ref()
.and_then(|attribute| attribute.value.returns.as_ref())
{
returns.as_type_hint().into()
} else {
returns.into()
},
),
]);
if is_async {
desc.insert("async", IntrospectionNode::Bool(true));
}
if let Some(ident) = ident {
desc.insert(
"id",
IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))),
);
}
let decorators = decorators.into_iter().map(|d| d.into()).collect::<Vec<_>>();
if !decorators.is_empty() {
desc.insert("decorators", IntrospectionNode::List(decorators));
}
if let Some(doc) = doc {
desc.insert("doc", IntrospectionNode::Doc(doc));
}
if let Some(parent) = parent {
desc.insert(
"parent",
IntrospectionNode::IntrospectionId(Some(Cow::Borrowed(parent))),
);
}
IntrospectionNode::Map(desc).emit(pyo3_crate_path)
}
pub fn attribute_introspection_code(
pyo3_crate_path: &PyO3CratePath,
parent: Option<&Type>,
name: String,
value: PyExpr,
rust_type: Type,
doc: Option<&PythonDoc>,
is_final: bool,
) -> TokenStream {
let mut desc = BTreeMap::from([
("type", IntrospectionNode::String("attribute".into())),
("name", IntrospectionNode::String(name.into())),
(
"parent",
IntrospectionNode::IntrospectionId(parent.map(Cow::Borrowed)),
),
]);
if value == PyExpr::ellipsis() {
// We need to set a type, but not need to set the value to ..., all attributes have a value
desc.insert(
"annotation",
if is_final {
PyExpr::subscript(
PyExpr::module_attr("typing", "Final"),
PyExpr::from_return_type(rust_type, parent),
)
.into()
} else {
PyExpr::from_return_type(rust_type, parent).into()
},
);
} else {
desc.insert(
"annotation",
if is_final {
// Type checkers can infer the type from the value because it's typing.Literal[value]
// So, following stubs best practices, we only write typing.Final and not
// typing.Final[typing.literal[value]]
PyExpr::module_attr("typing", "Final")
} else {
PyExpr::from_return_type(rust_type, parent)
}
.into(),
);
desc.insert("value", value.into());
}
if let Some(doc) = doc {
desc.insert("doc", IntrospectionNode::Doc(doc));
}
IntrospectionNode::Map(desc).emit(pyo3_crate_path)
}
fn arguments_introspection_data<'a>(
signature: &'a FunctionSignature<'a>,
first_argument: Option<&'a str>,
is_returning_not_implemented_on_extraction_error: bool,
class_type: Option<&Type>,
) -> IntrospectionNode<'a> {
let mut argument_desc = signature.arguments.iter().filter(|arg| {
matches!(
arg,
FnArg::Regular(_) | FnArg::VarArgs(_) | FnArg::KwArgs(_)
)
});
let mut posonlyargs = Vec::new();
let mut args = Vec::new();
let mut vararg = None;
let mut kwonlyargs = Vec::new();
let mut kwarg = None;
if let Some(first_argument) = first_argument {
posonlyargs.push(
IntrospectionNode::Map(
[("name", IntrospectionNode::String(first_argument.into()))].into(),
)
.into(),
);
}
for (i, param) in signature
.python_signature
.positional_parameters
.iter()
.enumerate()
{
let arg_desc = if let Some(FnArg::Regular(arg_desc)) = argument_desc.next() {
arg_desc
} else {
panic!("Less arguments than in python signature");
};
let arg = argument_introspection_data(
param,
arg_desc,
is_returning_not_implemented_on_extraction_error,
class_type,
);
if i < signature.python_signature.positional_only_parameters {
posonlyargs.push(arg);
} else {
args.push(arg)
}
}
if let Some(param) = &signature.python_signature.varargs {
let Some(FnArg::VarArgs(arg_desc)) = argument_desc.next() else {
panic!("Fewer arguments than in python signature");
};
let mut params = BTreeMap::from([("name", IntrospectionNode::String(param.into()))]);
if let Some(annotation) = &arg_desc.annotation {
params.insert("annotation", annotation.clone().into());
}
vararg = Some(IntrospectionNode::Map(params));
}
for (param, _) in &signature.python_signature.keyword_only_parameters {
let Some(FnArg::Regular(arg_desc)) = argument_desc.next() else {
panic!("Less arguments than in python signature");
};
kwonlyargs.push(argument_introspection_data(
param,
arg_desc,
is_returning_not_implemented_on_extraction_error,
class_type,
));
}
if let Some(param) = &signature.python_signature.kwargs {
let Some(FnArg::KwArgs(arg_desc)) = argument_desc.next() else {
panic!("Less arguments than in python signature");
};
let mut params = BTreeMap::from([("name", IntrospectionNode::String(param.into()))]);
if let Some(annotation) = &arg_desc.annotation {
params.insert("annotation", annotation.clone().into());
}
kwarg = Some(IntrospectionNode::Map(params));
}
let mut map = BTreeMap::new();
if !posonlyargs.is_empty() {
map.insert("posonlyargs", IntrospectionNode::List(posonlyargs));
}
if !args.is_empty() {
map.insert("args", IntrospectionNode::List(args));
}
if let Some(vararg) = vararg {
map.insert("vararg", vararg);
}
if !kwonlyargs.is_empty() {
map.insert("kwonlyargs", IntrospectionNode::List(kwonlyargs));
}
if let Some(kwarg) = kwarg {
map.insert("kwarg", kwarg);
}
IntrospectionNode::Map(map)
}
fn argument_introspection_data<'a>(
name: &'a str,
desc: &'a RegularArg<'_>,
is_returning_not_implemented_on_extraction_error: bool,
class_type: Option<&Type>,
) -> AttributedIntrospectionNode<'a> {
let mut params: BTreeMap<_, _> = [("name", IntrospectionNode::String(name.into()))].into();
if let Some(expr) = &desc.default_value {
params.insert("default", PyExpr::constant_from_expression(expr).into());
}
if is_returning_not_implemented_on_extraction_error {
// all inputs are allowed, we use `object`
params.insert("annotation", PyExpr::builtin("object").into());
} else if let Some(annotation) = &desc.annotation {
params.insert("annotation", annotation.clone().into());
} else if desc.from_py_with.is_none() {
// If from_py_with is set we don't know anything on the input type
params.insert(
"annotation",
PyExpr::from_argument_type(desc.ty.clone(), class_type).into(),
);
}
IntrospectionNode::Map(params).into()
}
enum IntrospectionNode<'a> {
String(Cow<'a, str>),
Bool(bool),
IntrospectionId(Option<Cow<'a, Type>>),
TypeHint(Cow<'a, PyExpr>),
Doc(&'a PythonDoc),
Map(BTreeMap<&'static str, IntrospectionNode<'a>>),
List(Vec<AttributedIntrospectionNode<'a>>),
}
impl IntrospectionNode<'_> {
fn emit(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream {
let mut content = ConcatenationBuilder::default();
self.add_to_serialization(&mut content, pyo3_crate_path);
content.into_static(
pyo3_crate_path,
format_ident!("PYO3_INTROSPECTION_1_{}", unique_element_id()),
)
}
fn add_to_serialization(
self,
content: &mut ConcatenationBuilder,
pyo3_crate_path: &PyO3CratePath,
) {
match self {
Self::String(string) => {
content.push_str_to_escape(&string);
}
Self::Bool(value) => content.push_str(if value { "true" } else { "false" }),
Self::IntrospectionId(ident) => {
content.push_str("\"");
content.push_tokens(if let Some(ident) = ident {
quote! { #ident::_PYO3_INTROSPECTION_ID.as_bytes() }
} else {
quote! { _PYO3_INTROSPECTION_ID.as_bytes() }
});
content.push_str("\"");
}
Self::TypeHint(hint) => {
content.push_tokens(serialize_type_hint(
hint.to_introspection_token_stream(pyo3_crate_path),
pyo3_crate_path,
));
}
Self::Doc(doc) => {
content.push_str("\"");
for part in &doc.parts {
match part {
StrOrExpr::Str {value, ..} => content.push_str(&escape_json_string(value)),
StrOrExpr::Expr(value) => content.push_tokens(quote! {{
const DOC: &str = #value;
const DOC_LEN: usize = #pyo3_crate_path::impl_::introspection::escaped_json_string_len(&DOC);
const DOC_SER: [u8; DOC_LEN] = {
let mut result: [u8; DOC_LEN] = [0; DOC_LEN];
#pyo3_crate_path::impl_::introspection::escape_json_string(&DOC, &mut result);
result
};
&DOC_SER
}}),
}
}
content.push_str("\"");
}
Self::Map(map) => {
content.push_str("{");
for (i, (key, value)) in map.into_iter().enumerate() {
if i > 0 {
content.push_str(",");
}
content.push_str_to_escape(key);
content.push_str(":");
value.add_to_serialization(content, pyo3_crate_path);
}
content.push_str("}");
}
Self::List(list) => {
content.push_str("[");
if list.iter().all(|element| element.attributes.is_empty()) {
for (i, element) in list.into_iter().enumerate() {
if i > 0 {
content.push_str(",");
}
element.node.add_to_serialization(content, pyo3_crate_path);
}
} else {
// A `,` must only be written if at least one of the elements before the one
// it precedes is compiled in, so we gate it behind an `any(..)` of their
// `cfg`s on top of the element's own. This needs no special case: `any()` is
// false, so the first element gets no separator, and `all()` is true, so an
// element without `cfg` makes every later separator unconditional.
let mut preceding = Vec::new();
for AttributedIntrospectionNode { node, attributes } in list {
content.push_tokens(
quote! { #[cfg(any(#(#preceding),*))] #(#attributes)* ",".as_bytes() },
);
let cfgs = attributes
.iter()
.filter_map(|attribute| attribute.meta.require_list().ok())
.map(|cfg| &cfg.tokens);
preceding.push(quote! { all(#(#cfgs),*) });
// We serialize the element to easily gate it behind the attributes
let mut nested_builder = ConcatenationBuilder::default();
node.add_to_serialization(&mut nested_builder, pyo3_crate_path);
let nested_content = nested_builder.into_token_stream(pyo3_crate_path);
content.push_tokens(quote! { #(#attributes)* #nested_content });
}
}
content.push_str("]");
}
}
}
}
impl From<PyExpr> for IntrospectionNode<'static> {
fn from(element: PyExpr) -> Self {
Self::TypeHint(Cow::Owned(element))
}
}
fn serialize_type_hint(hint: TokenStream, pyo3_crate_path: &PyO3CratePath) -> TokenStream {
quote! {{
const TYPE_HINT: #pyo3_crate_path::inspect::PyStaticExpr = #hint;
const TYPE_HINT_LEN: usize = #pyo3_crate_path::inspect::serialized_len_for_introspection(&TYPE_HINT);
const TYPE_HINT_SER: [u8; TYPE_HINT_LEN] = {
let mut result: [u8; TYPE_HINT_LEN] = [0; TYPE_HINT_LEN];
#pyo3_crate_path::inspect::serialize_for_introspection(&TYPE_HINT, &mut result);
result
};
&TYPE_HINT_SER
}}
}
struct AttributedIntrospectionNode<'a> {
node: IntrospectionNode<'a>,
attributes: &'a [Attribute],
}
impl<'a> From<IntrospectionNode<'a>> for AttributedIntrospectionNode<'a> {
fn from(node: IntrospectionNode<'a>) -> Self {
Self {
node,
attributes: &[],
}
}
}
impl<'a> From<PyExpr> for AttributedIntrospectionNode<'a> {
fn from(node: PyExpr) -> Self {
IntrospectionNode::from(node).into()
}
}
#[derive(Default)]
pub struct ConcatenationBuilder {
elements: Vec<ConcatenationBuilderElement>,
current_string: String,
}
impl ConcatenationBuilder {
pub fn push_tokens(&mut self, token_stream: TokenStream) {
if !self.current_string.is_empty() {
self.elements.push(ConcatenationBuilderElement::String(take(
&mut self.current_string,
)));
}
self.elements
.push(ConcatenationBuilderElement::TokenStream(token_stream));
}
pub fn push_str(&mut self, value: &str) {
self.current_string.push_str(value);
}
fn push_str_to_escape(&mut self, value: &str) {
self.current_string.push('"');
for c in value.chars() {
match c {
'\\' => self.current_string.push_str("\\\\"),
'"' => self.current_string.push_str("\\\""),
c => {
if c < char::from(32) {
panic!("ASCII chars below 32 are not allowed")
} else {
self.current_string.push(c);
}
}
}
}
self.current_string.push('"');
}
pub fn into_token_stream(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream {
let mut elements = self.elements;
if !self.current_string.is_empty() {
elements.push(ConcatenationBuilderElement::String(self.current_string));
}
if let [ConcatenationBuilderElement::String(string)] = elements.as_slice() {
// We avoid the const_concat! macro if there is only a single string
return quote! { #string.as_bytes() };
}
quote! {
{
const PIECES: &[&[u8]] = &[#(#elements , )*];
&#pyo3_crate_path::impl_::concat::combine_to_array::<{
#pyo3_crate_path::impl_::concat::combined_len(PIECES)
}>(PIECES)
}
}
}
fn into_static(self, pyo3_crate_path: &PyO3CratePath, ident: Ident) -> TokenStream {
let mut elements = self.elements;
if !self.current_string.is_empty() {
elements.push(ConcatenationBuilderElement::String(self.current_string));
}
// #[no_mangle] is required to make sure some linkers like Linux ones do not mangle the section name too.
quote! {
const _: () = {
const PIECES: &[&[u8]] = &[#(#elements , )*];
const PIECES_LEN: usize = #pyo3_crate_path::impl_::concat::combined_len(PIECES);
#[used]
#[no_mangle]
static #ident: #pyo3_crate_path::impl_::introspection::SerializedIntrospectionFragment<PIECES_LEN> = #pyo3_crate_path::impl_::introspection::SerializedIntrospectionFragment {
length: PIECES_LEN as u32,
fragment: #pyo3_crate_path::impl_::concat::combine_to_array::<PIECES_LEN>(PIECES)
};
};
}
}
}
enum ConcatenationBuilderElement {
String(String),
TokenStream(TokenStream),
}
impl ToTokens for ConcatenationBuilderElement {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::String(s) => quote! { #s.as_bytes() }.to_tokens(tokens),
Self::TokenStream(ts) => ts.to_tokens(tokens),
}
}
}
/// Generates a new unique identifier for linking introspection objects together
pub fn introspection_id_const() -> TokenStream {
let id = unique_element_id().to_string();
quote! {
#[doc(hidden)]
pub const _PYO3_INTROSPECTION_ID: &'static str = #id;
}
}
pub fn unique_element_id() -> u64 {
let mut hasher = DefaultHasher::new();
format!("{:?}", Span::call_site()).hash(&mut hasher); // Distinguishes between call sites
GLOBAL_COUNTER_FOR_UNIQUE_NAMES
.fetch_add(1, Ordering::Relaxed)
.hash(&mut hasher); // If there are multiple elements in the same call site
hasher.finish()
}
fn ident_to_type(ident: &Ident) -> Cow<'static, Type> {
Cow::Owned(
TypePath {
path: ident.clone().into(),
qself: None,
}
.into(),
)
}
fn escape_json_string(value: &str) -> String {
let mut output = String::with_capacity(value.len());
for c in value.chars() {
match c {
'\\' => output.push_str("\\\\"),
'"' => output.push_str("\\\""),
'\x08' => output.push_str("\\b"),
'\x0C' => output.push_str("\\f"),
'\n' => output.push_str("\\n"),
'\r' => output.push_str("\\r"),
'\t' => output.push_str("\\t"),
c @ '\0'..='\x1F' => {
write!(output, "\\u{:0>4x}", u32::from(c)).unwrap();
}
c => output.push(c),
}
}
output
}
+101
View File
@@ -0,0 +1,101 @@
use std::borrow::Cow;
use std::ffi::CString;
use crate::attributes::{self, get_pyo3_options, take_attributes, NameAttribute};
#[cfg(feature = "experimental-inspect")]
use crate::utils::PythonDoc;
use proc_macro2::{Ident, Span};
use syn::LitCStr;
use syn::{
ext::IdentExt,
parse::{Parse, ParseStream},
spanned::Spanned,
Result,
};
pub struct ConstSpec {
pub rust_ident: Ident,
pub attributes: ConstAttributes,
#[cfg(feature = "experimental-inspect")]
pub expr: Option<syn::Expr>,
#[cfg(feature = "experimental-inspect")]
pub ty: syn::Type,
#[cfg(feature = "experimental-inspect")]
pub doc: Option<PythonDoc>,
}
impl ConstSpec {
pub fn python_name(&self) -> Cow<'_, Ident> {
if let Some(name) = &self.attributes.name {
Cow::Borrowed(&name.value.0)
} else {
Cow::Owned(self.rust_ident.unraw())
}
}
/// Null-terminated Python name
pub fn null_terminated_python_name(&self) -> LitCStr {
let name = self.python_name().to_string();
LitCStr::new(&CString::new(name).unwrap(), Span::call_site())
}
}
pub struct ConstAttributes {
pub is_class_attr: bool,
pub name: Option<NameAttribute>,
}
pub enum PyO3ConstAttribute {
Name(NameAttribute),
}
impl Parse for PyO3ConstAttribute {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::name) {
input.parse().map(PyO3ConstAttribute::Name)
} else {
Err(lookahead.error())
}
}
}
impl ConstAttributes {
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> {
let mut attributes = ConstAttributes {
is_class_attr: false,
name: None,
};
take_attributes(attrs, |attr| {
if attr.path().is_ident("classattr") {
ensure_spanned!(
matches!(attr.meta, syn::Meta::Path(..)),
attr.span() => "`#[classattr]` does not take any arguments"
);
attributes.is_class_attr = true;
Ok(true)
} else if let Some(pyo3_attributes) = get_pyo3_options(attr)? {
for pyo3_attr in pyo3_attributes {
match pyo3_attr {
PyO3ConstAttribute::Name(name) => attributes.set_name(name)?,
}
}
Ok(true)
} else {
Ok(false)
}
})?;
Ok(attributes)
}
fn set_name(&mut self, name: NameAttribute) -> Result<()> {
ensure_spanned!(
self.name.is_none(),
name.span() => "`name` may only be specified once"
);
self.name = Some(name);
Ok(())
}
}
+36
View File
@@ -0,0 +1,36 @@
//! This crate contains the implementation of the proc macro attributes
#![warn(elided_lifetimes_in_paths, unused_lifetimes)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![recursion_limit = "1024"]
// Listed first so that macros in this module are available in the rest of the crate.
#[macro_use]
mod utils;
mod attributes;
mod combine_errors;
mod derive_attributes;
mod frompyobject;
mod intopyobject;
#[cfg(feature = "experimental-inspect")]
mod introspection;
mod konst;
mod method;
mod module;
mod params;
#[cfg(feature = "experimental-inspect")]
mod py_expr;
mod pyclass;
mod pyfunction;
mod pyimpl;
mod pymethod;
mod quotes;
pub use frompyobject::build_derive_from_pyobject;
pub use intopyobject::build_derive_into_pyobject;
pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions};
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 utils::get_doc;
File diff suppressed because it is too large. Load diff
+743
View File
@@ -0,0 +1,743 @@
//! Code generation for the function that initializes a python module and adds classes and function.
#[cfg(feature = "experimental-inspect")]
use crate::introspection::{
attribute_introspection_code, introspection_id_const, module_introspection_code,
};
#[cfg(feature = "experimental-inspect")]
use crate::py_expr::PyExpr;
use crate::{
attributes::{
self, kw, take_attributes, take_pyo3_options, CrateAttribute, GILUsedAttribute,
ModuleAttribute, NameAttribute, SubmoduleAttribute,
},
combine_errors::CombineErrors,
get_doc,
pyclass::PyClassPyO3Option,
pyfunction::{impl_wrap_pyfunction, PyFunctionOptions},
utils::{has_attribute, has_attribute_with_namespace, Ctx, IdentOrStr, PythonDoc},
};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use std::ffi::CString;
use syn::LitCStr;
use syn::{
ext::IdentExt,
parse::{Parse, ParseStream},
parse_quote, parse_quote_spanned,
punctuated::Punctuated,
spanned::Spanned,
token::Comma,
Item, Meta, Path, Result,
};
#[derive(Default)]
pub struct PyModuleOptions {
krate: Option<CrateAttribute>,
name: Option<NameAttribute>,
module: Option<ModuleAttribute>,
submodule: Option<kw::submodule>,
gil_used: Option<GILUsedAttribute>,
}
impl Parse for PyModuleOptions {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut options: PyModuleOptions = Default::default();
options.add_attributes(
Punctuated::<PyModulePyO3Option, syn::Token![,]>::parse_terminated(input)?,
)?;
Ok(options)
}
}
impl PyModuleOptions {
fn take_pyo3_options(&mut self, attrs: &mut Vec<syn::Attribute>) -> Result<()> {
self.add_attributes(take_pyo3_options(attrs)?)
}
fn add_attributes(
&mut self,
attrs: impl IntoIterator<Item = PyModulePyO3Option>,
) -> Result<()> {
macro_rules! set_option {
($key:ident $(, $extra:literal)?) => {
{
ensure_spanned!(
self.$key.is_none(),
$key.span() => concat!("`", stringify!($key), "` may only be specified once" $(, $extra)?)
);
self.$key = Some($key);
}
};
}
attrs
.into_iter()
.map(|attr| {
match attr {
PyModulePyO3Option::Crate(krate) => set_option!(krate),
PyModulePyO3Option::Name(name) => set_option!(name),
PyModulePyO3Option::Module(module) => set_option!(module),
PyModulePyO3Option::Submodule(submodule) => set_option!(
submodule,
" (it is implicitly always specified for nested modules)"
),
PyModulePyO3Option::GILUsed(gil_used) => {
set_option!(gil_used)
}
}
Ok(())
})
.try_combine_syn_errors()?;
Ok(())
}
}
pub fn pymodule_module_impl(
module: &mut syn::ItemMod,
mut options: PyModuleOptions,
) -> Result<TokenStream> {
let syn::ItemMod {
attrs,
vis,
unsafety: _,
ident,
mod_token,
content,
semi: _,
} = module;
let items = if let Some((_, items)) = content {
items
} else {
bail_spanned!(mod_token.span() => "`#[pymodule]` can only be used on inline modules")
};
options.take_pyo3_options(attrs)?;
let ctx = &Ctx::new(&options.krate, None);
let Ctx { pyo3_path, .. } = ctx;
let doc = get_doc(attrs, None);
let name = options
.name
.map_or_else(|| ident.unraw(), |name| name.value.0);
let full_name = if let Some(module) = &options.module {
format!("{}.{}", module.value.value(), name)
} else {
name.to_string()
};
let mut module_items = Vec::new();
let mut module_items_cfg_attrs = Vec::new();
#[cfg(feature = "experimental-inspect")]
let mut introspection_chunks = Vec::new();
#[cfg(not(feature = "experimental-inspect"))]
let introspection_chunks = Vec::<TokenStream>::new();
fn extract_use_items(
source: &syn::UseTree,
cfg_attrs: &[syn::Attribute],
target_items: &mut Vec<syn::Ident>,
target_cfg_attrs: &mut Vec<Vec<syn::Attribute>>,
) -> Result<()> {
match source {
syn::UseTree::Name(name) => {
target_items.push(name.ident.clone());
target_cfg_attrs.push(cfg_attrs.to_vec());
}
syn::UseTree::Path(path) => {
extract_use_items(&path.tree, cfg_attrs, target_items, target_cfg_attrs)?
}
syn::UseTree::Group(group) => {
for tree in &group.items {
extract_use_items(tree, cfg_attrs, target_items, target_cfg_attrs)?
}
}
syn::UseTree::Glob(glob) => {
bail_spanned!(glob.span() => "#[pymodule] cannot import glob statements")
}
syn::UseTree::Rename(rename) => {
target_items.push(rename.rename.clone());
target_cfg_attrs.push(cfg_attrs.to_vec());
}
}
Ok(())
}
let mut pymodule_init = None;
let mut module_consts = Vec::new();
let mut module_consts_cfg_attrs = Vec::new();
let _: Vec<()> = (*items).iter_mut().map(|item|{
match item {
Item::Use(item_use) => {
let is_pymodule_export =
find_and_remove_attribute(&mut item_use.attrs, "pymodule_export");
if is_pymodule_export {
let cfg_attrs = get_cfg_attributes(&item_use.attrs);
extract_use_items(
&item_use.tree,
&cfg_attrs,
&mut module_items,
&mut module_items_cfg_attrs,
)?;
}
}
Item::Fn(item_fn) => {
ensure_spanned!(
!has_attribute(&item_fn.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
let is_pymodule_init =
find_and_remove_attribute(&mut item_fn.attrs, "pymodule_init");
let ident = &item_fn.sig.ident;
if is_pymodule_init {
ensure_spanned!(
!has_attribute(&item_fn.attrs, "pyfunction"),
item_fn.span() => "`#[pyfunction]` cannot be used alongside `#[pymodule_init]`"
);
ensure_spanned!(pymodule_init.is_none(), item_fn.span() => "only one `#[pymodule_init]` may be specified");
pymodule_init = Some(quote! { #ident(module)?; });
} else if has_attribute(&item_fn.attrs, "pyfunction")
|| has_attribute_with_namespace(
&item_fn.attrs,
Some(pyo3_path),
&["pyfunction"],
)
|| has_attribute_with_namespace(
&item_fn.attrs,
Some(pyo3_path),
&["prelude", "pyfunction"],
)
{
module_items.push(ident.clone());
module_items_cfg_attrs.push(get_cfg_attributes(&item_fn.attrs));
}
}
Item::Struct(item_struct) => {
ensure_spanned!(
!has_attribute(&item_struct.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
if has_attribute(&item_struct.attrs, "pyclass")
|| has_attribute_with_namespace(
&item_struct.attrs,
Some(pyo3_path),
&["pyclass"],
)
|| has_attribute_with_namespace(
&item_struct.attrs,
Some(pyo3_path),
&["prelude", "pyclass"],
)
{
module_items.push(item_struct.ident.clone());
module_items_cfg_attrs.push(get_cfg_attributes(&item_struct.attrs));
if !has_pyo3_module_declared::<PyClassPyO3Option>(
&item_struct.attrs,
"pyclass",
|option| matches!(option, PyClassPyO3Option::Module(_)),
)? {
set_module_attribute(&mut item_struct.attrs, &full_name);
}
}
}
Item::Enum(item_enum) => {
ensure_spanned!(
!has_attribute(&item_enum.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
if has_attribute(&item_enum.attrs, "pyclass")
|| has_attribute_with_namespace(&item_enum.attrs, Some(pyo3_path), &["pyclass"])
|| has_attribute_with_namespace(
&item_enum.attrs,
Some(pyo3_path),
&["prelude", "pyclass"],
)
{
module_items.push(item_enum.ident.clone());
module_items_cfg_attrs.push(get_cfg_attributes(&item_enum.attrs));
if !has_pyo3_module_declared::<PyClassPyO3Option>(
&item_enum.attrs,
"pyclass",
|option| matches!(option, PyClassPyO3Option::Module(_)),
)? {
set_module_attribute(&mut item_enum.attrs, &full_name);
}
}
}
Item::Mod(item_mod) => {
ensure_spanned!(
!has_attribute(&item_mod.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
if has_attribute(&item_mod.attrs, "pymodule")
|| has_attribute_with_namespace(&item_mod.attrs, Some(pyo3_path), &["pymodule"])
|| has_attribute_with_namespace(
&item_mod.attrs,
Some(pyo3_path),
&["prelude", "pymodule"],
)
{
module_items.push(item_mod.ident.clone());
module_items_cfg_attrs.push(get_cfg_attributes(&item_mod.attrs));
if !has_pyo3_module_declared::<PyModulePyO3Option>(
&item_mod.attrs,
"pymodule",
|option| matches!(option, PyModulePyO3Option::Module(_)),
)? {
set_module_attribute(&mut item_mod.attrs, &full_name);
}
item_mod
.attrs
.push(parse_quote_spanned!(item_mod.mod_token.span()=> #[pyo3(submodule)]));
}
}
Item::ForeignMod(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
Item::Trait(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
Item::Const(item) => {
if !find_and_remove_attribute(&mut item.attrs, "pymodule_export") {
return Ok(());
}
module_consts.push(item.ident.clone());
module_consts_cfg_attrs.push(get_cfg_attributes(&item.attrs));
#[cfg(feature = "experimental-inspect")]
{
let cfg_attrs = get_cfg_attributes(&item.attrs);
let chunk = attribute_introspection_code(
pyo3_path,
None,
item.ident.unraw().to_string(),
PyExpr::constant_from_expression(&item.expr),
(*item.ty).clone(),
get_doc(&item.attrs, None).as_ref(),
true,
);
introspection_chunks.push(quote! {
#(#cfg_attrs)*
#chunk
});
}
}
Item::Static(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
Item::Macro(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
Item::ExternCrate(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
Item::Impl(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
Item::TraitAlias(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
Item::Type(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
Item::Union(item) => {
ensure_spanned!(
!has_attribute(&item.attrs, "pymodule_export"),
item.span() => "`#[pymodule_export]` may only be used on `use` or `const` statements"
);
}
_ => (),
}
Ok(())
}).try_combine_syn_errors()?;
#[cfg(feature = "experimental-inspect")]
let introspection = module_introspection_code(
pyo3_path,
&name.to_string(),
&module_items,
&module_items_cfg_attrs,
doc.as_ref(),
pymodule_init.is_some(),
);
#[cfg(not(feature = "experimental-inspect"))]
let introspection = quote! {};
#[cfg(feature = "experimental-inspect")]
let introspection_id = introspection_id_const();
#[cfg(not(feature = "experimental-inspect"))]
let introspection_id = quote! {};
let gil_used = options.gil_used.is_some_and(|op| op.value.value);
let initialization = module_initialization(
&full_name,
&name,
ctx,
quote! { __pyo3_pymodule },
options.submodule.is_some(),
gil_used,
doc.as_ref(),
)?;
let module_consts_names = module_consts.iter().map(|i| i.unraw().to_string());
Ok(quote!(
#(#attrs)*
#vis #mod_token #ident {
#(#items)*
#initialization
#introspection
#introspection_id
#(#introspection_chunks)*
fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> {
use #pyo3_path::impl_::pymodule::PyAddToModule;
#(
#(#module_items_cfg_attrs)*
#module_items::_PYO3_DEF.add_to_module(module)?;
)*
#(
#(#module_consts_cfg_attrs)*
#pyo3_path::types::PyModuleMethods::add(module, #module_consts_names, #module_consts)?;
)*
#pymodule_init
::std::result::Result::Ok(())
}
}
))
}
/// Generates the function that is called by the python interpreter to initialize the native
/// module
pub fn pymodule_function_impl(
function: &mut syn::ItemFn,
mut options: PyModuleOptions,
) -> Result<TokenStream> {
options.take_pyo3_options(&mut function.attrs)?;
process_functions_in_module(&options, function)?;
let ctx = &Ctx::new(&options.krate, None);
let Ctx { pyo3_path, .. } = ctx;
let ident = &function.sig.ident;
let name = options
.name
.map_or_else(|| ident.unraw(), |name| name.value.0);
let vis = &function.vis;
let doc = get_doc(&function.attrs, None);
let gil_used = options.gil_used.is_some_and(|op| op.value.value);
let initialization = module_initialization(
&name.to_string(),
&name,
ctx,
quote! { ModuleExec::__pyo3_module_exec },
false,
gil_used,
doc.as_ref(),
)?;
#[cfg(feature = "experimental-inspect")]
let introspection = module_introspection_code(
pyo3_path,
&name.unraw().to_string(),
&[],
&[],
doc.as_ref(),
true,
);
#[cfg(not(feature = "experimental-inspect"))]
let introspection = quote! {};
#[cfg(feature = "experimental-inspect")]
let introspection_id = introspection_id_const();
#[cfg(not(feature = "experimental-inspect"))]
let introspection_id = quote! {};
// Module function called with optional Python<'_> marker as first arg, followed by the module.
let mut module_args = Vec::new();
if function.sig.inputs.len() == 2 {
module_args.push(quote!(module.py()));
}
module_args.push(quote!(::std::convert::Into::into(module)));
Ok(quote! {
#[doc(hidden)]
#vis mod #ident {
#initialization
#introspection
#introspection_id
}
// Generate the definition inside an anonymous function in the same scope as the original function -
// this avoids complications around the fact that the generated module has a different scope
// (and `super` doesn't always refer to the outer scope, e.g. if the `#[pymodule] is
// inside a function body)
#[allow(unknown_lints, non_local_definitions)]
impl #ident::ModuleExec {
fn __pyo3_module_exec(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> {
#ident(#(#module_args),*)
}
}
})
}
fn module_initialization(
full_name: &str,
name: &syn::Ident,
ctx: &Ctx,
module_exec: TokenStream,
is_submodule: bool,
gil_used: bool,
doc: Option<&PythonDoc>,
) -> Result<TokenStream> {
let Ctx { pyo3_path, .. } = ctx;
let pyinit_symbol = format!("PyInit_{name}");
let pymodexport_symbol = format!("PyModExport_{name}");
let pyo3_name = LitCStr::new(&CString::new(full_name).unwrap(), Span::call_site());
let doc = if let Some(doc) = doc {
doc.to_cstr_stream(ctx)?
} else {
c"".into_token_stream()
};
let mut result = quote! {
#[doc(hidden)]
pub static __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_name;
// This structure exists for `fn` modules declared within `fn` bodies, where due to the hidden
// module (used for importing) the `fn` to initialize the module cannot be seen from the #module_def
// declaration just below.
#[doc(hidden)]
pub(super) struct ModuleExec;
#[doc(hidden)]
pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = {
use #pyo3_path::impl_::pymodule as impl_;
unsafe extern "C" fn __pyo3_module_exec(module: *mut #pyo3_path::ffi::PyObject) -> ::std::ffi::c_int {
#pyo3_path::impl_::trampoline::module_exec(module, #module_exec)
}
static DOC: &'static ::std::ffi::CStr = #doc;
static SLOTS: impl_::PrimaryModuleSlots = impl_::PyModuleSlotsBuilder::new()
.with_mod_exec(__pyo3_module_exec)
.with_abi_info()
.with_gil_used(#gil_used)
.with_name(__PYO3_NAME)
.with_doc(DOC)
.build();
static SECONDARY_SLOTS: impl_::SecondaryModuleSlots = impl_::secondary_slots(&SLOTS);
// Since the macros need to be written agnostic to the Python version
// we need to explicitly pass the name and docstring for PyModuleDef
// initialization.
impl_::ModuleDef::new(__PYO3_NAME, DOC, &SLOTS, &SECONDARY_SLOTS)
};
};
if !is_submodule {
result.extend(quote! {
// We want to define only one or the other of these initialization functions
// so we implement them in macros to allow the version gating to live there
// Defines the `PyInit_<name>` entry point used by Python 3.14 and older.
#pyo3_path::__pyo3_pyinit!(#pyinit_symbol, _PYO3_DEF);
// Defines the `PyModExport_<name>` entry point used by Python 3.15 and newer.
#pyo3_path::__pyo3_pymodexport!(#pymodexport_symbol, _PYO3_DEF);
});
}
Ok(result)
}
/// Finds and takes care of the #[pyfn(...)] in `#[pymodule]`
fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> {
let ctx = &Ctx::new(&options.krate, None);
let Ctx { pyo3_path, .. } = ctx;
let mut stmts: Vec<syn::Stmt> = Vec::new();
for mut stmt in func.block.stmts.drain(..) {
if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt {
if let Some((pyfn_span, pyfn_args)) = get_pyfn_attr(&mut func.attrs)? {
let module_name = pyfn_args.modname;
let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?;
let name = &func.sig.ident;
let statements: Vec<syn::Stmt> = syn::parse_quote_spanned! {
pyfn_span =>
#wrapped_function
{
use #pyo3_path::types::PyModuleMethods;
#module_name.add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?;
#[deprecated(note = "`pyfn` will be removed in a future PyO3 version, use declarative `#[pymodule]` with `mod` instead")]
#[allow(dead_code)]
const PYFN_ATTRIBUTE: () = ();
const _: () = PYFN_ATTRIBUTE;
}
};
stmts.extend(statements);
}
};
stmts.push(stmt);
}
func.block.stmts = stmts;
Ok(())
}
pub struct PyFnArgs {
modname: Path,
options: PyFunctionOptions,
}
impl Parse for PyFnArgs {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let modname = input.parse().map_err(
|e| err_spanned!(e.span() => "expected module as first argument to #[pyfn()]"),
)?;
if input.is_empty() {
return Ok(Self {
modname,
options: Default::default(),
});
}
let _: Comma = input.parse()?;
Ok(Self {
modname,
options: input.parse()?,
})
}
}
/// Extracts the data from the #[pyfn(...)] attribute of a function
fn get_pyfn_attr(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Option<(Span, PyFnArgs)>> {
let mut pyfn_args: Option<(Span, PyFnArgs)> = None;
take_attributes(attrs, |attr| {
if attr.path().is_ident("pyfn") {
ensure_spanned!(
pyfn_args.is_none(),
attr.span() => "`#[pyfn] may only be specified once"
);
pyfn_args = Some((attr.path().span(), attr.parse_args()?));
Ok(true)
} else {
Ok(false)
}
})?;
if let Some((_, pyfn_args)) = &mut pyfn_args {
pyfn_args
.options
.add_attributes(take_pyo3_options(attrs)?)?;
}
Ok(pyfn_args)
}
fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<syn::Attribute> {
attrs
.iter()
.filter(|attr| attr.path().is_ident("cfg"))
.cloned()
.collect()
}
fn find_and_remove_attribute(attrs: &mut Vec<syn::Attribute>, ident: &str) -> bool {
let mut found = false;
attrs.retain(|attr| {
if attr.path().is_ident(ident) {
found = true;
false
} else {
true
}
});
found
}
impl PartialEq<syn::Ident> for IdentOrStr<'_> {
fn eq(&self, other: &syn::Ident) -> bool {
match self {
IdentOrStr::Str(s) => other == s,
IdentOrStr::Ident(i) => other == i,
}
}
}
fn set_module_attribute(attrs: &mut Vec<syn::Attribute>, module_name: &str) {
attrs.push(parse_quote!(#[pyo3(module = #module_name)]));
}
fn has_pyo3_module_declared<T: Parse>(
attrs: &[syn::Attribute],
root_attribute_name: &str,
is_module_option: impl Fn(&T) -> bool + Copy,
) -> Result<bool> {
for attr in attrs {
if (attr.path().is_ident("pyo3") || attr.path().is_ident(root_attribute_name))
&& matches!(attr.meta, Meta::List(_))
{
for option in &attr.parse_args_with(Punctuated::<T, Comma>::parse_terminated)? {
if is_module_option(option) {
return Ok(true);
}
}
}
}
Ok(false)
}
enum PyModulePyO3Option {
Submodule(SubmoduleAttribute),
Crate(CrateAttribute),
Name(NameAttribute),
Module(ModuleAttribute),
GILUsed(GILUsedAttribute),
}
impl Parse for PyModulePyO3Option {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::name) {
input.parse().map(PyModulePyO3Option::Name)
} else if lookahead.peek(syn::Token![crate]) {
input.parse().map(PyModulePyO3Option::Crate)
} else if lookahead.peek(attributes::kw::module) {
input.parse().map(PyModulePyO3Option::Module)
} else if lookahead.peek(attributes::kw::submodule) {
input.parse().map(PyModulePyO3Option::Submodule)
} else if lookahead.peek(attributes::kw::gil_used) {
input.parse().map(PyModulePyO3Option::GILUsed)
} else {
Err(lookahead.error())
}
}
}
+318
View File
@@ -0,0 +1,318 @@
use crate::utils::Ctx;
use crate::{
attributes::FromPyWithAttribute,
method::{FnArg, FnSpec, RegularArg},
pyfunction::FunctionSignature,
quotes::some_wrap,
};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::spanned::Spanned;
pub struct Holders {
holders: Vec<syn::Ident>,
}
impl Holders {
pub fn new() -> Self {
Holders {
holders: Vec::new(),
}
}
pub fn push_holder(&mut self, span: Span) -> syn::Ident {
let holder = syn::Ident::new(&format!("holder_{}", self.holders.len()), span);
self.holders.push(holder.clone());
holder
}
pub fn init_holders(&self, ctx: &Ctx) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let holders = &self.holders;
quote! {
#[allow(clippy::let_unit_value, reason = "many holders are just `()`")]
#(let mut #holders = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT;)*
}
}
}
/// Return true if the argument list is simply (*args, **kwds).
pub fn is_forwarded_args(signature: &FunctionSignature<'_>) -> bool {
matches!(
signature.arguments.as_slice(),
[FnArg::VarArgs(..), FnArg::KwArgs(..),]
)
}
pub fn impl_arg_params(
spec: &FnSpec<'_>,
self_: Option<&syn::Type>,
fastcall: bool,
holders: &mut Holders,
ctx: &Ctx,
) -> (TokenStream, Vec<TokenStream>) {
let args_array = syn::Ident::new("output", Span::call_site());
let Ctx { pyo3_path, .. } = ctx;
let from_py_with = spec
.signature
.arguments
.iter()
.enumerate()
.filter_map(|(i, arg)| {
let from_py_with = &arg.from_py_with()?.value;
let from_py_with_holder = format_ident!("from_py_with_{}", i);
Some(quote_spanned! { from_py_with.span() =>
let #from_py_with_holder = #from_py_with;
})
})
.collect::<TokenStream>();
if !fastcall && is_forwarded_args(&spec.signature) {
// In the varargs convention, we can just pass though if the signature
// is (*args, **kwds).
let arg_convert = spec
.signature
.arguments
.iter()
.enumerate()
.map(|(i, arg)| impl_arg_param(arg, i, &mut 0, holders, ctx))
.collect();
return (
quote! {
let _args = unsafe { #pyo3_path::impl_::extract_argument::cast_function_argument(py, _args) };
let _kwargs = unsafe { #pyo3_path::impl_::extract_argument::cast_optional_function_argument(py, _kwargs) };
#from_py_with
},
arg_convert,
);
};
let positional_parameter_names = &spec.signature.python_signature.positional_parameters;
let positional_only_parameters = &spec.signature.python_signature.positional_only_parameters;
let required_positional_parameters = spec
.signature
.python_signature
.required_positional_parameters();
let keyword_only_parameters = spec
.signature
.python_signature
.keyword_only_parameters
.iter()
.map(|(name, default_value)| {
let required = default_value.is_none();
quote! {
#pyo3_path::impl_::extract_argument::KeywordOnlyParameterDescription {
name: #name,
required: #required,
}
}
});
let num_params = positional_parameter_names.len() + keyword_only_parameters.len();
let mut option_pos = 0usize;
let param_conversion = spec
.signature
.arguments
.iter()
.enumerate()
.map(|(i, arg)| impl_arg_param(arg, i, &mut option_pos, holders, ctx))
.collect();
let args_handler = if spec.signature.python_signature.varargs.is_some() {
quote! { #pyo3_path::impl_::extract_argument::TupleVarargs }
} else {
quote! { #pyo3_path::impl_::extract_argument::NoVarargs }
};
let kwargs_handler = if spec.signature.python_signature.kwargs.is_some() {
quote! { #pyo3_path::impl_::extract_argument::DictVarkeywords }
} else {
quote! { #pyo3_path::impl_::extract_argument::NoVarkeywords }
};
let cls_name = if let Some(cls) = self_ {
quote! { ::std::option::Option::Some(<#cls as #pyo3_path::PyClass>::NAME) }
} else {
quote! { ::std::option::Option::None }
};
let python_name = &spec.python_name;
let extract_expression = if fastcall {
quote! {
#pyo3_path::impl_::pymethods::maybe_extract_arguments_fastcall!(
DESCRIPTION,
py,
_args,
_nargs,
_kwargs,
#args_array,
#args_handler,
#kwargs_handler
)?
}
} else {
quote! {
DESCRIPTION.extract_arguments_tuple_dict::<#args_handler, #kwargs_handler>(
py,
_args,
_kwargs,
&mut #args_array
)?
}
};
// create array of arguments, and then parse
(
quote! {
const DESCRIPTION: #pyo3_path::impl_::extract_argument::FunctionDescription = #pyo3_path::impl_::extract_argument::FunctionDescription {
cls_name: #cls_name,
func_name: stringify!(#python_name),
positional_parameter_names: &[#(#positional_parameter_names),*],
positional_only_parameters: #positional_only_parameters,
required_positional_parameters: #required_positional_parameters,
keyword_only_parameters: &[#(#keyword_only_parameters),*],
};
let mut #args_array = [::std::option::Option::None; #num_params];
let (_args, _kwargs) = #extract_expression;
#from_py_with
},
param_conversion,
)
}
fn impl_arg_param(
arg: &FnArg<'_>,
pos: usize,
option_pos: &mut usize,
holders: &mut Holders,
ctx: &Ctx,
) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let args_array = syn::Ident::new("output", Span::call_site());
match arg {
FnArg::Regular(arg) => {
let from_py_with = format_ident!("from_py_with_{}", pos);
let arg_value = quote!(#args_array[#option_pos]);
*option_pos += 1;
impl_regular_arg_param(arg, from_py_with, arg_value, holders, ctx)
}
FnArg::VarArgs(arg) => {
let span = Span::call_site().located_at(arg.ty.span());
let holder = holders.push_holder(span);
let name_str = arg.name.to_string();
quote_spanned! { span =>
#pyo3_path::impl_::extract_argument::extract_argument(
_args.as_any().as_borrowed(),
&mut #holder,
#name_str
)?
}
}
FnArg::KwArgs(arg) => {
let span = Span::call_site().located_at(arg.ty.span());
let holder = holders.push_holder(span);
let name_str = arg.name.to_string();
quote_spanned! { span =>
#pyo3_path::impl_::extract_argument::extract_argument_with_default(
_kwargs.as_ref().map(|d| d.as_any().as_borrowed()),
&mut #holder,
#name_str,
|| ::std::option::Option::None
)?
}
}
FnArg::Py(arg) => {
let span = Span::call_site().located_at(arg.ty.span());
quote_spanned! { span => py }
}
FnArg::CancelHandle(arg) => {
let span = Span::call_site().located_at(arg.ty.span());
quote_spanned! { span => __cancel_handle }
}
}
}
/// Re option_pos: The option slice doesn't contain the py: Python argument, so the argument
/// index and the index in option diverge when using py: Python
pub(crate) fn impl_regular_arg_param(
arg: &RegularArg<'_>,
from_py_with: syn::Ident,
arg_value: TokenStream, // expected type: Option<&'a Bound<'py, PyAny>>
holders: &mut Holders,
ctx: &Ctx,
) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let pyo3_path = pyo3_path.to_tokens_spanned(arg.ty.span());
// Use this macro inside this function, to ensure that all code generated here is associated
// with the function argument
let use_probe = quote! {
#[allow(unused_imports, reason = "`Probe` trait used on negative case only")]
use #pyo3_path::impl_::pyclass::Probe as _;
};
macro_rules! quote_arg_span {
($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => { #use_probe $($tokens)* }) }
}
let name_str = arg.name.to_string();
let mut default = arg.default_value.as_ref().map(|expr| quote!(#expr));
// Option<T> arguments have special treatment: the default should be specified _without_ the
// Some() wrapper. Maybe this should be changed in future?!
if arg.option_wrapped_type.is_some() {
default = default.map(|tokens| some_wrap(tokens, ctx));
}
if let Some(FromPyWithAttribute { kw, .. }) = &arg.from_py_with.as_deref() {
let extractor = quote_spanned! { kw.span =>
{ let from_py_with: fn(_) -> _ = #from_py_with; from_py_with }
};
if let Some(default) = default {
quote_arg_span! {
#pyo3_path::impl_::extract_argument::from_py_with_with_default(
#arg_value.as_deref(),
#name_str,
#extractor,
#[allow(clippy::redundant_closure, reason = "wrapping user-provided default expression")]
{
|| #default
}
)?
}
} else {
let unwrap = quote! {unsafe { #pyo3_path::impl_::extract_argument::unwrap_required_argument_bound(#arg_value.as_deref()) }};
quote_arg_span! {
#pyo3_path::impl_::extract_argument::from_py_with(
#unwrap,
#name_str,
#extractor,
)?
}
}
} else if let Some(default) = default {
let holder = holders.push_holder(arg.ty.span());
quote_arg_span! {
#pyo3_path::impl_::extract_argument::extract_argument_with_default(
#arg_value,
&mut #holder,
#name_str,
#[allow(clippy::redundant_closure, reason = "wrapping user-provided default expression")]
{
|| #default
}
)?
}
} else {
let holder = holders.push_holder(arg.ty.span());
let unwrap = quote! { unsafe { #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value) } };
quote_arg_span! {
#pyo3_path::impl_::extract_argument::extract_argument(
#unwrap,
&mut #holder,
#name_str
)?
}
}
}
+322
View File
@@ -0,0 +1,322 @@
//! Define a data structure for Python type hints, mixing static data from macros and call to Pyo3 constants.
use crate::utils::PyO3CratePath;
use proc_macro2::TokenStream;
use quote::quote;
use std::borrow::Cow;
use syn::visit_mut::{visit_type_mut, VisitMut};
use syn::{Expr, ExprLit, ExprPath, Lit, Type};
/// A Python expression
///
/// Please do not construct directly but use the constructor methods that normalize the expression
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PyExpr {
/// The Python type hint of a FromPyObject implementation
FromPyObjectType(Type),
/// The Python type hint of a IntoPyObject implementation
IntoPyObjectType(Type),
/// The Python type hint of a IntoPyObject implementation on the ref type or the base type
IntoPyObjectMaybeRefType(Type),
/// The Python type matching the given Rust type given as a function argument
ArgumentType(Type),
/// The Python type matching the given Rust type given as a function returned value
ReturnType(Type),
/// The Python type matching the given Rust type
Type(Type),
/// A name
Name { id: Cow<'static, str> },
/// An attribute `value.attr`
Attribute {
value: Box<Self>,
attr: Cow<'static, str>,
},
/// A binary operator
BinOp {
left: Box<Self>,
op: PyOperator,
right: Box<Self>,
},
/// A tuple
Tuple { elts: Vec<Self> },
/// A subscript `value[slice]`
Subscript { value: Box<Self>, slice: Box<Self> },
/// A constant
Constant(PyConstant),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PyOperator {
/// `|` operator
BitOr,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PyConstant {
/// None
None,
/// The `True` and `False` booleans
Bool(bool),
/// `int` value written in base 10 ([+-]?[0-9]+)
Int(String),
/// `float` value written in base-10 ([+-]?[0-9]*(.[0-9]*)*([eE])[0-9]*), not including Inf and NaN
Float(String),
/// `str` value unescaped and without quotes
Str(String),
/// `...`
Ellipsis,
}
impl PyExpr {
/// Build from a builtins name like `str`
pub fn builtin(name: impl Into<Cow<'static, str>>) -> Self {
Self::Name { id: name.into() }
}
/// Build from a module and a name like `collections.abc` and `Sequence`
pub fn module_attr(
module: impl Into<Cow<'static, str>>,
name: impl Into<Cow<'static, str>>,
) -> Self {
Self::attribute(Self::Name { id: module.into() }, name)
}
/// The type hint of a `FromPyObject` implementation as a function argument
///
/// If self_type is set, self_type will replace Self in the given type
pub fn from_from_py_object(t: Type, self_type: Option<&Type>) -> Self {
Self::FromPyObjectType(clean_type(t, self_type))
}
/// The type hint of a `IntoPyObject` implementation as a function argument
///
/// If self_type is set, self_type will replace Self in the given type
pub fn from_into_py_object(t: Type, self_type: Option<&Type>) -> Self {
Self::IntoPyObjectType(clean_type(t, self_type))
}
/// The type hint of a `IntoPyObject` implementation used by a field getter
///
/// If self_type is set, self_type will replace Self in the given type
pub fn from_into_py_object_maybe_ref(t: Type, self_type: Option<&Type>) -> Self {
Self::IntoPyObjectMaybeRefType(clean_type(t, self_type))
}
/// The type hint of the Rust type used as a function argument
///
/// If self_type is set, self_type will replace Self in the given type
pub fn from_argument_type(t: Type, self_type: Option<&Type>) -> Self {
Self::ArgumentType(clean_type(t, self_type))
}
/// The type hint of the Rust type used as a function output type
///
/// If self_type is set, self_type will replace Self in the given type
pub fn from_return_type(t: Type, self_type: Option<&Type>) -> Self {
Self::ReturnType(clean_type(t, self_type))
}
/// The type hint of the Rust type `PyTypeCheck` trait.
///
/// If self_type is set, self_type will replace Self in the given type
pub fn from_type(t: Type, self_type: Option<&Type>) -> Self {
Self::Type(clean_type(t, self_type))
}
/// An attribute of a given value: `value.attr`
pub fn attribute(value: Self, attr: impl Into<Cow<'static, str>>) -> Self {
Self::Attribute {
value: Box::new(value),
attr: attr.into(),
}
}
/// Build the union of the different element
pub fn union(left: Self, right: Self) -> Self {
Self::BinOp {
left: Box::new(left),
op: PyOperator::BitOr,
right: Box::new(right),
}
}
/// Build the subscripted type value[slice]
pub fn subscript(value: Self, slice: Self) -> Self {
Self::Subscript {
value: Box::new(value),
slice: Box::new(slice),
}
}
/// Build a tuple
pub fn tuple(elts: impl IntoIterator<Item = Self>) -> Self {
Self::Tuple {
elts: elts.into_iter().collect(),
}
}
pub fn constant_from_expression(expr: &Expr) -> Self {
Self::Constant(match expr {
Expr::Lit(ExprLit { lit, .. }) => match lit {
Lit::Str(s) => PyConstant::Str(s.value()),
Lit::Char(c) => PyConstant::Str(c.value().into()),
Lit::Int(i) => PyConstant::Int(i.base10_digits().into()),
Lit::Float(f) => PyConstant::Float(f.base10_digits().into()),
Lit::Bool(b) => PyConstant::Bool(b.value()),
_ => PyConstant::Ellipsis, // TODO: implement ByteStr and CStr
},
Expr::Path(ExprPath { qself, path, .. })
if qself.is_none() && path.is_ident("None") =>
{
PyConstant::None
}
_ => PyConstant::Ellipsis,
})
}
pub fn str_constant(value: impl Into<String>) -> Self {
Self::Constant(PyConstant::Str(value.into()))
}
/// `...`
pub fn ellipsis() -> Self {
Self::Constant(PyConstant::Ellipsis)
}
/// `None`
pub fn none() -> Self {
Self::Constant(PyConstant::None)
}
pub fn to_introspection_token_stream(&self, pyo3_crate_path: &PyO3CratePath) -> TokenStream {
match self {
Self::FromPyObjectType(t) => {
quote! { <#t as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE }
}
Self::IntoPyObjectType(t) => {
quote! { <#t as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE }
}
Self::IntoPyObjectMaybeRefType(t) => {
quote! {{
#[allow(unused_imports)]
use #pyo3_crate_path::impl_::pyclass::Probe as _;
<#t as #pyo3_crate_path::impl_::introspection::PyIntoPyObjectMaybeRefType<{
#pyo3_crate_path::impl_::pyclass::IsIntoPyObjectRef::<#t>::VALUE
}>>::OUTPUT_TYPE
}}
}
Self::ArgumentType(t) => {
quote! {
<#t as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument<
{
#[allow(unused_imports, reason = "`Probe` trait used on negative case only")]
use #pyo3_crate_path::impl_::pyclass::Probe as _;
#pyo3_crate_path::impl_::pyclass::IsFromPyObject::<#t>::VALUE
}
>>::INPUT_TYPE
}
}
Self::ReturnType(t) => {
quote! {{
#[allow(unused_imports)]
use #pyo3_crate_path::impl_::pyclass::Probe as _;
const TYPE: #pyo3_crate_path::inspect::PyStaticExpr = if #pyo3_crate_path::impl_::pyclass::IsReturningEmptyTuple::<#t>::VALUE {
<#pyo3_crate_path::types::PyNone as #pyo3_crate_path::type_object::PyTypeInfo>::TYPE_HINT
} else {
<#t as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE
};
TYPE
}}
}
Self::Type(t) => {
quote! { <#t as #pyo3_crate_path::type_object::PyTypeCheck>::TYPE_HINT }
}
Self::Name { id } => {
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Name { id: #id } }
}
Self::Attribute { value, attr } => {
let value = value.to_introspection_token_stream(pyo3_crate_path);
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Attribute { value: &#value, attr: #attr } }
}
Self::BinOp { left, op, right } => {
let left = left.to_introspection_token_stream(pyo3_crate_path);
let op = match op {
PyOperator::BitOr => quote!(#pyo3_crate_path::inspect::PyStaticOperator::BitOr),
};
let right = right.to_introspection_token_stream(pyo3_crate_path);
quote! {
#pyo3_crate_path::inspect::PyStaticExpr::BinOp {
left: &#left,
op: #op,
right: &#right,
}
}
}
Self::Subscript { value, slice } => {
let value = value.to_introspection_token_stream(pyo3_crate_path);
let slice = slice.to_introspection_token_stream(pyo3_crate_path);
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Subscript { value: &#value, slice: &#slice } }
}
Self::Tuple { elts } => {
let elts = elts
.iter()
.map(|e| e.to_introspection_token_stream(pyo3_crate_path));
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Tuple { elts: &[#(#elts),*] } }
}
Self::Constant(c) => match c {
PyConstant::None => {
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Constant { value: #pyo3_crate_path::inspect::PyStaticConstant::None } }
}
PyConstant::Bool(v) => {
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Constant { value: #pyo3_crate_path::inspect::PyStaticConstant::Bool(#v) } }
}
PyConstant::Int(v) => {
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Constant { value: #pyo3_crate_path::inspect::PyStaticConstant::Int(#v) } }
}
PyConstant::Float(v) => {
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Constant { value: #pyo3_crate_path::inspect::PyStaticConstant::Float(#v) } }
}
PyConstant::Str(v) => {
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Constant { value: #pyo3_crate_path::inspect::PyStaticConstant::Str(#v) } }
}
PyConstant::Ellipsis => {
quote! { #pyo3_crate_path::inspect::PyStaticExpr::Constant { value: #pyo3_crate_path::inspect::PyStaticConstant::Ellipsis } }
}
},
}
}
}
fn clean_type(mut t: Type, self_type: Option<&Type>) -> Type {
if let Some(self_type) = self_type {
replace_self(&mut t, self_type);
}
crate::utils::elide_lifetimes(&mut t);
t
}
// Replace Self in types with the given type
fn replace_self(ty: &mut Type, self_target: &Type) {
struct SelfReplacementVisitor<'a> {
self_target: &'a Type,
}
impl VisitMut for SelfReplacementVisitor<'_> {
fn visit_type_mut(&mut self, ty: &mut Type) {
if let Type::Path(type_path) = ty {
if type_path.qself.is_none()
&& type_path.path.segments.len() == 1
&& type_path.path.segments[0].ident == "Self"
&& type_path.path.segments[0].arguments.is_empty()
{
// It is Self
*ty = self.self_target.clone();
return;
}
}
visit_type_mut(self, ty);
}
}
SelfReplacementVisitor { self_target }.visit_type_mut(ty);
}
File diff suppressed because it is too large. Load diff
+483
View File
@@ -0,0 +1,483 @@
use crate::attributes::KeywordAttribute;
use crate::combine_errors::CombineErrors;
#[cfg(feature = "experimental-inspect")]
use crate::introspection::{function_introspection_code, introspection_id_const};
#[cfg(feature = "experimental-inspect")]
use crate::py_expr::PyExpr;
#[cfg(feature = "experimental-inspect")]
use crate::utils::get_doc;
use crate::utils::Ctx;
use crate::{
attributes::{
self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute,
FromPyWithAttribute, NameAttribute, TextSignatureAttribute,
},
method::{self, CallingConvention, ClassMethodReceiver, FnArg, SelfConversionPolicy},
pymethod::check_generic,
};
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use std::cmp::PartialEq;
use std::ffi::CString;
#[cfg(feature = "experimental-inspect")]
use std::iter::empty;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
#[cfg(feature = "experimental-inspect")]
use syn::ReturnType;
use syn::{ext::IdentExt, spanned::Spanned, LitCStr, LitStr, Path, Result, Token};
mod signature;
pub use self::signature::{ConstructorAttribute, FunctionSignature, SignatureAttribute};
#[derive(Clone, Debug)]
pub struct PyFunctionArgPyO3Attributes {
pub from_py_with: Option<FromPyWithAttribute>,
pub cancel_handle: Option<attributes::kw::cancel_handle>,
}
enum PyFunctionArgPyO3Attribute {
FromPyWith(FromPyWithAttribute),
CancelHandle(attributes::kw::cancel_handle),
}
impl Parse for PyFunctionArgPyO3Attribute {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::cancel_handle) {
input.parse().map(PyFunctionArgPyO3Attribute::CancelHandle)
} else if lookahead.peek(attributes::kw::from_py_with) {
input.parse().map(PyFunctionArgPyO3Attribute::FromPyWith)
} else {
Err(lookahead.error())
}
}
}
impl PyFunctionArgPyO3Attributes {
/// Parses #[pyo3(from_python_with = "func")]
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> {
let mut attributes = PyFunctionArgPyO3Attributes {
from_py_with: None,
cancel_handle: None,
};
take_attributes(attrs, |attr| {
if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
for attr in pyo3_attrs {
match attr {
PyFunctionArgPyO3Attribute::FromPyWith(from_py_with) => {
ensure_spanned!(
attributes.from_py_with.is_none(),
from_py_with.span() => "`from_py_with` may only be specified once per argument"
);
attributes.from_py_with = Some(from_py_with);
}
PyFunctionArgPyO3Attribute::CancelHandle(cancel_handle) => {
ensure_spanned!(
attributes.cancel_handle.is_none(),
cancel_handle.span() => "`cancel_handle` may only be specified once per argument"
);
attributes.cancel_handle = Some(cancel_handle);
}
}
ensure_spanned!(
attributes.from_py_with.is_none() || attributes.cancel_handle.is_none(),
attributes.cancel_handle.unwrap().span() => "`from_py_with` and `cancel_handle` cannot be specified together"
);
}
Ok(true)
} else {
Ok(false)
}
})?;
Ok(attributes)
}
}
type PyFunctionWarningMessageAttribute = KeywordAttribute<attributes::kw::message, LitStr>;
type PyFunctionWarningCategoryAttribute = KeywordAttribute<attributes::kw::category, Path>;
pub struct PyFunctionWarningAttribute {
pub message: PyFunctionWarningMessageAttribute,
pub category: Option<PyFunctionWarningCategoryAttribute>,
pub span: Span,
}
#[derive(PartialEq, Clone)]
pub enum PyFunctionWarningCategory {
Path(Path),
UserWarning,
DeprecationWarning, // TODO: unused for now, intended for pyo3(deprecated) special-case
}
#[derive(Clone)]
pub struct PyFunctionWarning {
pub message: LitStr,
pub category: PyFunctionWarningCategory,
pub span: Span,
}
impl From<PyFunctionWarningAttribute> for PyFunctionWarning {
fn from(value: PyFunctionWarningAttribute) -> Self {
Self {
message: value.message.value,
category: value
.category
.map_or(PyFunctionWarningCategory::UserWarning, |cat| {
PyFunctionWarningCategory::Path(cat.value)
}),
span: value.span,
}
}
}
pub trait WarningFactory {
fn build_py_warning(&self, ctx: &Ctx) -> TokenStream;
fn span(&self) -> Span;
}
impl WarningFactory for PyFunctionWarning {
fn build_py_warning(&self, ctx: &Ctx) -> TokenStream {
let message = &self.message.value();
let c_message = LitCStr::new(
&CString::new(message.clone()).unwrap(),
Spanned::span(&message),
);
let pyo3_path = &ctx.pyo3_path;
let category = match &self.category {
PyFunctionWarningCategory::Path(path) => quote! {#path},
PyFunctionWarningCategory::UserWarning => {
quote! {#pyo3_path::exceptions::PyUserWarning}
}
PyFunctionWarningCategory::DeprecationWarning => {
quote! {#pyo3_path::exceptions::PyDeprecationWarning}
}
};
quote! {
#pyo3_path::PyErr::warn(py, &<#category as #pyo3_path::PyTypeInfo>::type_object(py), #c_message, 1)?;
}
}
fn span(&self) -> Span {
self.span
}
}
impl<T: WarningFactory> WarningFactory for Vec<T> {
fn build_py_warning(&self, ctx: &Ctx) -> TokenStream {
let warnings = self.iter().map(|warning| warning.build_py_warning(ctx));
quote! {
#(#warnings)*
}
}
fn span(&self) -> Span {
self.iter()
.map(|val| val.span())
.reduce(|acc, span| acc.join(span).unwrap_or(acc))
.unwrap()
}
}
impl Parse for PyFunctionWarningAttribute {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut message: Option<PyFunctionWarningMessageAttribute> = None;
let mut category: Option<PyFunctionWarningCategoryAttribute> = None;
let span = input.parse::<attributes::kw::warn>()?.span();
let content;
syn::parenthesized!(content in input);
while !content.is_empty() {
let lookahead = content.lookahead1();
if lookahead.peek(attributes::kw::message) {
message = content
.parse::<PyFunctionWarningMessageAttribute>()
.map(Some)?;
} else if lookahead.peek(attributes::kw::category) {
category = content
.parse::<PyFunctionWarningCategoryAttribute>()
.map(Some)?;
} else {
return Err(lookahead.error());
}
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
Ok(PyFunctionWarningAttribute {
message: message.ok_or(syn::Error::new(
content.span(),
"missing `message` in `warn` attribute",
))?,
category,
span,
})
}
}
impl ToTokens for PyFunctionWarningAttribute {
fn to_tokens(&self, tokens: &mut TokenStream) {
let message_tokens = self.message.to_token_stream();
let category_tokens = self
.category
.as_ref()
.map_or(quote! {}, |cat| cat.to_token_stream());
let token_stream = quote! {
warn(#message_tokens, #category_tokens)
};
tokens.extend(token_stream);
}
}
#[derive(Default)]
pub struct PyFunctionOptions {
pub pass_module: Option<attributes::kw::pass_module>,
pub name: Option<NameAttribute>,
pub signature: Option<SignatureAttribute>,
pub text_signature: Option<TextSignatureAttribute>,
pub krate: Option<CrateAttribute>,
pub warnings: Vec<PyFunctionWarning>,
}
impl Parse for PyFunctionOptions {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut options = PyFunctionOptions::default();
let attrs = Punctuated::<PyFunctionOption, syn::Token![,]>::parse_terminated(input)?;
options.add_attributes(attrs)?;
Ok(options)
}
}
pub enum PyFunctionOption {
Name(NameAttribute),
PassModule(attributes::kw::pass_module),
Signature(SignatureAttribute),
TextSignature(TextSignatureAttribute),
Crate(CrateAttribute),
Warning(PyFunctionWarningAttribute),
}
impl Parse for PyFunctionOption {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::name) {
input.parse().map(PyFunctionOption::Name)
} else if lookahead.peek(attributes::kw::pass_module) {
input.parse().map(PyFunctionOption::PassModule)
} else if lookahead.peek(attributes::kw::signature) {
input.parse().map(PyFunctionOption::Signature)
} else if lookahead.peek(attributes::kw::text_signature) {
input.parse().map(PyFunctionOption::TextSignature)
} else if lookahead.peek(syn::Token![crate]) {
input.parse().map(PyFunctionOption::Crate)
} else if lookahead.peek(attributes::kw::warn) {
input.parse().map(PyFunctionOption::Warning)
} else {
Err(lookahead.error())
}
}
}
impl PyFunctionOptions {
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> {
let mut options = PyFunctionOptions::default();
options.add_attributes(take_pyo3_options(attrs)?)?;
Ok(options)
}
pub fn add_attributes(
&mut self,
attrs: impl IntoIterator<Item = PyFunctionOption>,
) -> Result<()> {
macro_rules! set_option {
($key:ident) => {
{
ensure_spanned!(
self.$key.is_none(),
$key.span() => concat!("`", stringify!($key), "` may only be specified once")
);
self.$key = Some($key);
}
};
}
for attr in attrs {
match attr {
PyFunctionOption::Name(name) => set_option!(name),
PyFunctionOption::PassModule(pass_module) => set_option!(pass_module),
PyFunctionOption::Signature(signature) => set_option!(signature),
PyFunctionOption::TextSignature(text_signature) => set_option!(text_signature),
PyFunctionOption::Crate(krate) => set_option!(krate),
PyFunctionOption::Warning(warning) => {
self.warnings.push(warning.into());
}
}
}
Ok(())
}
}
pub fn build_py_function(
ast: &mut syn::ItemFn,
mut options: PyFunctionOptions,
) -> syn::Result<TokenStream> {
options.add_attributes(take_pyo3_options(&mut ast.attrs)?)?;
impl_wrap_pyfunction(ast, options)
}
/// Generates python wrapper over a function that allows adding it to a python module as a python
/// function
pub fn impl_wrap_pyfunction(
func: &mut syn::ItemFn,
options: PyFunctionOptions,
) -> syn::Result<TokenStream> {
check_generic(&func.sig)?;
let PyFunctionOptions {
pass_module,
name,
signature,
text_signature,
krate,
warnings,
} = options;
let ctx = &Ctx::new(&krate, Some(&func.sig));
let Ctx { pyo3_path, .. } = &ctx;
let python_name = name
.as_ref()
.map_or_else(|| &func.sig.ident, |name| &name.value.0)
.unraw();
let tp = if pass_module.is_some() {
let span = match func.sig.inputs.first() {
Some(syn::FnArg::Typed(first_arg)) => first_arg.ty.span(),
Some(syn::FnArg::Receiver(_)) | None => bail_spanned!(
func.sig.paren_token.span.join() => "expected `&PyModule` or `Py<PyModule>` as first argument with `pass_module`"
),
};
method::FnType::FnModule(span)
} else {
method::FnType::FnStatic
};
let arguments = func
.sig
.inputs
.iter_mut()
.skip(if tp.skip_first_rust_argument_in_python_signature() {
1
} else {
0
})
.map(FnArg::parse)
.try_combine_syn_errors()?;
let signature = if let Some(signature) = signature {
FunctionSignature::from_arguments_and_attribute(arguments, signature)?
} else {
FunctionSignature::from_arguments(arguments)
};
let spec = method::FnSpec {
tp,
name: &func.sig.ident,
python_name,
signature,
text_signature,
asyncness: func.sig.asyncness,
unsafety: func.sig.unsafety,
warnings,
output: func.sig.output.clone(),
};
let vis = &func.vis;
let name = &func.sig.ident;
#[cfg(feature = "experimental-inspect")]
let introspection = function_introspection_code(
pyo3_path,
Some(name),
// `name` is the Rust identifier, which `#[pyo3(name = "...")]` overrides for Python
&spec.python_name.to_string(),
&spec.signature,
None,
match &func.sig.output {
ReturnType::Type(_, t) => PyExpr::from_return_type((**t).clone(), None),
ReturnType::Default => PyExpr::none(),
},
empty(),
func.sig.asyncness.is_some(),
false,
get_doc(&func.attrs, None).as_ref(),
None,
);
#[cfg(not(feature = "experimental-inspect"))]
let introspection = quote! {};
#[cfg(feature = "experimental-inspect")]
let introspection_id = introspection_id_const();
#[cfg(not(feature = "experimental-inspect"))]
let introspection_id = quote! {};
let wrapper_ident = format_ident!("__pyfunction_{}", spec.name);
if spec.asyncness.is_some() {
ensure_spanned!(
cfg!(feature = "experimental-async"),
spec.asyncness.span() => "async functions are only supported with the `experimental-async` feature"
);
}
let calling_convention = CallingConvention::from_signature(&spec.signature);
let wrapper = spec.get_wrapper_function(
&wrapper_ident,
None,
calling_convention,
SelfConversionPolicy::checked(),
ClassMethodReceiver::Class,
ctx,
)?;
let methoddef = spec.get_methoddef(
wrapper_ident,
spec.get_doc(&func.attrs).as_ref(),
calling_convention,
ctx,
)?;
let wrapped_pyfunction = quote! {
// Create a module with the same name as the `#[pyfunction]` - this way `use <the function>`
// will actually bring both the module and the function into scope.
#[doc(hidden)]
#vis mod #name {
pub(crate) struct MakeDef;
pub static _PYO3_DEF: #pyo3_path::impl_::pyfunction::PyFunctionDef = MakeDef::_PYO3_DEF;
#introspection_id
}
// Generate the definition in the same scope as the original function -
// this avoids complications around the fact that the generated module has a different scope
// (and `super` doesn't always refer to the outer scope, e.g. if the `#[pyfunction] is
// inside a function body)
#[allow(unknown_lints, non_local_definitions)]
impl #name::MakeDef {
// We're using this to initialize a static, so it's fine.
#[allow(clippy::declare_interior_mutable_const)]
const _PYO3_DEF: #pyo3_path::impl_::pyfunction::PyFunctionDef =
#pyo3_path::impl_::pyfunction::PyFunctionDef::from_method_def(#methoddef);
}
#[allow(non_snake_case)]
#wrapper
#introspection
};
Ok(wrapped_pyfunction)
}
+657
View File
@@ -0,0 +1,657 @@
#[cfg(feature = "experimental-inspect")]
use crate::py_expr::PyExpr;
use crate::{
attributes::{kw, KeywordAttribute},
method::FnArg,
utils::expr_to_python,
};
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::{
ext::IdentExt,
parse::{Parse, ParseStream},
punctuated::Punctuated,
spanned::Spanned,
Expr, Token,
};
#[derive(Clone)]
pub struct Signature {
paren_token: syn::token::Paren,
pub items: Punctuated<SignatureItem, Token![,]>,
pub returns: Option<(Token![->], PyTypeAnnotation)>,
}
impl Parse for Signature {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let content;
let paren_token = syn::parenthesized!(content in input);
let items = content.parse_terminated(SignatureItem::parse, Token![,])?;
let returns = if input.peek(Token![->]) {
Some((input.parse()?, input.parse()?))
} else {
None
};
Ok(Signature {
paren_token,
items,
returns,
})
}
}
impl ToTokens for Signature {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.paren_token
.surround(tokens, |tokens| self.items.to_tokens(tokens));
if let Some((arrow, returns)) = &self.returns {
arrow.to_tokens(tokens);
returns.to_tokens(tokens);
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignatureItemArgument {
pub ident: syn::Ident,
pub colon_and_annotation: Option<(Token![:], PyTypeAnnotation)>,
pub eq_and_default: Option<(Token![=], syn::Expr)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignatureItemPosargsSep {
pub slash: Token![/],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignatureItemVarargsSep {
pub asterisk: Token![*],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignatureItemVarargs {
pub sep: SignatureItemVarargsSep,
pub ident: syn::Ident,
pub colon_and_annotation: Option<(Token![:], PyTypeAnnotation)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignatureItemKwargs {
pub asterisks: (Token![*], Token![*]),
pub ident: syn::Ident,
pub colon_and_annotation: Option<(Token![:], PyTypeAnnotation)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SignatureItem {
Argument(Box<SignatureItemArgument>),
PosargsSep(SignatureItemPosargsSep),
VarargsSep(SignatureItemVarargsSep),
Varargs(SignatureItemVarargs),
Kwargs(SignatureItemKwargs),
}
impl Parse for SignatureItem {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(Token![*]) {
if input.peek2(Token![*]) {
input.parse().map(SignatureItem::Kwargs)
} else {
let sep = input.parse()?;
if input.is_empty() || input.peek(Token![,]) {
Ok(SignatureItem::VarargsSep(sep))
} else {
Ok(SignatureItem::Varargs(SignatureItemVarargs {
sep,
ident: input.parse()?,
colon_and_annotation: if input.peek(Token![:]) {
Some((input.parse()?, input.parse()?))
} else {
None
},
}))
}
}
} else if lookahead.peek(Token![/]) {
input.parse().map(SignatureItem::PosargsSep)
} else {
input.parse().map(SignatureItem::Argument)
}
}
}
impl ToTokens for SignatureItem {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
SignatureItem::Argument(arg) => arg.to_tokens(tokens),
SignatureItem::Varargs(varargs) => varargs.to_tokens(tokens),
SignatureItem::VarargsSep(sep) => sep.to_tokens(tokens),
SignatureItem::Kwargs(kwargs) => kwargs.to_tokens(tokens),
SignatureItem::PosargsSep(sep) => sep.to_tokens(tokens),
}
}
}
impl Parse for SignatureItemArgument {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
ident: input.parse()?,
colon_and_annotation: if input.peek(Token![:]) {
Some((input.parse()?, input.parse()?))
} else {
None
},
eq_and_default: if input.peek(Token![=]) {
Some((input.parse()?, input.parse()?))
} else {
None
},
})
}
}
impl ToTokens for SignatureItemArgument {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.ident.to_tokens(tokens);
if let Some((colon, annotation)) = &self.colon_and_annotation {
colon.to_tokens(tokens);
annotation.to_tokens(tokens);
}
if let Some((eq, default)) = &self.eq_and_default {
eq.to_tokens(tokens);
default.to_tokens(tokens);
}
}
}
impl Parse for SignatureItemVarargsSep {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
asterisk: input.parse()?,
})
}
}
impl ToTokens for SignatureItemVarargsSep {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.asterisk.to_tokens(tokens);
}
}
impl Parse for SignatureItemVarargs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
sep: input.parse()?,
ident: input.parse()?,
colon_and_annotation: if input.peek(Token![:]) {
Some((input.parse()?, input.parse()?))
} else {
None
},
})
}
}
impl ToTokens for SignatureItemVarargs {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.sep.to_tokens(tokens);
self.ident.to_tokens(tokens);
}
}
impl Parse for SignatureItemKwargs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
asterisks: (input.parse()?, input.parse()?),
ident: input.parse()?,
colon_and_annotation: if input.peek(Token![:]) {
Some((input.parse()?, input.parse()?))
} else {
None
},
})
}
}
impl ToTokens for SignatureItemKwargs {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.asterisks.0.to_tokens(tokens);
self.asterisks.1.to_tokens(tokens);
self.ident.to_tokens(tokens);
}
}
impl Parse for SignatureItemPosargsSep {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
slash: input.parse()?,
})
}
}
impl ToTokens for SignatureItemPosargsSep {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.slash.to_tokens(tokens);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PyTypeAnnotation(syn::LitStr);
impl Parse for PyTypeAnnotation {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self(input.parse()?))
}
}
impl ToTokens for PyTypeAnnotation {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens);
}
}
impl PyTypeAnnotation {
#[cfg(feature = "experimental-inspect")]
pub fn as_type_hint(&self) -> PyExpr {
PyExpr::str_constant(self.0.value())
}
}
pub type SignatureAttribute = KeywordAttribute<kw::signature, Signature>;
pub type ConstructorAttribute = KeywordAttribute<kw::constructor, Signature>;
impl ConstructorAttribute {
pub fn into_signature(self) -> SignatureAttribute {
SignatureAttribute {
kw: kw::signature(self.kw.span),
value: self.value,
}
}
}
#[derive(Default, Clone)]
pub struct PythonSignature {
pub positional_parameters: Vec<String>,
pub positional_only_parameters: usize,
/// Vector of expressions representing positional defaults
pub default_positional_parameters: Vec<Expr>,
pub varargs: Option<String>,
// Tuples of keyword name and optional default value
pub keyword_only_parameters: Vec<(String, Option<Expr>)>,
pub kwargs: Option<String>,
}
impl PythonSignature {
pub fn has_no_args(&self) -> bool {
self.positional_parameters.is_empty()
&& self.keyword_only_parameters.is_empty()
&& self.varargs.is_none()
&& self.kwargs.is_none()
}
pub fn required_positional_parameters(&self) -> usize {
self.positional_parameters
.len()
.checked_sub(self.default_positional_parameters.len())
.expect("should always have positional defaults <= positional parameters")
}
/// Makes every positional parameter positional-only, exactly as a trailing `/` in a
/// signature does. Deliberately leaves keyword-only parameters alone.
pub fn make_all_parameters_positional_only(&mut self) {
self.positional_only_parameters = self.positional_parameters.len();
}
}
#[derive(Clone)]
pub struct FunctionSignature<'a> {
pub arguments: Vec<FnArg<'a>>,
pub python_signature: PythonSignature,
pub attribute: Option<SignatureAttribute>,
}
pub enum ParseState {
/// Accepting positional parameters, which might be positional only
Positional,
/// Accepting positional parameters after '/'
PositionalAfterPosargs,
/// Accepting keyword-only parameters after '*' or '*args'
Keywords,
/// After `**kwargs` nothing is allowed
Done,
}
impl ParseState {
fn add_argument(
&mut self,
signature: &mut PythonSignature,
name: String,
default_value: Option<Expr>,
span: Span,
) -> syn::Result<()> {
match self {
ParseState::Positional | ParseState::PositionalAfterPosargs => {
signature.positional_parameters.push(name);
if let Some(default_value) = default_value {
signature.default_positional_parameters.push(default_value);
// Now all subsequent positional parameters must also have defaults
} else if !signature.default_positional_parameters.is_empty() {
bail_spanned!(span => "cannot have required positional parameter after an optional parameter")
}
Ok(())
}
ParseState::Keywords => {
signature
.keyword_only_parameters
.push((name, default_value));
Ok(())
}
ParseState::Done => {
bail_spanned!(span => format!("no more arguments are allowed after `**{}`", signature.kwargs.as_deref().unwrap_or("")))
}
}
}
fn add_varargs(
&mut self,
signature: &mut PythonSignature,
varargs: &SignatureItemVarargs,
) -> syn::Result<()> {
match self {
ParseState::Positional | ParseState::PositionalAfterPosargs => {
signature.varargs = Some(varargs.ident.to_string());
*self = ParseState::Keywords;
Ok(())
}
ParseState::Keywords => {
bail_spanned!(varargs.span() => format!("`*{}` not allowed after `*{}`", varargs.ident, signature.varargs.as_deref().unwrap_or("")))
}
ParseState::Done => {
bail_spanned!(varargs.span() => format!("`*{}` not allowed after `**{}`", varargs.ident, signature.kwargs.as_deref().unwrap_or("")))
}
}
}
fn add_kwargs(
&mut self,
signature: &mut PythonSignature,
kwargs: &SignatureItemKwargs,
) -> syn::Result<()> {
match self {
ParseState::Positional | ParseState::PositionalAfterPosargs | ParseState::Keywords => {
signature.kwargs = Some(kwargs.ident.to_string());
*self = ParseState::Done;
Ok(())
}
ParseState::Done => {
bail_spanned!(kwargs.span() => format!("`**{}` not allowed after `**{}`", kwargs.ident, signature.kwargs.as_deref().unwrap_or("")))
}
}
}
fn finish_pos_only_args(
&mut self,
signature: &mut PythonSignature,
span: Span,
) -> syn::Result<()> {
match self {
ParseState::Positional => {
signature.make_all_parameters_positional_only();
*self = ParseState::PositionalAfterPosargs;
Ok(())
}
ParseState::PositionalAfterPosargs => {
bail_spanned!(span => "`/` not allowed after `/`")
}
ParseState::Keywords => {
bail_spanned!(span => format!("`/` not allowed after `*{}`", signature.varargs.as_deref().unwrap_or("")))
}
ParseState::Done => {
bail_spanned!(span => format!("`/` not allowed after `**{}`", signature.kwargs.as_deref().unwrap_or("")))
}
}
}
fn finish_pos_args(&mut self, signature: &PythonSignature, span: Span) -> syn::Result<()> {
match self {
ParseState::Positional | ParseState::PositionalAfterPosargs => {
*self = ParseState::Keywords;
Ok(())
}
ParseState::Keywords => {
bail_spanned!(span => format!("`*` not allowed after `*{}`", signature.varargs.as_deref().unwrap_or("")))
}
ParseState::Done => {
bail_spanned!(span => format!("`*` not allowed after `**{}`", signature.kwargs.as_deref().unwrap_or("")))
}
}
}
}
impl<'a> FunctionSignature<'a> {
pub fn from_arguments_and_attribute(
mut arguments: Vec<FnArg<'a>>,
attribute: SignatureAttribute,
) -> syn::Result<Self> {
let mut parse_state = ParseState::Positional;
let mut python_signature = PythonSignature::default();
let mut args_iter = arguments.iter_mut();
let mut next_non_py_argument_checked = |name: &syn::Ident| {
for fn_arg in args_iter.by_ref() {
match fn_arg {
FnArg::Py(..) => {
// If the user incorrectly tried to include py: Python in the
// signature, give a useful error as a hint.
ensure_spanned!(
name != fn_arg.name(),
name.span() => "arguments of type `Python` must not be part of the signature"
);
// Otherwise try next argument.
continue;
}
FnArg::CancelHandle(..) => {
// If the user incorrectly tried to include cancel: CoroutineCancel in the
// signature, give a useful error as a hint.
ensure_spanned!(
name != fn_arg.name(),
name.span() => "`cancel_handle` argument must not be part of the signature"
);
// Otherwise try next argument.
continue;
}
_ => {
ensure_spanned!(
name == fn_arg.name(),
name.span() => format!(
"expected argument from function definition `{}` but got argument `{}`",
fn_arg.name().unraw(),
name.unraw(),
)
);
return Ok(fn_arg);
}
}
}
bail_spanned!(
name.span() => "signature entry does not have a corresponding function argument"
)
};
for item in &attribute.value.items {
match item {
SignatureItem::Argument(arg) => {
let fn_arg = next_non_py_argument_checked(&arg.ident)?;
parse_state.add_argument(
&mut python_signature,
arg.ident.unraw().to_string(),
arg.eq_and_default
.as_ref()
.map(|(_, default)| default.clone()),
arg.span(),
)?;
let FnArg::Regular(fn_arg) = fn_arg else {
unreachable!(
"`Python` and `CancelHandle` are already handled above and `*args`/`**kwargs` are \
parsed and transformed below. Because the have to come last and are only allowed \
once, this has to be a regular argument."
);
};
if let Some((_, default)) = &arg.eq_and_default {
fn_arg.default_value = Some(Box::new(default.clone()));
}
#[cfg(feature = "experimental-inspect")]
if let Some((_, annotation)) = &arg.colon_and_annotation {
fn_arg.annotation = Some(annotation.as_type_hint());
}
}
SignatureItem::VarargsSep(sep) => {
parse_state.finish_pos_args(&python_signature, sep.span())?
}
SignatureItem::Varargs(varargs) => {
let fn_arg = next_non_py_argument_checked(&varargs.ident)?;
fn_arg.to_varargs_mut()?;
parse_state.add_varargs(&mut python_signature, varargs)?;
#[cfg(feature = "experimental-inspect")]
if let Some((_, annotation)) = &varargs.colon_and_annotation {
let FnArg::VarArgs(fn_arg) = fn_arg else {
unreachable!(
"`Python` and `CancelHandle` are already handled above and `*args`/`**kwargs` are \
parsed and transformed below. Because the have to come last and are only allowed \
once, this has to be a regular argument."
);
};
fn_arg.annotation = Some(annotation.as_type_hint());
}
}
SignatureItem::Kwargs(kwargs) => {
let fn_arg = next_non_py_argument_checked(&kwargs.ident)?;
fn_arg.to_kwargs_mut()?;
parse_state.add_kwargs(&mut python_signature, kwargs)?;
#[cfg(feature = "experimental-inspect")]
if let Some((_, annotation)) = &kwargs.colon_and_annotation {
let FnArg::KwArgs(fn_arg) = fn_arg else {
unreachable!(
"`Python` and `CancelHandle` are already handled above and `*args`/`**kwargs` are \
parsed and transformed below. Because the have to come last and are only allowed \
once, this has to be a regular argument."
);
};
fn_arg.annotation = Some(annotation.as_type_hint());
}
}
SignatureItem::PosargsSep(sep) => {
parse_state.finish_pos_only_args(&mut python_signature, sep.span())?
}
};
}
// Ensure no non-py arguments remain
if let Some(arg) =
args_iter.find(|arg| !matches!(arg, FnArg::Py(..) | FnArg::CancelHandle(..)))
{
bail_spanned!(
attribute.kw.span() => format!("missing signature entry for argument `{}`", arg.name())
);
}
Ok(FunctionSignature {
arguments,
python_signature,
attribute: Some(attribute),
})
}
/// Without `#[pyo3(signature)]` or `#[args]` - just take the Rust function arguments as positional.
pub fn from_arguments(arguments: Vec<FnArg<'a>>) -> Self {
let mut python_signature = PythonSignature::default();
for arg in &arguments {
// Python<'_> arguments don't show in Python signature
if matches!(arg, FnArg::Py(..) | FnArg::CancelHandle(..)) {
continue;
}
python_signature
.positional_parameters
.push(arg.name().unraw().to_string());
}
Self {
arguments,
python_signature,
attribute: None,
}
}
pub fn text_signature(&self, self_argument: Option<&str>) -> String {
let mut output = String::new();
output.push('(');
if let Some(arg) = self_argument {
output.push('$');
output.push_str(arg);
}
let mut maybe_push_comma = {
let mut first = self_argument.is_none();
move |output: &mut String| {
if !first {
output.push_str(", ");
} else {
first = false;
}
}
};
let py_sig = &self.python_signature;
let defaults = std::iter::repeat_n(None, py_sig.required_positional_parameters())
.chain(py_sig.default_positional_parameters.iter().map(Some));
for (i, (parameter, default)) in
std::iter::zip(&py_sig.positional_parameters, defaults).enumerate()
{
maybe_push_comma(&mut output);
output.push_str(parameter);
if let Some(expr) = default {
output.push('=');
output.push_str(&expr_to_python(expr));
}
if py_sig.positional_only_parameters > 0 && i + 1 == py_sig.positional_only_parameters {
output.push_str(", /")
}
}
if let Some(varargs) = &py_sig.varargs {
maybe_push_comma(&mut output);
output.push('*');
output.push_str(varargs);
} else if !py_sig.keyword_only_parameters.is_empty() {
maybe_push_comma(&mut output);
output.push('*');
}
for (parameter, default) in &py_sig.keyword_only_parameters {
maybe_push_comma(&mut output);
output.push_str(parameter);
if let Some(expr) = default {
output.push('=');
output.push_str(&expr_to_python(expr));
}
}
if let Some(kwargs) = &py_sig.kwargs {
maybe_push_comma(&mut output);
output.push_str("**");
output.push_str(kwargs);
}
output.push(')');
output
}
}
+516
View File
@@ -0,0 +1,516 @@
use std::collections::HashSet;
use crate::combine_errors::CombineErrors;
#[cfg(feature = "experimental-inspect")]
use crate::get_doc;
#[cfg(feature = "experimental-inspect")]
use crate::introspection::{attribute_introspection_code, function_introspection_code};
#[cfg(feature = "experimental-inspect")]
use crate::method::{FnSpec, FnType};
#[cfg(feature = "experimental-inspect")]
use crate::py_expr::PyExpr;
use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath};
use crate::{
attributes::{take_pyo3_options, CrateAttribute},
konst::{ConstAttributes, ConstSpec},
pyfunction::PyFunctionOptions,
pymethod::{
self, is_proto_method, GeneratedPyMethod, MethodAndMethodDef, MethodAndSlotDef, PyMethod,
},
};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
ImplItemFn, Result,
};
#[cfg(feature = "experimental-inspect")]
use syn::{parse_quote, Ident, ReturnType};
/// The mechanism used to collect `#[pymethods]` into the type object
#[derive(Copy, Clone)]
pub enum PyClassMethodsType {
Specialization,
Inventory,
}
enum PyImplPyO3Option {
Crate(CrateAttribute),
}
impl Parse for PyImplPyO3Option {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(syn::Token![crate]) {
input.parse().map(PyImplPyO3Option::Crate)
} else {
Err(lookahead.error())
}
}
}
#[derive(Default)]
pub struct PyImplOptions {
krate: Option<CrateAttribute>,
}
impl PyImplOptions {
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> Result<Self> {
let mut options: PyImplOptions = Default::default();
for option in take_pyo3_options(attrs)? {
match option {
PyImplPyO3Option::Crate(path) => options.set_crate(path)?,
}
}
Ok(options)
}
fn set_crate(&mut self, path: CrateAttribute) -> Result<()> {
ensure_spanned!(
self.krate.is_none(),
path.span() => "`crate` may only be specified once"
);
self.krate = Some(path);
Ok(())
}
}
pub fn build_py_methods(
ast: &mut syn::ItemImpl,
methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
if let Some((_, path, _)) = &ast.trait_ {
bail_spanned!(path.span() => "#[pymethods] cannot be used on trait impl blocks");
} else if ast.generics != Default::default() {
bail_spanned!(
ast.generics.span() =>
"#[pymethods] cannot be used with lifetime parameters or generics"
);
} else {
let options = PyImplOptions::from_attrs(&mut ast.attrs)?;
impl_methods(&ast.self_ty, &mut ast.items, methods_type, options)
}
}
fn check_pyfunction(pyo3_path: &PyO3CratePath, meth: &mut ImplItemFn) -> syn::Result<()> {
let mut error = None;
meth.attrs.retain(|attr| {
let attrs = [attr.clone()];
if has_attribute(&attrs, "pyfunction")
|| has_attribute_with_namespace(&attrs, Some(pyo3_path), &["pyfunction"])
|| has_attribute_with_namespace(&attrs, Some(pyo3_path), &["prelude", "pyfunction"]) {
error = Some(err_spanned!(meth.sig.span() => "functions inside #[pymethods] do not need to be annotated with #[pyfunction]"));
false
} else {
true
}
});
error.map_or(Ok(()), Err)
}
pub fn impl_methods(
ty: &syn::Type,
impls: &mut [syn::ImplItem],
methods_type: PyClassMethodsType,
options: PyImplOptions,
) -> syn::Result<TokenStream> {
let mut extra_fragments = Vec::new();
let mut proto_impls = Vec::new();
let mut methods = Vec::new();
let mut associated_methods = Vec::new();
let mut implemented_proto_fragments = HashSet::new();
let _: Vec<()> = impls
.iter_mut()
.map(|iimpl| {
match iimpl {
syn::ImplItem::Fn(meth) => {
let ctx = &Ctx::new(&options.krate, Some(&meth.sig));
let mut fun_options = PyFunctionOptions::from_attrs(&mut meth.attrs)?;
fun_options.krate = fun_options.krate.or_else(|| options.krate.clone());
check_pyfunction(&ctx.pyo3_path, meth)?;
let method = PyMethod::parse(&mut meth.sig, &mut meth.attrs, fun_options)?;
#[cfg(feature = "experimental-inspect")]
extra_fragments.push(method_introspection_code(
&method.spec,
&meth.attrs,
ty,
method.is_returning_not_implemented_on_extraction_error(),
ctx,
));
match pymethod::gen_py_method(ty, method, &meth.attrs, ctx)? {
GeneratedPyMethod::Method(MethodAndMethodDef {
associated_method,
method_def,
}) => {
let attrs = get_cfg_attributes(&meth.attrs);
associated_methods.push(quote!(#(#attrs)* #associated_method));
methods.push(quote!(#(#attrs)* #method_def));
}
GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => {
implemented_proto_fragments.insert(method_name);
let attrs = get_cfg_attributes(&meth.attrs);
extra_fragments.push(quote!(#(#attrs)* #token_stream));
}
GeneratedPyMethod::Proto(MethodAndSlotDef {
associated_method,
slot_def,
}) => {
let attrs = get_cfg_attributes(&meth.attrs);
proto_impls.push(quote!(#(#attrs)* #slot_def));
associated_methods.push(quote!(#(#attrs)* #associated_method));
}
}
}
syn::ImplItem::Const(konst) => {
let ctx = &Ctx::new(&options.krate, None);
#[cfg(feature = "experimental-inspect")]
let doc = get_doc(&konst.attrs, None);
let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?;
if attributes.is_class_attr {
let spec = ConstSpec {
rust_ident: konst.ident.clone(),
attributes,
#[cfg(feature = "experimental-inspect")]
expr: Some(konst.expr.clone()),
#[cfg(feature = "experimental-inspect")]
ty: konst.ty.clone(),
#[cfg(feature = "experimental-inspect")]
doc,
};
let attrs = get_cfg_attributes(&konst.attrs);
let MethodAndMethodDef {
associated_method,
method_def,
} = gen_py_const(ty, &spec, ctx);
methods.push(quote!(#(#attrs)* #method_def));
associated_methods.push(quote!(#(#attrs)* #associated_method));
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)]));
}
}
}
syn::ImplItem::Macro(m) => bail_spanned!(
m.span() =>
"macros cannot be used as items in `#[pymethods]` impl blocks\n\
= note: this was previously accepted and ignored"
),
_ => {}
}
Ok(())
})
.try_combine_syn_errors()?;
let ctx = &Ctx::new(&options.krate, None);
add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments, ctx);
let items = match methods_type {
PyClassMethodsType::Specialization => impl_py_methods(ty, methods, proto_impls, ctx),
PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods, proto_impls, ctx),
};
Ok(quote! {
#(#extra_fragments)*
#items
#[doc(hidden)]
#[allow(non_snake_case)]
impl #ty {
#(#associated_methods)*
}
})
}
pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMethodDef {
let member = &spec.rust_ident;
let wrapper_ident = format_ident!("__pymethod_{}__", member);
let python_name = spec.null_terminated_python_name();
let Ctx { pyo3_path, .. } = ctx;
let associated_method = quote! {
fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> {
#pyo3_path::IntoPyObjectExt::into_py_any(#cls::#member, py)
}
};
let method_def = quote! {
#pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({
#pyo3_path::impl_::pymethods::PyClassAttributeDef::new(
#python_name,
#cls::#wrapper_ident
)
})
};
#[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))]
let mut def = MethodAndMethodDef {
associated_method,
method_def,
};
#[cfg(feature = "experimental-inspect")]
def.add_introspection(attribute_introspection_code(
&ctx.pyo3_path,
Some(cls),
spec.python_name().to_string(),
spec.expr
.as_ref()
.map_or_else(PyExpr::ellipsis, PyExpr::constant_from_expression),
spec.ty.clone(),
spec.doc.as_ref(),
true,
));
def
}
fn impl_py_methods(
ty: &syn::Type,
methods: Vec<TokenStream>,
proto_impls: Vec<TokenStream>,
ctx: &Ctx,
) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
quote! {
#[allow(unknown_lints, non_local_definitions)]
impl #pyo3_path::impl_::pyclass::PyMethods<#ty>
for #pyo3_path::impl_::pyclass::PyClassImplCollector<#ty>
{
fn py_methods(self) -> &'static #pyo3_path::impl_::pyclass::PyClassItems {
static ITEMS: #pyo3_path::impl_::pyclass::PyClassItems = #pyo3_path::impl_::pyclass::PyClassItems {
methods: &[#(#methods),*],
slots: &[#(#proto_impls),*]
};
&ITEMS
}
}
}
}
fn add_shared_proto_slots(
ty: &syn::Type,
proto_impls: &mut Vec<TokenStream>,
mut implemented_proto_fragments: HashSet<String>,
ctx: &Ctx,
) {
let Ctx { pyo3_path, .. } = ctx;
macro_rules! try_add_shared_slot {
($slot:ident, $($fragments:literal),*) => {{
let mut implemented = false;
$(implemented |= implemented_proto_fragments.remove($fragments));*;
if implemented {
proto_impls.push(quote! { #pyo3_path::impl_::pyclass::$slot!(#ty) })
}
}};
}
try_add_shared_slot!(
generate_pyclass_getattro_slot,
"__getattribute__",
"__getattr__"
);
try_add_shared_slot!(generate_pyclass_setattr_slot, "__setattr__", "__delattr__");
try_add_shared_slot!(generate_pyclass_setdescr_slot, "__set__", "__delete__");
try_add_shared_slot!(generate_pyclass_setitem_slot, "__setitem__", "__delitem__");
try_add_shared_slot!(generate_pyclass_add_slot, "__add__", "__radd__");
try_add_shared_slot!(generate_pyclass_sub_slot, "__sub__", "__rsub__");
try_add_shared_slot!(generate_pyclass_mul_slot, "__mul__", "__rmul__");
try_add_shared_slot!(generate_pyclass_mod_slot, "__mod__", "__rmod__");
try_add_shared_slot!(generate_pyclass_divmod_slot, "__divmod__", "__rdivmod__");
try_add_shared_slot!(generate_pyclass_lshift_slot, "__lshift__", "__rlshift__");
try_add_shared_slot!(generate_pyclass_rshift_slot, "__rshift__", "__rrshift__");
try_add_shared_slot!(generate_pyclass_and_slot, "__and__", "__rand__");
try_add_shared_slot!(generate_pyclass_or_slot, "__or__", "__ror__");
try_add_shared_slot!(generate_pyclass_xor_slot, "__xor__", "__rxor__");
try_add_shared_slot!(generate_pyclass_matmul_slot, "__matmul__", "__rmatmul__");
try_add_shared_slot!(generate_pyclass_truediv_slot, "__truediv__", "__rtruediv__");
try_add_shared_slot!(
generate_pyclass_floordiv_slot,
"__floordiv__",
"__rfloordiv__"
);
try_add_shared_slot!(generate_pyclass_pow_slot, "__pow__", "__rpow__");
try_add_shared_slot!(
generate_pyclass_richcompare_slot,
"__lt__",
"__le__",
"__eq__",
"__ne__",
"__gt__",
"__ge__"
);
// if this assertion trips, a slot fragment has been implemented which has not been added in the
// list above
assert!(implemented_proto_fragments.is_empty());
}
fn submit_methods_inventory(
ty: &syn::Type,
methods: Vec<TokenStream>,
proto_impls: Vec<TokenStream>,
ctx: &Ctx,
) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
quote! {
#pyo3_path::inventory::submit! {
type Inventory = <#ty as #pyo3_path::impl_::pyclass::PyClassImpl>::Inventory;
Inventory::new(#pyo3_path::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] })
}
}
}
pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> {
attrs
.iter()
.filter(|attr| attr.path().is_ident("cfg"))
.collect()
}
#[cfg(feature = "experimental-inspect")]
pub fn method_introspection_code(
spec: &FnSpec<'_>,
attrs: &[syn::Attribute],
parent: &syn::Type,
is_returning_not_implemented_on_extraction_error: bool,
ctx: &Ctx,
) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
let name = spec.python_name.to_string();
// __richcmp__ special case
if name == "__richcmp__" {
// We expend into each individual method
return ["__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__"]
.into_iter()
.map(|method_name| {
let mut spec = (*spec).clone();
spec.python_name = Ident::new(method_name, spec.python_name.span());
// We remove the CompareOp arg, this is safe because the signature is always the same
// First the other value to compare with then the CompareOp
// We cant to keep the first argument type, hence this hack
spec.signature.arguments.pop();
spec.signature.python_signature.positional_parameters.pop();
// the `CompareOp` parameter is gone; keep the positional-only count in range
spec.signature
.python_signature
.make_all_parameters_positional_only();
method_introspection_code(
&spec,
attrs,
parent,
is_returning_not_implemented_on_extraction_error,
ctx,
)
})
.collect();
}
// We map or ignore some magic methods
// TODO: this might create a naming conflict
let name = match name.as_str() {
"__concat__" => "__add__".into(),
"__repeat__" => "__mul__".into(),
"__inplace_concat__" => "__iadd__".into(),
"__inplace_repeat__" => "__imul__".into(),
"__getbuffer__" | "__releasebuffer__" | "__traverse__" | "__clear__" => return quote! {},
_ => name,
};
// We introduce self/cls argument and setup decorators
let mut first_argument = None;
let mut decorators = Vec::new();
match &spec.tp {
FnType::Getter(_) => {
first_argument = Some("self");
decorators.push(PyExpr::builtin("property"));
}
FnType::Setter(_) => {
first_argument = Some("self");
decorators.push(PyExpr::attribute(
PyExpr::attribute(PyExpr::from_type(parent.clone(), None), name.clone()),
"setter",
));
}
FnType::Deleter(_) => {
first_argument = Some("self");
decorators.push(PyExpr::attribute(
PyExpr::attribute(PyExpr::from_type(parent.clone(), None), name.clone()),
"deleter",
));
}
FnType::Fn(_) => {
first_argument = Some("self");
}
FnType::FnClass(_) => {
first_argument = Some("cls");
if spec.python_name != "__new__" {
// special case __new__ - does not get the decorator
decorators.push(PyExpr::builtin("classmethod"));
}
}
FnType::FnStatic => {
if spec.python_name != "__new__" {
decorators.push(PyExpr::builtin("staticmethod"));
} else {
// special case __new__ - does not get the decorator and gets first argument
first_argument = Some("cls");
}
}
FnType::FnModule(_) => (), // TODO: not sure this can happen
FnType::ClassAttribute => {
// We return an attribute because there is no decorator for this case
return attribute_introspection_code(
pyo3_path,
Some(parent),
name,
PyExpr::ellipsis(),
if let ReturnType::Type(_, t) = &spec.output {
(**t).clone()
} else {
parse_quote!(#pyo3_path::Py<#pyo3_path::types::PyNone>)
},
get_doc(attrs, None).as_ref(),
true,
);
}
}
let return_type = if spec.python_name == "__new__" {
// Hack to return Self while implementing IntoPyObject
// TODO: use typing.Self?
PyExpr::from_return_type(parse_quote!(#pyo3_path::PyClassGuard<Self>), Some(parent))
} else {
match spec.output.clone() {
ReturnType::Type(_, t) => PyExpr::from_return_type(*t, Some(parent)),
ReturnType::Default => PyExpr::none(),
}
};
function_introspection_code(
pyo3_path,
None,
&name,
&spec.signature,
first_argument,
return_type,
decorators,
spec.asyncness.is_some(),
is_returning_not_implemented_on_extraction_error,
get_doc(attrs, None).as_ref(),
Some(parent),
)
}
File diff suppressed because it is too large. Load diff
+36
View File
@@ -0,0 +1,36 @@
use crate::utils::Ctx;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
pub(crate) fn some_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream {
let Ctx { pyo3_path, .. } = ctx;
quote! {
#pyo3_path::impl_::wrap::SomeWrap::wrap(#obj)
}
}
pub(crate) fn ok_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream {
let Ctx {
pyo3_path,
output_span,
} = ctx;
let pyo3_path = pyo3_path.to_tokens_spanned(*output_span);
quote_spanned! { *output_span => {
let obj = #obj;
#[allow(clippy::useless_conversion, reason = "needed for Into<PyErr> conversion, may be redundant")]
#pyo3_path::impl_::wrap::converter(&obj).wrap(obj).map_err(::core::convert::Into::<#pyo3_path::PyErr>::into)
}}
}
pub(crate) fn map_result_into_ptr(result: TokenStream, ctx: &Ctx) -> TokenStream {
let Ctx {
pyo3_path,
output_span,
} = ctx;
let pyo3_path = pyo3_path.to_tokens_spanned(*output_span);
let py = syn::Ident::new("py", proc_macro2::Span::call_site());
quote_spanned! { *output_span => {
let result = #result;
#pyo3_path::impl_::wrap::converter(&result).map_into_ptr(#py, result)
}}
}
+394
View File
@@ -0,0 +1,394 @@
use crate::attributes::{CrateAttribute, RenamingRule};
use proc_macro2::{Span, TokenStream, TokenTree};
use quote::{quote, ToTokens, TokenStreamExt};
use std::ffi::CString;
use std::mem::take;
use syn::spanned::Spanned;
use syn::{Expr, ExprLit, Lit, LitCStr};
/// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span.
macro_rules! err_spanned {
($span:expr => $msg:expr) => {
syn::Error::new($span, $msg)
};
}
/// Macro inspired by `anyhow::bail!` to return a compiler error with the given span.
macro_rules! bail_spanned {
($span:expr => $msg:expr) => {
return Err(err_spanned!($span => $msg))
};
}
/// Macro inspired by `anyhow::ensure!` to return a compiler error with the given span if the
/// specified condition is not met.
macro_rules! ensure_spanned {
($condition:expr, $span:expr => $msg:expr) => {
if !($condition) {
bail_spanned!($span => $msg);
}
};
($($condition:expr, $span:expr => $msg:expr;)*) => {
if let Some(e) = [$(
(!($condition)).then(|| err_spanned!($span => $msg)),
)*]
.into_iter()
.flatten()
.reduce(|mut acc, e| {
acc.combine(e);
acc
}) {
return Err(e);
}
};
}
/// Check if the given type `ty` is `pyo3::Python`.
pub fn is_python(ty: &syn::Type) -> bool {
match unwrap_ty_group(ty) {
syn::Type::Path(typath) => typath
.path
.segments
.last()
.map(|seg| seg.ident == "Python")
.unwrap_or(false),
_ => false,
}
}
/// If `ty` is `Option<T>`, return `Some(T)`, else `None`.
pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> {
if let syn::Type::Path(syn::TypePath { path, .. }) = ty {
let seg = path.segments.last().filter(|s| s.ident == "Option")?;
if let syn::PathArguments::AngleBracketed(params) = &seg.arguments {
if let syn::GenericArgument::Type(ty) = params.args.first()? {
return Some(ty);
}
}
}
None
}
/// A syntax tree which evaluates to a string for Python.
///
/// Typically, the tokens will just be that string, but if the original docs included macro
/// expressions then the tokens will be a concatenation expression of the strings and
/// macro parts. contents such as parse the string contents.
#[derive(Clone)]
pub struct PythonDoc {
pub parts: Vec<StrOrExpr>,
}
/// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a string.
///
/// If this doc is for a callable, the provided `text_signature` can be passed to prepend
/// this to the documentation suitable for Python to extract this into the `__text_signature__`
/// attribute.
pub fn get_doc(attrs: &[syn::Attribute], mut text_signature: Option<String>) -> Option<PythonDoc> {
// insert special divider between `__text_signature__` and doc
// (assume text_signature is itself well-formed)
if let Some(text_signature) = &mut text_signature {
text_signature.push_str("\n--\n\n");
}
let mut parts = Vec::new();
let mut first = true;
let mut current_part = text_signature.unwrap_or_default();
let mut current_part_span: Option<Span> = None;
for attr in attrs {
if attr.path().is_ident("doc") {
if let Ok(nv) = attr.meta.require_name_value() {
if !first {
current_part.push('\n');
} else {
first = false;
}
if let Expr::Lit(ExprLit {
lit: Lit::Str(lit_str),
..
}) = &nv.value
{
// Strip single left space from literal strings, if needed.
// e.g. `/// Hello world` expands to #[doc = " Hello world"]
let doc_line = lit_str.value();
current_part.push_str(doc_line.strip_prefix(' ').unwrap_or(&doc_line));
current_part_span = current_part_span
.map_or_else(|| Some(lit_str.span()), |s| s.join(lit_str.span()));
} else {
// This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)]
// Reset the string buffer, write that part, and then push this macro part too.
if !current_part.is_empty() {
parts.push(StrOrExpr::Str {
value: take(&mut current_part),
span: take(&mut current_part_span),
});
}
parts.push(StrOrExpr::Expr(nv.value.clone()));
}
}
}
}
if !current_part.is_empty() {
parts.push(StrOrExpr::Str {
value: current_part,
span: current_part_span,
});
}
if parts.is_empty() {
None
} else {
Some(PythonDoc { parts })
}
}
impl PythonDoc {
pub fn to_cstr_stream(&self, ctx: &Ctx) -> syn::Result<TokenStream> {
let parts = &self.parts;
if let [StrOrExpr::Str { value, span }] = &parts[..] {
// Simple case, a single string. We serialize as such
return match CString::new(value.clone()) {
Ok(null_terminated_value) => Ok(LitCStr::new(
&null_terminated_value,
span.unwrap_or_else(Span::call_site),
)
.into_token_stream()),
Err(e) => Err(syn::Error::new(
span.unwrap_or_else(Span::call_site),
format!(
"Python doc may not contain nul byte, found nul at position {}",
e.nul_position()
),
)),
};
}
let Ctx { pyo3_path, .. } = ctx;
Ok(quote!(#pyo3_path::ffi::c_str!(concat!(#(#parts),*))))
}
}
/// A plain string or an expression
#[derive(Clone)]
pub enum StrOrExpr {
Str { value: String, span: Option<Span> },
Expr(Expr),
}
impl ToTokens for StrOrExpr {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
StrOrExpr::Str { value, .. } => value.to_tokens(tokens),
StrOrExpr::Expr(expr) => expr.to_tokens(tokens),
}
}
}
pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type {
while let syn::Type::Group(g) = ty {
ty = &*g.elem;
}
ty
}
pub struct Ctx {
/// Where we can find the pyo3 crate
pub pyo3_path: PyO3CratePath,
/// If we are in a pymethod or pyfunction,
/// this will be the span of the return type
pub output_span: Span,
}
impl Ctx {
pub(crate) fn new(attr: &Option<CrateAttribute>, signature: Option<&syn::Signature>) -> Self {
let pyo3_path = match attr {
Some(attr) => PyO3CratePath::Given(attr.value.0.clone()),
None => PyO3CratePath::Default,
};
let output_span = if let Some(syn::Signature {
output: syn::ReturnType::Type(_, output_type),
..
}) = &signature
{
output_type.span()
} else {
Span::call_site()
};
Self {
pyo3_path,
output_span,
}
}
}
#[derive(Clone)]
pub enum PyO3CratePath {
Given(syn::Path),
Default,
}
impl PyO3CratePath {
pub fn to_tokens_spanned(&self, span: Span) -> TokenStream {
match self {
Self::Given(path) => quote::quote_spanned! { span => #path },
Self::Default => quote::quote_spanned! { span => ::pyo3 },
}
}
}
impl quote::ToTokens for PyO3CratePath {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Given(path) => path.to_tokens(tokens),
Self::Default => quote::quote! { ::pyo3 }.to_tokens(tokens),
}
}
}
pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String {
use heck::*;
match rule {
RenamingRule::CamelCase => name.to_lower_camel_case(),
RenamingRule::KebabCase => name.to_kebab_case(),
RenamingRule::Lowercase => name.to_lowercase(),
RenamingRule::PascalCase => name.to_upper_camel_case(),
RenamingRule::ScreamingKebabCase => name.to_shouty_kebab_case(),
RenamingRule::ScreamingSnakeCase => name.to_shouty_snake_case(),
RenamingRule::SnakeCase => name.to_snake_case(),
RenamingRule::Uppercase => name.to_uppercase(),
}
}
pub(crate) enum IdentOrStr<'a> {
Str(&'a str),
Ident(syn::Ident),
}
pub(crate) fn has_attribute(attrs: &[syn::Attribute], ident: &str) -> bool {
has_attribute_with_namespace(attrs, None, &[ident])
}
pub(crate) fn has_attribute_with_namespace(
attrs: &[syn::Attribute],
crate_path: Option<&PyO3CratePath>,
idents: &[&str],
) -> bool {
let mut segments = vec![];
if let Some(c) = crate_path {
match c {
PyO3CratePath::Given(paths) => {
for p in &paths.segments {
segments.push(IdentOrStr::Ident(p.ident.clone()));
}
}
PyO3CratePath::Default => segments.push(IdentOrStr::Str("pyo3")),
}
};
for i in idents {
segments.push(IdentOrStr::Str(i));
}
attrs.iter().any(|attr| {
segments
.iter()
.eq(attr.path().segments.iter().map(|v| &v.ident))
})
}
pub fn expr_to_python(expr: &syn::Expr) -> String {
match expr {
// literal values
syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit {
syn::Lit::Str(s) => s.token().to_string(),
syn::Lit::Char(c) => c.token().to_string(),
syn::Lit::Int(i) => i.base10_digits().to_string(),
syn::Lit::Float(f) => f.base10_digits().to_string(),
syn::Lit::Bool(b) => {
if b.value() {
"True".to_string()
} else {
"False".to_string()
}
}
_ => "...".to_string(),
},
// None
syn::Expr::Path(syn::ExprPath { qself, path, .. })
if qself.is_none() && path.is_ident("None") =>
{
"None".to_string()
}
// others, unsupported yet so defaults to `...`
_ => "...".to_string(),
}
}
/// Helper struct for hard-coded identifiers used in the macro code.
#[derive(Clone, Copy)]
pub struct StaticIdent(&'static str);
impl StaticIdent {
pub const fn new(name: &'static str) -> Self {
Self(name)
}
}
impl ToTokens for StaticIdent {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.append(syn::Ident::new(self.0, Span::call_site()));
}
}
/// Adjusts a tokes stream so that the location for the stream comes from `Span`.
///
/// This affects where error messages will arise in the compiler output.
pub(crate) fn locate_tokens_at(tokens: TokenStream, span: Span) -> TokenStream {
fn set_span_recursively(tokens: TokenStream, span: Span) -> TokenStream {
tokens
.into_iter()
.map(|tt| match tt {
TokenTree::Group(g) => {
let inner = set_span_recursively(g.stream(), span);
let mut new_group = proc_macro2::Group::new(g.delimiter(), inner);
new_group.set_span(span);
TokenTree::Group(new_group)
}
TokenTree::Ident(mut ident) => {
ident.set_span(span);
TokenTree::Ident(ident)
}
TokenTree::Punct(mut punct) => {
punct.set_span(span);
TokenTree::Punct(punct)
}
TokenTree::Literal(mut lit) => {
lit.set_span(span);
TokenTree::Literal(lit)
}
})
.collect()
}
let output_span = tokens.span().located_at(span);
set_span_recursively(tokens, output_span)
}
/// Replaces all explicit lifetimes in `self` with elided (`'_`) lifetimes
///
/// This is useful if `Self` is used in `const` context, where explicit
/// lifetimes are not allowed (yet).
#[cfg(feature = "experimental-inspect")]
pub(crate) fn elide_lifetimes(ty: &mut syn::Type) {
struct ElideLifetimesVisitor;
impl syn::visit_mut::VisitMut for ElideLifetimesVisitor {
fn visit_lifetime_mut(&mut self, l: &mut syn::Lifetime) {
*l = syn::Lifetime::new("'_", l.span());
}
}
syn::visit_mut::VisitMut::visit_type_mut(&mut ElideLifetimesVisitor, ty);
}