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.
@@ -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)