Skip to content

Commit ff1d372

Browse files
committed
Wrap native static methods in staticmethod descriptors
Assisted-by: Codex:GPT-6
1 parent b85582d commit ff1d372

6 files changed

Lines changed: 106 additions & 12 deletions

File tree

‎Lib/test/test_pickle.py‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,6 @@ def loads(self, buf, **kwds):
9797
u = self.unpickler(f, **kwds)
9898
return u.load()
9999

100-
@unittest.expectedFailure # TODO: RUSTPYTHON
101-
def test_c_methods(self):
102-
return super().test_c_methods()
103-
104100
@unittest.expectedFailureIf(sys.getrecursionlimit() == 256, "TODO: RUSTPYTHON; default debug recursion limit is 256")
105101
def test_deep_nested_struct_frozendict(self):
106102
return super().test_deep_nested_struct_frozendict()
@@ -126,10 +122,6 @@ def loads(self, buf, **kwds):
126122
test_find_class = None
127123
test_custom_find_class = None
128124

129-
@unittest.expectedFailure # TODO: RUSTPYTHON
130-
def test_c_methods(self):
131-
return super().test_c_methods()
132-
133125

134126
class PersistentPicklerUnpicklerMixin(object):
135127

‎crates/vm/src/builtins/staticmethod.rs‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
use super::{
2-
PyGenericAlias, PyStr, PyType, PyTypeRef,
2+
PyDictRef, PyGenericAlias, PyStr, PyType, PyTypeRef,
3+
builtin_func::PyNativeFunction,
34
classmethod::{
45
descriptor_get_wrapped_attribute, descriptor_set_wrapped_attribute, functools_wraps,
56
},
7+
type_::{PyAttributes, rendered_item_doc},
68
};
79
use crate::{
810
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
911
class::{PyClassDef, PyClassImpl},
12+
common::{hash::HashSecret, lock::PyMutex, wtf8::Wtf8Buf},
1013
function::{FuncArgs, PySetterValue},
1114
object::PyAtomicRef,
1215
types::{Callable, Constructor, GetDescriptor, Initializer, Representable},
1316
};
1417

18+
// Context::genesis builds native descriptors before the first interpreter
19+
// chooses its hash seed. Delay hashing their metadata until that seed exists.
20+
rustpython_common::static_cell! {
21+
static NATIVE_METADATA: PyMutex<NativeMetadata>;
22+
}
23+
24+
#[derive(Default)]
25+
struct NativeMetadata {
26+
hash_secret: Option<&'static HashSecret>,
27+
pending: Vec<(PyDictRef, PyAttributes)>,
28+
}
29+
1530
#[pyclass(module = false, name = "staticmethod", traverse)]
1631
#[derive(Debug)]
1732
pub struct PyStaticMethod {
@@ -75,6 +90,58 @@ impl Constructor for PyStaticMethod {
7590
}
7691

7792
impl PyStaticMethod {
93+
pub(crate) fn new_native(
94+
callable: PyRef<PyNativeFunction>,
95+
class: &Py<PyType>,
96+
ctx: &Context,
97+
) -> PyRef<Self> {
98+
let mut qualname = class.heaptype_ext.as_ref().map_or_else(
99+
|| Wtf8Buf::from_string(class.name().to_string()),
100+
|ext| ext.qualname.read().as_wtf8().to_owned(),
101+
);
102+
qualname.push_str(".");
103+
qualname.push_str(callable.value.name);
104+
let attrs = [
105+
(
106+
identifier!(ctx, __module__),
107+
callable.module.load_owned().unwrap_or_else(|| ctx.none()),
108+
),
109+
(
110+
identifier!(ctx, __name__),
111+
ctx.new_str(callable.value.name).into(),
112+
),
113+
(identifier!(ctx, __qualname__), ctx.new_str(qualname).into()),
114+
(
115+
identifier!(ctx, __doc__),
116+
rendered_item_doc(callable.value.name, callable.value.item_doc())
117+
.map_or_else(|| ctx.none(), |doc| ctx.new_str(doc).into()),
118+
),
119+
]
120+
.into_iter()
121+
.collect();
122+
let dict = ctx.new_dict();
123+
let wrapper = PyRef::new_ref(
124+
Self::new(callable.into()),
125+
ctx.types.staticmethod_type.to_owned(),
126+
Some(dict.clone()),
127+
);
128+
let mut metadata = NATIVE_METADATA.get_or_init(PyMutex::default).lock();
129+
if let Some(secret) = metadata.hash_secret {
130+
dict.entries.initialize_attributes(attrs, secret);
131+
} else {
132+
metadata.pending.push((dict, attrs));
133+
}
134+
wrapper
135+
}
136+
137+
pub(crate) fn init_native_metadata(secret: &'static HashSecret) {
138+
let mut metadata = NATIVE_METADATA.get_or_init(PyMutex::default).lock();
139+
metadata.hash_secret = Some(secret);
140+
for (dict, attrs) in metadata.pending.drain(..) {
141+
dict.entries.initialize_attributes(attrs, secret);
142+
}
143+
}
144+
78145
#[must_use]
79146
pub fn new(callable: PyObjectRef) -> Self {
80147
Self {

‎crates/vm/src/dict_inner.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,33 @@ impl<T> Default for Dict<T> {
252252
}
253253
}
254254

255+
impl Dict {
256+
// Initialize an unpublished, empty dict from unique interned names. This
257+
// needs the process hash secret, but cannot call Python during bootstrap.
258+
pub(crate) fn initialize_attributes(
259+
&self,
260+
attrs: crate::builtins::type_::PyAttributes,
261+
secret: &hash::HashSecret,
262+
) {
263+
let mut inner = self.write();
264+
assert_eq!(inner.used, 0);
265+
inner.entries = attrs
266+
.into_iter()
267+
.map(|(key, value)| {
268+
Some(DictEntry {
269+
hash: secret.hash_bytes(key.as_bytes()),
270+
key: key.to_owned().into(),
271+
index: 0,
272+
value,
273+
})
274+
})
275+
.collect();
276+
inner.used = inner.entries.len();
277+
let size = inner.used * 2;
278+
inner.resize(size);
279+
}
280+
}
281+
255282
#[derive(Clone)]
256283
struct DictEntry<T> {
257284
hash: HashValue,

‎crates/vm/src/function/method.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
Context, Py, PyObjectRef, PyPayload, PyRef, VirtualMachine,
33
builtins::{
4-
PyType,
4+
PyStaticMethod, PyType,
55
builtin_func::{PyNativeFunction, PyNativeMethod},
66
descriptor::{PyClassMethodDescriptor, PyMethodDescriptor},
77
},
@@ -183,7 +183,7 @@ impl PyMethodDef {
183183
} else if self.flags.contains(PyMethodFlags::CLASS) {
184184
self.build_classmethod(ctx, class).into()
185185
} else if self.flags.contains(PyMethodFlags::STATIC) {
186-
self.build_staticmethod(ctx, class).into()
186+
PyStaticMethod::new_native(self.build_staticmethod(ctx, class), class, ctx).into()
187187
} else {
188188
unreachable!()
189189
}

‎crates/vm/src/vm/mod.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,13 +1047,14 @@ static HASH_SECRET: std::sync::OnceLock<HashSecret> = std::sync::OnceLock::new()
10471047
/// `hash_seed` is used only when the secret is not set yet. `None` draws a
10481048
/// random seed. A later call keeps the existing secret and ignores `hash_seed`.
10491049
pub(crate) fn init_hash_secret(hash_seed: Option<u32>) {
1050-
let _ = HASH_SECRET.get_or_init(|| {
1050+
let secret = HASH_SECRET.get_or_init(|| {
10511051
let seed = hash_seed.unwrap_or_else(|| {
10521052
// os_random is expensive, but this runs only once per process.
10531053
u32::from_ne_bytes(rustpython_common::rand::os_random())
10541054
});
10551055
HashSecret::new(seed)
10561056
});
1057+
builtins::PyStaticMethod::init_native_metadata(secret);
10571058
}
10581059

10591060
/// Process-wide `_Py_HashSecret` used for str/bytes hashing.

‎extra_tests/snippets/builtin_type.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,3 +817,10 @@ class Diamond(FirstChild, SecondChild):
817817
value = Diamond()
818818
value.left = 43
819819
assert value.left == 43
820+
821+
# Native static methods retain their descriptor in the class dictionary.
822+
native_static = str.__dict__["maketrans"]
823+
assert isinstance(native_static, staticmethod)
824+
assert native_static.__func__ is str.maketrans
825+
for name in ("__module__", "__name__", "__qualname__", "__doc__"):
826+
assert getattr(native_static, name) == getattr(str.maketrans, name)

0 commit comments

Comments
 (0)