Vendor deps.
This commit is contained in:
1 parent
294527085e
commit
91e18fde3a
1714 files changed
+556667
-7
No files matched your search
@@ -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"}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "a70d17f898df41b3d1632ee987f624f5222d8d39"
|
||||
},
|
||||
"path_in_vcs": "pyo3-macros-backend"
|
||||
}
|
||||
+54
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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).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).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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 = ¶m.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
@@ -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 = ¶m.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
@@ -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
@@ -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
@@ -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;
|
||||
+1305
File diff suppressed because it is too large.
Load diff
+743
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
+3249
File diff suppressed because it is too large.
Load diff
+483
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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),
|
||||
)
|
||||
}
|
||||
+1971
File diff suppressed because it is too large.
Load diff
+36
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user