Compose native text signatures at compile time from FromArgs (#8807)

* Compose native text signatures at compile time from FromArgs

FromArgs gains a PARAMS const describing its Python parameters.
The derive builds the whole internal doc, `name(sig)\n--\n\ndoc`,
in a const context from those params, and `__text_signature__` /
`__doc__` parse it from the doc. The macro-time string path and the
runtime sig_parts composition are removed.

Signatures follow clinic conventions: `$module` for module functions,
Rust literal defaults rendered as Python reprs, other defaults as
`<unrepresentable>`, and a new `#[pyarg(py_default = "...")]` for the
few fields whose Rust default differs from the Python one. The
`#[pyarg]` keys are documented on the FromArgs derive.

Module functions keep their module object and return it from
`__self__`. PyNativeFunction now traverses its references.

os.stat, os.mkdir, os.listdir, os.open, dict.fromkeys, round, sorted,
int.to_bytes and bytes.hex take arguments matching their signatures.

Assisted-by: Grok:grok-4.7
Assisted-by: Claude:claude-opus-5-5

* Update builtin_signature snippet and fix mkdir on Windows

round and sum now report their FromArgs parameters, so the snippet
asserts their signatures instead of their absence. sum's start is a
PyObjectRef defaulting to int 0 with py_default "0". mkdir reads
dir_fd only on platforms that use it.

Assisted-by: Claude:claude-opus-5-5

* Remove expectedFailure from two signature tests

test_cmp_to_signature and test_autospec_on_bound_builtin_function
now pass. The TestCmpToKeyC override existed only for the marker.

Assisted-by: Claude:claude-opus-5-5
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-24 20:53:28 +09:00
1 parent e540b4152d
commit 7cd4a54e1c
25 files changed
+827 -219

No files matched your search

+1
View File
@@ -60,6 +60,7 @@ powf
powi
prepended
punct
qself
replacen
retag
rmatch
-4
View File
@@ -1238,10 +1238,6 @@ class TestCmpToKeyC(TestCmpToKey, unittest.TestCase):
if c_functools:
cmp_to_key = c_functools.cmp_to_key
@unittest.expectedFailure # TODO: RUSTPYTHON; inspect.signature reports (args, /) until FromArgs fields generate __text_signature__
def test_cmp_to_signature(self):
super().test_cmp_to_signature()
@support.cpython_only
def test_disallow_instantiation(self):
# Ensure that the type disallows instantiation (bpo-43916)
-1
View File
@@ -1363,7 +1363,6 @@ class TestClassesAndFunctions(unittest.TestCase):
spec = inspect.getfullargspec(test)
self.assertEqual(test.__annotations__, spec.annotations)
@unittest.expectedFailure # TODO: RUSTPYTHON; os.stat signature is generated as (path, dir_fd, follow_symlinks, /)
@unittest.skipIf(MISSING_C_DOCSTRINGS,
"Signature information for builtins requires docstrings")
def test_getfullargspec_builtin_methods(self):
-1
View File
@@ -1573,7 +1573,6 @@ class TestDescriptions(unittest.TestCase):
"dump(obj, /) method of _pickle.Pickler instance")
# this should *never* include self!
@unittest.expectedFailure # TODO: RUSTPYTHON; os.stat signature is generated as (path, dir_fd, follow_symlinks, /)
@requires_docstrings
def test_module_level_callable(self):
self.assertEqual(self._get_summary_line(os.stat),
-1
View File
@@ -929,7 +929,6 @@ class SpecSignatureTest(unittest.TestCase):
check_data_descriptor(foo.desc)
@unittest.expectedFailure # TODO: RUSTPYTHON; time.ctime has an auto-generated __text_signature__, so inspect.signature() succeeds instead of raising ValueError
def test_autospec_on_bound_builtin_function(self):
meth = types.MethodType(time.ctime, time.time())
self.assertIsInstance(meth(), str)
+1 -1
View File
@@ -14,7 +14,7 @@ rustpython-compiler-core = { workspace = true }
rustpython-doc = { workspace = true }
itertools = { workspace = true }
syn = { workspace = true, features = ["full", "extra-traits"] }
syn = { workspace = true, features = ["full", "extra-traits", "visit", "visit-mut"] }
proc-macro2 = { workspace = true }
quote = { workspace = true }
+126 -14
View File
@@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::ext::IdentExt;
use syn::meta::ParseNestedMeta;
use syn::{Attribute, Data, DeriveInput, Expr, Field, Ident, Result, Token, parse_quote};
use syn::{Attribute, Data, DeriveInput, Expr, Field, Ident, Lit, Result, Token, parse_quote};
/// 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)
@@ -37,6 +37,7 @@ struct ArgAttribute {
name: Option<String>,
kind: ParameterKind,
default: Option<DefaultValue>,
py_default: Option<String>,
error_msg: Option<String>,
}
@@ -64,6 +65,7 @@ impl ArgAttribute {
name: None,
kind,
default: None,
py_default: None,
error_msg: None,
});
return Ok(());
@@ -91,6 +93,13 @@ impl ArgAttribute {
if self.default.is_none() {
self.default = Some(None);
}
// Text-only signature default. See the `FromArgs` derive docs.
} else if meta.path.is_ident("py_default") {
if self.py_default.is_some() {
return Err(meta.error("py_default already set"));
}
let val = meta.value()?.parse::<syn::LitStr>()?;
self.py_default = Some(val.value());
} else if meta.path.is_ident("name") {
if self.name.is_some() {
return Err(meta.error("already have a name"));
@@ -212,6 +221,96 @@ fn generate_field((i, field): (usize, &Field)) -> Result<TokenStream> {
Ok(file_output)
}
fn is_phantom(field: &Field) -> bool {
field
.ident
.as_ref()
.is_some_and(|ident| ident.unraw().to_string().starts_with("_phantom"))
}
fn python_str_repr(s: &str) -> String {
let mut out = String::from("'");
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("\\'"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('\'');
out
}
fn python_default_repr(default: Option<&DefaultValue>, py_default: Option<&str>) -> Option<String> {
if let Some(py_default) = py_default {
return Some(py_default.to_owned());
}
match default {
None => None,
Some(None) => Some("<unrepresentable>".to_owned()),
Some(Some(expr)) => match expr {
Expr::Lit(syn::ExprLit { lit, .. }) => match lit {
Lit::Bool(b) => Some(if b.value {
"True".to_owned()
} else {
"False".to_owned()
}),
Lit::Int(i) => Some(i.base10_digits().to_owned()),
Lit::Float(f) => Some(f.base10_digits().to_owned()),
Lit::Str(s) => Some(python_str_repr(&s.value())),
_ => Some("<unrepresentable>".to_owned()),
},
Expr::Path(path) if path.qself.is_none() && path.path.is_ident("None") => {
Some("None".to_owned())
}
_ => Some("<unrepresentable>".to_owned()),
},
}
}
fn param_token(field: &Field, attr: &ArgAttribute) -> Result<TokenStream> {
if let ParameterKind::Flatten = attr.kind {
let ty = &field.ty;
return Ok(quote! {
::rustpython_vm::function::Param::flatten(
<#ty as ::rustpython_vm::function::FromArgs>::PARAMS
)
});
}
let name = field.ident.as_ref().map(|ident| ident.unraw().to_string());
let pyname = attr
.name
.clone()
.or(name)
.ok_or_else(|| err_span!(field, "field in tuple struct must have name attribute"))?;
let default = python_default_repr(attr.default.as_ref(), attr.py_default.as_deref());
let default_tok = match &default {
Some(default) => quote!(Some(#default)),
None => quote!(None),
};
let kind = match attr.kind {
ParameterKind::PositionalOnly => {
quote!(::rustpython_vm::function::ParamKind::PositionalOnly)
}
ParameterKind::PositionalOrKeyword => {
quote!(::rustpython_vm::function::ParamKind::PositionalOrKeyword)
}
ParameterKind::KeywordOnly => quote!(::rustpython_vm::function::ParamKind::KeywordOnly),
ParameterKind::Flatten => unreachable!(),
};
Ok(quote! {
::rustpython_vm::function::Param {
name: #pyname,
kind: #kind,
default: #default_tok,
}
})
}
fn compute_arity_bounds(field_attrs: &[ArgAttribute]) -> (usize, usize) {
let positional_fields = field_attrs.iter().filter(|attr| {
matches!(
@@ -230,27 +329,40 @@ fn compute_arity_bounds(field_attrs: &[ArgAttribute]) -> (usize, usize) {
}
pub(crate) fn impl_from_args(input: DeriveInput) -> Result<TokenStream> {
let (fields, field_attrs) = match input.data {
Data::Struct(syn::DataStruct { fields, .. }) => (
fields
.iter()
.enumerate()
.map(generate_field)
.collect::<Result<TokenStream>>()?,
fields
.iter()
.filter_map(|field| field.try_into().ok())
.collect::<Vec<ArgAttribute>>(),
),
_ => bail_span!(input, "FromArgs input must be a struct"),
let Data::Struct(syn::DataStruct {
fields: struct_fields,
..
}) = input.data
else {
bail_span!(input, "FromArgs input must be a struct")
};
let fields = struct_fields
.iter()
.enumerate()
.map(generate_field)
.collect::<Result<TokenStream>>()?;
let field_attrs = struct_fields
.iter()
.map(ArgAttribute::try_from)
.collect::<Result<Vec<_>>>()?;
let (min_arity, max_arity) = compute_arity_bounds(&field_attrs);
let mut params = Vec::new();
for field in &struct_fields {
if is_phantom(field) {
continue;
}
params.push(param_token(field, &ArgAttribute::try_from(field)?)?);
}
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 {
const PARAMS: Option<&'static [::rustpython_vm::function::Param]> =
Some(&[#(#params),*]);
fn arity() -> ::std::ops::RangeInclusive<usize> {
#min_arity..=#max_arity
}
+18 -14
View File
@@ -1,8 +1,8 @@
use super::Diagnostic;
use crate::util::{
ALL_ALLOWED_NAMES, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ExceptionItemMeta,
ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, format_doc, infer_native_call_flags,
pyclass_ident_and_attrs, pyexception_ident_and_attrs, text_signature,
ItemMeta, ItemMetaInner, ItemNursery, SimpleItemMeta, infer_native_call_flags,
internal_doc_tokens, pyclass_ident_and_attrs, pyexception_ident_and_attrs,
};
use core::str::FromStr;
use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree};
@@ -71,6 +71,9 @@ struct ImplContext {
extend_slots_items: ItemNursery,
class_extensions: Vec<TokenStream>,
errors: Vec<syn::Error>,
/// Set when the impl has no generic parameters, so `Self` in argument
/// types can be replaced before it appears in a nested const.
self_ty_subst: Option<syn::Type>,
}
fn extract_items_into_context<'a, Item>(
@@ -104,6 +107,9 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul
let mut context = ImplContext::default();
let mut tokens = match item {
Item::Impl(mut imp) => {
if imp.generics.params.is_empty() {
context.self_ty_subst = Some((*imp.self_ty).clone());
}
extract_items_into_context(&mut context, imp.items.iter_mut());
let attr_nonempty = !attr.is_empty();
@@ -1133,7 +1139,6 @@ where
}
};
let drop_first_typed = usize::from(implicit_self.is_some());
let sig_doc = text_signature(func.sig(), &py_name, implicit_self);
let call_flags = infer_native_call_flags(func.sig(), drop_first_typed);
// Add #[allow(non_snake_case)] for setter methods like set___name__
@@ -1143,11 +1148,14 @@ where
args.attrs.push(allow_attr);
}
let doc = match (sig_doc, args.attrs.doc()) {
(Some(sig_doc), Some(doc)) => Some(format_doc(&sig_doc, &doc)),
(Some(sig_doc), None) => Some(format_doc(&sig_doc, "")),
(None, doc) => doc,
};
let doc = internal_doc_tokens(
func.sig(),
&py_name,
implicit_self,
args.attrs.doc(),
args.context.self_ty_subst.as_ref(),
None,
);
args.context.method_items.add_item(MethodNurseryItem {
py_name,
cfgs: args.cfgs.to_vec(),
@@ -1383,7 +1391,7 @@ struct MethodNurseryItem {
ident: Ident,
raw: bool,
coexist: bool,
doc: Option<String>,
doc: TokenStream,
attr_name: AttrName,
call_flags: TokenStream,
}
@@ -1411,11 +1419,7 @@ impl ToTokens for MethodNursery {
let py_name = &item.py_name;
let ident = &item.ident;
let cfgs = &item.cfgs;
let doc = if let Some(doc) = item.doc.as_ref() {
quote! { Some(#doc) }
} else {
quote! { None }
};
let doc = &item.doc;
let binding_flags = match &item.attr_name {
AttrName::Method => {
quote! { rustpython_vm::function::PyMethodFlags::METHOD }
+5 -13
View File
@@ -2,8 +2,8 @@ use crate::error::Diagnostic;
use crate::pystructseq::PyStructSequenceMeta;
use crate::util::{
ALL_ALLOWED_NAMES, AttrItemMeta, AttributeExt, ClassItemMeta, ContentItem, ContentItemInner,
ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, format_doc,
infer_native_call_flags, iter_use_idents, pyclass_ident_and_attrs, text_signature,
ErrorVec, ItemMeta, ItemNursery, ModuleItemMeta, SimpleItemMeta, infer_native_call_flags,
internal_doc_tokens, iter_use_idents, pyclass_ident_and_attrs,
};
use core::str::FromStr;
use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
@@ -524,7 +524,7 @@ struct FunctionNurseryItem {
py_names: Vec<String>,
cfgs: Vec<Attribute>,
ident: Ident,
doc: Option<String>,
doc: TokenStream,
call_flags: TokenStream,
}
@@ -556,10 +556,7 @@ impl ToTokens for ValidatedFunctionNursery {
let cfgs = &item.cfgs;
let cfgs = quote!(#(#cfgs)*);
let py_names = &item.py_names;
let doc = match &item.doc {
Some(doc) => quote!(Some(#doc)),
None => quote!(None),
};
let doc = &item.doc;
let flags = &item.call_flags;
inner_tokens.extend(quote![
@@ -664,7 +661,6 @@ impl ModuleItem for FunctionItem {
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, None);
let module = args.module_name();
// TODO: doc must exist at least one of code or CPython
@@ -673,11 +669,7 @@ impl ModuleItem for FunctionItem {
.copied()
.map(str::to_owned)
});
let doc = match (sig_doc, doc) {
(Some(sig_doc), Some(doc)) => Some(format_doc(&sig_doc, &doc)),
(Some(sig_doc), None) => Some(format_doc(&sig_doc, "")),
(None, doc) => doc,
};
let doc = internal_doc_tokens(func.sig(), &py_name, None, doc, None, Some("$module"));
let py_names = {
if self.py_attrs.is_empty() {
+155 -73
View File
@@ -2,7 +2,11 @@ use itertools::Itertools;
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote};
use std::collections::{HashMap, HashSet};
use syn::{Attribute, FnArg, Ident, Result, Signature, UseTree, ext::IdentExt, spanned::Spanned};
use syn::visit::Visit;
use syn::visit_mut::VisitMut;
use syn::{
Attribute, FnArg, Ident, Result, Signature, Type, UseTree, ext::IdentExt, spanned::Spanned,
};
use syn_ext::{
ext::{AttributeExt as SynAttributeExt, *},
types::*,
@@ -736,26 +740,157 @@ where
Ok(())
}
// Best effort attempt to generate a template from which a
// __text_signature__ can be created.
//
// Unlike CPython, a `#[pyfunction]` doesn't take the module as an argument yet,
// so there's no module to mark with `$module`.
pub(crate) fn text_signature(
sig: &Signature,
name: &str,
implicit_self: Option<&str>,
) -> Option<String> {
let signature = func_sig(sig, implicit_self)?;
// Arguments bind through `FuncArgs::take_positional`, which never consults
// the keyword map, so they are positional-only. `*args`/`**kwargs` cannot be
// followed by `/`, and an empty parameter list has nothing to mark.
let signature = if signature.is_empty() || signature.contains('*') {
format!("{name}({signature})")
} else {
format!("{name}({signature}, /)")
enum SigPiece {
Marker(String),
Arg {
name: String,
ty: Type,
},
/// Parameters of the argument a method binds as its receiver.
Implicit(Type),
}
fn is_vm_or_callee(ty: &Type) -> bool {
let ty = quote!(#ty).to_string().replace(' ', "");
(ty.starts_with('&') && ty.ends_with("VirtualMachine")) || ty.ends_with("Callee")
}
fn arg_name(pat: &syn::Pat) -> String {
let syn::Pat::Ident(pat) = pat else {
return String::new();
};
Some(signature)
let ident = pat.ident.unraw().to_string();
ident.strip_prefix('_').unwrap_or(&ident).to_owned()
}
fn mentions_self(ty: &Type) -> bool {
struct Finder(bool);
impl Visit<'_> for Finder {
fn visit_ident(&mut self, ident: &Ident) {
if ident == "Self" {
self.0 = true;
}
}
}
let mut finder = Finder(false);
finder.visit_type(ty);
finder.0
}
fn subst_self(ty: &Type, self_ty: Option<&Type>) -> Type {
struct Subst<'a>(&'a Type);
impl VisitMut for Subst<'_> {
fn visit_type_mut(&mut self, ty: &mut Type) {
if let Type::Path(path) = ty
&& path.qself.is_none()
&& path.path.is_ident("Self")
{
*ty = self.0.clone();
return;
}
syn::visit_mut::visit_type_mut(self, ty);
}
}
let Some(self_ty) = self_ty else {
return ty.clone();
};
let mut ty = ty.clone();
Subst(self_ty).visit_type_mut(&mut ty);
ty
}
fn sig_pieces(
sig: &Signature,
implicit_self: Option<&str>,
self_ty: Option<&Type>,
leading_marker: Option<&str>,
) -> Vec<SigPiece> {
let mut pieces = Vec::new();
if let Some(marker) = leading_marker {
pieces.push(SigPiece::Marker(marker.to_owned()));
}
let mut implicit_self = implicit_self.map(str::to_owned);
for arg in &sig.inputs {
match arg {
FnArg::Receiver(_) => pieces.push(SigPiece::Marker("$self".to_owned())),
FnArg::Typed(typed) => {
if is_vm_or_callee(&typed.ty) {
continue;
}
let name = arg_name(&typed.pat);
let ty = subst_self(&typed.ty, self_ty);
// References carry a lifetime, so `FromArgs` cannot be named
// from a const item. They are leaf positional parameters.
let leaf = mentions_self(&ty) || matches!(ty, Type::Reference(_));
if let Some(marker) = implicit_self.take() {
// A nested const cannot name `Self`. The receiver is the
// marker; only a type that supplies parameters is kept.
pieces.push(SigPiece::Marker(marker));
if !leaf {
pieces.push(SigPiece::Implicit(ty));
}
} else if leaf {
pieces.push(SigPiece::Marker(name));
} else {
pieces.push(SigPiece::Arg { name, ty });
}
}
}
}
pieces
}
fn sig_arg_tokens(piece: &SigPiece) -> TokenStream {
match piece {
SigPiece::Marker(name) => quote!(::rustpython_vm::function::SigArg::marker(#name)),
SigPiece::Arg { name, ty } => {
quote!(::rustpython_vm::function::SigArg::from_arg::<#ty>(#name))
}
SigPiece::Implicit(ty) => quote!(::rustpython_vm::function::SigArg::implicit::<#ty>()),
}
}
fn args_const(pieces: &[SigPiece]) -> TokenStream {
let args = pieces.iter().map(sig_arg_tokens);
quote! {
const ARGS: &[::rustpython_vm::function::SigArg] = &[#(#args),*];
}
}
/// Expression of type `Option<&'static str>`: the internal doc, or the plain
/// doc when the arguments cannot form a signature.
pub(crate) fn internal_doc_tokens(
sig: &Signature,
py_name: &str,
implicit_self: Option<&str>,
doc: Option<String>,
self_ty: Option<&Type>,
leading_marker: Option<&str>,
) -> TokenStream {
let args_const = args_const(&sig_pieces(sig, implicit_self, self_ty, leading_marker));
let plain = match &doc {
Some(doc) => quote!(Some(#doc)),
None => quote!(None),
};
let doc_text = doc.unwrap_or_default();
quote! {
{
#args_const
if !::rustpython_vm::function::has_signature(ARGS) {
#plain
} else {
const DOC: &str = #doc_text;
const N: usize = ::rustpython_vm::function::internal_doc_len(#py_name, ARGS, DOC);
const B: [u8; N] =
::rustpython_vm::function::internal_doc_bytes::<N>(#py_name, ARGS, DOC);
const S: &str = match ::core::str::from_utf8(&B) {
Ok(s) => s,
Err(_) => panic!(),
};
Some(S)
}
}
}
}
pub(crate) fn infer_native_call_flags(sig: &Signature, drop_first_typed: usize) -> TokenStream {
@@ -829,56 +964,3 @@ pub(crate) fn infer_native_call_flags(sig: &Signature, drop_first_typed: usize)
}
}
}
/// Returns None when an argument has no name to report, in which case no
/// signature can be generated for the function.
///
/// `implicit_self` is the marker to report for a first argument that the call
/// binds to without a `&self` receiver.
fn func_sig(sig: &Signature, mut implicit_self: Option<&str>) -> Option<String> {
let mut params = Vec::new();
for arg in &sig.inputs {
let arg = match arg {
FnArg::Typed(typed) => typed,
FnArg::Receiver(_) => {
params.push("$self".to_owned());
continue;
}
};
let ty = arg.ty.as_ref();
let ty = quote!(#ty).to_string();
if ty == "FuncArgs" {
// The bundle carries the receiver along with everything else, so
// report both rather than spending the marker on it.
params.extend(implicit_self.take().map(str::to_owned));
params.push("*args, **kwargs".to_owned());
continue;
}
if (ty.starts_with('&') && ty.ends_with("VirtualMachine")) || ty.ends_with("Callee") {
continue;
}
if let Some(marker) = implicit_self.take() {
params.push(marker.to_owned());
continue;
}
// An argument bound by a destructuring pattern, e.g.
// `fn round(RoundArgs { number, ndigits }: RoundArgs, ..)`, has no name
// to report. Stringifying the pattern would emit Rust syntax, which
// makes inspect.signature() raise "invalid signature".
let syn::Pat::Ident(pat) = arg.pat.as_ref() else {
return None;
};
let ident = pat.ident.unraw().to_string();
if ident == "vm" {
unreachable!("type &VirtualMachine(`{ty}`) must be filtered already");
}
// A leading `_` only marks the argument unused in Rust. A parameter whose
// Python name really starts with `_` has to be a FromArgs field instead.
params.push(ident.strip_prefix('_').unwrap_or(&ident).to_owned());
}
Some(params.join(", "))
}
pub(crate) fn format_doc(sig: &str, doc: &str) -> String {
format!("{sig}\n--\n\n{doc}")
}
+39
View File
@@ -7,6 +7,45 @@ use rustpython_derive_impl as derive_impl;
use syn::parse_macro_input;
use syn::punctuated::Punctuated;
/// Derive `FromArgs` for a struct whose fields are Python arguments.
///
/// Each field takes one `#[pyarg(...)]`. The first item is the parameter kind.
/// # Arguments
/// - `positional`: positional-only.
/// - `any`: positional or keyword.
/// - `named`: keyword-only.
/// - `flatten`: take this field from the same argument list. No other keys.
/// - `name = "..."`: Python parameter name. The field name is used when omitted.
/// - `default`: missing argument stores `Default::default()`. Affects parsing.
/// - `default = <expr>`: missing argument stores that Rust value. Affects parsing.
/// - `optional`: same as a bare `default`.
/// - `py_default = "<python source>"`: text copied verbatim into `__text_signature__`.
/// Never affects parsing.
/// - `error_msg = "..."`: type-error text when conversion fails.
/// # Signature default
/// An explicit `py_default` is used as written. Otherwise a Rust literal is
/// converted to its Python repr (`True`/`False`, an int, a float, a quoted
/// str, and the path `None`). Anything else renders `<unrepresentable>`, and
/// `inspect.signature` raises `ValueError`.
///
/// Prefer `default = <literal>` when that literal is the Python default.
/// Use `py_default` only when the Rust value must differ.
/// ```rust, ignore
/// #[derive(FromArgs)]
/// struct OpenArgs {
/// #[pyarg(any, default = 0o777)]
/// mode: i32, // signature shows 511
/// }
///
/// #[derive(FromArgs)]
/// struct PrintOptions {
/// // None means a space; the string is filled in when printing.
/// #[pyarg(named, default, py_default = "' '")]
/// sep: Option<PyStrRef>,
/// #[pyarg(named, default = None)]
/// file: Option<PyObjectRef>,
/// }
/// ```
#[proc_macro_derive(FromArgs, attributes(pyarg))]
pub fn derive_from_args(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input);
+23 -2
View File
@@ -11,10 +11,14 @@ use alloc::fmt;
// PyCFunctionObject in CPython
#[repr(C)]
#[pyclass(name = "builtin_function_or_method", module = false)]
#[pyclass(name = "builtin_function_or_method", module = false, traverse)]
pub struct PyNativeFunction {
#[pytraverse(skip)]
pub(crate) value: &'static PyMethodDef,
pub(crate) zelf: Option<PyObjectRef>,
// Module that owns this function. Not passed as a call argument.
pub(crate) module_object: Option<PyObjectRef>,
#[pytraverse(skip)]
pub(crate) module: Option<&'static PyStrInterned>, // None for bound method
/// Prevent HeapMethodDef from being freed while this function references it
pub(crate) _method_def_owner: Option<PyObjectRef>,
@@ -46,6 +50,11 @@ impl PyNativeFunction {
self
}
pub fn with_module_object(mut self, module: PyObjectRef) -> Self {
self.module_object = Some(module);
self
}
pub fn into_ref(self, ctx: &Context) -> PyRef<Self> {
PyRef::new_ref(
self,
@@ -99,6 +108,12 @@ impl Comparable for PyNativeFunction {
(None, None) => true,
_ => false,
};
let eq = eq
&& match (zelf.module_object.as_ref(), other.module_object.as_ref()) {
(Some(z), Some(o)) => z.is(o),
(None, None) => true,
_ => false,
};
let eq = eq && core::ptr::eq(zelf.value, other.value);
Ok(eq.into())
} else {
@@ -176,7 +191,13 @@ impl PyNativeFunction {
// meth_get__self__ in CPython
#[pygetset]
fn __self__(zelf: NativeFunctionOrMethod, vm: &VirtualMachine) -> PyObjectRef {
zelf.0.zelf.clone().unwrap_or_else(|| vm.ctx.none())
if let Some(bound) = &zelf.0.zelf {
return bound.clone();
}
if let Some(module) = &zelf.0.module_object {
return module.clone();
}
vm.ctx.none()
}
// meth_reduce in CPython
+6 -10
View File
@@ -144,11 +144,9 @@ impl PyMethodDescriptor {
}
#[pygetset]
fn __text_signature__(&self) -> Option<String> {
self.method.doc.and_then(|doc| {
type_::get_text_signature_from_internal_doc(self.method.name, doc)
.map(|signature| signature.to_string())
})
fn __text_signature__(&self) -> Option<&'static str> {
let doc = self.method.doc?;
type_::get_text_signature_from_internal_doc(self.method.name, doc)
}
#[pygetset]
@@ -305,11 +303,9 @@ impl PyClassMethodDescriptor {
}
#[pygetset]
fn __text_signature__(&self) -> Option<String> {
self.method.doc.and_then(|doc| {
type_::get_text_signature_from_internal_doc(self.method.name, doc)
.map(|signature| signature.to_string())
})
fn __text_signature__(&self) -> Option<&'static str> {
let doc = self.method.doc?;
type_::get_text_signature_from_internal_doc(self.method.name, doc)
}
#[pygetset]
+11 -7
View File
@@ -369,6 +369,14 @@ impl PyDict {
}
}
#[derive(FromArgs)]
struct FromKeysArgs {
#[pyarg(positional)]
iterable: ArgIterable,
#[pyarg(positional, default = None)]
value: Option<PyObjectRef>,
}
// Python dict methods:
#[pyclass(
with(
@@ -387,13 +395,9 @@ impl PyDict {
)]
impl PyDict {
#[pyclassmethod]
fn fromkeys(
class: PyTypeRef,
iterable: ArgIterable,
value: OptionalArg<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult {
let value = value.unwrap_or_none(vm);
fn fromkeys(class: PyTypeRef, args: FromKeysArgs, vm: &VirtualMachine) -> PyResult {
let FromKeysArgs { iterable, value } = args;
let value = value.unwrap_or_else(|| vm.ctx.none());
let d = PyType::call(&class, ().into(), vm)?;
match d.downcast_exact::<Self>(vm) {
Ok(pydict) => {
+5 -4
View File
@@ -589,7 +589,7 @@ impl PyInt {
#[pymethod]
fn to_bytes(&self, args: IntToByteArgs, vm: &VirtualMachine) -> PyResult<PyBytes> {
let signed = args.signed.map_or(false, Into::into);
let signed = args.signed;
let byte_len = args.length;
let value = self.as_bigint();
@@ -820,10 +820,11 @@ struct IntFromByteArgs {
struct IntToByteArgs {
#[pyarg(any, default = 1)]
length: usize,
#[pyarg(any, default = ArgByteOrder::Big)]
// ArgByteOrder::Big is not the text 'big'.
#[pyarg(any, default = ArgByteOrder::Big, py_default = "'big'")]
byteorder: ArgByteOrder,
#[pyarg(named, optional)]
signed: OptionalArg<ArgIntoBool>,
#[pyarg(named, default = false)]
signed: bool,
}
fn try_int_radix(obj: &PyObject, base: u32, vm: &VirtualMachine) -> PyResult<BigInt> {
+1 -1
View File
@@ -158,7 +158,7 @@ impl PyList {
#[derive(FromArgs, Default, Traverse)]
pub(crate) struct SortOptions {
#[pyarg(named, default)]
#[pyarg(named, default = None)]
key: Option<PyObjectRef>,
#[pytraverse(skip)]
#[pyarg(named, default = false)]
+1
View File
@@ -141,6 +141,7 @@ impl Py<PyModule> {
let func = method
.to_function()
.with_module(self.name.unwrap())
.with_module_object(self.to_owned().into())
.into_ref(&vm.ctx);
vm.__module_set_attr(self, vm.ctx.intern_str(method.name), func)?;
}
+5 -10
View File
@@ -512,7 +512,7 @@ impl PyBytesInner {
swapcase_ascii(self.as_bytes())
}
pub fn hex(&self, sep: Option<u8>, bytes_per_sep: OptionalArg<isize>) -> String {
pub fn hex(&self, sep: Option<u8>, bytes_per_sep: isize) -> String {
bytes_to_hex(self.elements.as_slice(), sep, bytes_per_sep)
}
@@ -1231,8 +1231,8 @@ pub(crate) fn bytes_decode(
pub(crate) struct ByteInnerHexOptions {
#[pyarg(any, optional)]
pub sep: OptionalArg<Either<PyStrRef, PyBytesRef>>,
#[pyarg(any, optional)]
pub bytes_per_sep: OptionalArg<isize>,
#[pyarg(any, default = 1)]
pub bytes_per_sep: isize,
}
impl ByteInnerHexOptions {
@@ -1240,7 +1240,7 @@ impl ByteInnerHexOptions {
///
/// Measuring the separator runs Python, so it happens here, before the
/// bytes to be written out are borrowed. _Py_strhex_impl
pub(crate) fn resolve(self, vm: &VirtualMachine) -> PyResult<(Option<u8>, OptionalArg<isize>)> {
pub(crate) fn resolve(self, vm: &VirtualMachine) -> PyResult<(Option<u8>, isize)> {
let Self { sep, bytes_per_sep } = self;
let OptionalArg::Present(sep) = sep else {
return Ok((None, bytes_per_sep));
@@ -1323,12 +1323,7 @@ fn hex_impl(bytes: &[u8], sep: u8, bytes_per_sep: isize) -> String {
unsafe { String::from_utf8_unchecked(buf) }
}
pub(crate) fn bytes_to_hex(
bytes: &[u8],
sep: Option<u8>,
bytes_per_sep: OptionalArg<isize>,
) -> String {
let bytes_per_sep = bytes_per_sep.unwrap_or(1);
pub(crate) fn bytes_to_hex(bytes: &[u8], sep: Option<u8>, bytes_per_sep: isize) -> String {
match sep {
Some(sep) if bytes_per_sep != 0 && !bytes.is_empty() => hex_impl(bytes, sep, bytes_per_sep),
_ => hex_impl_no_sep(bytes),
+22
View File
@@ -1,3 +1,4 @@
use super::signature::Param;
use crate::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine,
builtins::{PyBaseExceptionRef, PyTupleRef, PyType},
@@ -99,6 +100,9 @@ impl From<KwArgs> for FuncArgs {
}
impl FromArgs for FuncArgs {
const PARAMS: Option<&'static [Param]> =
Some(&[Param::var_positional("args"), Param::var_keyword("kwargs")]);
fn from_args(_vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
Ok(core::mem::take(args))
}
@@ -519,6 +523,14 @@ pub trait FromArgs: Sized {
0..=0
}
/// Parameters this type contributes to a text signature.
///
/// `None`: the argument is one positional-only parameter, named by the
/// function argument. `Some(&[])`: the argument contributes nothing.
/// `Some(params)`: the type supplies those parameters and the argument name
/// is ignored.
const PARAMS: Option<&'static [Param]> = None;
/// Extracts this item from the next argument(s).
fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError>;
}
@@ -662,6 +674,8 @@ impl<T> FromArgs for KwArgs<T>
where
T: TryFromObject,
{
const PARAMS: Option<&'static [Param]> = Some(&[Param::var_keyword("kwargs")]);
fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
let mut kwargs = KwArgsMap::default();
for (name, value) in args.remaining_keywords() {
@@ -736,6 +750,8 @@ impl<T> FromArgs for PosArgs<T>
where
T: TryFromObject,
{
const PARAMS: Option<&'static [Param]> = Some(&[Param::var_positional("args")]);
fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
let mut varargs = Vec::new();
while let Some(value) = args.take_positional() {
@@ -808,6 +824,12 @@ impl<T> FromArgs for OptionalArg<T>
where
T: TryFromObject,
{
const PARAMS: Option<&'static [Param]> = Some(&[Param {
name: "",
kind: super::signature::ParamKind::PositionalOnly,
default: Some("<unrepresentable>"),
}]);
fn arity() -> RangeInclusive<usize> {
0..=1
}
+4
View File
@@ -124,6 +124,7 @@ impl PyMethodDef {
PyNativeFunction {
zelf: None,
value: self,
module_object: None,
module: None,
_method_def_owner: None,
}
@@ -146,6 +147,7 @@ impl PyMethodDef {
func: PyNativeFunction {
zelf: Some(obj),
value: self,
module_object: None,
module: None,
_method_def_owner: None,
},
@@ -165,6 +167,7 @@ impl PyMethodDef {
let function = PyNativeFunction {
zelf: Some(obj),
value: self,
module_object: None,
module: None,
_method_def_owner: None,
};
@@ -218,6 +221,7 @@ impl PyMethodDef {
let func = PyNativeFunction {
zelf: Some(class.to_owned().into()),
value: self,
module_object: None,
module: None,
_method_def_owner: None,
};
+4
View File
@@ -9,6 +9,7 @@ mod getset;
pub(crate) mod method;
mod number;
mod protocol;
mod signature;
mod time;
pub use argument::{
@@ -29,6 +30,9 @@ pub(super) use getset::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySett
pub use method::{HeapMethodDef, PyMethodDef, PyMethodFlags};
pub use number::{ArgIndex, ArgIntoBool, ArgIntoComplex, ArgIntoFloat, ArgPrimitiveIndex, ArgSize};
pub use protocol::{ArgCallable, ArgIterable, ArgMapping, ArgSequence};
pub use signature::{
Param, ParamKind, SigArg, has_signature, internal_doc_bytes, internal_doc_len,
};
pub use time::TimeoutSeconds;
use crate::{
+322
View File
@@ -0,0 +1,322 @@
//! Compile-time text signatures for native functions.
//!
//! The internal doc is `name(params)\n--\n\ndoc`, built in a const context from
//! [`FromArgs`] metadata. Nothing here allocates.
use super::argument::FromArgs;
#[derive(Clone, Copy, Debug)]
pub enum ParamKind {
PositionalOnly,
PositionalOrKeyword,
KeywordOnly,
VarPositional,
VarKeyword,
/// Parameters contributed by a nested [`FromArgs`] type.
/// `None` contributes nothing.
Flatten(Option<&'static [Param]>),
}
#[derive(Clone, Copy, Debug)]
pub struct Param {
pub name: &'static str,
pub kind: ParamKind,
pub default: Option<&'static str>,
}
impl Param {
#[must_use]
pub const fn positional_only(name: &'static str) -> Self {
Self {
name,
kind: ParamKind::PositionalOnly,
default: None,
}
}
#[must_use]
pub const fn positional_or_keyword(name: &'static str) -> Self {
Self {
name,
kind: ParamKind::PositionalOrKeyword,
default: None,
}
}
#[must_use]
pub const fn keyword_only(name: &'static str, default: Option<&'static str>) -> Self {
Self {
name,
kind: ParamKind::KeywordOnly,
default,
}
}
#[must_use]
pub const fn var_positional(name: &'static str) -> Self {
Self {
name,
kind: ParamKind::VarPositional,
default: None,
}
}
#[must_use]
pub const fn var_keyword(name: &'static str) -> Self {
Self {
name,
kind: ParamKind::VarKeyword,
default: None,
}
}
#[must_use]
pub const fn flatten(params: Option<&'static [Self]>) -> Self {
Self {
name: "",
kind: ParamKind::Flatten(params),
default: None,
}
}
}
/// One Rust argument of a native function.
///
/// `params == None`: one positional-only parameter named [`name`](Self::name).
/// `Some(ps)`: the type supplies `ps` and `name` is ignored.
/// `Some(&[])`: the argument contributes nothing.
#[derive(Clone, Copy, Debug)]
pub struct SigArg {
pub name: &'static str,
pub params: Option<&'static [Param]>,
}
impl SigArg {
#[must_use]
pub const fn from_arg<T: FromArgs>(name: &'static str) -> Self {
Self {
name,
params: T::PARAMS,
}
}
/// `$self` / `$type` receiver marker. Renders as one positional-only parameter.
#[must_use]
pub const fn marker(name: &'static str) -> Self {
Self { name, params: None }
}
/// Parameters supplied by the argument a method binds as its receiver.
/// `None` metadata contributes nothing, so this never looks nameless.
#[must_use]
pub const fn implicit<T: FromArgs>() -> Self {
Self {
name: "",
params: Some(match T::PARAMS {
Some(ps) => ps,
None => &[],
}),
}
}
}
/// False when an argument is a destructured pattern whose type has no [`FromArgs::PARAMS`].
#[must_use]
pub const fn has_signature(args: &[SigArg]) -> bool {
let mut i = 0;
while i < args.len() {
if args[i].params.is_none() && args[i].name.is_empty() {
return false;
}
i += 1;
}
true
}
#[must_use]
pub const fn internal_doc_len(name: &str, args: &[SigArg], doc: &str) -> usize {
write_internal_doc(&mut [], name, args, doc)
}
#[must_use]
pub const fn internal_doc_bytes<const N: usize>(name: &str, args: &[SigArg], doc: &str) -> [u8; N] {
let mut buf = [0u8; N];
let written = write_internal_doc(&mut buf, name, args, doc);
assert!(written == N);
buf
}
struct St {
n: usize,
emitted: bool,
po_left: usize,
var_pos_seen: bool,
star_emitted: bool,
}
const fn write_internal_doc(buf: &mut [u8], name: &str, args: &[SigArg], doc: &str) -> usize {
let mut n = put_str(buf, 0, name);
n = put_byte(buf, n, b'(');
let st = write_args(
buf,
St {
n,
emitted: false,
po_left: count_po_args(args),
var_pos_seen: false,
star_emitted: false,
},
args,
);
n = put_str(buf, st.n, ")\n--\n\n");
put_str(buf, n, doc)
}
const fn put(buf: &mut [u8], i: usize, bytes: &[u8]) -> usize {
let mut k = 0;
while k < bytes.len() {
let at = i + k;
if at < buf.len() {
buf[at] = bytes[k];
}
k += 1;
}
i + bytes.len()
}
const fn put_str(buf: &mut [u8], i: usize, s: &str) -> usize {
put(buf, i, s.as_bytes())
}
const fn put_byte(buf: &mut [u8], i: usize, b: u8) -> usize {
put(buf, i, &[b])
}
const fn count_po_params(params: &[Param]) -> usize {
let mut n = 0;
let mut i = 0;
while i < params.len() {
match params[i].kind {
ParamKind::PositionalOnly => n += 1,
ParamKind::Flatten(Some(inner)) => n += count_po_params(inner),
_ => {}
}
i += 1;
}
n
}
const fn count_po_args(args: &[SigArg]) -> usize {
let mut n = 0;
let mut i = 0;
while i < args.len() {
match args[i].params {
None => n += 1,
Some(ps) => n += count_po_params(ps),
}
i += 1;
}
n
}
const fn emit_sep(buf: &mut [u8], st: St) -> St {
if st.emitted {
St {
n: put_str(buf, st.n, ", "),
..st
}
} else {
st
}
}
const fn emit_text(buf: &mut [u8], st: St, text: &str) -> St {
let st = emit_sep(buf, st);
St {
n: put_str(buf, st.n, text),
emitted: true,
..st
}
}
const fn emit_named(buf: &mut [u8], st: St, prefix: &str, name: &str, default: Option<&str>) -> St {
let st = emit_sep(buf, st);
let n = put_str(buf, st.n, prefix);
let n = put_str(buf, n, name);
let n = if let Some(default) = default {
let n = put_byte(buf, n, b'=');
put_str(buf, n, default)
} else {
n
};
St {
n,
emitted: true,
..st
}
}
const fn param_name<'a>(name: &'a str, fallback: &'a str) -> &'a str {
if name.is_empty() { fallback } else { name }
}
const fn write_params(buf: &mut [u8], mut st: St, params: &[Param], fallback: &str) -> St {
let mut i = 0;
while i < params.len() {
st = write_one(buf, st, params[i], fallback);
i += 1;
}
st
}
const fn write_one(buf: &mut [u8], mut st: St, param: Param, fallback: &str) -> St {
let name = param_name(param.name, fallback);
match param.kind {
ParamKind::Flatten(None) => st,
ParamKind::Flatten(Some(inner)) => write_params(buf, st, inner, ""),
ParamKind::PositionalOnly => {
st = emit_named(buf, st, "", name, param.default);
st.po_left -= 1;
if st.po_left == 0 {
st = emit_text(buf, st, "/");
}
st
}
ParamKind::PositionalOrKeyword => emit_named(buf, st, "", name, param.default),
ParamKind::KeywordOnly => {
if !st.var_pos_seen && !st.star_emitted {
st = emit_text(buf, st, "*");
st.star_emitted = true;
}
emit_named(buf, st, "", name, param.default)
}
ParamKind::VarPositional => {
st.var_pos_seen = true;
emit_named(buf, st, "*", name, param.default)
}
ParamKind::VarKeyword => emit_named(buf, st, "**", name, param.default),
}
}
const fn write_args(buf: &mut [u8], mut st: St, args: &[SigArg]) -> St {
let mut i = 0;
while i < args.len() {
match args[i].params {
None => {
st = write_one(
buf,
st,
Param {
name: args[i].name,
kind: ParamKind::PositionalOnly,
default: None,
},
"",
);
}
Some(ps) => st = write_params(buf, st, ps, args[i].name),
}
i += 1;
}
st
}
+15 -14
View File
@@ -1076,14 +1076,17 @@ mod builtins {
#[derive(Debug, Default, FromArgs)]
pub struct PrintOptions {
#[pyarg(named, default)]
// None means a space; the string is filled in when printing.
#[pyarg(named, default, py_default = "' '")]
sep: Option<PyStrRef>,
#[pyarg(named, default)]
// None means a newline; the string is filled in when printing.
#[pyarg(named, default, py_default = "'\\n'")]
end: Option<PyStrRef>,
#[pyarg(named, default = ArgIntoBool::FALSE)]
flush: ArgIntoBool,
#[pyarg(named, default)]
#[pyarg(named, default = None)]
file: Option<PyObjectRef>,
// ArgIntoBool::FALSE is not the literal false.
#[pyarg(named, default = ArgIntoBool::FALSE, py_default = "False")]
flush: ArgIntoBool,
}
#[pyfunction]
@@ -1139,8 +1142,8 @@ mod builtins {
#[derive(FromArgs)]
pub(super) struct RoundArgs {
number: PyObjectRef,
#[pyarg(any, optional)]
ndigits: OptionalOption<PyObjectRef>,
#[pyarg(any, default = None)]
ndigits: Option<PyObjectRef>,
}
#[pyfunction]
@@ -1153,7 +1156,7 @@ mod builtins {
number.class().slot_name()
))
})?;
match ndigits.flatten() {
match ndigits {
Some(obj) => {
let ndigits = obj.try_index(vm)?;
meth.invoke((ndigits,), vm)
@@ -1198,8 +1201,9 @@ mod builtins {
pub(super) struct SumArgs {
#[pyarg(positional)]
iterable: ArgIterable,
#[pyarg(any, optional)]
start: OptionalArg<PyObjectRef>,
// The int object needs the VM, so the default is not a literal.
#[pyarg(any, default = vm.ctx.new_int(0).into(), py_default = "0")]
start: PyObjectRef,
}
#[expect(
@@ -1208,10 +1212,7 @@ mod builtins {
)]
#[pyfunction]
fn sum(SumArgs { iterable, start }: SumArgs, vm: &VirtualMachine) -> PyResult {
// Start with zero and add at will:
let mut sum = start
.into_option()
.unwrap_or_else(|| vm.ctx.new_int(0).into());
let mut sum = start;
match_class!(match sum {
PyStr =>
+55 -29
View File
@@ -5,7 +5,7 @@ use crate::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine,
builtins::{PyModule, PySet},
convert::{IntoPyException, ToPyException, ToPyObject},
function::{ArgumentError, FromArgs, FuncArgs},
function::{ArgumentError, FromArgs, FuncArgs, Param},
host_env::{crt_fd, posix::RawMode},
ospath::OsPath,
};
@@ -36,6 +36,7 @@ const DEFAULT_DIR_FD: crt_fd::Borrowed<'static> = unsafe { crt_fd::Borrowed::bor
pub trait DirFdKeyword: Clone + Copy + Eq + PartialEq {
const NAME: &'static str;
const PARAMS: &'static [Param] = &[Param::keyword_only(Self::NAME, Some("None"))];
}
#[derive(Clone, Copy, Eq, PartialEq)]
@@ -91,6 +92,8 @@ impl<'fd, KW: DirFdKeyword> DirFd<'fd, 1, KW> {
}
impl<const AVAILABLE: usize, KW: DirFdKeyword> FromArgs for DirFd<'_, AVAILABLE, KW> {
const PARAMS: Option<&'static [Param]> = Some(KW::PARAMS);
fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
let fd = match args.take_keyword(KW::NAME) {
Some(o) if vm.is_none(&o) => Ok(DEFAULT_DIR_FD),
@@ -271,8 +274,8 @@ pub(super) mod _os {
struct OpenArgs<'fd> {
path: OsPath,
flags: i32,
#[pyarg(any, default)]
mode: Option<i32>,
#[pyarg(any, default = 0o777)]
mode: i32,
#[pyarg(flatten)]
dir_fd: DirFd<'fd, { OPEN_DIR_FD as usize }>,
}
@@ -286,11 +289,10 @@ pub(super) mod _os {
pub(crate) fn os_open(
name: OsPath,
flags: i32,
mode: Option<i32>,
mode: i32,
dir_fd: DirFd<'_, { OPEN_DIR_FD as usize }>,
vm: &VirtualMachine,
) -> PyResult<crt_fd::Owned> {
let mode = mode.unwrap_or(0o777);
#[cfg(windows)]
let fd = {
let [] = dir_fd.0;
@@ -399,24 +401,26 @@ pub(super) mod _os {
}
}
#[pyfunction]
fn mkdir(
#[derive(FromArgs)]
struct MkdirArgs<'a> {
#[pyarg(any)]
path: OsPath,
mode: OptionalArg<RawMode>,
#[cfg_attr(not(any(unix, target_os = "wasi")), expect(unused_variables))] dir_fd: DirFd<
'_,
{ MKDIR_DIR_FD as usize },
>,
vm: &VirtualMachine,
) -> PyResult<()> {
let mode = mode.unwrap_or(0o777);
#[pyarg(any, default = 0o777)]
mode: RawMode,
#[pyarg(flatten)]
#[cfg_attr(not(any(unix, target_os = "wasi")), expect(unused))]
dir_fd: DirFd<'a, { MKDIR_DIR_FD as usize }>,
}
#[pyfunction]
fn mkdir(args: MkdirArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
#[cfg(any(unix, target_os = "wasi"))]
let dir_fd = dir_fd.get_opt();
let dir_fd = args.dir_fd.get_opt();
#[cfg(not(any(unix, target_os = "wasi")))]
let dir_fd = None;
crate::host_env::posix::make_dir(dir_fd, &path.path, mode)
.map_err(|err| OSErrorBuilder::with_filename(&err, path, vm))
crate::host_env::posix::make_dir(dir_fd, &args.path.path, args.mode)
.map_err(|err| OSErrorBuilder::with_filename(&err, args.path, vm))
}
#[pyfunction]
@@ -446,13 +450,16 @@ pub(super) mod _os {
const LISTDIR_FD: bool = cfg!(all(unix, not(target_os = "redox")));
#[derive(FromArgs)]
struct ListDirArgs<'a> {
#[pyarg(any, default = None)]
path: Option<OsPathOrFd<'a>>,
}
#[pyfunction]
fn listdir(
path: OptionalArg<Option<OsPathOrFd<'_>>>,
vm: &VirtualMachine,
) -> PyResult<Vec<PyObjectRef>> {
let path = path
.flatten()
fn listdir(args: ListDirArgs<'_>, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
let path = args
.path
.unwrap_or_else(|| OsPathOrFd::Path(OsPath::new_str(".")));
let list = match path {
OsPathOrFd::Path(path) => {
@@ -822,7 +829,7 @@ pub(super) mod _os {
dir_fd
};
let do_stat = |follow_symlinks| {
stat(
stat_at(
OsPath {
path: self.pathval.as_os_str().to_owned(),
origin: None,
@@ -1457,9 +1464,17 @@ pub(super) mod _os {
}
}
#[pyfunction]
#[pyfunction(name = "fstat")]
fn stat(
#[derive(FromArgs)]
struct StatArgs<'a> {
#[pyarg(any)]
path: OsPathOrFd<'a>,
#[pyarg(flatten)]
dir_fd: DirFd<'a, { STAT_DIR_FD as usize }>,
#[pyarg(flatten)]
follow_symlinks: FollowSymlinks,
}
fn stat_at(
path: OsPathOrFd<'_>,
dir_fd: DirFd<'_, { STAT_DIR_FD as usize }>,
follow_symlinks: FollowSymlinks,
@@ -1474,13 +1489,24 @@ pub(super) mod _os {
Ok(StatResultData::from_stat(&stat, vm).to_pyobject(vm))
}
#[pyfunction]
#[pyfunction(name = "fstat")]
fn stat(args: StatArgs<'_>, vm: &VirtualMachine) -> PyResult {
let StatArgs {
path,
dir_fd,
follow_symlinks,
} = args;
stat_at(path, dir_fd, follow_symlinks, vm)
}
#[pyfunction]
fn lstat(
path: OsPath,
dir_fd: DirFd<'_, { STAT_DIR_FD as usize }>,
vm: &VirtualMachine,
) -> PyResult {
stat(path.into(), dir_fd, FollowSymlinks(false), vm)
stat_at(path.into(), dir_fd, FollowSymlinks(false), vm)
}
fn curdir_inner(vm: &VirtualMachine) -> PyResult<PathBuf> {
+8 -20
View File
@@ -1,13 +1,12 @@
import inspect
import os
import sys
# __text_signature__ is generated from the Rust parameter list, so it must not
# describe parameters the function does not actually take, and must mark the
# ones it does take as positional-only.
# No phantom `module` parameter. RustPython's #[pyfunction]s take no module
# argument, so `__self__` is None and inspect has nothing to strip.
# No phantom `module` parameter. The signature marks it as `$module` and
# `__self__` is the module, so inspect strips it.
for f in (len, abs, hash, id, repr, bin, ord, divmod, hex, oct, chr, callable):
assert "module" not in inspect.signature(f).parameters, f.__name__
@@ -59,20 +58,9 @@ assert str(inspect.signature(list.__dir__)) == "(self, /)"
assert str(inspect.signature([].__dir__)) == "()"
assert str(inspect.signature(float.fromhex)) == "(string, /)"
if sys.implementation.name == "rustpython":
# Functions whose Rust arguments are destructuring patterns rather than
# plain names get no signature at all, instead of emitting text that is not
# valid Python and makes inspect.signature() raise "invalid signature".
#
# CPython does have signatures for these, hand-written via Argument Clinic.
# We cannot derive them until FromArgs reports the parameters of its own
# structs, so until then we report no signature, which is at least how
# CPython behaves for the builtins it has no signature for.
for f in (round, sum):
assert f.__text_signature__ is None, f.__name__
try:
inspect.signature(f)
except ValueError as e:
assert "no signature found" in str(e), str(e)
else:
raise AssertionError(f"{f.__name__} should have no signature")
# Functions whose arguments come from a FromArgs struct report the struct's
# parameters.
assert round.__text_signature__ == "($module, /, number, ndigits=None)"
assert sum.__text_signature__ == "($module, iterable, /, start=0)"
assert str(inspect.signature(round)) == "(number, ndigits=None)"
assert str(inspect.signature(sum)) == "(iterable, /, start=0)"