diff --git a/Lib/_threading_local.py b/Lib/_threading_local.py index e52043399..0b9e5d3bb 100644 --- a/Lib/_threading_local.py +++ b/Lib/_threading_local.py @@ -4,133 +4,6 @@ class. Depending on the version of Python you're using, there may be a faster one available. You should always import the `local` class from `threading`.) - -Thread-local objects support the management of thread-local data. -If you have data that you want to be local to a thread, simply create -a thread-local object and use its attributes: - - >>> mydata = local() - >>> mydata.number = 42 - >>> mydata.number - 42 - -You can also access the local-object's dictionary: - - >>> mydata.__dict__ - {'number': 42} - >>> mydata.__dict__.setdefault('widgets', []) - [] - >>> mydata.widgets - [] - -What's important about thread-local objects is that their data are -local to a thread. If we access the data in a different thread: - - >>> log = [] - >>> def f(): - ... items = sorted(mydata.__dict__.items()) - ... log.append(items) - ... mydata.number = 11 - ... log.append(mydata.number) - - >>> import threading - >>> thread = threading.Thread(target=f) - >>> thread.start() - >>> thread.join() - >>> log - [[], 11] - -we get different data. Furthermore, changes made in the other thread -don't affect data seen in this thread: - - >>> mydata.number - 42 - -Of course, values you get from a local object, including a __dict__ -attribute, are for whatever thread was current at the time the -attribute was read. For that reason, you generally don't want to save -these values across threads, as they apply only to the thread they -came from. - -You can create custom local objects by subclassing the local class: - - >>> class MyLocal(local): - ... number = 2 - ... initialized = False - ... def __init__(self, **kw): - ... if self.initialized: - ... raise SystemError('__init__ called too many times') - ... self.initialized = True - ... self.__dict__.update(kw) - ... def squared(self): - ... return self.number ** 2 - -This can be useful to support default values, methods and -initialization. Note that if you define an __init__ method, it will be -called each time the local object is used in a separate thread. This -is necessary to initialize each thread's dictionary. - -Now if we create a local object: - - >>> mydata = MyLocal(color='red') - -Now we have a default number: - - >>> mydata.number - 2 - -an initial color: - - >>> mydata.color - 'red' - >>> del mydata.color - -And a method that operates on the data: - - >>> mydata.squared() - 4 - -As before, we can access the data in a separate thread: - - >>> log = [] - >>> thread = threading.Thread(target=f) - >>> thread.start() - >>> thread.join() - >>> log - [[('color', 'red'), ('initialized', True)], 11] - -without affecting this thread's data: - - >>> mydata.number - 2 - >>> mydata.color - Traceback (most recent call last): - ... - AttributeError: 'MyLocal' object has no attribute 'color' - -Note that subclasses can define slots, but they are not thread -local. They are shared across threads: - - >>> class MyLocal(local): - ... __slots__ = 'number' - - >>> mydata = MyLocal() - >>> mydata.number = 42 - >>> mydata.color = 'red' - -So, the separate thread: - - >>> thread = threading.Thread(target=f) - >>> thread.start() - >>> thread.join() - -affects what we see: - - >>> # TODO: RUSTPYTHON, __slots__ - >>> mydata.number #doctest: +SKIP - 11 - ->>> del mydata """ from weakref import ref @@ -194,7 +67,6 @@ class _localimpl: @contextmanager def _patch(self): - old = object.__getattribute__(self, '__dict__') impl = object.__getattribute__(self, '_local__impl') try: dct = impl.get_dict() @@ -205,13 +77,12 @@ def _patch(self): with impl.locallock: object.__setattr__(self, '__dict__', dct) yield - object.__setattr__(self, '__dict__', old) class local: __slots__ = '_local__impl', '__dict__' - def __new__(cls, *args, **kw): + def __new__(cls, /, *args, **kw): if (args or kw) and (cls.__init__ is object.__init__): raise TypeError("Initialization arguments are not supported") self = object.__new__(cls) diff --git a/stdlib/src/pyexpat.rs b/stdlib/src/pyexpat.rs index 2363e6bed..d74b54310 100644 --- a/stdlib/src/pyexpat.rs +++ b/stdlib/src/pyexpat.rs @@ -18,7 +18,7 @@ pub fn make_module(vm: &VirtualMachine) -> PyRef { macro_rules! create_property { ($ctx: expr, $attributes: expr, $name: expr, $class: expr, $element: ident) => { - let attr = $ctx.new_getset( + let attr = $ctx.new_static_getset( $name, $class, move |this: &PyExpatLikeXmlParser| this.$element.read().clone(), diff --git a/vm/src/builtins/getset.rs b/vm/src/builtins/getset.rs index cccd66d27..4b966bbc3 100644 --- a/vm/src/builtins/getset.rs +++ b/vm/src/builtins/getset.rs @@ -4,6 +4,7 @@ use super::PyType; use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + builtins::type_::PointerSlot, class::PyClassImpl, function::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySetterFunc, PySetterValue}, types::{GetDescriptor, Unconstructible}, @@ -12,7 +13,7 @@ use crate::{ #[pyclass(module = false, name = "getset_descriptor")] pub struct PyGetSet { name: String, - class: &'static Py, + class: PointerSlot>, // A class type freed before getset is non-sense. getter: Option, setter: Option, // doc: Option, @@ -72,7 +73,7 @@ impl PyGetSet { pub fn new(name: String, class: &'static Py) -> Self { Self { name, - class, + class: PointerSlot::from(class), getter: None, setter: None, } @@ -138,13 +139,17 @@ impl PyGetSet { #[pygetset] fn __qualname__(&self) -> String { - format!("{}.{}", self.class.slot_name(), self.name.clone()) + format!( + "{}.{}", + unsafe { self.class.borrow_static() }.slot_name(), + self.name.clone() + ) } #[pymember] fn __objclass__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { let zelf: &Py = zelf.try_to_value(vm)?; - Ok(zelf.class.to_owned().into()) + Ok(unsafe { zelf.class.borrow_static() }.to_owned().into()) } } impl Unconstructible for PyGetSet {} diff --git a/vm/src/builtins/property.rs b/vm/src/builtins/property.rs index e7cc8a6c4..d515f431e 100644 --- a/vm/src/builtins/property.rs +++ b/vm/src/builtins/property.rs @@ -386,7 +386,7 @@ pub(crate) fn init(context: &Context) { // This is a bit unfortunate, but this instance attribute overlaps with the // class __doc__ string.. extend_class!(context, context.types.property_type, { - "__doc__" => context.new_getset( + "__doc__" => context.new_static_getset( "__doc__", context.types.property_type, PyProperty::doc_getter, diff --git a/vm/src/builtins/type.rs b/vm/src/builtins/type.rs index ae6cfd68e..cc6c7c1d4 100644 --- a/vm/src/builtins/type.rs +++ b/vm/src/builtins/type.rs @@ -31,7 +31,7 @@ use crate::{ }; use indexmap::{IndexMap, map::Entry}; use itertools::Itertools; -use std::{borrow::Borrow, collections::HashSet, fmt, ops::Deref, pin::Pin, ptr::NonNull}; +use std::{borrow::Borrow, collections::HashSet, ops::Deref, pin::Pin, ptr::NonNull}; #[pyclass(module = false, name = "type", traverse = "manual")] pub struct PyType { @@ -69,6 +69,9 @@ pub struct HeapTypeExt { pub struct PointerSlot(NonNull); +unsafe impl Sync for PointerSlot {} +unsafe impl Send for PointerSlot {} + impl PointerSlot { pub unsafe fn borrow_static(&self) -> &'static T { unsafe { self.0.as_ref() } @@ -126,14 +129,14 @@ unsafe impl Traverse for PyAttributes { } } -impl fmt::Display for PyType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.name(), f) +impl std::fmt::Display for PyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.name(), f) } } -impl fmt::Debug for PyType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Debug for PyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "[PyType {}]", &self.name()) } } @@ -908,7 +911,7 @@ impl Constructor for PyType { let __dict__ = identifier!(vm, __dict__); attributes.entry(__dict__).or_insert_with(|| { vm.ctx - .new_getset( + .new_static_getset( "__dict__", vm.ctx.types.type_type, subtype_get_dict, diff --git a/vm/src/class.rs b/vm/src/class.rs index d3595f980..799fde164 100644 --- a/vm/src/class.rs +++ b/vm/src/class.rs @@ -85,7 +85,7 @@ pub trait PyClassImpl: PyClassDef { let __dict__ = identifier!(ctx, __dict__); class.set_attr( __dict__, - ctx.new_getset( + ctx.new_static_getset( "__dict__", class, crate::builtins::object::object_get_dict, diff --git a/vm/src/vm/context.rs b/vm/src/vm/context.rs index 924c52820..78a5b0c3d 100644 --- a/vm/src/vm/context.rs +++ b/vm/src/vm/context.rs @@ -570,7 +570,7 @@ impl Context { PyRef::new_ref(getset, self.types.getset_type.to_owned(), None) } - pub fn new_getset( + pub fn new_static_getset( &self, name: impl Into, class: &'static Py, @@ -586,6 +586,26 @@ impl Context { PyRef::new_ref(getset, self.types.getset_type.to_owned(), None) } + /// Creates a new `PyGetSet` with a heap type. + /// + /// # Safety + /// In practice, this constructor is safe because a getset is always owned by its `class` type. + /// However, it can be broken if used unconventionally. + pub unsafe fn new_getset( + &self, + name: impl Into, + class: &Py, + g: G, + s: S, + ) -> PyRef + where + G: IntoPyGetterFunc, + S: IntoPySetterFunc, + { + let class = unsafe { &*(class as *const _) }; + self.new_static_getset(name, class, g, s) + } + pub fn new_base_object(&self, class: PyTypeRef, dict: Option) -> PyObjectRef { debug_assert_eq!( class.slots.flags.contains(PyTypeFlags::HAS_DICT),