python-extracontext 1.0.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_extracontext-1.0.0b1/PKG-INFO +305 -0
- python_extracontext-1.0.0b1/README.md +279 -0
- python_extracontext-1.0.0b1/extracontext/__init__.py +14 -0
- python_extracontext-1.0.0b1/extracontext/_future_task.py +59 -0
- python_extracontext-1.0.0b1/extracontext/base.py +20 -0
- python_extracontext-1.0.0b1/extracontext/contextlocal.py +263 -0
- python_extracontext-1.0.0b1/extracontext/contextlocal_native.py +262 -0
- python_extracontext-1.0.0b1/extracontext/mapping.py +51 -0
- python_extracontext-1.0.0b1/pyproject.toml +51 -0
- python_extracontext-1.0.0b1/python_extracontext.egg-info/PKG-INFO +305 -0
- python_extracontext-1.0.0b1/python_extracontext.egg-info/SOURCES.txt +19 -0
- python_extracontext-1.0.0b1/python_extracontext.egg-info/dependency_links.txt +1 -0
- python_extracontext-1.0.0b1/python_extracontext.egg-info/requires.txt +6 -0
- python_extracontext-1.0.0b1/python_extracontext.egg-info/top_level.txt +1 -0
- python_extracontext-1.0.0b1/setup.cfg +4 -0
- python_extracontext-1.0.0b1/tests/test_async.py +242 -0
- python_extracontext-1.0.0b1/tests/test_contextmanager.py +148 -0
- python_extracontext-1.0.0b1/tests/test_generators.py +157 -0
- python_extracontext-1.0.0b1/tests/test_mapping.py +286 -0
- python_extracontext-1.0.0b1/tests/test_plain.py +395 -0
- python_extracontext-1.0.0b1/tests/test_thread.py +94 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-extracontext
|
|
3
|
+
Version: 1.0.0b1
|
|
4
|
+
Summary: Context Variable namespaces supporting generators, asyncio and multi-threading
|
|
5
|
+
Author: Joao S. O. Bueno
|
|
6
|
+
Project-URL: repository, https://github.com/jsbueno/extracontext
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
17
|
+
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
|
+
Requires-Dist: black; extra == "dev"
|
|
24
|
+
Requires-Dist: pyflakes; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-coverage; extra == "dev"
|
|
26
|
+
|
|
27
|
+
Context Local Variables
|
|
28
|
+
==========================
|
|
29
|
+
|
|
30
|
+
Implements a Pythonic way to work with PEP 567
|
|
31
|
+
contextvars (https://peps.python.org/pep-0567/ )
|
|
32
|
+
|
|
33
|
+
Introduced in Python 3.7, a design decision by the
|
|
34
|
+
authors of the feature decided to opt-out of
|
|
35
|
+
the simple namespace used by Python's own `threading.local`
|
|
36
|
+
implementation, and requires an explicit top level
|
|
37
|
+
declaration of each context-local variable, and
|
|
38
|
+
the (rather "unpythonic") usage of an explicit
|
|
39
|
+
call to `get` and `set` methods to manipulate
|
|
40
|
+
those.
|
|
41
|
+
|
|
42
|
+
This package does away with that, and brings simplicity
|
|
43
|
+
back - simply instantiate a `ContextLocal` namespace,
|
|
44
|
+
and any attributes set in that namespace will be unique
|
|
45
|
+
per thread and per asynchronous call chain (i.e.
|
|
46
|
+
unique for each independent task).
|
|
47
|
+
|
|
48
|
+
In a sense, these are a drop-in replacement for
|
|
49
|
+
`threading.local`, which will also work for
|
|
50
|
+
asynchronous programming without any change in code.
|
|
51
|
+
|
|
52
|
+
One should just avoid creating the "ContextLocal" instance itself
|
|
53
|
+
in a non-setup function or method - as the implementation
|
|
54
|
+
uses Python contextvars in by default, those are not
|
|
55
|
+
cleaned-up along with the local scope where they are
|
|
56
|
+
created - check the docs on the contextvar module for more
|
|
57
|
+
details.
|
|
58
|
+
|
|
59
|
+
However, creating the actual variables to use inside this namespace
|
|
60
|
+
can be made local to functions or methods: the same inner
|
|
61
|
+
ContextVar instance will be re-used when re-entering the function
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
|
|
66
|
+
Create one or more project-wide instances of "extracontext.ContextLocal"
|
|
67
|
+
Decorate your functions, co-routines, worker-methods and generators
|
|
68
|
+
that should hold their own states with that instance itself, using it as a decorator
|
|
69
|
+
|
|
70
|
+
and use the instance as namespace for private variables that will be local
|
|
71
|
+
and non-local until entering another callable decorated
|
|
72
|
+
with the instance itself - that will create a new, separated scope
|
|
73
|
+
visible inside the decorated callable.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
|
|
77
|
+
from extracontext import ContextLocal
|
|
78
|
+
|
|
79
|
+
# global namespace, available in any thread or async task:
|
|
80
|
+
ctx = ContextLocal()
|
|
81
|
+
|
|
82
|
+
def myworker():
|
|
83
|
+
# value set only visible in the current thread or asyncio task:
|
|
84
|
+
ctx.value = "test"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
More Features:
|
|
90
|
+
|
|
91
|
+
Unlike `threading.local` namespaces, one can explicitly isolate a contextlocal namespace
|
|
92
|
+
when calling a function even on the same thread or same async call chain (task). And unlike
|
|
93
|
+
`contextvars.ContextVar`, there is no need to have an explicit context copy
|
|
94
|
+
and often an intermediate function call to switch context: `extracontext.ContextLocal`
|
|
95
|
+
can isolate the context using either a `with` block or as a decorator
|
|
96
|
+
(when entering the decorated function, all variables in the namespace are automatically
|
|
97
|
+
protected against any changes that would be visible when that call returns,
|
|
98
|
+
the previous values being restored).
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
Example showing context separation for concurrent generators:
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from extracontext import ContextLocal
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
ctx = ContextLocal()
|
|
110
|
+
|
|
111
|
+
results = []
|
|
112
|
+
@ctx
|
|
113
|
+
def contexted_generator(value):
|
|
114
|
+
ctx.value = value
|
|
115
|
+
yield None
|
|
116
|
+
results.append(ctx.value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def runner():
|
|
121
|
+
generators = [contexted_generator(i) for i in range(10)]
|
|
122
|
+
any(next(gen) for gen in generators)
|
|
123
|
+
any(next(gen, None) for gen in generators)
|
|
124
|
+
assert results == list(range(10))
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
ContextLocal namespaces can also be isolated by context-manager blocks (`with` statement):
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from extracontext import ContextLocal
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def with_block_example():
|
|
134
|
+
|
|
135
|
+
ctx = ContextLocal()
|
|
136
|
+
ctx.value = 1
|
|
137
|
+
with ctx:
|
|
138
|
+
ctx.value = 2
|
|
139
|
+
assert ctx.value == 2
|
|
140
|
+
|
|
141
|
+
assert ctx.value == 1
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
This is what one has to do if "isolated_function" will use a contextvar value
|
|
149
|
+
for other nested calls, but should not change the caller's visible value:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
##########################
|
|
153
|
+
# using stdlib contextvars:
|
|
154
|
+
|
|
155
|
+
import contextvars
|
|
156
|
+
|
|
157
|
+
# Each variable has to be declared at top-level:
|
|
158
|
+
value = contextvars.ContextVar("value")
|
|
159
|
+
|
|
160
|
+
def parent():
|
|
161
|
+
# explicit use of setter method for each value:
|
|
162
|
+
value.set(5)
|
|
163
|
+
# call to nested function which needs and isolated copy of context
|
|
164
|
+
# must be done in two stages:
|
|
165
|
+
new_context = contextvars.copy_context()
|
|
166
|
+
new_context.run(isolated_function)
|
|
167
|
+
# explicit use of getter method:
|
|
168
|
+
assert value.get() == 5
|
|
169
|
+
|
|
170
|
+
def isolated_function()
|
|
171
|
+
value.set(23)
|
|
172
|
+
# run other code that needs "23"
|
|
173
|
+
# ...
|
|
174
|
+
assert value.get(23)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This is the same code using this package:
|
|
180
|
+
```python
|
|
181
|
+
from extracontext import NativeContextLocal
|
|
182
|
+
|
|
183
|
+
# instantiate a namespace at top level:
|
|
184
|
+
ctx = NativeContextLocal()
|
|
185
|
+
|
|
186
|
+
def parent():
|
|
187
|
+
# create variables in the namespace without prior declaration:
|
|
188
|
+
# and just use the assignment operator (=)
|
|
189
|
+
ctx.value = 5
|
|
190
|
+
# no boilerplate to call function:
|
|
191
|
+
isolated_function()
|
|
192
|
+
# no need to call a getter:
|
|
193
|
+
assert ctx.value == 5
|
|
194
|
+
|
|
195
|
+
# Decorate function that should run in an isolated context:
|
|
196
|
+
@ctx
|
|
197
|
+
def isolated_function()
|
|
198
|
+
assert ctx.value == 5
|
|
199
|
+
ctx.value = 23
|
|
200
|
+
# run other code that needs "23"
|
|
201
|
+
# ...
|
|
202
|
+
assert ctx.value == 23
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Map namespaces
|
|
207
|
+
-----------------
|
|
208
|
+
|
|
209
|
+
The `ContextMap` class works just the same way, but works
|
|
210
|
+
as a mapping:
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
|
|
215
|
+
from extracontext import ContextMap
|
|
216
|
+
|
|
217
|
+
# global namespace, available in any thread or async task:
|
|
218
|
+
ctx = ContextMap()
|
|
219
|
+
|
|
220
|
+
def myworker():
|
|
221
|
+
# value set only visible in the current thread or asyncio task:
|
|
222
|
+
ctx["value"] = "test"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Non Leaky Contexts
|
|
228
|
+
-------------------
|
|
229
|
+
Contrary to default contextvars usage, generators
|
|
230
|
+
(and async generators) running in another context do
|
|
231
|
+
take effect inside the generator, and doesn't
|
|
232
|
+
leak back to the calling scope:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
import extracontext
|
|
236
|
+
ctx = extracontext.ContextLocal()
|
|
237
|
+
@ctx
|
|
238
|
+
def isolatedgen(n):
|
|
239
|
+
for i in range(n):
|
|
240
|
+
ctx.myvar = i
|
|
241
|
+
yield i
|
|
242
|
+
print (ctx.myvar)
|
|
243
|
+
def test():
|
|
244
|
+
ctx.myvar = "lambs"
|
|
245
|
+
for j in isolatedgen(2):
|
|
246
|
+
print(ctx.myvar)
|
|
247
|
+
ctx.myvar = "wolves"
|
|
248
|
+
|
|
249
|
+
In [11]: test()
|
|
250
|
+
lambs
|
|
251
|
+
0
|
|
252
|
+
wolves
|
|
253
|
+
1
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
By using a stdlib `contextvars.ContextVar` one simply
|
|
257
|
+
can't isolate the body of a generator, save by
|
|
258
|
+
not running a `for` at all, and running all
|
|
259
|
+
iterations manually by calling `ctx_copy.run(next, mygenerator)`
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
New for 1.0
|
|
264
|
+
-----------
|
|
265
|
+
|
|
266
|
+
Switch the backend to use native Python contextvars (exposed in
|
|
267
|
+
the stdlib "contextvars" module by default.
|
|
268
|
+
|
|
269
|
+
Up to the update in July/Aug 2024 the core package functionality
|
|
270
|
+
was provided by a pure Python implementation which keeps context state
|
|
271
|
+
in a hidden frame-local variables - while that is throughfully tested
|
|
272
|
+
it performs a linear lookup in all the callchain for the context namespace.
|
|
273
|
+
|
|
274
|
+
For the 0.3 release, the "native" stdlib contextvars.ContextVar backed class,
|
|
275
|
+
has reached first class status, and is now the default method used.
|
|
276
|
+
|
|
277
|
+
The extracontext.NativeContextLocal class builds on Python's contextvars
|
|
278
|
+
instead of reimplementing all the functionality from scratch, and makes
|
|
279
|
+
simple namespaces and decorator-based scope isolation just work, with
|
|
280
|
+
all the safety and performance of the Python native implementation,
|
|
281
|
+
with none of the boilerplate or confuse API.
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
Next Steps:
|
|
285
|
+
-----------
|
|
286
|
+
(not so sure about these - they are fruit of some 2018 brainstorming for
|
|
287
|
+
features in a project I am not coding for anymore)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
1. Add a way to chain-contexts, so, for example
|
|
291
|
+
and app can have a root context with default values
|
|
292
|
+
|
|
293
|
+
1. Describe the capabilities of each Context class clearly in a data-scheme,
|
|
294
|
+
so one gets to know, and how to retrieve classes that can behave like maps, or
|
|
295
|
+
allow/hide outter context values, work as a full stack, support the context protocol (`with` command),
|
|
296
|
+
etc... (this is more pressing since stlib contextvar backed Context classes will
|
|
297
|
+
not allow for some of the capabilities in the pure-Python reimplementation in "ContextLocal")
|
|
298
|
+
|
|
299
|
+
1. Add a way to merge wrappers for different ContextLocal instances on the same function
|
|
300
|
+
|
|
301
|
+
1. Add an "auto" flag - all called functions/generators/co-routines create a child context by default.
|
|
302
|
+
|
|
303
|
+
1. Add support for a descriptor-like variable slot - so that values can trigger code when set or retrieved
|
|
304
|
+
|
|
305
|
+
1. Shared values and locks: values that are guarranteed to be the same across tasks/threads, and a lock mechanism allowing atomic operations with these values. (extra bonus: try to develop a deadlock-proof lock)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Context Local Variables
|
|
2
|
+
==========================
|
|
3
|
+
|
|
4
|
+
Implements a Pythonic way to work with PEP 567
|
|
5
|
+
contextvars (https://peps.python.org/pep-0567/ )
|
|
6
|
+
|
|
7
|
+
Introduced in Python 3.7, a design decision by the
|
|
8
|
+
authors of the feature decided to opt-out of
|
|
9
|
+
the simple namespace used by Python's own `threading.local`
|
|
10
|
+
implementation, and requires an explicit top level
|
|
11
|
+
declaration of each context-local variable, and
|
|
12
|
+
the (rather "unpythonic") usage of an explicit
|
|
13
|
+
call to `get` and `set` methods to manipulate
|
|
14
|
+
those.
|
|
15
|
+
|
|
16
|
+
This package does away with that, and brings simplicity
|
|
17
|
+
back - simply instantiate a `ContextLocal` namespace,
|
|
18
|
+
and any attributes set in that namespace will be unique
|
|
19
|
+
per thread and per asynchronous call chain (i.e.
|
|
20
|
+
unique for each independent task).
|
|
21
|
+
|
|
22
|
+
In a sense, these are a drop-in replacement for
|
|
23
|
+
`threading.local`, which will also work for
|
|
24
|
+
asynchronous programming without any change in code.
|
|
25
|
+
|
|
26
|
+
One should just avoid creating the "ContextLocal" instance itself
|
|
27
|
+
in a non-setup function or method - as the implementation
|
|
28
|
+
uses Python contextvars in by default, those are not
|
|
29
|
+
cleaned-up along with the local scope where they are
|
|
30
|
+
created - check the docs on the contextvar module for more
|
|
31
|
+
details.
|
|
32
|
+
|
|
33
|
+
However, creating the actual variables to use inside this namespace
|
|
34
|
+
can be made local to functions or methods: the same inner
|
|
35
|
+
ContextVar instance will be re-used when re-entering the function
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
|
|
40
|
+
Create one or more project-wide instances of "extracontext.ContextLocal"
|
|
41
|
+
Decorate your functions, co-routines, worker-methods and generators
|
|
42
|
+
that should hold their own states with that instance itself, using it as a decorator
|
|
43
|
+
|
|
44
|
+
and use the instance as namespace for private variables that will be local
|
|
45
|
+
and non-local until entering another callable decorated
|
|
46
|
+
with the instance itself - that will create a new, separated scope
|
|
47
|
+
visible inside the decorated callable.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
|
|
51
|
+
from extracontext import ContextLocal
|
|
52
|
+
|
|
53
|
+
# global namespace, available in any thread or async task:
|
|
54
|
+
ctx = ContextLocal()
|
|
55
|
+
|
|
56
|
+
def myworker():
|
|
57
|
+
# value set only visible in the current thread or asyncio task:
|
|
58
|
+
ctx.value = "test"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
More Features:
|
|
64
|
+
|
|
65
|
+
Unlike `threading.local` namespaces, one can explicitly isolate a contextlocal namespace
|
|
66
|
+
when calling a function even on the same thread or same async call chain (task). And unlike
|
|
67
|
+
`contextvars.ContextVar`, there is no need to have an explicit context copy
|
|
68
|
+
and often an intermediate function call to switch context: `extracontext.ContextLocal`
|
|
69
|
+
can isolate the context using either a `with` block or as a decorator
|
|
70
|
+
(when entering the decorated function, all variables in the namespace are automatically
|
|
71
|
+
protected against any changes that would be visible when that call returns,
|
|
72
|
+
the previous values being restored).
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
Example showing context separation for concurrent generators:
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from extracontext import ContextLocal
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
ctx = ContextLocal()
|
|
84
|
+
|
|
85
|
+
results = []
|
|
86
|
+
@ctx
|
|
87
|
+
def contexted_generator(value):
|
|
88
|
+
ctx.value = value
|
|
89
|
+
yield None
|
|
90
|
+
results.append(ctx.value)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def runner():
|
|
95
|
+
generators = [contexted_generator(i) for i in range(10)]
|
|
96
|
+
any(next(gen) for gen in generators)
|
|
97
|
+
any(next(gen, None) for gen in generators)
|
|
98
|
+
assert results == list(range(10))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
ContextLocal namespaces can also be isolated by context-manager blocks (`with` statement):
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from extracontext import ContextLocal
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def with_block_example():
|
|
108
|
+
|
|
109
|
+
ctx = ContextLocal()
|
|
110
|
+
ctx.value = 1
|
|
111
|
+
with ctx:
|
|
112
|
+
ctx.value = 2
|
|
113
|
+
assert ctx.value == 2
|
|
114
|
+
|
|
115
|
+
assert ctx.value == 1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
This is what one has to do if "isolated_function" will use a contextvar value
|
|
123
|
+
for other nested calls, but should not change the caller's visible value:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
##########################
|
|
127
|
+
# using stdlib contextvars:
|
|
128
|
+
|
|
129
|
+
import contextvars
|
|
130
|
+
|
|
131
|
+
# Each variable has to be declared at top-level:
|
|
132
|
+
value = contextvars.ContextVar("value")
|
|
133
|
+
|
|
134
|
+
def parent():
|
|
135
|
+
# explicit use of setter method for each value:
|
|
136
|
+
value.set(5)
|
|
137
|
+
# call to nested function which needs and isolated copy of context
|
|
138
|
+
# must be done in two stages:
|
|
139
|
+
new_context = contextvars.copy_context()
|
|
140
|
+
new_context.run(isolated_function)
|
|
141
|
+
# explicit use of getter method:
|
|
142
|
+
assert value.get() == 5
|
|
143
|
+
|
|
144
|
+
def isolated_function()
|
|
145
|
+
value.set(23)
|
|
146
|
+
# run other code that needs "23"
|
|
147
|
+
# ...
|
|
148
|
+
assert value.get(23)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This is the same code using this package:
|
|
154
|
+
```python
|
|
155
|
+
from extracontext import NativeContextLocal
|
|
156
|
+
|
|
157
|
+
# instantiate a namespace at top level:
|
|
158
|
+
ctx = NativeContextLocal()
|
|
159
|
+
|
|
160
|
+
def parent():
|
|
161
|
+
# create variables in the namespace without prior declaration:
|
|
162
|
+
# and just use the assignment operator (=)
|
|
163
|
+
ctx.value = 5
|
|
164
|
+
# no boilerplate to call function:
|
|
165
|
+
isolated_function()
|
|
166
|
+
# no need to call a getter:
|
|
167
|
+
assert ctx.value == 5
|
|
168
|
+
|
|
169
|
+
# Decorate function that should run in an isolated context:
|
|
170
|
+
@ctx
|
|
171
|
+
def isolated_function()
|
|
172
|
+
assert ctx.value == 5
|
|
173
|
+
ctx.value = 23
|
|
174
|
+
# run other code that needs "23"
|
|
175
|
+
# ...
|
|
176
|
+
assert ctx.value == 23
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Map namespaces
|
|
181
|
+
-----------------
|
|
182
|
+
|
|
183
|
+
The `ContextMap` class works just the same way, but works
|
|
184
|
+
as a mapping:
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
|
|
189
|
+
from extracontext import ContextMap
|
|
190
|
+
|
|
191
|
+
# global namespace, available in any thread or async task:
|
|
192
|
+
ctx = ContextMap()
|
|
193
|
+
|
|
194
|
+
def myworker():
|
|
195
|
+
# value set only visible in the current thread or asyncio task:
|
|
196
|
+
ctx["value"] = "test"
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Non Leaky Contexts
|
|
202
|
+
-------------------
|
|
203
|
+
Contrary to default contextvars usage, generators
|
|
204
|
+
(and async generators) running in another context do
|
|
205
|
+
take effect inside the generator, and doesn't
|
|
206
|
+
leak back to the calling scope:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
import extracontext
|
|
210
|
+
ctx = extracontext.ContextLocal()
|
|
211
|
+
@ctx
|
|
212
|
+
def isolatedgen(n):
|
|
213
|
+
for i in range(n):
|
|
214
|
+
ctx.myvar = i
|
|
215
|
+
yield i
|
|
216
|
+
print (ctx.myvar)
|
|
217
|
+
def test():
|
|
218
|
+
ctx.myvar = "lambs"
|
|
219
|
+
for j in isolatedgen(2):
|
|
220
|
+
print(ctx.myvar)
|
|
221
|
+
ctx.myvar = "wolves"
|
|
222
|
+
|
|
223
|
+
In [11]: test()
|
|
224
|
+
lambs
|
|
225
|
+
0
|
|
226
|
+
wolves
|
|
227
|
+
1
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
By using a stdlib `contextvars.ContextVar` one simply
|
|
231
|
+
can't isolate the body of a generator, save by
|
|
232
|
+
not running a `for` at all, and running all
|
|
233
|
+
iterations manually by calling `ctx_copy.run(next, mygenerator)`
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
New for 1.0
|
|
238
|
+
-----------
|
|
239
|
+
|
|
240
|
+
Switch the backend to use native Python contextvars (exposed in
|
|
241
|
+
the stdlib "contextvars" module by default.
|
|
242
|
+
|
|
243
|
+
Up to the update in July/Aug 2024 the core package functionality
|
|
244
|
+
was provided by a pure Python implementation which keeps context state
|
|
245
|
+
in a hidden frame-local variables - while that is throughfully tested
|
|
246
|
+
it performs a linear lookup in all the callchain for the context namespace.
|
|
247
|
+
|
|
248
|
+
For the 0.3 release, the "native" stdlib contextvars.ContextVar backed class,
|
|
249
|
+
has reached first class status, and is now the default method used.
|
|
250
|
+
|
|
251
|
+
The extracontext.NativeContextLocal class builds on Python's contextvars
|
|
252
|
+
instead of reimplementing all the functionality from scratch, and makes
|
|
253
|
+
simple namespaces and decorator-based scope isolation just work, with
|
|
254
|
+
all the safety and performance of the Python native implementation,
|
|
255
|
+
with none of the boilerplate or confuse API.
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
Next Steps:
|
|
259
|
+
-----------
|
|
260
|
+
(not so sure about these - they are fruit of some 2018 brainstorming for
|
|
261
|
+
features in a project I am not coding for anymore)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
1. Add a way to chain-contexts, so, for example
|
|
265
|
+
and app can have a root context with default values
|
|
266
|
+
|
|
267
|
+
1. Describe the capabilities of each Context class clearly in a data-scheme,
|
|
268
|
+
so one gets to know, and how to retrieve classes that can behave like maps, or
|
|
269
|
+
allow/hide outter context values, work as a full stack, support the context protocol (`with` command),
|
|
270
|
+
etc... (this is more pressing since stlib contextvar backed Context classes will
|
|
271
|
+
not allow for some of the capabilities in the pure-Python reimplementation in "ContextLocal")
|
|
272
|
+
|
|
273
|
+
1. Add a way to merge wrappers for different ContextLocal instances on the same function
|
|
274
|
+
|
|
275
|
+
1. Add an "auto" flag - all called functions/generators/co-routines create a child context by default.
|
|
276
|
+
|
|
277
|
+
1. Add support for a descriptor-like variable slot - so that values can trigger code when set or retrieved
|
|
278
|
+
|
|
279
|
+
1. Shared values and locks: values that are guarranteed to be the same across tasks/threads, and a lock mechanism allowing atomic operations with these values. (extra bonus: try to develop a deadlock-proof lock)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .base import ContextLocal
|
|
2
|
+
from .contextlocal import PyContextLocal, ContextError
|
|
3
|
+
from .mapping import ContextMap
|
|
4
|
+
from .contextlocal_native import NativeContextLocal
|
|
5
|
+
|
|
6
|
+
__version__ = "1.0.0b1"
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ContextLocal",
|
|
10
|
+
"ContextMap",
|
|
11
|
+
"PyContextLocal",
|
|
12
|
+
"ContextError",
|
|
13
|
+
"NativeContextLocal",
|
|
14
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code backported from Python 3.12:
|
|
3
|
+
Task.__init__ will accept a "contxt" parameter that is needed
|
|
4
|
+
in order to re-use contexts for async generator iterations.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
The subclass is created in order for the minimal of "copy-pasting" around
|
|
8
|
+
to be needed.
|
|
9
|
+
|
|
10
|
+
# **** License for this file: PSF License - ****
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import contextvars
|
|
14
|
+
import itertools
|
|
15
|
+
from asyncio.tasks import _PyTask, _register_task
|
|
16
|
+
|
|
17
|
+
# from asyncio import futures
|
|
18
|
+
|
|
19
|
+
from asyncio import coroutines
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_task_name_counter = itertools.count(1).__next__
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FutureTask(_PyTask):
|
|
26
|
+
# Just overrides __init__ with Python 3.12 _PyTask.__init__,
|
|
27
|
+
# which accepts the context as argument
|
|
28
|
+
|
|
29
|
+
def __init__(self, coro, *, loop=None, name=None, context=None, eager_start=False):
|
|
30
|
+
# skip Python < 3.10 Task.__init__ :
|
|
31
|
+
super(_PyTask, self).__init__(loop=loop)
|
|
32
|
+
if self._source_traceback:
|
|
33
|
+
del self._source_traceback[-1]
|
|
34
|
+
if not coroutines.iscoroutine(coro):
|
|
35
|
+
# raise after Future.__init__(), attrs are required for __del__
|
|
36
|
+
# prevent logging for pending task in __del__
|
|
37
|
+
self._log_destroy_pending = False
|
|
38
|
+
raise TypeError(f"a coroutine was expected, got {coro!r}")
|
|
39
|
+
|
|
40
|
+
if name is None:
|
|
41
|
+
self._name = f"FutureTask-{_task_name_counter()}"
|
|
42
|
+
else:
|
|
43
|
+
self._name = str(name)
|
|
44
|
+
|
|
45
|
+
self._num_cancels_requested = 0
|
|
46
|
+
self._must_cancel = False
|
|
47
|
+
self._fut_waiter = None
|
|
48
|
+
self._coro = coro
|
|
49
|
+
if context is None:
|
|
50
|
+
# this is the only codepath in Python < 3.10, and the reason for this hack:
|
|
51
|
+
self._context = contextvars.copy_context()
|
|
52
|
+
else:
|
|
53
|
+
self._context = context
|
|
54
|
+
|
|
55
|
+
if eager_start and self._loop.is_running():
|
|
56
|
+
self.__eager_start()
|
|
57
|
+
else:
|
|
58
|
+
self._loop.call_soon(self._Task__step, context=self._context)
|
|
59
|
+
_register_task(self)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class ContextLocal:
|
|
2
|
+
_backend_registry = {}
|
|
3
|
+
|
|
4
|
+
def __new__(cls, *args, backend=None, **kwargs):
|
|
5
|
+
if backend is None:
|
|
6
|
+
backend = getattr(cls, "_backend_key", "native")
|
|
7
|
+
|
|
8
|
+
cls = cls._backend_registry[backend]
|
|
9
|
+
## Do not forward arguments to object.__new__
|
|
10
|
+
if len(__class__.__mro__) == 2:
|
|
11
|
+
args, kwargs = (), {}
|
|
12
|
+
return super().__new__(cls, *args, **kwargs)
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, backend=None):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def __init_subclass__(cls, *args, **kw):
|
|
18
|
+
if hasattr(cls, "_backend_key"):
|
|
19
|
+
cls._backend_registry[cls._backend_key] = cls
|
|
20
|
+
super().__init_subclass__(*args, **kw)
|