I am setting some context variables using contextvars
module that can be accessed across the modules running on the same thread.
Initially I was creating contextvars.ContextVars()
object in each python file hoping that there is only single context shared amongts all the python files of the module running on same thread. But for each file it did create new context variables.
I took inspiration from flask library how it sets context of the web request in request object so that only thread on which web request came will be able to access it. Resources: (1) Request Contex working in flask (2) Flask Contexts advance
Basically, the Local class below is copy pasted from werkzeug library (werkzeug.local module : https://werkzeug.palletsprojects.com/en/2.3.x/local/#werkzeug.local.Local)
customContextObject.py
from contextvars import ContextVar
import typing as t
import warnings
class Local:
__slots__ = ("_storage",)
def __init__(self) -> None:
object.__setattr__(self, "_storage", ContextVar("local_storage"))
@property
def __storage__(self) -> t.Dict[str, t.Any]:
warnings.warn(
"'__storage__' is deprecated and will be removed in Werkzeug 2.1.",
DeprecationWarning,
stacklevel=2,
)
return self._storage.get({}) # type: ignore
def __iter__(self) -> t.Iterator[t.Tuple[int, t.Any]]:
return iter(self._storage.get({}).items())
def __getattr__(self, name: str) -> t.Any:
values = self._storage.get({})
try:
print(f"_storage : {self._storage} | values : {values}")
return values[name]
except KeyError:
raise AttributeError(name) from None
def __setattr__(self, name: str, value: t.Any) -> None:
values = self._storage.get({}).copy()
values[name] = value
self._storage.set(values)
def __delattr__(self, name: str) -> None:
values = self._storage.get({}).copy()
try:
del values[name]
self._storage.set(values)
except KeyError:
raise AttributeError(name) from None
localContextObject = Local()
The localContextObject
know can be imported in any python file in the project and they will have access to same ContextVar object.
Example: I am setting email property in localContextObject variable in contextVARSDifferentModulesCUSTOM.py file contextVARSexperiments module. We import and call check_true_false() function from utils.py
from contextVARSexperiments.utils import check_true_false, check_true_false
from contextVARSexperiments.customContextObject import localContextObject
import threading
localContextObject.email = "example@email.com"
print(f"localContextObject : {localContextObject} | email : {localContextObject.email}")
def callingUtils(a):
print(f"{threading.current_thread()} | {threading.main_thread()}")
check_true_false(a)
callingUtils('MAIN CALL')
Now the other file utils.py in the same module will have access to the same contextVars through localContextObject. It will print the same email as set in above file.
utils.py
import threading
import contextvars
from contextVARSexperiments.customContextObject import localContextObject
def decorator(func):
def wrapper(*args, **kwargs):
print("\n~~~ENTERING check_true_false~~~~~~ ")
func(*args, **kwargs)
print("~~~EXITED check_true_false~~~~~~\n")
return wrapper
@decorator
def check_true_false(a):
print(f"check_true_false2 {threading.current_thread()} | {threading.main_thread()}")
print(f" a : {a}")
print(f"localContextObject : {localContextObject}")
print(f"email : {localContextObject.email}")
Below is the output when we run contextVARSDifferentModulesCUSTOM.py
/Users/<user>/PycharmProjects/Temp/contextVARSexperiments/contextVARSDifferentModulesCUSTOM.py
localContextObject : <_thread._local object at 0x7fcfb85fdd58> | email : example@email.com
<_MainThread(MainThread, started 8671015616)> | <_MainThread(MainThread, started 8671015616)>
~~~ENTERING check_true_false~~~~~~
check_true_false <_MainThread(MainThread, started 8671015616)> | <_MainThread(MainThread, started 8671015616)>
a : MAIN CALL
localContextObject : <_thread._local object at 0x7fcfb85fdd58>
email : example@email.com
~~~EXITED check_true_false~~~~~~
Now, I updated contextVARSDifferentModulesCUSTOM.py to call callingUtils() function on a new thread.
from contextVARSexperiments.utils import check_true_false
from contextVARSexperiments.customContextObject import localContextObject
import threading
localContextObject.email = "example@email.com"
print(f"localContextObject : {localContextObject} | email : {localContextObject.email}")
def callingUtils(a):
print(f"{threading.current_thread()} | {threading.main_thread()}")
check_true_false(a)
t1 = threading.Thread(target=callingUtils, args=('THREAD"S CALL',))
t1.start()
t1.join()
But this threw error because child thread didn't have access to parent thread's ContextVars. Output:
/Users/<user>/PycharmProjects/Temp/contextVARSexperiments/contextVARSDifferentModulesCUSTOM.py
_storage : <ContextVar name='local_storage' at 7ff1d0435668> | values : {'email': 'example@email.com'}
localContextObject : <contextVARSexperiments.customContextObject.Local object at 0x7ff1c02162e8> | email : example@email.com
<Thread(Thread-1, started 12937875456)> | <_MainThread(MainThread, started 8609043136)>
~~~ENTERING check_true_false~~~~~~
check_true_false <Thread(Thread-1, started 12937875456)> | <_MainThread(MainThread, started 8609043136)>
a : THREAD"S CALL
localContextObject : <contextVARSexperiments.customContextObject.Local object at 0x7ff1c02162e8>
_storage : <ContextVar name='local_storage' at 7ff1d0435668> | values : {}
Exception in thread Thread-1:
Traceback (most recent call last):
File "/Users/<user>/miniconda3/envs/test_env/lib/python3.6/threading.py", line 916, in _bootstrap_inner
self.run()
File "/Users/<user>/miniconda3/envs/test_env/lib/python3.6/threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "/Users/<user>/PycharmProjects/Temp/contextVARSexperiments/contextVARSDifferentModulesCUSTOM.py", line 13, in callingUtils
check_true_false(a)
File "/Users/<user>/PycharmProjects/Temp/contextVARSexperiments/utils.py", line 26, in wrapper
func(*args, **kwargs)
File "/Users/<user>/PycharmProjects/Temp/contextVARSexperiments/utils.py", line 43, in check_true_false
print(f"email : {localContextObject.email}")
File "/Users/<user>/PycharmProjects/Temp/contextVARSexperiments/customContextObject.py", line 31, in __getattr__
raise AttributeError(name) from None
AttributeError: email
Now, I am trying to inherit Thread class and create my own custom implementation which will pass the context from parent thread to child thread.
I tried to replace threading.Thread
class with a CustomThread
class. Following are the implementations of CustomThread
class inside customThreading.py :
More about Context
object returned by copy_context()
method of contextvars library : https://docs.python.org/3/library/contextvars.html#contextvars.Context
- Using
Context
object returned bycopy_context()
to run initialiser of Threading class:
import threading
import contextvars
class CustomThread(threading.Thread):
def __init__(self, *args, **kwargs):
self.current_context = contextvars.copy_context()
self.current_context.run(super().__init__, *args, **kwargs)
def start(self) -> None:
super().start()
- Using
Context
object returned bycopy_context()
while callingstart()
of Threading class:
import threading
import contextvars
class CustomThread(threading.Thread):
def __init__(self, *args, **kwargs):
self.current_context = contextvars.copy_context()
super().__init__(*args, **kwargs)
def start(self) -> None:
self.current_context.run(super().start)
- Using
contextmanager
decorator from contextlib onstart()
of my class:
import threading
import contextvars
from contextlib import contextmanager
class CustomThread(threading.Thread):
def __init__(self, *args, **kwargs):
self.current_context = contextvars.copy_context()
super().__init__(*args, **kwargs)
@contextmanager
def start(self) -> None:
super().start()
But none of this worked.
Also, I am looking for custom implementation of ThreadPoolExecutor
from concurrent.futures
module.