python-extracontext 1.0.0__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,562 @@
1
+ Metadata-Version: 2.1
2
+ Name: python-extracontext
3
+ Version: 1.0.0
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
+ # Extracontext: Context Local Variables for everyone
28
+
29
+ ## Description
30
+
31
+ Provides [PEP 567](https://peps.python.org/pep-0567/)
32
+ compliant drop-in replacement for `threading.local`
33
+ namespaces.
34
+
35
+ The main goal of PEP 567, supersedding [PEP 550](https://peps.python.org/pep-0550/)
36
+ is to create a way to preserve information in
37
+ concurrent running contexts, including multithreading
38
+ and asynchronous (asyncio) tasks, allowing
39
+ each call stack to have its own versions of
40
+ variables containing settings, or request
41
+ parameters.
42
+
43
+ ### Quoting from PEP 567 Rationalle:
44
+ > Thread-local variables are insufficient for asynchronous
45
+ > tasks that execute concurrently in the same OS thread.
46
+ > Any context manager that saves and restores a context
47
+ > value using threading.local() will have its context values
48
+ > bleed to other code unexpectedly when used in async/await code.
49
+
50
+ ## Rationale for "extracontext"
51
+
52
+ Contextcars, introduced in Python 3.7, were
53
+ implemented following a design decision by the
54
+ which opted-out of the namespace approach
55
+ used by Python's own `threading.local`
56
+ implementation. It then requires an explicit top level
57
+ declaration of each context-local variable, and
58
+ the (rather "unpythonic") usage of an explicit
59
+ call to `get` and `set` methods to manipulate
60
+ those. Also, the only way to run some code in
61
+ an isolated context copy is to call a function
62
+ indirectly through means of the context object `.run` method.
63
+ This implies that:
64
+
65
+ 1. Knowing when to run something in a different context is responsability of the caller code
66
+ 2. Breaks the easy-to-use, easy-to-read, aesthetics, and overal complicates one of the most fundamental blocks of programming in inperative languages: calling functions.
67
+
68
+ This package does away with that, and brings simplicity
69
+ back, using dotted attributes to a namespace and `=`
70
+ for value assigment:
71
+
72
+ with stdlib native contexvars:
73
+
74
+ ```python
75
+ import contextvars
76
+
77
+ # Variable declaration: top level declaration and WET (write everything twice)
78
+ ctx_color = contextvars.ContextVar("ctx_color")
79
+ ctx_font = contextvars.ContextVar("ctx_font")
80
+
81
+ def blah():
82
+ ...
83
+ # use a set method:
84
+ ctx_color.set("red")
85
+ ctx_font.set("arial")
86
+
87
+ ...
88
+ myttext = ...
89
+ # call a markup render function,
90
+ # but take care it wont mix our attributes in temporary context changes
91
+ contextvars.context_copy().run(render_markup, mytext))
92
+ ...
93
+
94
+ def render_markup(text):
95
+ # markup function: knows it will mess up the context, but can't do
96
+ # a thing about it - the caller has to take care!
97
+ ...
98
+ ```
99
+
100
+ with extracontext:
101
+
102
+ ```python
103
+ import extracontext
104
+
105
+ # the only declaration needed at top level code
106
+ ctx = extracontext.ContextLocal()
107
+
108
+ def blah():
109
+ ctx.color = "red"
110
+ ctx.font = "arial"
111
+
112
+ mytext = ...
113
+ # simply calls the function
114
+ render_markup(mytext)
115
+ ...
116
+
117
+ @ctx
118
+ def render_markup(text):
119
+ # we will mess the context - but the decorator
120
+ # ensures no changes leak back to the caller
121
+ ...
122
+
123
+ ```
124
+
125
+ ## Usage
126
+ simply instantiate a `ContextLocal` namespace,
127
+ and any attributes set in that namespace will be unique
128
+ per thread and per asynchronous call chain (i.e.
129
+ unique for each independent task).
130
+
131
+ In a sense, these are a drop-in replacement for
132
+ `threading.local`, which will also work for
133
+ asynchronous programming without any change in code.
134
+
135
+ One should just avoid creating the "ContextLocal" instance itself
136
+ in a non-setup function or method - as the implementation
137
+ uses Python contextvars in by default, those are not
138
+ cleaned-up along with the local scope where they are
139
+ created - check the docs on the contextvar module for more
140
+ details.
141
+
142
+ However, creating the actual variables to use inside this namespace
143
+ can be made local to functions or methods: the same inner
144
+ ContextVar instance will be re-used when re-entering the function
145
+
146
+
147
+ Create one or more project-wide instances of "extracontext.ContextLocal"
148
+ Decorate your functions, co-routines, worker-methods and generators
149
+ that should hold their own states with that instance itself, using it as a decorator
150
+
151
+ and use the instance as namespace for private variables that will be local
152
+ and non-local until entering another callable decorated
153
+ with the instance itself - that will create a new, separated scope
154
+ visible inside the decorated callable.
155
+
156
+ ```python
157
+
158
+ from extracontext import ContextLocal
159
+
160
+ # global namespace, available in any thread or async task:
161
+ ctx = ContextLocal()
162
+
163
+ def myworker():
164
+ # value set only visible in the current thread or asyncio task:
165
+ ctx.value = "test"
166
+
167
+
168
+ ```
169
+
170
+ ## More Features:
171
+
172
+ ### extracontext namespaces work for generators
173
+
174
+ Unlike PEP 567 contextvars, extracontext
175
+ will sucessfully isolate contexts whe used with
176
+ generator-functions - meaning,
177
+ the generator body is actually executed in
178
+ an isolated context:
179
+
180
+ Example showing context separation for concurrent generators:
181
+
182
+ ```python
183
+ from extracontext import ContextLocal
184
+
185
+
186
+ ctx = ContextLocal()
187
+
188
+ results = []
189
+ @ctx
190
+ def contexted_generator(value):
191
+ ctx.value = value
192
+ yield None
193
+ results.append(ctx.value)
194
+
195
+
196
+ def runner():
197
+ generators = [contexted_generator(i) for i in range(10)]
198
+ any(next(gen) for gen in generators)
199
+ any(next(gen, None) for gen in generators)
200
+ assert results == list(range(10))
201
+ ```
202
+
203
+ This is virtually impossible with contextvars. (Ok,
204
+ not impossible - the default extracontext backend
205
+ does that using contextvars after all - but it encapsulates
206
+ the complications for you)
207
+
208
+ This feature also works with async generators`
209
+
210
+
211
+ Another example of this feature:
212
+
213
+ ```python
214
+ import extracontext
215
+ ctx = extracontext.ContextLocal()
216
+ @ctx
217
+ def isolatedgen(n):
218
+ for i in range(n):
219
+ ctx.myvar = i
220
+ yield i
221
+ print (ctx.myvar)
222
+ def test():
223
+ ctx.myvar = "lambs"
224
+ for j in isolatedgen(2):
225
+ print(ctx.myvar)
226
+ ctx.myvar = "wolves"
227
+
228
+ In [11]: test()
229
+ lambs
230
+ 0
231
+ wolves
232
+ 1
233
+ ```
234
+
235
+
236
+ ### Change context within a context-manager `with` block:
237
+
238
+ ContextLocal namespaces can also be isolated by context-manager blocks (`with` statement):
239
+
240
+ ```python
241
+ from extracontext import ContextLocal
242
+
243
+
244
+ def with_block_example():
245
+
246
+ ctx = ContextLocal()
247
+ ctx.value = 1
248
+ with ctx:
249
+ ctx.value = 2
250
+ assert ctx.value == 2
251
+
252
+ assert ctx.value == 1
253
+
254
+
255
+ ```
256
+
257
+
258
+ ### Map namespaces
259
+
260
+ Beyond namespace usages, `extracontext` offer ways
261
+ to have contexts working as mutable mappings,
262
+ using the `ContextMap` class.
263
+
264
+
265
+ ```python
266
+
267
+ from extracontext import ContextMap
268
+
269
+ # global namespace, available in any thread or async task:
270
+ ctx = ContextMap()
271
+
272
+ def myworker():
273
+ # value set only visible in the current thread or asyncio task:
274
+ ctx["value"] = "test"
275
+
276
+
277
+ ```
278
+
279
+ ### typing support
280
+ There is no explicit typing support yet - but note that through the use of
281
+ `ContextMap` it is possible to have declare some types, by
282
+ simple declaring `Mapping[type1:type2]` typing.
283
+
284
+
285
+ ## Specification and Implementation
286
+
287
+ ### ContextLocal
288
+
289
+ `ContextLocal` is the main class, and should suffice for most uses.
290
+ It only takes the `backend` keyword-only argument, which selects
291
+ the usage of the pure-Python backend (`"python"`) or using
292
+ a contextvars.ContextVar backend (`"native"`). The later is the default
293
+ behavior. Calling this class will actually create
294
+ an instance of the appropriate subclass, according to
295
+ the backend: either `PyContextLocal` or `NativeContextLocal` -
296
+ in the same way stdlib `pathlib.Path` creates
297
+ an instance of Path appropriate for Posix, or Windows style
298
+ paths. (This pattern probably have a name - help welcome).
299
+
300
+ An instance of it will create a new, fresh, namespace.
301
+ Use dotted attribute access to populate it - each variable set
302
+ in this way will persist through the context lifetime.
303
+
304
+ #### Usage as a decorator:
305
+ When used as a decorator for a function or method, that callable
306
+ will automatically be executed in a copy of the calling context -
307
+ meaning no changes it makes to any variable in the namespace
308
+ is visible outside of the call.
309
+
310
+ The decorator (and the isolation provided) works for
311
+ both plain functions, generator functions, co-routine functions
312
+ and async generator functions - meaning that whenever the
313
+ execution switches to the caller context
314
+ (in `yield` or `await` expression) the context is
315
+ restored to that of the caller, and when it
316
+ re-enters the paused code block, the isolated
317
+ context is restored.
318
+
319
+
320
+ ```python
321
+ from extracontext import ContextLocal
322
+
323
+ ctx = ContextLocal()
324
+
325
+ @ctx
326
+ def isolated_example():
327
+
328
+ ctx.value = 2
329
+ assert ctx.value = 2
330
+
331
+ ctx.value = 1
332
+ isolated_example()
333
+ assert ctx.value == 1
334
+
335
+ ```
336
+
337
+ #### Usage as a context manager
338
+
339
+ A `ContextLocal` instance can simply be used in a
340
+ context manager `with` statement, and any variables
341
+ set or changed within the block will not be
342
+ persisted after the block is over.
343
+
344
+ ```python
345
+ from extracontext import ContextLocal
346
+
347
+
348
+ def with_block_example():
349
+
350
+ ctx = ContextLocal()
351
+ ctx.value = 1
352
+ with ctx:
353
+ ctx.value = 2
354
+ assert ctx.value == 2
355
+
356
+ assert ctx.value == 1
357
+
358
+ ```
359
+
360
+ Also, they are re-entrant, so if in a function called
361
+ within the block, the context is used again
362
+ as a context manager, it will just work.
363
+
364
+
365
+ #### Semantic difference to contextvars.ContextVar
366
+ Note that a fresh `ContextLocal()` instance will
367
+ be empty, and have access to none of the values _or names_
368
+ set in another instance. This contrasts sharply with
369
+ `contextvars.Context`, for which each `contextvars.ContextVar`
370
+ created anywhere else in the program (even 3rd party
371
+ modules) is a valid key.
372
+
373
+
374
+ ### PyContextLocal
375
+ ContextLocal implementation using pure Python code, and
376
+ reimplementing the functionalities of Contexts and ContextVars
377
+ as implemented by PEP 567 fro scratch.
378
+
379
+ It works by seeting, in a "hidden" way, values in the caller's
380
+ closure (the `locals()` namespace). Though writting
381
+ to this namespace has traditionally been a "grey area"
382
+ in Python, the way it makes use of this data is compliant
383
+ with the specs in [PEP-558](https://peps.python.org/pep-0558/)
384
+ which officializes this use for Python 3.13 and beyond
385
+ (and it has always worked since Python 3.0.
386
+ The first implementations of this code where
387
+ tested against Python 3.4 and forward)
388
+
389
+ It should be kept in place for the time being,
390
+ and could be useful to allow customizations,
391
+ workarounds, or buggy behavior bypassing
392
+ where the native implementation presents
393
+ any short-commings.
394
+
395
+ It is not an easy to follow code, as in
396
+ one hand there are introspection and meta-programming
397
+ patterns to handle access to the data in a containirized way.
398
+
399
+ Keep in mind that native contexvars use an
400
+ internal copy-on-write structure in native code
401
+ which should be much more performant than
402
+ the chain-mapping checks used in this backend.
403
+
404
+
405
+ It has been throughfully tested and should be bug free,
406
+ though less performant.
407
+
408
+ ### NativeContextLocal
409
+
410
+ This leverages on PEP 567 Contexts and ContextVars
411
+ to perform all the isolation and setting mechanics,
412
+ and provides an convenient wrapper layer
413
+ which works as a namespace (and as mapping in NativeContextMap)
414
+
415
+ It was made the default mechanism due to obvious
416
+ performances and updates taking place in the
417
+ embedded implementation in the language.
418
+
419
+ The normal ContextVarsAPI exposed to Python
420
+ would not allow for changing context inside the
421
+ same function, requiring a `Context.run` call
422
+ as the only way to switch contexts. Instead of releasing this
423
+ backend without this mechanism, it has been opted
424
+ to call the native cAPI for changing
425
+ context (using `ctypes` in cPython, and the relevant internal
426
+ calls on pypy) so that the feature can work.
427
+
428
+ When this feature was implemented, `NativeContextLocal`
429
+ instances could then work as a context-manager using
430
+ the `with` statement, and there were no reasons why
431
+ they should not be the default backend. Some
432
+ coding effort were placed in the "Reverse subclass picking"
433
+ mechanism, and it was made te default in a backwards-
434
+ compatible way.
435
+
436
+ ### ContextMap
437
+
438
+ `ContextMap` is a `ContextLocal` subclass which implements
439
+ [the `MutableMapping` interface](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping).
440
+ It is pretty straightforward in
441
+ that, so that assigments and retrievals using the `ctx["key"]`
442
+ syntax are made available, functionality with the
443
+ `in`, `==`, `!=` operators and the `keys`, `items`, `values`, `get`, `pop`, `popitem`, `clear`, `update`, and `setdefault` methods.
444
+
445
+ It supports loadding a mapping with the initial context contents, passed as
446
+ the `initial` positional argument - but not keyword-args mapping to initial
447
+ content (as in `dict(a=1)`).
448
+
449
+ Also, it is a subclass of ContextLocal - so it also allows access to the
450
+ keys with the dotted attribute syntax:
451
+
452
+ ```python
453
+
454
+ a = extracontext.ContextMap
455
+
456
+ a["b"] = 1
457
+
458
+ assert a.b == 1
459
+
460
+ ```
461
+
462
+ And finally, it uses the same `backend` keyword-arg mechanism to switch between the default
463
+ native-context vars backend and the pure Python backend, which will yield either
464
+ a `PyContextMap` or a `NativeContextMap` instance, accordingly.
465
+
466
+ ### PyContextMap
467
+ `ContextMap` implementation as a subclass of `PyContextLocal`
468
+
469
+ ### NativeContextMap
470
+ `ContextMap` implementation as a subclass of `NativeContextLocal`
471
+
472
+
473
+
474
+ ### History
475
+ The original implementation from 2019 re-creates
476
+ all the functionality provided by the PEP 567
477
+ contextvars using pure Python code and a lot
478
+ of introspection and meta-programming.
479
+ Not sure why it did that - but one thing is that
480
+ it coud provide the functionality for older
481
+ Pythons at the time, and possibly also because
482
+ I did not see, at the time, other ways
483
+ to workaround the need to call a function
484
+ in order to switch contexts.
485
+
486
+ At some revival sprint in 2021, a backend
487
+ using native contextvars was created -
488
+ and it just got to completion,
489
+ with all features and tests for the edge clases in
490
+ August 2024, after other periods of non-activity.
491
+
492
+ At this point, a mechanism for picking the
493
+ desired backend was implemented, and the native
494
+ `ContextLocal` class was switched to use the
495
+ native stdlib contextvars as backend by default.
496
+ (This should be much faster - benchmark
497
+ contributions are welcome, though :-) )
498
+
499
+
500
+ ## New for 1.0
501
+
502
+ Switch the backend to use native Python contextvars (exposed in
503
+ the stdlib "contextvars" module by default.
504
+
505
+ Up to the update in July/Aug 2024 the core package functionality
506
+ was provided by a pure Python implementation which keeps context state
507
+ in a hidden frame-local variables - while that is throughfully tested
508
+ it performs a linear lookup in all the callchain for the context namespace.
509
+
510
+ For the 0.3 release, the "native" stdlib contextvars.ContextVar backed class,
511
+ has reached first class status, and is now the default method used.
512
+
513
+ The extracontext.NativeContextLocal class builds on Python's contextvars
514
+ instead of reimplementing all the functionality from scratch, and makes
515
+ simple namespaces and decorator-based scope isolation just work, with
516
+ all the safety and performance of the Python native implementation,
517
+ with none of the boilerplate or confuse API.
518
+
519
+
520
+ ## Next Steps
521
+
522
+ 1. Implementing more of the features possible with the contextvars semantics
523
+ - `.run` and `.copy` methods
524
+ - direct access to "`Token`"s as used by contextvars
525
+ - default value setting for variables
526
+
527
+ 1. A feature allowing other threads to start from a copy of the current context, instead of an empty context. (asyncio independent tasks always see a copy)
528
+
529
+ 1. Bringing in some more typing support
530
+ (not sure what will be possible, but I believe some
531
+ `typing.Protocol` templates at least. On an
532
+ initial search, typing for namespaces is not
533
+ a widelly known feature (if at all)
534
+
535
+ 1. (maybe?) Proper multiprocessing support:
536
+ - ironing out probable serialization issues,
537
+ - allowing subprocess workers to start from a copy of the current context.
538
+
539
+ 1. (maybe?) support for nested namespaces and maps.
540
+
541
+ ### Old "Next Steps":
542
+ -----------
543
+ (not so sure about these - they are fruit of some 2019 brainstorming for
544
+ features in a project I am not coding for anymore)
545
+
546
+
547
+ 1. Add a way to chain-contexts, so, for example
548
+ and app can have a root context with default values
549
+
550
+ 1. Describe the capabilities of each Context class clearly in a data-scheme,
551
+ so one gets to know, and how to retrieve classes that can behave like maps, or
552
+ allow/hide outter context values, work as a full stack, support the context protocol (`with` command),
553
+ etc... (this is more pressing since stlib contextvar backed Context classes will
554
+ not allow for some of the capabilities in the pure-Python reimplementation in "ContextLocal")
555
+
556
+ 1. Add a way to merge wrappers for different ContextLocal instances on the same function
557
+
558
+ 1. Add an "auto" flag - all called functions/generators/co-routines create a child context by default.
559
+
560
+ 1. Add support for a descriptor-like variable slot - so that values can trigger code when set or retrieved
561
+
562
+ 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.