stdpy: Annotate threading functions (#1765)
This commit is contained in:
parent
f00cec70e6
commit
0e1a3f2ab7
|
|
@ -5,6 +5,8 @@ in some compilation models, Panda's threading constructs are
|
|||
incompatible with the OS-provided threads used by Python's thread
|
||||
module. """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
'error', 'LockType',
|
||||
'start_new_thread',
|
||||
|
|
@ -19,6 +21,9 @@ __all__ = [
|
|||
from panda3d import core
|
||||
import sys
|
||||
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import Any
|
||||
|
||||
if sys.platform == "win32":
|
||||
TIMEOUT_MAX = float(0xffffffff // 1000)
|
||||
else:
|
||||
|
|
@ -41,12 +46,12 @@ class LockType:
|
|||
allows a different thread to release the lock than the one that
|
||||
acquired it. """
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.__lock = core.Mutex('PythonLock')
|
||||
self.__cvar = core.ConditionVar(self.__lock)
|
||||
self.__locked = False
|
||||
|
||||
def acquire(self, waitflag = 1, timeout = -1):
|
||||
def acquire(self, waitflag: bool = True, timeout: float = -1) -> bool:
|
||||
self.__lock.acquire()
|
||||
try:
|
||||
if self.__locked and not waitflag:
|
||||
|
|
@ -65,7 +70,7 @@ class LockType:
|
|||
finally:
|
||||
self.__lock.release()
|
||||
|
||||
def release(self):
|
||||
def release(self) -> None:
|
||||
self.__lock.acquire()
|
||||
try:
|
||||
if not self.__locked:
|
||||
|
|
@ -90,18 +95,23 @@ class LockType:
|
|||
_counter = 0
|
||||
|
||||
|
||||
def _newname(template="Thread-%d"):
|
||||
def _newname(template: str = "Thread-%d") -> str:
|
||||
global _counter
|
||||
_counter = _counter + 1
|
||||
return template % _counter
|
||||
|
||||
|
||||
_threads = {}
|
||||
_threads: dict[int, tuple[core.Thread, dict[int, dict[str, Any]], Any | None]] = {}
|
||||
_nextThreadId = 0
|
||||
_threadsLock = core.Mutex('thread._threadsLock')
|
||||
|
||||
|
||||
def start_new_thread(function, args, kwargs = {}, name = None):
|
||||
def start_new_thread(
|
||||
function: Callable[..., object],
|
||||
args: Iterable[Any],
|
||||
kwargs: Mapping[str, Any] = {},
|
||||
name: str | None = None,
|
||||
) -> int:
|
||||
def threadFunc(threadId, function = function, args = args, kwargs = kwargs):
|
||||
try:
|
||||
try:
|
||||
|
|
@ -132,7 +142,7 @@ def start_new_thread(function, args, kwargs = {}, name = None):
|
|||
_threadsLock.release()
|
||||
|
||||
|
||||
def _add_thread(thread, wrapper):
|
||||
def _add_thread(thread: core.Thread, wrapper: Any) -> int:
|
||||
""" Adds the indicated core.Thread object, with the indicated Python
|
||||
wrapper, to the thread list. Returns the new thread ID. """
|
||||
|
||||
|
|
@ -150,7 +160,7 @@ def _add_thread(thread, wrapper):
|
|||
_threadsLock.release()
|
||||
|
||||
|
||||
def _get_thread_wrapper(thread, wrapperClass):
|
||||
def _get_thread_wrapper(thread: core.Thread, wrapperClass: Callable[[core.Thread, int], Any]) -> Any:
|
||||
""" Returns the thread wrapper for the indicated thread. If there
|
||||
is not one, creates an instance of the indicated wrapperClass
|
||||
instead. """
|
||||
|
|
@ -222,7 +232,7 @@ def _get_thread_locals(thread, i):
|
|||
_threadsLock.release()
|
||||
|
||||
|
||||
def _remove_thread_id(threadId):
|
||||
def _remove_thread_id(threadId: int) -> None:
|
||||
""" Removes the thread with the indicated ID from the thread list. """
|
||||
|
||||
# On interpreter shutdown, Python may set module globals to None.
|
||||
|
|
@ -254,7 +264,7 @@ def allocate_lock() -> LockType:
|
|||
return LockType()
|
||||
|
||||
|
||||
def get_ident():
|
||||
def get_ident() -> int:
|
||||
return core.Thread.getCurrentThread().this
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,17 @@ easier to use and understand.
|
|||
It is permissible to mix-and-match both threading and threading2
|
||||
within the same application. """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from panda3d import core
|
||||
from direct.stdpy import thread as _thread
|
||||
import sys as _sys
|
||||
|
||||
import weakref
|
||||
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import Any, NoReturn
|
||||
|
||||
__all__ = [
|
||||
'Thread',
|
||||
'Lock', 'RLock',
|
||||
|
|
@ -54,10 +59,14 @@ class ThreadBase:
|
|||
""" A base class for both Thread and ExternalThread in this
|
||||
module. """
|
||||
|
||||
def __init__(self):
|
||||
name: str
|
||||
ident: int
|
||||
daemon: bool
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def getName(self):
|
||||
def getName(self) -> str:
|
||||
return self.name
|
||||
|
||||
def isDaemon(self):
|
||||
|
|
@ -92,7 +101,15 @@ class Thread(ThreadBase):
|
|||
object. The wrapper is designed to emulate Python's own
|
||||
threading.Thread object. """
|
||||
|
||||
def __init__(self, group=None, target=None, name=None, args=(), kwargs={}, daemon=None):
|
||||
def __init__(
|
||||
self,
|
||||
group: None = None,
|
||||
target: Callable[..., object] | None = None,
|
||||
name: str | None = None,
|
||||
args: Iterable[Any] = (),
|
||||
kwargs: Mapping[str, Any] = {},
|
||||
daemon: bool | None = None,
|
||||
) -> None:
|
||||
ThreadBase.__init__(self)
|
||||
|
||||
assert group is None
|
||||
|
|
@ -131,7 +148,7 @@ class Thread(ThreadBase):
|
|||
|
||||
isAlive = is_alive
|
||||
|
||||
def start(self):
|
||||
def start(self) -> None:
|
||||
thread = self.__thread
|
||||
if thread is None or thread.is_started():
|
||||
raise RuntimeError
|
||||
|
|
@ -147,7 +164,7 @@ class Thread(ThreadBase):
|
|||
|
||||
self.__target(*self.__args, **self.__kwargs)
|
||||
|
||||
def join(self, timeout = None):
|
||||
def join(self, timeout: float | None = None) -> None:
|
||||
# We don't support a timed join here, sorry.
|
||||
assert timeout is None
|
||||
thread = self.__thread
|
||||
|
|
@ -157,7 +174,7 @@ class Thread(ThreadBase):
|
|||
self.__thread = None
|
||||
_thread._remove_thread_id(self.ident)
|
||||
|
||||
def setName(self, name):
|
||||
def setName(self, name: str) -> None:
|
||||
self.__dict__['name'] = name
|
||||
self.__thread.setName(name)
|
||||
|
||||
|
|
@ -166,7 +183,7 @@ class ExternalThread(ThreadBase):
|
|||
""" Returned for a Thread object that wasn't created by this
|
||||
interface. """
|
||||
|
||||
def __init__(self, extThread, threadId):
|
||||
def __init__(self, extThread: core.Thread, threadId: int) -> None:
|
||||
ThreadBase.__init__(self)
|
||||
|
||||
self.__thread = extThread
|
||||
|
|
@ -196,7 +213,7 @@ class ExternalThread(ThreadBase):
|
|||
class MainThread(ExternalThread):
|
||||
""" Returned for the MainThread object. """
|
||||
|
||||
def __init__(self, extThread, threadId):
|
||||
def __init__(self, extThread: core.Thread, threadId: int) -> None:
|
||||
ExternalThread.__init__(self, extThread, threadId)
|
||||
self.__dict__['daemon'] = False
|
||||
|
||||
|
|
@ -206,7 +223,7 @@ class Lock(core.Mutex):
|
|||
The wrapper is designed to emulate Python's own threading.Lock
|
||||
object. """
|
||||
|
||||
def __init__(self, name = "PythonLock"):
|
||||
def __init__(self, name: str = "PythonLock") -> None:
|
||||
core.Mutex.__init__(self, name)
|
||||
|
||||
|
||||
|
|
@ -224,7 +241,7 @@ class Condition(core.ConditionVar):
|
|||
object. The wrapper is designed to emulate Python's own
|
||||
threading.Condition object. """
|
||||
|
||||
def __init__(self, lock = None):
|
||||
def __init__(self, lock: Lock | RLock | None = None) -> None:
|
||||
if not lock:
|
||||
lock = Lock()
|
||||
|
||||
|
|
@ -241,7 +258,7 @@ class Condition(core.ConditionVar):
|
|||
def release(self):
|
||||
self.__lock.release()
|
||||
|
||||
def wait(self, timeout = None):
|
||||
def wait(self, timeout: float | None = None) -> None:
|
||||
if timeout is None:
|
||||
core.ConditionVar.wait(self)
|
||||
else:
|
||||
|
|
@ -373,8 +390,9 @@ class Timer(Thread):
|
|||
self.finished.set()
|
||||
|
||||
|
||||
def _create_thread_wrapper(t, threadId):
|
||||
def _create_thread_wrapper(t: core.Thread, threadId: int) -> ExternalThread:
|
||||
""" Creates a thread wrapper for the indicated external thread. """
|
||||
pyt: ExternalThread
|
||||
if isinstance(t, core.MainThread):
|
||||
pyt = MainThread(t, threadId)
|
||||
else:
|
||||
|
|
@ -383,7 +401,7 @@ def _create_thread_wrapper(t, threadId):
|
|||
return pyt
|
||||
|
||||
|
||||
def current_thread():
|
||||
def current_thread() -> ThreadBase:
|
||||
t = core.Thread.getCurrentThread()
|
||||
return _thread._get_thread_wrapper(t, _create_thread_wrapper)
|
||||
|
||||
|
|
@ -430,5 +448,5 @@ def setprofile(func):
|
|||
_setprofile_func = func
|
||||
|
||||
|
||||
def stack_size(size = None):
|
||||
def stack_size(size: object = None) -> NoReturn:
|
||||
raise ThreadError
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ to import Panda's thread reimplementation instead of the system thread
|
|||
module, and so it is therefore layered on top of Panda's thread
|
||||
implementation. """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys as _sys
|
||||
import atexit as _atexit
|
||||
|
||||
|
|
@ -21,8 +23,10 @@ from direct.stdpy.thread import stack_size, _newname, _local as local
|
|||
from panda3d import core
|
||||
_sleep = core.Thread.sleep
|
||||
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from time import time as _time
|
||||
from traceback import format_exc as _format_exc
|
||||
from typing import Any
|
||||
|
||||
__all__ = ['get_ident', 'active_count', 'Condition', 'current_thread',
|
||||
'enumerate', 'main_thread', 'TIMEOUT_MAX',
|
||||
|
|
@ -51,12 +55,12 @@ if __debug__:
|
|||
|
||||
class _Verbose(object):
|
||||
|
||||
def __init__(self, verbose=None):
|
||||
def __init__(self, verbose: bool | None = None) -> None:
|
||||
if verbose is None:
|
||||
verbose = _VERBOSE
|
||||
self.__verbose = verbose
|
||||
|
||||
def _note(self, format, *args):
|
||||
def _note(self, format: str, *args: Any) -> None:
|
||||
if self.__verbose:
|
||||
format = format % args
|
||||
format = "%s: %s\n" % (
|
||||
|
|
@ -66,9 +70,9 @@ if __debug__:
|
|||
else:
|
||||
# Disable this when using "python -O"
|
||||
class _Verbose(object): # type: ignore[no-redef]
|
||||
def __init__(self, verbose=None):
|
||||
def __init__(self, verbose: bool | None = None) -> None:
|
||||
pass
|
||||
def _note(self, *args):
|
||||
def _note(self, *args) -> None:
|
||||
pass
|
||||
|
||||
# Support for profile and trace hooks
|
||||
|
|
@ -88,15 +92,15 @@ def settrace(func):
|
|||
|
||||
Lock = _allocate_lock
|
||||
|
||||
def RLock(*args, **kwargs):
|
||||
return _RLock(*args, **kwargs)
|
||||
def RLock(verbose: bool | None = None) -> _RLock:
|
||||
return _RLock(verbose)
|
||||
|
||||
class _RLock(_Verbose):
|
||||
|
||||
def __init__(self, verbose=None):
|
||||
def __init__(self, verbose: bool | None = None) -> None:
|
||||
_Verbose.__init__(self, verbose)
|
||||
self.__block = _allocate_lock()
|
||||
self.__owner = None
|
||||
self.__owner: Thread | None = None
|
||||
self.__count = 0
|
||||
|
||||
def __repr__(self):
|
||||
|
|
@ -105,13 +109,13 @@ class _RLock(_Verbose):
|
|||
self.__owner and self.__owner.getName(),
|
||||
self.__count)
|
||||
|
||||
def acquire(self, blocking=1):
|
||||
def acquire(self, blocking: bool = True) -> bool:
|
||||
me = currentThread()
|
||||
if self.__owner is me:
|
||||
self.__count = self.__count + 1
|
||||
if __debug__:
|
||||
self._note("%s.acquire(%s): recursive success", self, blocking)
|
||||
return 1
|
||||
return True
|
||||
rc = self.__block.acquire(blocking)
|
||||
if rc:
|
||||
self.__owner = me
|
||||
|
|
@ -125,7 +129,7 @@ class _RLock(_Verbose):
|
|||
|
||||
__enter__ = acquire
|
||||
|
||||
def release(self):
|
||||
def release(self) -> None:
|
||||
me = currentThread()
|
||||
assert self.__owner is me, "release() of un-acquire()d lock"
|
||||
self.__count = count = self.__count - 1
|
||||
|
|
@ -163,12 +167,12 @@ class _RLock(_Verbose):
|
|||
return self.__owner is currentThread()
|
||||
|
||||
|
||||
def Condition(*args, **kwargs):
|
||||
return _Condition(*args, **kwargs)
|
||||
def Condition(lock: _thread.LockType | _RLock | None = None, verbose: bool | None = None) -> _Condition:
|
||||
return _Condition(lock, verbose)
|
||||
|
||||
class _Condition(_Verbose):
|
||||
|
||||
def __init__(self, lock=None, verbose=None):
|
||||
def __init__(self, lock: _thread.LockType | _RLock | None = None, verbose: bool | None = None) -> None:
|
||||
_Verbose.__init__(self, verbose)
|
||||
if lock is None:
|
||||
lock = RLock()
|
||||
|
|
@ -180,18 +184,18 @@ class _Condition(_Verbose):
|
|||
# these override the default implementations (which just call
|
||||
# release() and acquire() on the lock). Ditto for _is_owned().
|
||||
try:
|
||||
self._release_save = lock._release_save
|
||||
self._release_save = lock._release_save # type: ignore[method-assign, union-attr]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
self._acquire_restore = lock._acquire_restore
|
||||
self._acquire_restore = lock._acquire_restore # type: ignore[method-assign, union-attr]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
self._is_owned = lock._is_owned
|
||||
self._is_owned = lock._is_owned # type: ignore[method-assign, union-attr]
|
||||
except AttributeError:
|
||||
pass
|
||||
self.__waiters = []
|
||||
self.__waiters: list[_thread.LockType] = []
|
||||
|
||||
def __enter__(self):
|
||||
return self.__lock.__enter__()
|
||||
|
|
@ -202,22 +206,22 @@ class _Condition(_Verbose):
|
|||
def __repr__(self):
|
||||
return "<Condition(%s, %d)>" % (self.__lock, len(self.__waiters))
|
||||
|
||||
def _release_save(self): # pylint: disable=method-hidden
|
||||
def _release_save(self) -> Any: # pylint: disable=method-hidden
|
||||
self.__lock.release() # No state to save
|
||||
|
||||
def _acquire_restore(self, x): # pylint: disable=method-hidden
|
||||
def _acquire_restore(self, x) -> None: # pylint: disable=method-hidden
|
||||
self.__lock.acquire() # Ignore saved state
|
||||
|
||||
def _is_owned(self): # pylint: disable=method-hidden
|
||||
def _is_owned(self) -> bool: # pylint: disable=method-hidden
|
||||
# Return True if lock is owned by currentThread.
|
||||
# This method is called only if __lock doesn't have _is_owned().
|
||||
if self.__lock.acquire(0):
|
||||
if self.__lock.acquire(False):
|
||||
self.__lock.release()
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def wait(self, timeout=None):
|
||||
def wait(self, timeout: float | None = None) -> None:
|
||||
assert self._is_owned(), "wait() of un-acquire()d lock"
|
||||
waiter = _allocate_lock()
|
||||
waiter.acquire()
|
||||
|
|
@ -237,7 +241,7 @@ class _Condition(_Verbose):
|
|||
endtime = _time() + timeout
|
||||
delay = 0.0005 # 500 us -> initial delay of 1 ms
|
||||
while True:
|
||||
gotit = waiter.acquire(0)
|
||||
gotit = waiter.acquire(False)
|
||||
if gotit:
|
||||
break
|
||||
remaining = endtime - _time()
|
||||
|
|
@ -258,7 +262,7 @@ class _Condition(_Verbose):
|
|||
finally:
|
||||
self._acquire_restore(saved_state)
|
||||
|
||||
def notify(self, n=1):
|
||||
def notify(self, n: int = 1) -> None:
|
||||
assert self._is_owned(), "notify() of un-acquire()d lock"
|
||||
__waiters = self.__waiters
|
||||
waiters = __waiters[:n]
|
||||
|
|
@ -275,7 +279,7 @@ class _Condition(_Verbose):
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
def notifyAll(self):
|
||||
def notifyAll(self) -> None:
|
||||
self.notify(len(self.__waiters))
|
||||
|
||||
|
||||
|
|
@ -381,7 +385,7 @@ class _Event(_Verbose):
|
|||
|
||||
# Active thread administration
|
||||
_active_limbo_lock = _allocate_lock()
|
||||
_active = {} # maps thread id to Thread object
|
||||
_active: dict[int, Thread] = {} # maps thread id to Thread object
|
||||
_limbo = {}
|
||||
|
||||
|
||||
|
|
@ -400,8 +404,16 @@ class Thread(_Verbose):
|
|||
# Protected by _active_limbo_lock.
|
||||
__registered_atexit = False
|
||||
|
||||
def __init__(self, group=None, target=None, name=None,
|
||||
args=(), kwargs=None, verbose=None, daemon=None):
|
||||
def __init__(
|
||||
self,
|
||||
group: None = None,
|
||||
target: Callable[..., object] | None = None,
|
||||
name: object = None,
|
||||
args: Iterable[Any] = (),
|
||||
kwargs: Mapping[str, Any] | None = None,
|
||||
verbose: bool | None = None,
|
||||
daemon: bool | None = None,
|
||||
) -> None:
|
||||
assert group is None, "group argument must be None for now"
|
||||
_Verbose.__init__(self, verbose)
|
||||
if kwargs is None:
|
||||
|
|
@ -422,7 +434,7 @@ class Thread(_Verbose):
|
|||
# sys.exc_info since it can be changed between instances
|
||||
self.__stderr = _sys.stderr
|
||||
|
||||
def _set_daemon(self):
|
||||
def _set_daemon(self) -> bool:
|
||||
# Overridden in _MainThread and _DummyThread
|
||||
return currentThread().isDaemon()
|
||||
|
||||
|
|
@ -437,7 +449,7 @@ class Thread(_Verbose):
|
|||
status = status + " daemon"
|
||||
return "<%s(%s, %s)>" % (self.__class__.__name__, self.__name, status)
|
||||
|
||||
def start(self):
|
||||
def start(self) -> None:
|
||||
assert self.__initialized, "Thread.__init__() not called"
|
||||
assert not self.__started, "thread already started"
|
||||
if __debug__:
|
||||
|
|
@ -457,11 +469,11 @@ class Thread(_Verbose):
|
|||
self.__started = True
|
||||
_sleep(0.000001) # 1 usec, to let the thread run (Solaris hack)
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
if self.__target:
|
||||
self.__target(*self.__args, **self.__kwargs)
|
||||
|
||||
def __bootstrap(self):
|
||||
def __bootstrap(self) -> None:
|
||||
try:
|
||||
self.__started = True
|
||||
_active_limbo_lock.acquire()
|
||||
|
|
@ -497,7 +509,7 @@ class Thread(_Verbose):
|
|||
# Do the best job possible w/o a huge amt. of code to
|
||||
# approximate a traceback (code ideas from
|
||||
# Lib/traceback.py)
|
||||
exc_type, exc_value, exc_tb = self.__exc_info()
|
||||
exc_type, exc_value, exc_tb = self.__exc_info() # type: ignore[misc]
|
||||
try:
|
||||
self.__stderr.write("Exception in thread " + self.getName() +
|
||||
" (most likely raised during interpreter shutdown):\n")
|
||||
|
|
@ -523,13 +535,13 @@ class Thread(_Verbose):
|
|||
except:
|
||||
pass
|
||||
|
||||
def __stop(self):
|
||||
def __stop(self) -> None:
|
||||
self.__block.acquire()
|
||||
self.__stopped = True
|
||||
self.__block.notifyAll()
|
||||
self.__block.release()
|
||||
|
||||
def __delete(self):
|
||||
def __delete(self) -> None:
|
||||
"Remove current thread from the dict of currently running threads."
|
||||
|
||||
# Notes about running with dummy_thread:
|
||||
|
|
@ -563,7 +575,7 @@ class Thread(_Verbose):
|
|||
finally:
|
||||
_active_limbo_lock.release()
|
||||
|
||||
def join(self, timeout=None):
|
||||
def join(self, timeout: float | None = None) -> None:
|
||||
assert self.__initialized, "Thread.__init__() not called"
|
||||
assert self.__started, "cannot join thread before it is started"
|
||||
assert self is not currentThread(), "cannot join current thread"
|
||||
|
|
@ -592,11 +604,11 @@ class Thread(_Verbose):
|
|||
finally:
|
||||
self.__block.release()
|
||||
|
||||
def getName(self):
|
||||
def getName(self) -> str:
|
||||
assert self.__initialized, "Thread.__init__() not called"
|
||||
return self.__name
|
||||
|
||||
def setName(self, name):
|
||||
def setName(self, name: object) -> None:
|
||||
assert self.__initialized, "Thread.__init__() not called"
|
||||
self.__name = str(name)
|
||||
|
||||
|
|
@ -606,7 +618,7 @@ class Thread(_Verbose):
|
|||
|
||||
isAlive = is_alive
|
||||
|
||||
def isDaemon(self):
|
||||
def isDaemon(self) -> bool:
|
||||
assert self.__initialized, "Thread.__init__() not called"
|
||||
return self.__daemonic
|
||||
|
||||
|
|
@ -688,13 +700,13 @@ class _MainThread(Thread):
|
|||
|
||||
class _DummyThread(Thread):
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
Thread.__init__(self, name=_newname("Dummy-%d"), daemon=True)
|
||||
|
||||
# Thread.__block consumes an OS-level locking primitive, which
|
||||
# can never be used by a _DummyThread. Since a _DummyThread
|
||||
# instance is immortal, that's bad, so release this resource.
|
||||
del self._Thread__block
|
||||
del self._Thread__block # type: ignore[attr-defined]
|
||||
|
||||
self._Thread__started = True
|
||||
_active_limbo_lock.acquire()
|
||||
|
|
@ -710,7 +722,7 @@ class _DummyThread(Thread):
|
|||
|
||||
# Global API functions
|
||||
|
||||
def current_thread():
|
||||
def current_thread() -> Thread:
|
||||
try:
|
||||
return _active[get_ident()]
|
||||
except KeyError:
|
||||
|
|
|
|||
Loading…
Reference in New Issue