Skip to content

[BREAKING CHANGE] Writing magic method names as full name #5842

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 27, 2025
Merged
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
2 changes: 2 additions & 0 deletions .cspell.dict/cpython.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ SA_ONSTACK
stackdepth
stringlib
structseq
subparams
tok_oldval
tvars
unaryop
unparse
unparser
Expand Down
6 changes: 6 additions & 0 deletions .cspell.dict/python-more.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ getrandom
getrecursionlimit
getrefcount
getsizeof
getswitchinterval
getweakrefcount
getweakrefs
getwindowsversion
Expand Down Expand Up @@ -167,13 +168,17 @@ pycs
pyexpat
PYTHONBREAKPOINT
PYTHONDEBUG
PYTHONDONTWRITEBYTECODE
PYTHONHASHSEED
PYTHONHOME
PYTHONINSPECT
PYTHONINTMAXSTRDIGITS
PYTHONNOUSERSITE
PYTHONOPTIMIZE
PYTHONPATH
PYTHONPATH
PYTHONSAFEPATH
PYTHONUNBUFFERED
PYTHONVERBOSE
PYTHONWARNDEFAULTENCODING
PYTHONWARNINGS
Expand Down Expand Up @@ -206,6 +211,7 @@ seennl
setattro
setcomp
setrecursionlimit
setswitchinterval
showwarnmsg
signum
slotnames
Expand Down
4 changes: 4 additions & 0 deletions .cspell.dict/rust-more.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ arrayvec
bidi
biguint
bindgen
bitand
bitflags
bitor
bitxor
bstr
byteorder
byteset
Expand All @@ -15,6 +17,7 @@ cranelift
cstring
datelike
deserializer
deserializers
fdiv
flamescope
flate2
Expand All @@ -31,6 +34,7 @@ keccak
lalrpop
lexopt
libc
libcall
libloading
libz
longlong
Expand Down
3 changes: 3 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
"GetSet",
"groupref",
"internable",
"jitted",
"jitting",
"lossily",
"makeunicodedata",
"miri",
Expand All @@ -85,6 +87,7 @@
"pygetset",
"pyimpl",
"pylib",
"pymath",
"pymember",
"PyMethod",
"PyModule",
Expand Down
3 changes: 2 additions & 1 deletion common/src/str.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// cspell:ignore uncomputed
use crate::atomic::{PyAtomic, Radium};
use crate::format::CharLen;
use crate::wtf8::{CodePoint, Wtf8, Wtf8Buf};
Expand Down Expand Up @@ -424,7 +425,7 @@ pub fn zfill(bytes: &[u8], width: usize) -> Vec<u8> {
}
}

/// Convert a string to ascii compatible, escaping unicodes into escape
/// Convert a string to ascii compatible, escaping unicode-s into escape
/// sequences.
pub fn to_ascii(value: &str) -> AsciiString {
let mut ascii = Vec::new();
Expand Down
26 changes: 13 additions & 13 deletions compiler/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2361,7 +2361,7 @@ impl Compiler<'_> {
// self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse)?;
// }

// // Check that the number of subpatterns is not absurd.
// // Check that the number of sub-patterns is not absurd.
// if size.saturating_sub(1) > (i32::MAX as usize) {
// panic!("too many sub-patterns in mapping pattern");
// // return self.compiler_error("too many sub-patterns in mapping pattern");
Expand Down Expand Up @@ -2469,41 +2469,41 @@ impl Compiler<'_> {
emit!(self, Instruction::CopyItem { index: 1_u32 });
self.compile_pattern(alt, pc)?;

let nstores = pc.stores.len();
let n_stores = pc.stores.len();
if i == 0 {
// Save the captured names from the first alternative.
control = Some(pc.stores.clone());
} else {
let control_vec = control.as_ref().unwrap();
if nstores != control_vec.len() {
if n_stores != control_vec.len() {
return Err(self.error(CodegenErrorType::ConflictingNameBindPattern));
} else if nstores > 0 {
} else if n_stores > 0 {
// Check that the names occur in the same order.
for icontrol in (0..nstores).rev() {
let name = &control_vec[icontrol];
for i_control in (0..n_stores).rev() {
let name = &control_vec[i_control];
// Find the index of `name` in the current stores.
let istores =
let i_stores =
pc.stores.iter().position(|n| n == name).ok_or_else(|| {
self.error(CodegenErrorType::ConflictingNameBindPattern)
})?;
if icontrol != istores {
if i_control != i_stores {
// The orders differ; we must reorder.
assert!(istores < icontrol, "expected istores < icontrol");
let rotations = istores + 1;
assert!(i_stores < i_control, "expected i_stores < i_control");
let rotations = i_stores + 1;
// Rotate pc.stores: take a slice of the first `rotations` items...
let rotated = pc.stores[0..rotations].to_vec();
// Remove those elements.
for _ in 0..rotations {
pc.stores.remove(0);
}
// Insert the rotated slice at the appropriate index.
let insert_pos = icontrol - istores;
let insert_pos = i_control - i_stores;
for (j, elem) in rotated.into_iter().enumerate() {
pc.stores.insert(insert_pos + j, elem);
}
// Also perform the same rotation on the evaluation stack.
for _ in 0..(istores + 1) {
self.pattern_helper_rotate(icontrol + 1)?;
for _ in 0..(i_stores + 1) {
self.pattern_helper_rotate(i_control + 1)?;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ pub fn compile(
source_path: &str,
opts: CompileOpts,
) -> Result<CodeObject, CompileError> {
// TODO: do this less hackily; ruff's parser should translate a CRLF line
// TODO: do this less hacky; ruff's parser should translate a CRLF line
// break in a multiline string into just an LF in the parsed value
#[cfg(windows)]
let source = &source.replace("\r\n", "\n");
Expand Down
65 changes: 43 additions & 22 deletions derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,13 @@ where
let raw = item_meta.raw()?;
let sig_doc = text_signature(func.sig(), &py_name);

// Add #[allow(non_snake_case)] for setter methods like set___name__
let method_name = ident.to_string();
if method_name.starts_with("set_") && method_name.contains("__") {
let allow_attr: Attribute = parse_quote!(#[allow(non_snake_case)]);
args.attrs.push(allow_attr);
}

let doc = args.attrs.doc().map(|doc| format_doc(&sig_doc, &doc));
args.context.method_items.add_item(MethodNurseryItem {
py_name,
Expand Down Expand Up @@ -780,6 +787,13 @@ where
let item_meta = GetSetItemMeta::from_attr(ident.clone(), &item_attr)?;

let (py_name, kind) = item_meta.getset_name()?;

// Add #[allow(non_snake_case)] for setter methods
if matches!(kind, GetSetItemKind::Set) {
let allow_attr: Attribute = parse_quote!(#[allow(non_snake_case)]);
args.attrs.push(allow_attr);
}

args.context
.getset_items
.add_item(py_name, args.cfgs.to_vec(), kind, ident.clone())?;
Expand Down Expand Up @@ -934,6 +948,12 @@ where
_ => MemberKind::ObjectEx,
};

// Add #[allow(non_snake_case)] for setter methods
if matches!(member_item_kind, MemberItemKind::Set) {
let allow_attr: Attribute = parse_quote!(#[allow(non_snake_case)]);
args.attrs.push(allow_attr);
}

args.context.member_items.add_item(
py_name,
member_item_kind,
Expand Down Expand Up @@ -1208,7 +1228,7 @@ impl ToTokens for MemberNursery {
struct MethodItemMeta(ItemMetaInner);

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

fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
Expand All @@ -1225,27 +1245,18 @@ impl MethodItemMeta {
fn method_name(&self) -> Result<String> {
let inner = self.inner();
let name = inner._optional_str("name")?;
let magic = inner._bool("magic")?;
if magic && name.is_some() {
bail_span!(
&inner.meta_ident,
"A #[{}] method cannot be magic and have a specified name, choose one.",
inner.meta_name()
);
}
Ok(if let Some(name) = name {
name
} else {
let name = inner.item_name();
if magic { format!("__{name}__") } else { name }
inner.item_name()
})
}
}

struct GetSetItemMeta(ItemMetaInner);

impl ItemMeta for GetSetItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["name", "magic", "setter", "deleter"];
const ALLOWED_NAMES: &'static [&'static str] = &["name", "setter", "deleter"];

fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
Expand All @@ -1258,7 +1269,6 @@ impl ItemMeta for GetSetItemMeta {
impl GetSetItemMeta {
fn getset_name(&self) -> Result<(String, GetSetItemKind)> {
let inner = self.inner();
let magic = inner._bool("magic")?;
let kind = match (inner._bool("setter")?, inner._bool("deleter")?) {
(false, false) => GetSetItemKind::Get,
(true, false) => GetSetItemKind::Set,
Expand Down Expand Up @@ -1299,12 +1309,11 @@ impl GetSetItemMeta {
))
}
};
let name = match kind {
match kind {
GetSetItemKind::Get => sig_name,
GetSetItemKind::Set => extract_prefix_name("set_", "setter")?,
GetSetItemKind::Delete => extract_prefix_name("del_", "deleter")?,
};
if magic { format!("__{name}__") } else { name }
}
};
Ok((py_name, kind))
}
Expand Down Expand Up @@ -1353,7 +1362,7 @@ impl ItemMeta for SlotItemMeta {
impl SlotItemMeta {
fn slot_name(&self) -> Result<Ident> {
let inner = self.inner();
let slot_name = if let Some((_, meta)) = inner.meta_map.get("name") {
let method_name = if let Some((_, meta)) = inner.meta_map.get("name") {
match meta {
Meta::Path(path) => path.get_ident().cloned(),
_ => None,
Expand All @@ -1367,19 +1376,32 @@ impl SlotItemMeta {
};
Some(name)
};
slot_name.ok_or_else(|| {
let method_name = method_name.ok_or_else(|| {
err_span!(
inner.meta_ident,
"#[pyslot] must be of the form #[pyslot] or #[pyslot(slot_name)]",
)
})
})?;

// Strip double underscores from slot names like __init__ -> init
let method_name_str = method_name.to_string();
let slot_name = if method_name_str.starts_with("__")
&& method_name_str.ends_with("__")
&& method_name_str.len() > 4
{
&method_name_str[2..method_name_str.len() - 2]
} else {
&method_name_str
};

Ok(proc_macro2::Ident::new(slot_name, slot_name.span()))
}
}

struct MemberItemMeta(ItemMetaInner);

impl ItemMeta for MemberItemMeta {
const ALLOWED_NAMES: &'static [&'static str] = &["magic", "type", "setter"];
const ALLOWED_NAMES: &'static [&'static str] = &["type", "setter"];

fn from_inner(inner: ItemMetaInner) -> Self {
Self(inner)
Expand Down Expand Up @@ -1416,7 +1438,6 @@ impl MemberItemMeta {
))
}
};
let magic = inner._bool("magic")?;
let kind = if inner._bool("setter")? {
MemberItemKind::Set
} else {
Expand All @@ -1426,7 +1447,7 @@ impl MemberItemMeta {
MemberItemKind::Get => sig_name,
MemberItemKind::Set => extract_prefix_name("set_", "setter")?,
};
Ok((if magic { format!("__{name}__") } else { name }, kind))
Ok((name, kind))
}

fn member_kind(&self) -> Result<Option<String>> {
Expand Down
11 changes: 0 additions & 11 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,6 @@ pub fn derive_from_args(input: TokenStream) -> TokenStream {
/// but so does any object that implements `PyValue`.
/// Consider using `OptionalArg` for optional arguments.
/// #### Arguments
/// - `magic`: marks the method as a magic method: the method name is surrounded with double underscores.
/// ```rust, ignore
/// #[pyclass]
/// impl MyStruct {
/// // This will be called as the `__add__` method in Python.
/// #[pymethod(magic)]
/// fn add(&self, other: &Self) -> PyResult<i32> {
/// ...
/// }
/// }
/// ```
/// - `name`: the name of the method in Python,
/// by default it is the same as the Rust method, or surrounded by double underscores if magic is present.
/// This overrides `magic` and the default name and cannot be used with `magic` to prevent ambiguity.
Expand Down
3 changes: 3 additions & 0 deletions example_projects/barebone/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ edition = "2021"
rustpython-vm = { path = "../../vm", default-features = false }

[workspace]

[patch.crates-io]
radium = { version = "1.1.0", git = "https://github.com/youknowone/ferrilab", branch = "fix-nightly" }
3 changes: 3 additions & 0 deletions example_projects/frozen_stdlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ rustpython-vm = { path = "../../vm", default-features = false, features = ["free
rustpython-pylib = { path = "../../pylib", default-features = false, features = ["freeze-stdlib"] }

[workspace]

[patch.crates-io]
radium = { version = "1.1.0", git = "https://github.com/youknowone/ferrilab", branch = "fix-nightly" }
1 change: 1 addition & 0 deletions example_projects/frozen_stdlib/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// cspell:ignore aheui
/// Setting up a project with a frozen stdlib can be done *either* by using `rustpython::InterpreterConfig` or `rustpython_vm::Interpreter::with_init`.
/// See each function for example.
///
Expand Down
2 changes: 1 addition & 1 deletion examples/parse_folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fn parse_folder(path: &Path) -> std::io::Result<Vec<ParsedFile>> {
let parsed_file = parse_python_file(&path);
match &parsed_file.result {
Ok(_) => {}
Err(y) => error!("Erreur in file {path:?} {y:?}"),
Err(y) => error!("Error in file {path:?} {y:?}"),
}

res.push(parsed_file);
Expand Down
4 changes: 2 additions & 2 deletions jit/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ impl StackMachine {
}

pub fn run(&mut self, code: CodeObject) {
let mut oparg_state = OpArgState::default();
let mut op_arg_state = OpArgState::default();
let _ = code.instructions.iter().try_for_each(|&word| {
let (instruction, arg) = oparg_state.get(word);
let (instruction, arg) = op_arg_state.get(word);
self.process_instruction(instruction, arg, &code.constants, &code.names)
});
}
Expand Down
Loading
Loading