Skip to content

Commit d96358f

Browse files
sobolevnAlexWaygoodgpshead
authored
gh-114315: Make threading.Lock a real class, not a factory function (#114479)
`threading.Lock` is now the underlying class and is constructable rather than the old factory function. This allows for type annotations to refer to it which had no non-ugly way to be expressed prior to this. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent b52fc70 commit d96358f

File tree

5 files changed

+52
-14
lines changed

5 files changed

+52
-14
lines changed

Doc/library/threading.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -534,9 +534,10 @@ All methods are executed atomically.
534534
lock, subsequent attempts to acquire it block, until it is released; any
535535
thread may release it.
536536

537-
Note that ``Lock`` is actually a factory function which returns an instance
538-
of the most efficient version of the concrete Lock class that is supported
539-
by the platform.
537+
.. versionchanged:: 3.13
538+
``Lock`` is now a class. In earlier Pythons, ``Lock`` was a factory
539+
function which returned an instance of the underlying private lock
540+
type.
540541

541542

542543
.. method:: acquire(blocking=True, timeout=-1)

Lib/test/test_threading.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,21 @@ def test_args_argument(self):
171171
t.start()
172172
t.join()
173173

174-
@cpython_only
175-
def test_disallow_instantiation(self):
176-
# Ensure that the type disallows instantiation (bpo-43916)
177-
lock = threading.Lock()
178-
test.support.check_disallow_instantiation(self, type(lock))
174+
def test_lock_no_args(self):
175+
threading.Lock() # works
176+
self.assertRaises(TypeError, threading.Lock, 1)
177+
self.assertRaises(TypeError, threading.Lock, a=1)
178+
self.assertRaises(TypeError, threading.Lock, 1, 2, a=1, b=2)
179+
180+
def test_lock_no_subclass(self):
181+
# Intentionally disallow subclasses of threading.Lock because they have
182+
# never been allowed, so why start now just because the type is public?
183+
with self.assertRaises(TypeError):
184+
class MyLock(threading.Lock): pass
185+
186+
def test_lock_or_none(self):
187+
import types
188+
self.assertIsInstance(threading.Lock | None, types.UnionType)
179189

180190
# Create a bunch of threads, let each do some work, wait until all are
181191
# done.

Lib/threading.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import _thread
66
import functools
77
import warnings
8-
import _weakref
98

109
from time import monotonic as _time
1110
from _weakrefset import WeakSet
@@ -37,6 +36,7 @@
3736
_start_joinable_thread = _thread.start_joinable_thread
3837
_daemon_threads_allowed = _thread.daemon_threads_allowed
3938
_allocate_lock = _thread.allocate_lock
39+
_LockType = _thread.LockType
4040
_set_sentinel = _thread._set_sentinel
4141
get_ident = _thread.get_ident
4242
_is_main_interpreter = _thread._is_main_interpreter
@@ -115,7 +115,7 @@ def gettrace():
115115

116116
# Synchronization classes
117117

118-
Lock = _allocate_lock
118+
Lock = _LockType
119119

120120
def RLock(*args, **kwargs):
121121
"""Factory function that returns a new reentrant lock.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make :class:`threading.Lock` a real class, not a factory function. Add
2+
``__new__`` to ``_thread.lock`` type.

Modules/_threadmodule.c

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "Python.h"
66
#include "pycore_interp.h" // _PyInterpreterState.threads.count
77
#include "pycore_moduleobject.h" // _PyModule_GetState()
8+
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
89
#include "pycore_pylifecycle.h"
910
#include "pycore_pystate.h" // _PyThreadState_SetCurrent()
1011
#include "pycore_sysmodule.h" // _PySys_GetAttr()
@@ -349,6 +350,27 @@ lock__at_fork_reinit(lockobject *self, PyObject *Py_UNUSED(args))
349350
}
350351
#endif /* HAVE_FORK */
351352

353+
static lockobject *newlockobject(PyObject *module);
354+
355+
static PyObject *
356+
lock_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
357+
{
358+
// convert to AC?
359+
if (!_PyArg_NoKeywords("lock", kwargs)) {
360+
goto error;
361+
}
362+
if (!_PyArg_CheckPositional("lock", PyTuple_GET_SIZE(args), 0, 0)) {
363+
goto error;
364+
}
365+
366+
PyObject *module = PyType_GetModuleByDef(type, &thread_module);
367+
assert(module != NULL);
368+
return (PyObject *)newlockobject(module);
369+
370+
error:
371+
return NULL;
372+
}
373+
352374

353375
static PyMethodDef lock_methods[] = {
354376
{"acquire_lock", _PyCFunction_CAST(lock_PyThread_acquire_lock),
@@ -398,14 +420,15 @@ static PyType_Slot lock_type_slots[] = {
398420
{Py_tp_methods, lock_methods},
399421
{Py_tp_traverse, lock_traverse},
400422
{Py_tp_members, lock_type_members},
423+
{Py_tp_new, lock_new},
401424
{0, 0}
402425
};
403426

404427
static PyType_Spec lock_type_spec = {
405428
.name = "_thread.lock",
406429
.basicsize = sizeof(lockobject),
407430
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
408-
Py_TPFLAGS_DISALLOW_INSTANTIATION | Py_TPFLAGS_IMMUTABLETYPE),
431+
Py_TPFLAGS_IMMUTABLETYPE),
409432
.slots = lock_type_slots,
410433
};
411434

@@ -1442,8 +1465,6 @@ A subthread can use this function to interrupt the main thread.\n\
14421465
Note: the default signal handler for SIGINT raises ``KeyboardInterrupt``."
14431466
);
14441467

1445-
static lockobject *newlockobject(PyObject *module);
1446-
14471468
static PyObject *
14481469
thread_PyThread_allocate_lock(PyObject *module, PyObject *Py_UNUSED(ignored))
14491470
{
@@ -1841,10 +1862,14 @@ thread_module_exec(PyObject *module)
18411862
}
18421863

18431864
// Lock
1844-
state->lock_type = (PyTypeObject *)PyType_FromSpec(&lock_type_spec);
1865+
state->lock_type = (PyTypeObject *)PyType_FromModuleAndSpec(module, &lock_type_spec, NULL);
18451866
if (state->lock_type == NULL) {
18461867
return -1;
18471868
}
1869+
if (PyModule_AddType(module, state->lock_type) < 0) {
1870+
return -1;
1871+
}
1872+
// Old alias: lock -> LockType
18481873
if (PyDict_SetItemString(d, "LockType", (PyObject *)state->lock_type) < 0) {
18491874
return -1;
18501875
}

0 commit comments

Comments
 (0)