Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/derive-impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod pymodule;
mod pypayload;
mod pystructseq;
mod pytraverse;
mod text_signature;

use error::Diagnostic;
use metadata::PunctuatedNestedMeta;
Expand Down
3 changes: 2 additions & 1 deletion crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,7 @@ where
),
args.context.self_ty_subst.as_ref(),
None,
item_meta.text_signature()?.as_ref(),
);
let doc_body_pending =
doc_body_pending_expr(args.context.self_ty_subst.as_ref(), &py_name, rust_doc);
Expand Down Expand Up @@ -1823,7 +1824,7 @@ impl ToTokens for GetSetNursery {
struct MethodItemMeta(ItemMetaInner);

impl ItemMeta for MethodItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw", "coexist"];
const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw", "coexist", "text_signature"];

fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
Expand Down
25 changes: 23 additions & 2 deletions crates/derive-impl/src/pymodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ impl ModuleItem for FunctionItem {

let py_name = item_meta.simple_name()?;
let mut flags_signatures = vec![item_meta.signature_from_flags()?];
let mut text_signatures = vec![item_meta.text_signature()?];
let mut py_names = vec![py_name];
for attr_index in self.py_attrs.iter().rev() {
let mut loop_unit = || {
Expand All @@ -692,19 +693,31 @@ impl ModuleItem for FunctionItem {
));
}
flags_signatures.push(item_meta.signature_from_flags()?);
text_signatures.push(item_meta.text_signature()?);
py_names.push(py_name);
Ok(())
};
let r = loop_unit();
args.context.errors.ok_or_push(r);
}

if flags_signatures
.iter()
.zip(&text_signatures)
.any(|(flags, text)| *flags && text.is_some())
{
return Err(self.new_syn_error(
ident.span(),
"text_signature and signature_from_flags are mutually exclusive",
));
}
let module = args.doc_module();
let rust_doc = args.attrs.doc().filter(|doc| !doc.is_empty());
let docs = py_names
.iter()
.zip(flags_signatures)
.map(|(py_name, from_flags)| {
.zip(text_signatures)
.map(|((py_name, from_flags), text_signature)| {
let db_doc = if rust_doc.is_some() {
None
} else {
Expand All @@ -716,7 +729,15 @@ impl ModuleItem for FunctionItem {
if from_flags {
flags_signature_doc_tokens(func.sig(), py_name, doc)
} else {
internal_doc_tokens(func.sig(), py_name, None, doc, None, Some("$module"))
internal_doc_tokens(
func.sig(),
py_name,
None,
doc,
None,
Some("$module"),
text_signature.as_ref(),
)
}
})
.collect();
Expand Down
249 changes: 249 additions & 0 deletions crates/derive-impl/src/text_signature.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
//! Explicit legacy text signatures. This deliberately accepts a small grammar:
//! positional parameters, a `/`, literal defaults, `**kwargs`, and trailing
//! nested optional positional groups. It is not a Python expression parser.
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{Error, Result};

pub(crate) struct TextSignature {
pub(crate) text: String,
params: Vec<Parameter>,
}

struct Parameter {
name: String,
positional: bool,
optional: bool,
keywords: bool,
}

fn identifier(name: &str) -> bool {
let name = name.strip_prefix('$').unwrap_or(name);
if matches!(
name,
"False"
| "None"
| "True"
| "and"
| "as"
| "assert"
| "async"
| "await"
| "break"
| "class"
| "continue"
| "def"
| "del"
| "elif"
| "else"
| "except"
| "finally"
| "for"
| "from"
| "global"
| "if"
| "import"
| "in"
| "is"
| "lambda"
| "nonlocal"
| "not"
| "or"
| "pass"
| "raise"
| "return"
| "try"
| "while"
| "with"
| "yield"
) {
return false;
}
let mut bytes = name.bytes();
matches!(bytes.next(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'_'))
&& bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
}

impl TextSignature {
pub(crate) fn parse(text: String, span: Span) -> Result<Self> {
let invalid = || Error::new(span, "invalid or unsupported text_signature override");
let body = text
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.ok_or_else(invalid)?;
let mut params = Vec::new();
if body.contains("[,") {
let mut parts = body.split("[,").collect::<Vec<_>>();
let optional_count = parts.len() - 1;
let ending = format!("{}, /", "]".repeat(optional_count));
let last = parts
.pop()
.unwrap()
.strip_suffix(&ending)
.ok_or_else(invalid)?;
let required = parts.remove(0);
for name in required.split(',').map(str::trim) {
params.push(Parameter {
name: name.into(),
positional: true,
optional: false,
keywords: false,
});
}
for name in parts.into_iter().chain([last]).map(str::trim) {
params.push(Parameter {
name: name.into(),
positional: true,
optional: true,
keywords: false,
});
}
} else {
let mut slash = false;
let mut optional = false;
let mut keywords = false;
for part in body.split(',').map(str::trim) {
if keywords {
return Err(invalid());
}
if part == "/" {
if slash || params.is_empty() {
return Err(invalid());
}
slash = true;
for param in &mut params {
param.positional = true;
}
continue;
}
let (name, default) = match part.split_once('=') {
Some((name, default)) => {
let default = default.trim();
let quoted = (default.starts_with('\'') && default.ends_with('\''))
|| (default.starts_with('"') && default.ends_with('"'));
let literal = if quoted && default.len() >= 2 {
!default[1..default.len() - 1].contains(['\'', '"', '\\', '\n', '\r'])
} else {
matches!(default, "None" | "True" | "False")
|| default
.parse::<i128>()
.is_ok_and(|value| value.to_string() == default)
};
if !literal {
return Err(invalid());
}
(name.trim(), true)
}
None => (part, false),
};
let name = if let Some(name) = name.strip_prefix("**") {
if default {
return Err(invalid());
}
keywords = true;
name
} else {
name
};
if optional && !default && !keywords {
return Err(invalid());
}
optional |= default;
params.push(Parameter {
name: name.into(),
positional: false,
optional: default,
keywords,
});
}
}
for (index, param) in params.iter().enumerate() {
if !identifier(&param.name)
|| params[..index].iter().any(|other| other.name == param.name)
|| (param.name.starts_with('$')
&& (index != 0
|| !param.positional
|| param.optional
|| !matches!(param.name.as_str(), "$self" | "$type" | "$module")))
{
return Err(invalid());
}
}
Ok(Self { text, params })
}

pub(crate) fn params_tokens(&self) -> Vec<TokenStream> {
self.params.iter().map(|param| {
let name = &param.name;
let kind = if param.keywords { quote!(VarKeyword) } else if param.positional { quote!(PositionalOnly) } else { quote!(PositionalOrKeyword) };
let default = if param.optional { quote!(Some(::rustpython_vm::function::DefaultRepr::Unrepresentable)) } else { quote!(None) };
quote! { ::rustpython_vm::function::Param { name: #name, kind: ::rustpython_vm::function::ParamKind::#kind, default: #default } }
}).collect()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn supported_overrides() {
for name in ["sub", "prefix", "suffix"] {
let sig = TextSignature::parse(
format!("($self, {name}[, start[, end]], /)"),
Span::call_site(),
)
.unwrap();
assert_eq!(sig.params.len(), 4);
assert!(sig.params[2..].iter().all(|p| p.positional && p.optional));
assert!(!sig.params[1].optional);
}
for name in ["iterable", "fileobj", "name"] {
let sig = TextSignature::parse(
format!("($module, {name}, /, dialect='excel', **fmtparams)"),
Span::call_site(),
)
.unwrap();
assert_eq!(sig.params.len(), 4);
assert!(sig.params[1].positional);
assert!(!sig.params[2].positional);
assert!(sig.params[2].optional);
assert!(sig.params[3].keywords);
}
}

#[test]
fn malformed_overrides() {
for text in [
"()",
"(class, /)",
"(value=01, /)",
"($self, sub[, ], /)",
"($self, sub[, start[, ]], /)",
"($self, sub[, start[, end], /)",
"($self, $self, /)",
"($module, name, /, **name)",
"($module, name, /, **kwargs, **kwargs)",
"($self, sub[, start[, end]]], /)",
"($self, sub[, start], end, /)",
"($self, sub[, start=0], /)",
"($self, sub[, start], /, **kwargs)",
"($self, sub[, start, end], /)",
"($module, name, /, dialect=call(), **fmtparams)",
"($module, name, /, dialect='excel', **fmtparams, extra)",
"($module, name, /, /)",
"($module, name, /, name=None)",
"($module, name)",
"(name, $self, /)",
"($unknown, name, /)",
"($module, name, /, dialect='excel', required)",
"($module, name, /, **kwargs=None)",
"($module, name, /, dialect='excel'junk')",
] {
assert!(
TextSignature::parse(text.into(), Span::call_site()).is_err(),
"{text}"
);
}
}
}
Loading
Loading