Split derive-impl and derive

This commit is contained in:
Noa committed 2022-12-19 18:48:17 -06:00
1 parent 38a735af3b
commit 185dd4a795
15 files changed
+148 -97

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
[package]
name = "rustpython-derive-impl"
version = "0.0.0"
edition = "2021"
[dependencies]
rustpython-compiler-core = { path = "../compiler/core", version = "0.1.1" }
rustpython-doc = { git = "https://github.com/RustPython/__doc__", branch = "main" }
indexmap = "1.8.1"
itertools = "0.10.3"
maplit = "1.0.2"
once_cell = "1.10.0"
proc-macro2 = "1.0.37"
quote = "1.0.18"
syn = { version = "1.0.91", features = ["full", "extra-traits"] }
syn-ext = { version = "0.4.0", features = ["full"] }
textwrap = { version = "0.15.0", default-features = false }
+402
View File
@@ -0,0 +1,402 @@
//! Parsing and processing for this form:
//! ```ignore
//! py_compile!(
//! // either:
//! source = "python_source_code",
//! // or
//! file = "file/path/relative/to/$CARGO_MANIFEST_DIR",
//!
//! // the mode to compile the code in
//! mode = "exec", // or "eval" or "single"
//! // the path put into the CodeObject, defaults to "frozen"
//! module_name = "frozen",
//! )
//! ```
use crate::{extract_spans, Diagnostic};
use once_cell::sync::Lazy;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use rustpython_compiler_core::{CodeObject, FrozenModule, Mode};
use std::{
collections::HashMap,
env, fs,
path::{Path, PathBuf},
};
use syn::{
self,
parse::{Parse, ParseStream, Result as ParseResult},
parse2,
spanned::Spanned,
Lit, LitByteStr, LitStr, Macro, Meta, MetaNameValue, Token,
};
static CARGO_MANIFEST_DIR: Lazy<PathBuf> = Lazy::new(|| {
PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not present"))
});
enum CompilationSourceKind {
/// Source is a File (Path)
File(PathBuf),
/// Direct Raw sourcecode
SourceCode(String),
/// Source is a directory
Dir(PathBuf),
}
struct CompilationSource {
kind: CompilationSourceKind,
span: (Span, Span),
}
pub trait Compiler {
fn compile(
&self,
source: &str,
mode: Mode,
module_name: String,
) -> Result<CodeObject, Box<dyn std::error::Error>>;
}
impl CompilationSource {
fn compile_string<D: std::fmt::Display, F: FnOnce() -> D>(
&self,
source: &str,
mode: Mode,
module_name: String,
compiler: &dyn Compiler,
origin: F,
) -> Result<CodeObject, Diagnostic> {
compiler.compile(source, mode, module_name).map_err(|err| {
Diagnostic::spans_error(
self.span,
format!("Python compile error from {}: {}", origin(), err),
)
})
}
fn compile(
&self,
mode: Mode,
module_name: String,
compiler: &dyn Compiler,
) -> Result<HashMap<String, FrozenModule>, Diagnostic> {
match &self.kind {
CompilationSourceKind::Dir(rel_path) => self.compile_dir(
&CARGO_MANIFEST_DIR.join(rel_path),
String::new(),
mode,
compiler,
),
_ => Ok(hashmap! {
module_name.clone() => FrozenModule {
code: self.compile_single(mode, module_name, compiler)?,
package: false,
},
}),
}
}
fn compile_single(
&self,
mode: Mode,
module_name: String,
compiler: &dyn Compiler,
) -> Result<CodeObject, Diagnostic> {
match &self.kind {
CompilationSourceKind::File(rel_path) => {
let path = CARGO_MANIFEST_DIR.join(rel_path);
let source = fs::read_to_string(&path).map_err(|err| {
Diagnostic::spans_error(
self.span,
format!("Error reading file {:?}: {}", path, err),
)
})?;
self.compile_string(&source, mode, module_name, compiler, || rel_path.display())
}
CompilationSourceKind::SourceCode(code) => {
self.compile_string(&textwrap::dedent(code), mode, module_name, compiler, || {
"string literal"
})
}
CompilationSourceKind::Dir(_) => {
unreachable!("Can't use compile_single with directory source")
}
}
}
fn compile_dir(
&self,
path: &Path,
parent: String,
mode: Mode,
compiler: &dyn Compiler,
) -> Result<HashMap<String, FrozenModule>, Diagnostic> {
let mut code_map = HashMap::new();
let paths = fs::read_dir(path)
.or_else(|e| {
if cfg!(windows) {
if let Ok(real_path) = fs::read_to_string(path.canonicalize().unwrap()) {
return fs::read_dir(real_path.trim());
}
}
Err(e)
})
.map_err(|err| {
Diagnostic::spans_error(self.span, format!("Error listing dir {:?}: {}", path, err))
})?;
for path in paths {
let path = path.map_err(|err| {
Diagnostic::spans_error(self.span, format!("Failed to list file: {}", err))
})?;
let path = path.path();
let file_name = path.file_name().unwrap().to_str().ok_or_else(|| {
Diagnostic::spans_error(self.span, format!("Invalid UTF-8 in file name {:?}", path))
})?;
if path.is_dir() {
code_map.extend(self.compile_dir(
&path,
if parent.is_empty() {
file_name.to_string()
} else {
format!("{}.{}", parent, file_name)
},
mode,
compiler,
)?);
} else if file_name.ends_with(".py") {
let stem = path.file_stem().unwrap().to_str().unwrap();
let is_init = stem == "__init__";
let module_name = if is_init {
parent.clone()
} else if parent.is_empty() {
stem.to_owned()
} else {
format!("{}.{}", parent, stem)
};
let compile_path = |src_path: &Path| {
let source = fs::read_to_string(src_path).map_err(|err| {
Diagnostic::spans_error(
self.span,
format!("Error reading file {:?}: {}", path, err),
)
})?;
self.compile_string(&source, mode, module_name.clone(), compiler, || {
path.strip_prefix(&*CARGO_MANIFEST_DIR)
.ok()
.unwrap_or(&path)
.display()
})
};
let code = compile_path(&path).or_else(|e| {
if cfg!(windows) {
if let Ok(real_path) = fs::read_to_string(path.canonicalize().unwrap()) {
let joined = path.parent().unwrap().join(real_path.trim());
if joined.exists() {
return compile_path(&joined);
} else {
return Err(e);
}
}
}
Err(e)
});
let code = match code {
Ok(code) => code,
Err(_)
if stem.starts_with("badsyntax_")
| parent.ends_with(".encoded_modules") =>
{
// TODO: handle with macro arg rather than hard-coded path
continue;
}
Err(e) => return Err(e),
};
code_map.insert(
module_name,
FrozenModule {
code,
package: is_init,
},
);
}
}
Ok(code_map)
}
}
/// This is essentially just a comma-separated list of Meta nodes, aka the inside of a MetaList.
struct PyCompileInput {
span: Span,
metas: Vec<Meta>,
}
impl PyCompileInput {
fn parse(&self, allow_dir: bool) -> Result<PyCompileArgs, Diagnostic> {
let mut module_name = None;
let mut mode = None;
let mut source: Option<CompilationSource> = None;
let mut crate_name = None;
fn assert_source_empty(source: &Option<CompilationSource>) -> Result<(), Diagnostic> {
if let Some(source) = source {
Err(Diagnostic::spans_error(
source.span,
"Cannot have more than one source",
))
} else {
Ok(())
}
}
for meta in &self.metas {
if let Meta::NameValue(name_value) = meta {
let ident = match name_value.path.get_ident() {
Some(ident) => ident,
None => continue,
};
let check_str = || match &name_value.lit {
Lit::Str(s) => Ok(s),
_ => Err(err_span!(name_value.lit, "{ident} must be a string")),
};
if ident == "mode" {
let s = check_str()?;
match s.value().parse() {
Ok(mode_val) => mode = Some(mode_val),
Err(e) => bail_span!(s, "{}", e),
}
} else if ident == "module_name" {
module_name = Some(check_str()?.value())
} else if ident == "source" {
assert_source_empty(&source)?;
let code = check_str()?.value();
source = Some(CompilationSource {
kind: CompilationSourceKind::SourceCode(code),
span: extract_spans(&name_value).unwrap(),
});
} else if ident == "file" {
assert_source_empty(&source)?;
let path = check_str()?.value().into();
source = Some(CompilationSource {
kind: CompilationSourceKind::File(path),
span: extract_spans(&name_value).unwrap(),
});
} else if ident == "dir" {
if !allow_dir {
bail_span!(ident, "py_compile doesn't accept dir")
}
assert_source_empty(&source)?;
let path = check_str()?.value().into();
source = Some(CompilationSource {
kind: CompilationSourceKind::Dir(path),
span: extract_spans(&name_value).unwrap(),
});
} else if ident == "crate_name" {
let name = check_str()?.parse()?;
crate_name = Some(name);
}
}
}
let source = source.ok_or_else(|| {
syn::Error::new(
self.span,
"Must have either file or source in py_compile!()/py_freeze!()",
)
})?;
Ok(PyCompileArgs {
source,
mode: mode.unwrap_or(Mode::Exec),
module_name: module_name.unwrap_or_else(|| "frozen".to_owned()),
crate_name: crate_name.unwrap_or_else(|| syn::parse_quote!(::rustpython_vm::bytecode)),
})
}
}
fn parse_meta(input: ParseStream) -> ParseResult<Meta> {
let path = input.call(syn::Path::parse_mod_style)?;
let eq_token: Token![=] = input.parse()?;
let span = input.span();
if input.peek(LitStr) {
Ok(Meta::NameValue(MetaNameValue {
path,
eq_token,
lit: Lit::Str(input.parse()?),
}))
} else if let Ok(mac) = input.parse::<Macro>() {
Ok(Meta::NameValue(MetaNameValue {
path,
eq_token,
lit: Lit::Str(LitStr::new(&mac.tokens.to_string(), mac.span())),
}))
} else {
Err(syn::Error::new(span, "Expected string or stringify macro"))
}
}
impl Parse for PyCompileInput {
fn parse(input: ParseStream) -> ParseResult<Self> {
let span = input.cursor().span();
let metas = input
.parse_terminated::<Meta, Token![,]>(parse_meta)?
.into_iter()
.collect();
Ok(PyCompileInput { span, metas })
}
}
struct PyCompileArgs {
source: CompilationSource,
mode: Mode,
module_name: String,
crate_name: syn::Path,
}
pub fn impl_py_compile(
input: TokenStream,
compiler: &dyn Compiler,
) -> Result<TokenStream, Diagnostic> {
let input: PyCompileInput = parse2(input)?;
let args = input.parse(false)?;
let crate_name = args.crate_name;
let code = args
.source
.compile_single(args.mode, args.module_name, compiler)?;
let bytes = code.to_bytes();
let bytes = LitByteStr::new(&bytes, Span::call_site());
let output = quote! {
#crate_name::CodeObject::from_bytes(#bytes)
.expect("Deserializing CodeObject failed")
};
Ok(output)
}
pub fn impl_py_freeze(
input: TokenStream,
compiler: &dyn Compiler,
) -> Result<TokenStream, Diagnostic> {
let input: PyCompileInput = parse2(input)?;
let args = input.parse(true)?;
let crate_name = args.crate_name;
let code_map = args.source.compile(args.mode, args.module_name, compiler)?;
let data =
rustpython_compiler_core::frozen_lib::encode_lib(code_map.iter().map(|(k, v)| (&**k, v)));
let bytes = LitByteStr::new(&data, Span::call_site());
let output = quote! {
#crate_name::frozen_lib::decode_lib(#bytes)
};
Ok(output)
}
+156
View File
@@ -0,0 +1,156 @@
// Taken from https://github.com/rustwasm/wasm-bindgen/blob/master/crates/backend/src/error.rs
//
// Copyright (c) 2014 Alex Crichton
//
// 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.
#![allow(dead_code)]
use proc_macro2::*;
use quote::{ToTokens, TokenStreamExt};
use syn::parse::Error;
macro_rules! err_span {
($span:expr, $($msg:tt)*) => (
syn::Error::new_spanned(&$span, format_args!($($msg)*))
)
}
macro_rules! bail_span {
($($t:tt)*) => (
return Err(err_span!($($t)*).into())
)
}
// macro_rules! push_err_span {
// ($diagnostics:expr, $($t:tt)*) => {
// $diagnostics.push(err_span!($($t)*))
// };
// }
// macro_rules! push_diag_result {
// ($diags:expr, $x:expr $(,)?) => {
// if let Err(e) = $x {
// $diags.push(e);
// }
// };
// }
#[derive(Debug)]
pub struct Diagnostic {
inner: Repr,
}
#[derive(Debug)]
enum Repr {
Single {
text: String,
span: Option<(Span, Span)>,
},
SynError(Error),
Multi {
diagnostics: Vec<Diagnostic>,
},
}
impl Diagnostic {
pub fn error<T: Into<String>>(text: T) -> Diagnostic {
Diagnostic {
inner: Repr::Single {
text: text.into(),
span: None,
},
}
}
pub(crate) fn spans_error<T: Into<String>>(spans: (Span, Span), text: T) -> Diagnostic {
Diagnostic {
inner: Repr::Single {
text: text.into(),
span: Some(spans),
},
}
}
pub fn from_vec(diagnostics: Vec<Diagnostic>) -> Result<(), Diagnostic> {
if diagnostics.is_empty() {
Ok(())
} else {
Err(Diagnostic {
inner: Repr::Multi { diagnostics },
})
}
}
pub fn panic(&self) -> ! {
match &self.inner {
Repr::Single { text, .. } => panic!("{}", text),
Repr::SynError(error) => panic!("{}", error),
Repr::Multi { diagnostics } => diagnostics[0].panic(),
}
}
}
impl From<Error> for Diagnostic {
fn from(err: Error) -> Diagnostic {
Diagnostic {
inner: Repr::SynError(err),
}
}
}
pub fn extract_spans(node: &dyn ToTokens) -> Option<(Span, Span)> {
let mut t = TokenStream::new();
node.to_tokens(&mut t);
let mut tokens = t.into_iter();
let start = tokens.next().map(|t| t.span());
let end = tokens.last().map(|t| t.span());
start.map(|start| (start, end.unwrap_or(start)))
}
impl ToTokens for Diagnostic {
fn to_tokens(&self, dst: &mut TokenStream) {
match &self.inner {
Repr::Single { text, span } => {
let cs2 = (Span::call_site(), Span::call_site());
let (start, end) = span.unwrap_or(cs2);
dst.append(Ident::new("compile_error", start));
dst.append(Punct::new('!', Spacing::Alone));
let mut message = TokenStream::new();
message.append(Literal::string(text));
let mut group = Group::new(Delimiter::Brace, message);
group.set_span(end);
dst.append(group);
}
Repr::Multi { diagnostics } => {
for diagnostic in diagnostics {
diagnostic.to_tokens(dst);
}
}
Repr::SynError(err) => {
err.to_compile_error().to_tokens(dst);
}
}
}
}
+228
View File
@@ -0,0 +1,228 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
parse_quote, Attribute, Data, DeriveInput, Expr, Field, Ident, Lit, Meta, NestedMeta, Result,
};
/// The kind of the python parameter, this corresponds to the value of Parameter.kind
/// (https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind)
enum ParameterKind {
PositionalOnly,
PositionalOrKeyword,
KeywordOnly,
Flatten,
}
impl ParameterKind {
fn from_ident(ident: &Ident) -> Option<ParameterKind> {
match ident.to_string().as_str() {
"positional" => Some(ParameterKind::PositionalOnly),
"any" => Some(ParameterKind::PositionalOrKeyword),
"named" => Some(ParameterKind::KeywordOnly),
"flatten" => Some(ParameterKind::Flatten),
_ => None,
}
}
}
struct ArgAttribute {
name: Option<String>,
kind: ParameterKind,
default: Option<DefaultValue>,
}
// None == quote!(Default::default())
type DefaultValue = Option<Expr>;
impl ArgAttribute {
fn from_attribute(attr: &Attribute) -> Option<Result<ArgAttribute>> {
if !attr.path.is_ident("pyarg") {
return None;
}
let inner = move || {
let Meta::List(list) = attr.parse_meta()? else {
bail_span!(attr, "pyarg must be a list, like #[pyarg(...)]")
};
let mut iter = list.nested.iter();
let first_arg = iter.next().ok_or_else(|| {
err_span!(list, "There must be at least one argument to #[pyarg()]")
})?;
let kind = match first_arg {
NestedMeta::Meta(Meta::Path(path)) => {
path.get_ident().and_then(ParameterKind::from_ident)
}
_ => None,
};
let kind = kind.ok_or_else(|| {
err_span!(
first_arg,
"The first argument to #[pyarg()] must be the parameter type, either \
'positional', 'any', 'named', or 'flatten'."
)
})?;
let mut attribute = ArgAttribute {
name: None,
kind,
default: None,
};
for arg in iter {
attribute.parse_argument(arg)?;
}
Ok(attribute)
};
Some(inner())
}
fn parse_argument(&mut self, arg: &NestedMeta) -> Result<()> {
if let ParameterKind::Flatten = self.kind {
bail_span!(arg, "can't put additional arguments on a flatten arg")
}
match arg {
NestedMeta::Meta(Meta::Path(path)) => {
if path.is_ident("default") || path.is_ident("optional") {
if self.default.is_none() {
self.default = Some(None);
}
} else {
bail_span!(path, "Unrecognised pyarg attribute");
}
}
NestedMeta::Meta(Meta::NameValue(name_value)) => {
if name_value.path.is_ident("default") {
if matches!(self.default, Some(Some(_))) {
bail_span!(name_value, "Default already set");
}
match name_value.lit {
Lit::Str(ref val) => self.default = Some(Some(val.parse()?)),
_ => bail_span!(name_value, "Expected string value for default argument"),
}
} else if name_value.path.is_ident("name") {
if self.name.is_some() {
bail_span!(name_value, "already have a name")
}
match &name_value.lit {
Lit::Str(val) => self.name = Some(val.value()),
_ => bail_span!(name_value, "Expected string value for name argument"),
}
} else {
bail_span!(name_value, "Unrecognised pyarg attribute");
}
}
_ => bail_span!(arg, "Unrecognised pyarg attribute"),
}
Ok(())
}
}
fn generate_field((i, field): (usize, &Field)) -> Result<TokenStream> {
let mut pyarg_attrs = field
.attrs
.iter()
.filter_map(ArgAttribute::from_attribute)
.collect::<std::result::Result<Vec<_>, _>>()?;
let attr = if pyarg_attrs.is_empty() {
ArgAttribute {
name: None,
kind: ParameterKind::PositionalOrKeyword,
default: None,
}
} else if pyarg_attrs.len() == 1 {
pyarg_attrs.remove(0)
} else {
bail_span!(field, "Multiple pyarg attributes on field");
};
let name = field.ident.as_ref();
let namestring = name.map(Ident::to_string);
if matches!(&namestring, Some(s) if s.starts_with("_phantom")) {
return Ok(quote! {
#name: ::std::marker::PhantomData,
});
}
let fieldname = match name {
Some(id) => id.to_token_stream(),
None => syn::Index::from(i).into_token_stream(),
};
if let ParameterKind::Flatten = attr.kind {
return Ok(quote! {
#fieldname: ::rustpython_vm::function::FromArgs::from_args(vm, args)?,
});
}
let pyname = attr
.name
.or(namestring)
.ok_or_else(|| err_span!(field, "field in tuple struct must have name attribute"))?;
let middle = quote! {
.map(|x| ::rustpython_vm::convert::TryFromObject::try_from_object(vm, x)).transpose()?
};
let ending = if let Some(default) = attr.default {
let default = default.unwrap_or_else(|| parse_quote!(::std::default::Default::default()));
quote! {
.map(::rustpython_vm::function::FromArgOptional::from_inner)
.unwrap_or_else(|| #default)
}
} else {
let err = match attr.kind {
ParameterKind::PositionalOnly | ParameterKind::PositionalOrKeyword => quote! {
::rustpython_vm::function::ArgumentError::TooFewArgs
},
ParameterKind::KeywordOnly => quote! {
::rustpython_vm::function::ArgumentError::RequiredKeywordArgument(#pyname.to_owned())
},
ParameterKind::Flatten => unreachable!(),
};
quote! {
.ok_or_else(|| #err)?
}
};
let file_output = match attr.kind {
ParameterKind::PositionalOnly => {
quote! {
#fieldname: args.take_positional()#middle #ending,
}
}
ParameterKind::PositionalOrKeyword => {
quote! {
#fieldname: args.take_positional_keyword(#pyname)#middle #ending,
}
}
ParameterKind::KeywordOnly => {
quote! {
#fieldname: args.take_keyword(#pyname)#middle #ending,
}
}
ParameterKind::Flatten => unreachable!(),
};
Ok(file_output)
}
pub fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
let fields = match input.data {
Data::Struct(syn::DataStruct { fields, .. }) => fields
.iter()
.enumerate()
.map(generate_field)
.collect::<Result<TokenStream>>()?,
_ => bail_span!(input, "FromArgs input must be a struct"),
};
let name = input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let output = quote! {
impl #impl_generics ::rustpython_vm::function::FromArgs for #name #ty_generics #where_clause {
fn from_args(
vm: &::rustpython_vm::VirtualMachine,
args: &mut ::rustpython_vm::function::FuncArgs
) -> ::std::result::Result<Self, ::rustpython_vm::function::ArgumentError> {
Ok(#name { #fields })
}
}
};
Ok(output)
}
+79
View File
@@ -0,0 +1,79 @@
#![recursion_limit = "128"]
#![doc(html_logo_url = "https://raw.githubusercontent.com/RustPython/RustPython/main/logo.png")]
#![doc(html_root_url = "https://docs.rs/rustpython-derive/")]
extern crate proc_macro;
#[macro_use]
extern crate maplit;
#[macro_use]
mod error;
#[macro_use]
mod util;
mod compile_bytecode;
mod from_args;
mod pyclass;
mod pymodule;
mod pypayload;
mod pystructseq;
use error::{extract_spans, Diagnostic};
use proc_macro2::TokenStream;
use quote::ToTokens;
use rustpython_doc as doc;
use syn::{AttributeArgs, DeriveInput, Item};
pub use compile_bytecode::Compiler;
fn result_to_tokens(result: Result<TokenStream, impl Into<Diagnostic>>) -> TokenStream {
result
.map_err(|e| e.into())
.unwrap_or_else(ToTokens::into_token_stream)
}
pub fn derive_from_args(input: DeriveInput) -> TokenStream {
result_to_tokens(from_args::impl_from_args(input))
}
pub fn pyclass(attr: AttributeArgs, item: Item) -> TokenStream {
if matches!(item, syn::Item::Impl(_) | syn::Item::Trait(_)) {
result_to_tokens(pyclass::impl_pyimpl(attr, item))
} else {
result_to_tokens(pyclass::impl_pyclass(attr, item))
}
}
pub use pyclass::PyExceptionDef;
pub fn define_exception(exc_def: PyExceptionDef) -> TokenStream {
result_to_tokens(pyclass::impl_define_exception(exc_def))
}
pub fn pyexception(attr: AttributeArgs, item: Item) -> TokenStream {
result_to_tokens(pyclass::impl_pyexception(attr, item))
}
pub fn pymodule(attr: AttributeArgs, item: Item) -> TokenStream {
result_to_tokens(pymodule::impl_pymodule(attr, item))
}
pub fn pystruct_sequence(input: DeriveInput) -> TokenStream {
result_to_tokens(pystructseq::impl_pystruct_sequence(input))
}
pub fn pystruct_sequence_try_from_object(input: DeriveInput) -> TokenStream {
result_to_tokens(pystructseq::impl_pystruct_sequence_try_from_object(input))
}
pub fn py_compile(input: TokenStream, compiler: &dyn Compiler) -> TokenStream {
result_to_tokens(compile_bytecode::impl_py_compile(input, compiler))
}
pub fn py_freeze(input: TokenStream, compiler: &dyn Compiler) -> TokenStream {
result_to_tokens(compile_bytecode::impl_py_freeze(input, compiler))
}
pub fn pypayload(input: DeriveInput) -> TokenStream {
result_to_tokens(pypayload::impl_pypayload(input))
}
File diff suppressed because it is too large. Load diff
+631
View File
@@ -0,0 +1,631 @@
use crate::error::Diagnostic;
use crate::util::{
iter_use_idents, pyclass_ident_and_attrs, text_signature, AttrItemMeta, AttributeExt,
ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ItemMeta, ItemNursery, SimpleItemMeta,
ALL_ALLOWED_NAMES,
};
use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
use std::{collections::HashSet, str::FromStr};
use syn::{parse_quote, spanned::Spanned, Attribute, AttributeArgs, Ident, Item, Result};
use syn_ext::ext::*;
#[derive(Clone, Copy, Eq, PartialEq)]
enum AttrName {
Function,
Attr,
Class,
}
impl std::fmt::Display for AttrName {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s = match self {
Self::Function => "pyfunction",
Self::Attr => "pyattr",
Self::Class => "pyclass",
};
s.fmt(f)
}
}
impl FromStr for AttrName {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(match s {
"pyfunction" => Self::Function,
"pyattr" => Self::Attr,
"pyclass" => Self::Class,
s => {
return Err(s.to_owned());
}
})
}
}
#[derive(Default)]
struct ModuleContext {
name: String,
module_extend_items: ItemNursery,
errors: Vec<syn::Error>,
}
pub fn impl_pymodule(attr: AttributeArgs, module_item: Item) -> Result<TokenStream> {
let (doc, mut module_item) = match module_item {
Item::Mod(m) => (m.attrs.doc(), m),
other => bail_span!(other, "#[pymodule] can only be on a full module"),
};
let fake_ident = Ident::new("pymodule", module_item.span());
let module_meta =
SimpleItemMeta::from_nested(module_item.ident.clone(), fake_ident, attr.into_iter())?;
// generation resources
let mut context = ModuleContext {
name: module_meta.simple_name()?,
..Default::default()
};
let items = module_item.items_mut().ok_or_else(|| {
module_meta.new_meta_error("requires actual module, not a module declaration")
})?;
// collect to context
for item in items.iter_mut() {
if matches!(item, Item::Impl(_) | Item::Trait(_)) {
// #[pyclass] implementations
continue;
}
let r = item.try_split_attr_mut(|attrs, item| {
let (pyitems, cfgs) = attrs_to_module_items(attrs, module_item_new)?;
for pyitem in pyitems.iter().rev() {
let r = pyitem.gen_module_item(ModuleItemArgs {
item,
attrs,
context: &mut context,
cfgs: cfgs.as_slice(),
});
context.errors.ok_or_push(r);
}
Ok(())
});
context.errors.ok_or_push(r);
}
// append additional items
let module_name = context.name.as_str();
let module_extend_items = context.module_extend_items.validate()?;
let doc = doc.or_else(|| {
crate::doc::Database::shared()
.try_path(module_name)
.ok()
.flatten()
.map(str::to_owned)
});
let doc = if let Some(doc) = doc {
quote!(Some(#doc))
} else {
quote!(None)
};
items.extend(iter_chain![
parse_quote! {
pub(crate) const MODULE_NAME: &'static str = #module_name;
},
parse_quote! {
pub(crate) const DOC: Option<&'static str> = #doc;
},
parse_quote! {
pub(crate) fn extend_module(
vm: &::rustpython_vm::VirtualMachine,
module: &::rustpython_vm::PyObject,
) {
#module_extend_items
}
},
parse_quote! {
#[allow(dead_code)]
pub(crate) fn make_module(
vm: &::rustpython_vm::VirtualMachine
) -> ::rustpython_vm::PyObjectRef {
let module = vm.new_module(MODULE_NAME, vm.ctx.new_dict(), DOC);
extend_module(vm, &module);
module
}
},
]);
Ok(if let Some(error) = context.errors.into_error() {
let error = Diagnostic::from(error);
quote! {
#module_item
#error
}
} else {
module_item.into_token_stream()
})
}
fn module_item_new(
index: usize,
attr_name: AttrName,
pyattrs: Vec<usize>,
) -> Box<dyn ModuleItem<AttrName = AttrName>> {
match attr_name {
AttrName::Function => Box::new(FunctionItem {
inner: ContentItemInner { index, attr_name },
pyattrs,
}),
AttrName::Attr => Box::new(AttributeItem {
inner: ContentItemInner { index, attr_name },
pyattrs,
}),
AttrName::Class => Box::new(ClassItem {
inner: ContentItemInner { index, attr_name },
pyattrs,
}),
}
}
fn attrs_to_module_items<F, R>(attrs: &[Attribute], item_new: F) -> Result<(Vec<R>, Vec<Attribute>)>
where
F: Fn(usize, AttrName, Vec<usize>) -> R,
{
let mut cfgs: Vec<Attribute> = Vec::new();
let mut result = Vec::new();
let mut iter = attrs.iter().enumerate().peekable();
while let Some((_, attr)) = iter.peek() {
// take all cfgs but no py items
let attr = *attr;
if let Some(ident) = attr.get_ident() {
let attr_name = ident.to_string();
if attr_name == "cfg" {
cfgs.push(attr.clone());
} else if ALL_ALLOWED_NAMES.contains(&attr_name.as_str()) {
break;
}
}
iter.next();
}
let mut closed = false;
let mut pyattrs = Vec::new();
for (i, attr) in iter {
// take py items but no cfgs
let attr_name = if let Some(ident) = attr.get_ident() {
ident.to_string()
} else {
continue;
};
if attr_name == "cfg" {
bail_span!(attr, "#[py*] items must be placed under `cfgs`")
}
let attr_name = match AttrName::from_str(attr_name.as_str()) {
Ok(name) => name,
Err(wrong_name) => {
if !ALL_ALLOWED_NAMES.contains(&wrong_name.as_str()) {
continue;
} else if closed {
bail_span!(attr, "Only one #[pyattr] annotated #[py*] item can exist")
} else {
bail_span!(attr, "#[pymodule] doesn't accept #[{}]", wrong_name)
}
}
};
if attr_name == AttrName::Attr {
if !result.is_empty() {
bail_span!(
attr,
"#[pyattr] must be placed on top of other #[py*] items",
)
}
pyattrs.push(i);
continue;
}
if pyattrs.is_empty() {
result.push(item_new(i, attr_name, Vec::new()));
} else {
match attr_name {
AttrName::Class | AttrName::Function => {
result.push(item_new(i, attr_name, pyattrs.clone()));
}
_ => {
bail_span!(
attr,
"#[pyclass] or #[pyfunction] only can follow #[pyattr]",
)
}
}
pyattrs.clear();
closed = true;
}
}
if let Some(last) = pyattrs.pop() {
assert!(!closed);
result.push(item_new(last, AttrName::Attr, pyattrs));
}
Ok((result, cfgs))
}
/// #[pyfunction]
struct FunctionItem {
inner: ContentItemInner<AttrName>,
pyattrs: Vec<usize>,
}
/// #[pyclass]
struct ClassItem {
inner: ContentItemInner<AttrName>,
pyattrs: Vec<usize>,
}
/// #[pyattr]
struct AttributeItem {
inner: ContentItemInner<AttrName>,
pyattrs: Vec<usize>,
}
impl ContentItem for FunctionItem {
type AttrName = AttrName;
fn inner(&self) -> &ContentItemInner<AttrName> {
&self.inner
}
}
impl ContentItem for ClassItem {
type AttrName = AttrName;
fn inner(&self) -> &ContentItemInner<AttrName> {
&self.inner
}
}
impl ContentItem for AttributeItem {
type AttrName = AttrName;
fn inner(&self) -> &ContentItemInner<AttrName> {
&self.inner
}
}
struct ModuleItemArgs<'a> {
item: &'a mut Item,
attrs: &'a mut Vec<Attribute>,
context: &'a mut ModuleContext,
cfgs: &'a [Attribute],
}
impl<'a> ModuleItemArgs<'a> {
fn module_name(&'a self) -> &'a str {
self.context.name.as_str()
}
}
trait ModuleItem: ContentItem {
fn gen_module_item(&self, args: ModuleItemArgs<'_>) -> Result<()>;
}
impl ModuleItem for FunctionItem {
fn gen_module_item(&self, args: ModuleItemArgs<'_>) -> Result<()> {
let func = args
.item
.function_or_method()
.map_err(|_| self.new_syn_error(args.item.span(), "can only be on a function"))?;
let ident = &func.sig().ident;
let item_attr = args.attrs.remove(self.index());
let item_meta = SimpleItemMeta::from_attr(ident.clone(), &item_attr)?;
let py_name = item_meta.simple_name()?;
let sig_doc = text_signature(func.sig(), &py_name);
let (tokens, py_names) = {
let module = args.module_name();
let doc = args.attrs.doc().or_else(|| {
crate::doc::Database::shared()
.try_module_item(module, &py_name)
.ok() // TODO: doc must exist at least one of code or CPython
.flatten()
.map(str::to_owned)
});
let doc = if let Some(doc) = doc {
format!("{}\n--\n\n{}", sig_doc, doc)
} else {
sig_doc
};
let doc = quote!(.with_doc(#doc.to_owned(), &vm.ctx));
let new_func = quote_spanned!(ident.span()=>
vm.ctx.make_funcdef(#py_name, #ident)
#doc
.into_function()
.with_module(vm.new_pyobj(#module.to_owned()))
.into_ref(&vm.ctx)
);
if self.pyattrs.is_empty() {
(
quote_spanned! { ident.span() => {
let func = #new_func;
vm.__module_set_attr(module, #py_name, func).unwrap();
}},
vec![py_name],
)
} else {
let mut py_names = HashSet::new();
py_names.insert(py_name);
for attr_index in self.pyattrs.iter().rev() {
let mut loop_unit = || {
let attr_attr = args.attrs.remove(*attr_index);
let item_meta = SimpleItemMeta::from_attr(ident.clone(), &attr_attr)?;
let py_name = item_meta.simple_name()?;
let inserted = py_names.insert(py_name.clone());
if !inserted {
return Err(self.new_syn_error(
ident.span(),
&format!(
"`{py_name}` is duplicated name for multiple py* attribute"
),
));
}
Ok(())
};
let r = loop_unit();
args.context.errors.ok_or_push(r);
}
let py_names: Vec<_> = py_names.into_iter().collect();
(
quote_spanned! { ident.span().resolved_at(Span::call_site()) => {
let func = #new_func;
for name in [#(#py_names,)*] {
vm.__module_set_attr(module, name, func.clone()).unwrap();
}
}},
py_names,
)
}
};
args.context.module_extend_items.add_item(
ident.clone(),
py_names,
args.cfgs.to_vec(),
tokens,
10,
)?;
Ok(())
}
}
impl ModuleItem for ClassItem {
fn gen_module_item(&self, args: ModuleItemArgs<'_>) -> Result<()> {
let (ident, _) = pyclass_ident_and_attrs(args.item)?;
let (class_name, class_new) = {
let class_attr = &mut args.attrs[self.inner.index];
let noattr = class_attr.try_remove_name("noattr")?;
if self.pyattrs.is_empty() {
// check noattr before ClassItemMeta::from_attr
if noattr.is_none() {
bail_span!(
ident,
"#[{name}] requires #[pyattr] to be a module attribute. \
To keep it free type, try #[{name}(noattr)]",
name = self.attr_name()
)
}
}
let noattr = noattr.is_some();
let is_use = matches!(&args.item, syn::Item::Use(_));
let class_meta = ClassItemMeta::from_attr(ident.clone(), class_attr)?;
let module_name = args.context.name.clone();
let module_name = if let Some(class_module_name) = class_meta.module().ok().flatten() {
class_module_name
} else {
class_attr.fill_nested_meta("module", || {
parse_quote! {module = #module_name}
})?;
module_name
};
let class_name = if noattr && is_use {
"<NO ATTR>".to_owned()
} else {
class_meta.class_name()?
};
let class_new = quote_spanned!(ident.span() =>
let new_class = <#ident as ::rustpython_vm::class::PyClassImpl>::make_class(&vm.ctx);
new_class.set_attr(rustpython_vm::identifier!(vm, __module__), vm.new_pyobj(#module_name));
);
(class_name, class_new)
};
let mut py_names = Vec::new();
for attr_index in self.pyattrs.iter().rev() {
let mut loop_unit = || {
let attr_attr = args.attrs.remove(*attr_index);
let item_meta = SimpleItemMeta::from_attr(ident.clone(), &attr_attr)?;
let py_name = item_meta
.optional_name()
.unwrap_or_else(|| class_name.clone());
py_names.push(py_name);
Ok(())
};
let r = loop_unit();
args.context.errors.ok_or_push(r);
}
let set_attr = match py_names.len() {
0 => quote! {
let _ = new_class; // suppress warning
},
1 => {
let py_name = &py_names[0];
quote! {
vm.__module_set_attr(&module, #py_name, new_class).unwrap();
}
}
_ => quote! {
for name in [#(#py_names,)*] {
vm.__module_set_attr(&module, name, new_class.clone()).unwrap();
}
},
};
args.context.module_extend_items.add_item(
ident.clone(),
py_names,
args.cfgs.to_vec(),
quote_spanned! { ident.span() =>
#class_new
#set_attr
},
0,
)?;
Ok(())
}
}
impl ModuleItem for AttributeItem {
fn gen_module_item(&self, args: ModuleItemArgs<'_>) -> Result<()> {
let cfgs = args.cfgs.to_vec();
let attr = args.attrs.remove(self.index());
let (ident, py_name, let_obj) = match args.item {
Item::Fn(syn::ItemFn { sig, block, .. }) => {
let ident = &sig.ident;
// If `once` keyword is in #[pyattr],
// wrapping it with static_cell for preventing it from using it as function
let attr_meta = AttrItemMeta::from_attr(ident.clone(), &attr)?;
if attr_meta.inner()._bool("once")? {
let stmts = &block.stmts;
let return_type = match &sig.output {
syn::ReturnType::Default => {
unreachable!("#[pyattr] attached function must have return type.")
}
syn::ReturnType::Type(_, ty) => ty,
};
let stmt: syn::Stmt = parse_quote! {
{
rustpython_common::static_cell! {
static ERROR: #return_type;
}
ERROR
.get_or_init(|| {
#(#stmts)*
})
.clone()
}
};
block.stmts = vec![stmt];
}
let py_name = attr_meta.simple_name()?;
(
ident.clone(),
py_name,
quote_spanned! { ident.span() =>
let obj = vm.new_pyobj(#ident(vm));
},
)
}
Item::Const(syn::ItemConst { ident, .. }) => {
let item_meta = SimpleItemMeta::from_attr(ident.clone(), &attr)?;
let py_name = item_meta.simple_name()?;
(
ident.clone(),
py_name,
quote_spanned! { ident.span() =>
let obj = vm.new_pyobj(#ident);
},
)
}
Item::Use(item) => {
if !self.pyattrs.is_empty() {
return Err(self
.new_syn_error(item.span(), "Only single #[pyattr] is allowed for `use`"));
}
let _ = iter_use_idents(item, |ident, is_unique| {
let item_meta = SimpleItemMeta::from_attr(ident.clone(), &attr)?;
let py_name = if is_unique {
item_meta.simple_name()?
} else if item_meta.optional_name().is_some() {
// this check actually doesn't need to be placed in loop
return Err(self.new_syn_error(
ident.span(),
"`name` attribute is not allowed for multiple use items",
));
} else {
ident.to_string()
};
let tokens = quote_spanned! { ident.span() =>
vm.__module_set_attr(module, #py_name, vm.new_pyobj(#ident)).unwrap();
};
args.context.module_extend_items.add_item(
ident.clone(),
vec![py_name],
cfgs.clone(),
tokens,
1,
)?;
Ok(())
})?;
return Ok(());
}
other => {
return Err(
self.new_syn_error(other.span(), "can only be on a function, const and use")
)
}
};
let (tokens, py_names) = if self.pyattrs.is_empty() {
(
quote_spanned! { ident.span() => {
#let_obj
vm.__module_set_attr(module, #py_name, obj).unwrap();
}},
vec![py_name],
)
} else {
let mut names = vec![py_name];
for attr_index in self.pyattrs.iter().rev() {
let mut loop_unit = || {
let attr_attr = args.attrs.remove(*attr_index);
let item_meta = AttrItemMeta::from_attr(ident.clone(), &attr_attr)?;
if item_meta.inner()._bool("once")? {
return Err(self.new_syn_error(
ident.span(),
"#[pyattr(once)] is only allowed for the bottom-most item",
));
}
let py_name = item_meta.optional_name().ok_or_else(|| {
self.new_syn_error(
ident.span(),
"#[pyattr(name = ...)] is mandatory except for the bottom-most item",
)
})?;
names.push(py_name);
Ok(())
};
let r = loop_unit();
args.context.errors.ok_or_push(r);
}
(
quote_spanned! { ident.span() => {
#let_obj
for name in [(#(#names,)*)] {
vm.__module_set_attr(module, name, obj.clone()).unwrap();
}
}},
names,
)
};
args.context
.module_extend_items
.add_item(ident, py_names, cfgs, tokens, 1)?;
Ok(())
}
}
+16
View File
@@ -0,0 +1,16 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Result};
pub(crate) fn impl_pypayload(input: DeriveInput) -> Result<TokenStream> {
let ty = &input.ident;
let ret = quote! {
impl ::rustpython_vm::PyPayload for #ty {
fn class(_vm: &::rustpython_vm::VirtualMachine) -> &'static rustpython_vm::Py<::rustpython_vm::builtins::PyType> {
<Self as ::rustpython_vm::class::StaticType>::static_type()
}
}
};
Ok(ret)
}
+70
View File
@@ -0,0 +1,70 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Ident, Result};
fn field_names(input: &DeriveInput) -> Result<Vec<&Ident>> {
let fields = if let syn::Data::Struct(ref struc) = input.data {
&struc.fields
} else {
bail_span!(
input,
"#[pystruct_sequence] can only be on a struct declaration"
)
};
let field_names: Vec<_> = match fields {
syn::Fields::Named(fields) => fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap())
.collect(),
_ => bail_span!(
input,
"#[pystruct_sequence] can only be on a struct with named fields"
),
};
Ok(field_names)
}
pub(crate) fn impl_pystruct_sequence(input: DeriveInput) -> Result<TokenStream> {
let field_names = field_names(&input)?;
let ty = &input.ident;
let ret = quote! {
impl ::rustpython_vm::types::PyStructSequence for #ty {
const FIELD_NAMES: &'static [&'static str] = &[#(stringify!(#field_names)),*];
fn into_tuple(self, vm: &::rustpython_vm::VirtualMachine) -> ::rustpython_vm::builtins::PyTuple {
let items = vec![#(::rustpython_vm::convert::ToPyObject::to_pyobject(
self.#field_names,
vm,
)),*];
::rustpython_vm::builtins::PyTuple::new_unchecked(items.into_boxed_slice())
}
}
impl ::rustpython_vm::convert::ToPyObject for #ty {
fn to_pyobject(self, vm: &::rustpython_vm::VirtualMachine) -> ::rustpython_vm::PyObjectRef {
::rustpython_vm::types::PyStructSequence::into_struct_sequence(self, vm).into()
}
}
};
Ok(ret)
}
pub(crate) fn impl_pystruct_sequence_try_from_object(input: DeriveInput) -> Result<TokenStream> {
let field_names = field_names(&input)?;
let ty = &input.ident;
let ret = quote! {
impl ::rustpython_vm::TryFromObject for #ty {
fn try_from_object(vm: &::rustpython_vm::VirtualMachine, seq: ::rustpython_vm::PyObjectRef) -> ::rustpython_vm::PyResult<Self> {
const LEN: usize = #ty::FIELD_NAMES.len();
let seq = Self::try_elements_from::<LEN>(seq, vm)?;
// TODO: this is possible to be written without iterator
let mut iter = seq.into_iter();
Ok(Self {#(
#field_names: iter.next().unwrap().clone().try_into_value(vm)?
),*})
}
}
};
Ok(ret)
}
+586
View File
@@ -0,0 +1,586 @@
use itertools::Itertools;
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use std::collections::{HashMap, HashSet};
use syn::{
spanned::Spanned, Attribute, Ident, Meta, MetaList, NestedMeta, Result, Signature, UseTree,
};
use syn_ext::{
ext::{AttributeExt as SynAttributeExt, *},
types::PunctuatedNestedMeta,
};
pub(crate) const ALL_ALLOWED_NAMES: &[&str] = &[
"pymethod",
"pyclassmethod",
"pystaticmethod",
"pygetset",
"pyfunction",
"pyclass",
"pyexception",
"pystruct_sequence",
"pyattr",
"pyslot",
"extend_class",
"pymember",
];
#[derive(Clone)]
struct NurseryItem {
attr_name: Ident,
py_names: Vec<String>,
cfgs: Vec<Attribute>,
tokens: TokenStream,
sort_order: usize,
}
#[derive(Default)]
pub(crate) struct ItemNursery(Vec<NurseryItem>);
pub(crate) struct ValidatedItemNursery(ItemNursery);
impl ItemNursery {
pub fn add_item(
&mut self,
attr_name: Ident,
py_names: Vec<String>,
cfgs: Vec<Attribute>,
tokens: TokenStream,
sort_order: usize,
) -> Result<()> {
self.0.push(NurseryItem {
attr_name,
py_names,
cfgs,
tokens,
sort_order,
});
Ok(())
}
pub fn validate(self) -> Result<ValidatedItemNursery> {
let mut by_name: HashSet<(String, Vec<Attribute>)> = HashSet::new();
for item in &self.0 {
for py_name in &item.py_names {
let inserted = by_name.insert((py_name.clone(), item.cfgs.clone()));
if !inserted {
return Err(syn::Error::new(
item.attr_name.span(),
format!("Duplicated #[py*] attribute found for {:?}", &item.py_names),
));
}
}
}
Ok(ValidatedItemNursery(self))
}
}
impl ToTokens for ValidatedItemNursery {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut sorted = self.0 .0.clone();
sorted.sort_by(|a, b| a.sort_order.cmp(&b.sort_order));
tokens.extend(sorted.iter().map(|item| {
let cfgs = &item.cfgs;
let tokens = &item.tokens;
quote! {
#( #cfgs )*
{
#tokens
}
}
}))
}
}
#[derive(Clone)]
pub(crate) struct ContentItemInner<T> {
pub index: usize,
pub attr_name: T,
}
pub(crate) trait ContentItem {
type AttrName: std::str::FromStr + std::fmt::Display;
fn inner(&self) -> &ContentItemInner<Self::AttrName>;
fn index(&self) -> usize {
self.inner().index
}
fn attr_name(&self) -> &Self::AttrName {
&self.inner().attr_name
}
fn new_syn_error(&self, span: Span, message: &str) -> syn::Error {
syn::Error::new(span, format!("#[{}] {}", self.attr_name(), message))
}
}
pub(crate) struct ItemMetaInner {
pub item_ident: Ident,
pub meta_ident: Ident,
pub meta_map: HashMap<String, (usize, Meta)>,
}
impl ItemMetaInner {
pub fn from_nested<I>(
item_ident: Ident,
meta_ident: Ident,
nested: I,
allowed_names: &[&'static str],
) -> Result<Self>
where
I: std::iter::Iterator<Item = NestedMeta>,
{
let (meta_map, lits) = nested.into_unique_map_and_lits(|path| {
if let Some(ident) = path.get_ident() {
let name = ident.to_string();
if allowed_names.contains(&name.as_str()) {
Ok(Some(name))
} else {
Err(err_span!(
ident,
"#[{meta_ident}({name})] is not one of allowed attributes [{}]",
allowed_names.iter().format(", ")
))
}
} else {
Ok(None)
}
})?;
if !lits.is_empty() {
bail_span!(meta_ident, "#[{meta_ident}(..)] cannot contain literal")
}
Ok(Self {
item_ident,
meta_ident,
meta_map,
})
}
pub fn item_name(&self) -> String {
self.item_ident.to_string()
}
pub fn meta_name(&self) -> String {
self.meta_ident.to_string()
}
pub fn _optional_str(&self, key: &str) -> Result<Option<String>> {
let value = if let Some((_, meta)) = self.meta_map.get(key) {
let Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit), ..
}) = meta else {
bail_span!(meta, "#[{}({} = ...)] must exist as a string", self.meta_name(), key)
};
Some(lit.value())
} else {
None
};
Ok(value)
}
pub fn _bool(&self, key: &str) -> Result<bool> {
let value = if let Some((_, meta)) = self.meta_map.get(key) {
match meta {
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Bool(lit),
..
}) => lit.value,
Meta::Path(_) => true,
_ => bail_span!(meta, "#[{}({})] is expected", self.meta_name(), key),
}
} else {
false
};
Ok(value)
}
}
pub(crate) trait ItemMeta: Sized {
const ALLOWED_NAMES: &'static [&'static str];
fn from_attr(item_ident: Ident, attr: &Attribute) -> Result<Self> {
let (meta_ident, nested) = attr.ident_and_promoted_nested()?;
Self::from_nested(item_ident, meta_ident.clone(), nested.into_iter())
}
fn from_nested<I>(item_ident: Ident, meta_ident: Ident, nested: I) -> Result<Self>
where
I: std::iter::Iterator<Item = NestedMeta>,
{
Ok(Self::from_inner(ItemMetaInner::from_nested(
item_ident,
meta_ident,
nested,
Self::ALLOWED_NAMES,
)?))
}
fn from_inner(inner: ItemMetaInner) -> Self;
fn inner(&self) -> &ItemMetaInner;
fn simple_name(&self) -> Result<String> {
let inner = self.inner();
Ok(inner
._optional_str("name")?
.unwrap_or_else(|| inner.item_name()))
}
fn optional_name(&self) -> Option<String> {
self.inner()._optional_str("name").ok().flatten()
}
fn new_meta_error(&self, msg: &str) -> syn::Error {
let inner = self.inner();
err_span!(inner.meta_ident, "#[{}] {}", inner.meta_name(), msg)
}
}
pub(crate) struct SimpleItemMeta(pub ItemMetaInner);
impl ItemMeta for SimpleItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["name"];
fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
}
fn inner(&self) -> &ItemMetaInner {
&self.0
}
}
pub(crate) struct AttrItemMeta(pub ItemMetaInner);
impl ItemMeta for AttrItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["name", "once"];
fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
}
fn inner(&self) -> &ItemMetaInner {
&self.0
}
}
pub(crate) struct ClassItemMeta(ItemMetaInner);
impl ItemMeta for ClassItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["module", "name", "base", "metaclass"];
fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
}
fn inner(&self) -> &ItemMetaInner {
&self.0
}
}
impl ClassItemMeta {
pub fn class_name(&self) -> Result<String> {
const KEY: &str = "name";
let inner = self.inner();
if let Some((_, meta)) = inner.meta_map.get(KEY) {
match meta {
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit),
..
}) => return Ok(lit.value()),
Meta::Path(_) => return Ok(inner.item_name()),
_ => {}
}
}
bail_span!(
inner.meta_ident,
"#[{attr_name}(name = ...)] must exist as a string. Try \
#[{attr_name}(name)] to use rust type name.",
attr_name = inner.meta_name()
)
}
pub fn base(&self) -> Result<Option<String>> {
self.inner()._optional_str("base")
}
pub fn metaclass(&self) -> Result<Option<String>> {
self.inner()._optional_str("metaclass")
}
pub fn module(&self) -> Result<Option<String>> {
const KEY: &str = "module";
let inner = self.inner();
let value = if let Some((_, meta)) = inner.meta_map.get(KEY) {
match meta {
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit),
..
}) => Ok(Some(lit.value())),
Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Bool(lit),
..
}) => if lit.value {
Err(lit.span())
} else {
Ok(None)
}
other => Err(other.span()),
}
} else {
Err(inner.item_ident.span())
}.map_err(|span| syn::Error::new(
span,
format!(
"#[{attr_name}(module = ...)] must exist as a string or false. Try #[{attr_name}(module = false)] for built-in types.",
attr_name=inner.meta_name()
),
))?;
Ok(value)
}
// pub fn mandatory_module(&self) -> Result<String> {
// let inner = self.inner();
// let value = self.module().ok().flatten().
// ok_or_else(|| err_span!(
// inner.meta_ident,
// "#[{attr_name}(module = ...)] must exist as a string. Built-in module is not allowed here.",
// attr_name = inner.meta_name()
// ))?;
// Ok(value)
// }
}
pub(crate) trait AttributeExt: SynAttributeExt {
fn promoted_nested(&self) -> Result<PunctuatedNestedMeta>;
fn ident_and_promoted_nested(&self) -> Result<(&Ident, PunctuatedNestedMeta)>;
fn try_remove_name(&mut self, name: &str) -> Result<Option<syn::NestedMeta>>;
fn fill_nested_meta<F>(&mut self, name: &str, new_item: F) -> Result<()>
where
F: Fn() -> NestedMeta;
}
impl AttributeExt for Attribute {
fn promoted_nested(&self) -> Result<PunctuatedNestedMeta> {
let list = self.promoted_list().map_err(|mut e| {
let name = self.get_ident().unwrap().to_string();
e.combine(err_span!(
self,
"#[{name} = \"...\"] cannot be a name/value, you probably meant \
#[{name}(name = \"...\")]",
));
e
})?;
Ok(list.nested)
}
fn ident_and_promoted_nested(&self) -> Result<(&Ident, PunctuatedNestedMeta)> {
Ok((self.get_ident().unwrap(), self.promoted_nested()?))
}
fn try_remove_name(&mut self, item_name: &str) -> Result<Option<syn::NestedMeta>> {
self.try_meta_mut(|meta| {
let nested = match meta {
Meta::List(MetaList { ref mut nested, .. }) => Ok(nested),
other => Err(syn::Error::new(
other.span(),
format!(
"#[{name}(...)] doesn't contain '{item}' to remove",
name = other.get_ident().unwrap(),
item = item_name
),
)),
}?;
let mut found = None;
for (i, item) in nested.iter().enumerate() {
let ident = if let Some(ident) = item.get_ident() {
ident
} else {
continue;
};
if *ident != item_name {
continue;
}
if found.is_some() {
return Err(syn::Error::new(
item.span(),
format!(
"#[py..({}...)] must be unique but found multiple times",
item_name,
),
));
}
found = Some(i);
}
Ok(found.map(|idx| nested.remove(idx).into_value()))
})
}
fn fill_nested_meta<F>(&mut self, name: &str, new_item: F) -> Result<()>
where
F: Fn() -> NestedMeta,
{
self.try_meta_mut(|meta| {
let list = meta.promote_to_list(Default::default())?;
let has_name = list
.nested
.iter()
.any(|nmeta| nmeta.get_path().map_or(false, |p| p.is_ident(name)));
if !has_name {
list.nested.push(new_item())
}
Ok(())
})
}
}
pub(crate) fn pyclass_ident_and_attrs(item: &syn::Item) -> Result<(&Ident, &[Attribute])> {
use syn::Item::*;
Ok(match item {
Struct(syn::ItemStruct { ident, attrs, .. }) => (ident, attrs),
Enum(syn::ItemEnum { ident, attrs, .. }) => (ident, attrs),
Use(item_use) => (
iter_use_idents(item_use, |ident, _is_unique| Ok(ident))?
.into_iter()
.exactly_one()
.map_err(|_| {
err_span!(
item_use,
"#[pyclass] can only be on single name use statement",
)
})?,
&item_use.attrs,
),
other => {
bail_span!(
other,
"#[pyclass] can only be on a struct, enum or use declaration",
)
}
})
}
pub(crate) trait ErrorVec: Sized {
fn into_error(self) -> Option<syn::Error>;
fn into_result(self) -> Result<()> {
if let Some(error) = self.into_error() {
Err(error)
} else {
Ok(())
}
}
fn ok_or_push<T>(&mut self, r: Result<T>) -> Option<T>;
}
impl ErrorVec for Vec<syn::Error> {
fn into_error(self) -> Option<syn::Error> {
let mut iter = self.into_iter();
if let Some(mut first) = iter.next() {
for err in iter {
first.combine(err);
}
Some(first)
} else {
None
}
}
fn ok_or_push<T>(&mut self, r: Result<T>) -> Option<T> {
match r {
Ok(v) => Some(v),
Err(e) => {
self.push(e);
None
}
}
}
}
macro_rules! iter_chain {
($($it:expr),*$(,)?) => {
::std::iter::empty()
$(.chain(::std::iter::once($it)))*
};
}
pub(crate) fn iter_use_idents<'a, F, R: 'a>(item_use: &'a syn::ItemUse, mut f: F) -> Result<Vec<R>>
where
F: FnMut(&'a syn::Ident, bool) -> Result<R>,
{
let mut result = Vec::new();
match &item_use.tree {
UseTree::Name(name) => result.push(f(&name.ident, true)?),
UseTree::Rename(rename) => result.push(f(&rename.rename, true)?),
UseTree::Path(path) => match &*path.tree {
UseTree::Name(name) => result.push(f(&name.ident, true)?),
UseTree::Rename(rename) => result.push(f(&rename.rename, true)?),
other => iter_use_tree_idents(other, &mut result, &mut f)?,
},
other => iter_use_tree_idents(other, &mut result, &mut f)?,
}
Ok(result)
}
fn iter_use_tree_idents<'a, F, R: 'a>(
tree: &'a syn::UseTree,
result: &mut Vec<R>,
f: &mut F,
) -> Result<()>
where
F: FnMut(&'a syn::Ident, bool) -> Result<R>,
{
match tree {
UseTree::Name(name) => result.push(f(&name.ident, false)?),
UseTree::Rename(rename) => result.push(f(&rename.rename, false)?),
UseTree::Path(path) => iter_use_tree_idents(&path.tree, result, f)?,
UseTree::Group(syn::UseGroup { items, .. }) => {
for subtree in items {
iter_use_tree_idents(subtree, result, f)?;
}
}
UseTree::Glob(glob) => {
bail_span!(glob, "#[py*] doesn't allow '*'")
}
}
Ok(())
}
// Best effort attempt to generate a template from which a
// __text_signature__ can be created.
pub(crate) fn text_signature(sig: &Signature, name: &str) -> String {
let signature = func_sig(sig);
if signature.starts_with("$self") {
format!("{}({})", name, signature)
} else {
format!("{}({}, {})", name, "$module", signature)
}
}
fn func_sig(sig: &Signature) -> String {
sig.inputs
.iter()
.filter_map(|arg| {
use syn::FnArg::*;
let arg = match arg {
Typed(typed) => typed,
Receiver(_) => return Some("$self".to_owned()),
};
let ty = arg.ty.as_ref();
let ty = quote!(#ty).to_string();
if ty == "FuncArgs" {
return Some("*args, **kwargs".to_owned());
}
if ty.starts_with('&') && ty.ends_with("VirtualMachine") {
return None;
}
let ident = match arg.pat.as_ref() {
syn::Pat::Ident(p) => p.ident.to_string(),
// FIXME: other => unreachable!("function arg pattern must be ident but found `{}`", quote!(fn #ident(.. #other ..))),
other => quote!(#other).to_string(),
};
if ident == "zelf" {
return Some("$self".to_owned());
}
if ident == "vm" {
unreachable!("type &VirtualMachine(`{}`) must be filtered already", ty);
}
Some(ident)
})
.collect::<Vec<_>>()
.join(", ")
}