Skip to content

Commit 07a04ac

Browse files
authored
Allow heap getset creation (#5854)
* Update _threading_local.py * allow heap-getset
1 parent 3a85499 commit 07a04ac

File tree

7 files changed

+44
-145
lines changed

7 files changed

+44
-145
lines changed

Lib/_threading_local.py

Lines changed: 1 addition & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -4,133 +4,6 @@
44
class. Depending on the version of Python you're using, there may be a
55
faster one available. You should always import the `local` class from
66
`threading`.)
7-
8-
Thread-local objects support the management of thread-local data.
9-
If you have data that you want to be local to a thread, simply create
10-
a thread-local object and use its attributes:
11-
12-
>>> mydata = local()
13-
>>> mydata.number = 42
14-
>>> mydata.number
15-
42
16-
17-
You can also access the local-object's dictionary:
18-
19-
>>> mydata.__dict__
20-
{'number': 42}
21-
>>> mydata.__dict__.setdefault('widgets', [])
22-
[]
23-
>>> mydata.widgets
24-
[]
25-
26-
What's important about thread-local objects is that their data are
27-
local to a thread. If we access the data in a different thread:
28-
29-
>>> log = []
30-
>>> def f():
31-
... items = sorted(mydata.__dict__.items())
32-
... log.append(items)
33-
... mydata.number = 11
34-
... log.append(mydata.number)
35-
36-
>>> import threading
37-
>>> thread = threading.Thread(target=f)
38-
>>> thread.start()
39-
>>> thread.join()
40-
>>> log
41-
[[], 11]
42-
43-
we get different data. Furthermore, changes made in the other thread
44-
don't affect data seen in this thread:
45-
46-
>>> mydata.number
47-
42
48-
49-
Of course, values you get from a local object, including a __dict__
50-
attribute, are for whatever thread was current at the time the
51-
attribute was read. For that reason, you generally don't want to save
52-
these values across threads, as they apply only to the thread they
53-
came from.
54-
55-
You can create custom local objects by subclassing the local class:
56-
57-
>>> class MyLocal(local):
58-
... number = 2
59-
... initialized = False
60-
... def __init__(self, **kw):
61-
... if self.initialized:
62-
... raise SystemError('__init__ called too many times')
63-
... self.initialized = True
64-
... self.__dict__.update(kw)
65-
... def squared(self):
66-
... return self.number ** 2
67-
68-
This can be useful to support default values, methods and
69-
initialization. Note that if you define an __init__ method, it will be
70-
called each time the local object is used in a separate thread. This
71-
is necessary to initialize each thread's dictionary.
72-
73-
Now if we create a local object:
74-
75-
>>> mydata = MyLocal(color='red')
76-
77-
Now we have a default number:
78-
79-
>>> mydata.number
80-
2
81-
82-
an initial color:
83-
84-
>>> mydata.color
85-
'red'
86-
>>> del mydata.color
87-
88-
And a method that operates on the data:
89-
90-
>>> mydata.squared()
91-
4
92-
93-
As before, we can access the data in a separate thread:
94-
95-
>>> log = []
96-
>>> thread = threading.Thread(target=f)
97-
>>> thread.start()
98-
>>> thread.join()
99-
>>> log
100-
[[('color', 'red'), ('initialized', True)], 11]
101-
102-
without affecting this thread's data:
103-
104-
>>> mydata.number
105-
2
106-
>>> mydata.color
107-
Traceback (most recent call last):
108-
...
109-
AttributeError: 'MyLocal' object has no attribute 'color'
110-
111-
Note that subclasses can define slots, but they are not thread
112-
local. They are shared across threads:
113-
114-
>>> class MyLocal(local):
115-
... __slots__ = 'number'
116-
117-
>>> mydata = MyLocal()
118-
>>> mydata.number = 42
119-
>>> mydata.color = 'red'
120-
121-
So, the separate thread:
122-
123-
>>> thread = threading.Thread(target=f)
124-
>>> thread.start()
125-
>>> thread.join()
126-
127-
affects what we see:
128-
129-
>>> # TODO: RUSTPYTHON, __slots__
130-
>>> mydata.number #doctest: +SKIP
131-
11
132-
133-
>>> del mydata
1347
"""
1358

1369
from weakref import ref
@@ -194,7 +67,6 @@ def thread_deleted(_, idt=idt):
19467

19568
@contextmanager
19669
def _patch(self):
197-
old = object.__getattribute__(self, '__dict__')
19870
impl = object.__getattribute__(self, '_local__impl')
19971
try:
20072
dct = impl.get_dict()
@@ -205,13 +77,12 @@ def _patch(self):
20577
with impl.locallock:
20678
object.__setattr__(self, '__dict__', dct)
20779
yield
208-
object.__setattr__(self, '__dict__', old)
20980

21081

21182
class local:
21283
__slots__ = '_local__impl', '__dict__'
21384

214-
def __new__(cls, *args, **kw):
85+
def __new__(cls, /, *args, **kw):
21586
if (args or kw) and (cls.__init__ is object.__init__):
21687
raise TypeError("Initialization arguments are not supported")
21788
self = object.__new__(cls)

stdlib/src/pyexpat.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub fn make_module(vm: &VirtualMachine) -> PyRef<PyModule> {
1818

1919
macro_rules! create_property {
2020
($ctx: expr, $attributes: expr, $name: expr, $class: expr, $element: ident) => {
21-
let attr = $ctx.new_getset(
21+
let attr = $ctx.new_static_getset(
2222
$name,
2323
$class,
2424
move |this: &PyExpatLikeXmlParser| this.$element.read().clone(),

vm/src/builtins/getset.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use super::PyType;
55
use crate::{
66
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine,
7+
builtins::type_::PointerSlot,
78
class::PyClassImpl,
89
function::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySetterFunc, PySetterValue},
910
types::{GetDescriptor, Unconstructible},
@@ -12,7 +13,7 @@ use crate::{
1213
#[pyclass(module = false, name = "getset_descriptor")]
1314
pub struct PyGetSet {
1415
name: String,
15-
class: &'static Py<PyType>,
16+
class: PointerSlot<Py<PyType>>, // A class type freed before getset is non-sense.
1617
getter: Option<PyGetterFunc>,
1718
setter: Option<PySetterFunc>,
1819
// doc: Option<String>,
@@ -72,7 +73,7 @@ impl PyGetSet {
7273
pub fn new(name: String, class: &'static Py<PyType>) -> Self {
7374
Self {
7475
name,
75-
class,
76+
class: PointerSlot::from(class),
7677
getter: None,
7778
setter: None,
7879
}
@@ -138,13 +139,17 @@ impl PyGetSet {
138139

139140
#[pygetset]
140141
fn __qualname__(&self) -> String {
141-
format!("{}.{}", self.class.slot_name(), self.name.clone())
142+
format!(
143+
"{}.{}",
144+
unsafe { self.class.borrow_static() }.slot_name(),
145+
self.name.clone()
146+
)
142147
}
143148

144149
#[pymember]
145150
fn __objclass__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult {
146151
let zelf: &Py<Self> = zelf.try_to_value(vm)?;
147-
Ok(zelf.class.to_owned().into())
152+
Ok(unsafe { zelf.class.borrow_static() }.to_owned().into())
148153
}
149154
}
150155
impl Unconstructible for PyGetSet {}

vm/src/builtins/property.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ pub(crate) fn init(context: &Context) {
386386
// This is a bit unfortunate, but this instance attribute overlaps with the
387387
// class __doc__ string..
388388
extend_class!(context, context.types.property_type, {
389-
"__doc__" => context.new_getset(
389+
"__doc__" => context.new_static_getset(
390390
"__doc__",
391391
context.types.property_type,
392392
PyProperty::doc_getter,

vm/src/builtins/type.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use crate::{
3131
};
3232
use indexmap::{IndexMap, map::Entry};
3333
use itertools::Itertools;
34-
use std::{borrow::Borrow, collections::HashSet, fmt, ops::Deref, pin::Pin, ptr::NonNull};
34+
use std::{borrow::Borrow, collections::HashSet, ops::Deref, pin::Pin, ptr::NonNull};
3535

3636
#[pyclass(module = false, name = "type", traverse = "manual")]
3737
pub struct PyType {
@@ -69,6 +69,9 @@ pub struct HeapTypeExt {
6969

7070
pub struct PointerSlot<T>(NonNull<T>);
7171

72+
unsafe impl<T> Sync for PointerSlot<T> {}
73+
unsafe impl<T> Send for PointerSlot<T> {}
74+
7275
impl<T> PointerSlot<T> {
7376
pub unsafe fn borrow_static(&self) -> &'static T {
7477
unsafe { self.0.as_ref() }
@@ -126,14 +129,14 @@ unsafe impl Traverse for PyAttributes {
126129
}
127130
}
128131

129-
impl fmt::Display for PyType {
130-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131-
fmt::Display::fmt(&self.name(), f)
132+
impl std::fmt::Display for PyType {
133+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134+
std::fmt::Display::fmt(&self.name(), f)
132135
}
133136
}
134137

135-
impl fmt::Debug for PyType {
136-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138+
impl std::fmt::Debug for PyType {
139+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137140
write!(f, "[PyType {}]", &self.name())
138141
}
139142
}
@@ -908,7 +911,7 @@ impl Constructor for PyType {
908911
let __dict__ = identifier!(vm, __dict__);
909912
attributes.entry(__dict__).or_insert_with(|| {
910913
vm.ctx
911-
.new_getset(
914+
.new_static_getset(
912915
"__dict__",
913916
vm.ctx.types.type_type,
914917
subtype_get_dict,

vm/src/class.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub trait PyClassImpl: PyClassDef {
8585
let __dict__ = identifier!(ctx, __dict__);
8686
class.set_attr(
8787
__dict__,
88-
ctx.new_getset(
88+
ctx.new_static_getset(
8989
"__dict__",
9090
class,
9191
crate::builtins::object::object_get_dict,

vm/src/vm/context.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ impl Context {
570570
PyRef::new_ref(getset, self.types.getset_type.to_owned(), None)
571571
}
572572

573-
pub fn new_getset<G, S, T, U>(
573+
pub fn new_static_getset<G, S, T, U>(
574574
&self,
575575
name: impl Into<String>,
576576
class: &'static Py<PyType>,
@@ -586,6 +586,26 @@ impl Context {
586586
PyRef::new_ref(getset, self.types.getset_type.to_owned(), None)
587587
}
588588

589+
/// Creates a new `PyGetSet` with a heap type.
590+
///
591+
/// # Safety
592+
/// In practice, this constructor is safe because a getset is always owned by its `class` type.
593+
/// However, it can be broken if used unconventionally.
594+
pub unsafe fn new_getset<G, S, T, U>(
595+
&self,
596+
name: impl Into<String>,
597+
class: &Py<PyType>,
598+
g: G,
599+
s: S,
600+
) -> PyRef<PyGetSet>
601+
where
602+
G: IntoPyGetterFunc<T>,
603+
S: IntoPySetterFunc<U>,
604+
{
605+
let class = unsafe { &*(class as *const _) };
606+
self.new_static_getset(name, class, g, s)
607+
}
608+
589609
pub fn new_base_object(&self, class: PyTypeRef, dict: Option<PyDictRef>) -> PyObjectRef {
590610
debug_assert_eq!(
591611
class.slots.flags.contains(PyTypeFlags::HAS_DICT),

0 commit comments

Comments
 (0)