python-extracontext 1.0.0b1__py3-none-any.whl

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,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)
extracontext/base.py ADDED
@@ -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)
@@ -0,0 +1,263 @@
1
+ """
2
+ Super context wrapper -
3
+
4
+ meant to be simpler to use and work in more scenarios than
5
+ Python's contextvars.
6
+
7
+ Usage:
8
+ Create one or more project-wide instances of "ContextLocal"
9
+ Decorate your functions, co-routines, worker-methods and generators
10
+ that should hold their own states with that instance's `context` method -
11
+
12
+ and use the instance as namespace for private variables that will be local
13
+ and non-local until entering another callable decorated
14
+ with that instance - that will create a new, separated scope
15
+ visible inside the decorated callable.
16
+
17
+
18
+ """
19
+
20
+ import uuid
21
+ import sys
22
+ import typing as T
23
+
24
+ from functools import wraps
25
+ from types import FrameType
26
+ from weakref import WeakKeyDictionary
27
+
28
+ from .base import ContextLocal
29
+
30
+ __author__ = "João S. O. Bueno"
31
+ __license__ = "LGPL v. 3.0+"
32
+
33
+
34
+ class ContextError(AttributeError):
35
+ pass
36
+
37
+
38
+ _sentinel = object()
39
+
40
+
41
+ class _WeakableId:
42
+ """Used internally to identify Frames with context data attached using weakrefs"""
43
+
44
+ __slots__ = ["__weakref__", "value"]
45
+
46
+ def __init__(self, v=0):
47
+ if not v:
48
+ v = int(uuid.uuid4())
49
+ self.value = v
50
+
51
+ def __eq__(self, other):
52
+ return self.value == other.value
53
+
54
+ def __hash__(self):
55
+ return hash(self.value)
56
+
57
+ def __repr__(self):
58
+ return f"ID({uuid.UUID(int=self.value)})"
59
+
60
+
61
+ class PyContextLocal(ContextLocal):
62
+ """Creates a namespace object whose attributes can keep individual and distinct values for
63
+ the same key for code running in parallel - either in asyncio tasks, or threads.
64
+
65
+ The bennefits are the same one gets by using contextvars.ContextVar from the stdlib as
66
+ specified on PEP 567. However extracontext.ContextLocal is designed to be easier
67
+ and more convenient to use - as a single instance can hold values for several
68
+ keys, just as happens with threading.local objects. And no special getter and
69
+ setter methods are needed to retrieve the unique value stored in the current
70
+ context: normal attribute access and assignment works transparently.
71
+
72
+ Internally, the current implementation uses a completly different way to
73
+ keep distinct states where needed: the "locals" mapping for each execution
74
+ frame is used as storage for the unique values in an async task context, or in
75
+ a thread. Although not recomended up to now, read/write access to non-local-variables
76
+ in the "locals" mapping is specified on PEP 558. While that PEP is not
77
+ final, it is clear in its texts that the capability of using "locals" as
78
+ a mapping to convey data will be kept and made official.
79
+
80
+ References to the frames containing context data is kept using
81
+ weakreferences, so when a Frame ends up execution, its contents
82
+ are deleted normally, with no risks of frame data
83
+ hanging around due to PyContextLocal data.
84
+
85
+
86
+ """
87
+
88
+ # TODO: change _BASEDIST to a property counting the intermediate
89
+ # methods between subclasses and the methods here.
90
+ _BASEDIST = 0
91
+
92
+ _backend_key = "python"
93
+
94
+ def __init__(self, **kwargs):
95
+ super().__init__(**kwargs)
96
+ super().__setattr__("_et_registry", WeakKeyDictionary())
97
+
98
+ def _introspect_registry(
99
+ self, name: T.Optional[str] = None, starting_frame: int = 2
100
+ ) -> T.Tuple[dict, T.Tuple[int, int]]:
101
+ """
102
+ returns the first namespace found for this context, if name is None
103
+ else, the first namespace where the name exists. The second return
104
+ value is a tuple inticatind the frame distance to the topmost namespace
105
+ and the frame distance to the returned namespace.
106
+ This way callers can tell if the searched name is on the topmost
107
+ namespace and act accordingly. ("del" needs this information,
108
+ as it can't remove information on an outter namespace)
109
+ """
110
+ starting_frame += self._BASEDIST
111
+ f: T.Optional[FrameType] = sys._getframe(starting_frame)
112
+ count = 0
113
+ first_ns = None
114
+ while f:
115
+ hf = self._frameid(f)
116
+ if hf in self._et_registry:
117
+ if first_ns is None:
118
+ first_ns = count
119
+ registered_namespaces = f.f_locals["$contexts"]
120
+ for namespace_index in reversed(self._et_registry[hf]):
121
+ namespace = registered_namespaces[namespace_index]
122
+ if name is None or name in namespace:
123
+ return namespace, (first_ns, count)
124
+ count += 1
125
+ f = f.f_back
126
+
127
+ if name:
128
+ raise ContextError(f"{name !r} not defined in any previous context")
129
+ raise ContextError("No previous context set")
130
+
131
+ def _frameid(self, frame: FrameType) -> _WeakableId:
132
+ if not "$contexts_salt" in frame.f_locals:
133
+ frame.f_locals["$contexts_salt"] = _WeakableId()
134
+ return frame.f_locals["$contexts_salt"]
135
+
136
+ def _register_context(self, f: FrameType) -> None:
137
+ hf = self._frameid(f)
138
+ contexts_list = f.f_locals.setdefault("$contexts", [])
139
+ contexts_list.append({})
140
+ self._et_registry.setdefault(hf, []).append(len(contexts_list) - 1)
141
+
142
+ def _pop_context(self, f: FrameType) -> None:
143
+ hf = self._frameid(f)
144
+ context_being_popped = self._et_registry[hf].pop()
145
+ contexts_list = f.f_locals["$contexts"]
146
+ contexts_list[context_being_popped] = None
147
+
148
+ def __getattr__(self, name: str) -> T.Any:
149
+ try:
150
+ namespace, _ = self._introspect_registry(name)
151
+ result = namespace[name]
152
+ if result is _sentinel:
153
+ raise KeyError(name)
154
+ return result
155
+ except (ContextError, KeyError):
156
+ raise AttributeError(f"Attribute not set: {name}")
157
+
158
+ def __setattr__(self, name: str, value: T.Any) -> None:
159
+ try:
160
+ namespace, _ = self._introspect_registry()
161
+ except ContextError:
162
+ # Automatically creates a new namespace if not inside
163
+ # any explicit denominated context:
164
+ self._register_context(sys._getframe(1 + self._BASEDIST))
165
+ namespace, _ = self._introspect_registry()
166
+
167
+ namespace[name] = value
168
+
169
+ def __delattr__(self, name: str) -> None:
170
+ try:
171
+ namespace, (topmost_ns, found_ns) = self._introspect_registry(name)
172
+ except ContextError:
173
+ raise AttributeError(name)
174
+ if topmost_ns == found_ns:
175
+ result = namespace[name]
176
+ if result is not _sentinel:
177
+ if "$deleted" in namespace and name in namespace["$deleted"]:
178
+ # attribute exists in target namespace, but the outter
179
+ # attribute had previously been shadowed by a delete -
180
+ # restore the shadowing:
181
+ setattr(self, name, _sentinel)
182
+
183
+ else:
184
+ # Remove topmost name assignemnt, and outer value is exposed
185
+ # ("one_level" attribute stacking behavior as described in 'features.py'
186
+ # disbled as unecessaryly complex):
187
+ # del namespace[name]
188
+
189
+ # To preserve "entry_only" behavior:
190
+ namespace.setdefault("$deleted", set()).add(name)
191
+ setattr(self, name, _sentinel)
192
+ return
193
+ # value is already shadowed:
194
+ raise AttributeError(name)
195
+
196
+ # Name is found, but it is not on the top-most level, so attribute is shadowed:
197
+ setattr(self, name, _sentinel)
198
+ # fossil: namespace, _ = self._introspect_registry(name)
199
+ namespace.setdefault("$deleted", set()).add(name)
200
+
201
+ def __call__(self, callable_: T.Callable) -> T.Callable:
202
+ @wraps(callable_)
203
+ def wrapper(*args, **kw):
204
+ f = sys._getframe()
205
+ self._register_context(f)
206
+ f_id = self._frameid(f)
207
+ result = _sentinel
208
+ try:
209
+ result = callable_(*args, **kw)
210
+ finally:
211
+ if f_id in self._et_registry:
212
+ del self._et_registry[f_id]
213
+ # Setup context for generator, async generator or coroutine if one was returned:
214
+ if result is not _sentinel:
215
+ frame = None
216
+ for frame_attr in ("gi_frame", "ag_frame", "cr_frame"):
217
+ frame = getattr(result, frame_attr, None)
218
+ if frame:
219
+ self._register_context(frame)
220
+ return result
221
+
222
+ return wrapper
223
+
224
+ def __enter__(self):
225
+ self._register_context(sys._getframe(1))
226
+ return self
227
+
228
+ def __exit__(self, exc_type, exc_value, traceback):
229
+ self._pop_context(sys._getframe(1))
230
+
231
+ def _run(self, callable_, *args, **kw):
232
+ """Runs callable with an isolated context
233
+ no need to decorate the target callable
234
+ """
235
+ with self:
236
+ return callable_(*args, **kw)
237
+
238
+ def __dir__(self) -> T.List[str]:
239
+ frame_count = 2
240
+ all_attrs = set()
241
+ seen_namespaces = set()
242
+ while True:
243
+ try:
244
+ namespace, _ = self._introspect_registry(starting_frame=frame_count)
245
+ except (
246
+ ValueError,
247
+ ContextError,
248
+ ): # ValueError can be raised sys._getframe inside _introspect_registry
249
+ break
250
+ frame_count += 1
251
+ if id(namespace) in seen_namespaces:
252
+ continue
253
+ for key, value in namespace.items():
254
+ if not key.startswith("$") and value is not _sentinel:
255
+ all_attrs.add(key)
256
+
257
+ seen_namespaces.add(id(namespace))
258
+ all_attrs = (
259
+ attr
260
+ for attr in all_attrs
261
+ if getattr(self, attr, _sentinel) is not _sentinel
262
+ )
263
+ return sorted(all_attrs)
@@ -0,0 +1,262 @@
1
+ """
2
+ Super context wrapper -
3
+
4
+ Meant to have the same interface as the easy-to-use PyContextLocal,
5
+ implemented 100% in Python, but backed by PEP 567 stdlib contextvar.ContextVar
6
+
7
+
8
+ """
9
+
10
+ import asyncio
11
+ import inspect
12
+ import sys
13
+ import threading
14
+
15
+ from functools import wraps
16
+ from contextvars import ContextVar, copy_context
17
+
18
+ from .base import ContextLocal
19
+
20
+ if sys.implementation.name == "pypy":
21
+ pypy = True
22
+ from __pypy__ import (
23
+ get_contextvar_context as _get_contextvar_context,
24
+ set_contextvar_context as _set_contextvar_context,
25
+ )
26
+
27
+ else:
28
+ pypy = False
29
+ try:
30
+ import ctypes
31
+ except ImportError as error:
32
+ import warnings
33
+
34
+ warnings.warn(
35
+ f"Couldn't import ctypes! `with` context blocks for NativeContextLocal won't work:\n {error.msg}"
36
+ )
37
+ warnings.warn(
38
+ "\n\nIf you need this feature in subinterpreters, please open a project issue"
39
+ )
40
+
41
+ if sys.version_info < (3, 10):
42
+ from types import AsyncGeneratorType
43
+
44
+ anext = AsyncGeneratorType.__anext__
45
+
46
+ __author__ = "João S. O. Bueno"
47
+ __license__ = "LGPL v. 3.0+"
48
+
49
+ _sentinel = object()
50
+
51
+
52
+ class NativeContextLocal(ContextLocal):
53
+ """Uses th native contextvar module in the stdlib (PEP 567)
54
+ to provide a context-local namespace in the way
55
+ threading.local works for threads.
56
+
57
+ Assignements and reading from the namespace
58
+ should work naturally with no need to call `get` and `set` methods.
59
+
60
+ A new contextvar variable is created in the current (contextvars) context
61
+ for _each_ attribute acessed on this namespace.
62
+
63
+ Also, attributes prefixed with a single "_et_" are intended for internal
64
+ use and will not be namespaced contextvars.
65
+
66
+ # In contrast to the pure-Python implementation there are
67
+ # some limitations,such as the impossibility to work
68
+ # in as a contextmanager (Python `with` block),.
69
+
70
+ [Work In Progress]
71
+ """
72
+
73
+ _backend_key = "native"
74
+ _ctypes_initialized = False
75
+
76
+ def __init__(self, **kwargs):
77
+ super().__init__(**kwargs)
78
+ self._et_registry = {}
79
+ self._et_stack = {}
80
+ self._et_lock = threading.Lock()
81
+
82
+ def __getattr__(self, name):
83
+ var = self._et_registry.get(name, None)
84
+ if var is None:
85
+ raise AttributeError(f"Attribute not set: {name}")
86
+ try:
87
+ value = var.get()
88
+ except LookupError as error:
89
+ raise AttributeError from error
90
+ if value is _sentinel:
91
+ raise AttributeError(f"Attribute not set: {name}")
92
+ return value
93
+
94
+ def __setattr__(self, name, value):
95
+ if name.startswith("_et_"):
96
+ return super().__setattr__(name, value)
97
+ var = self._et_registry.get(name, _sentinel)
98
+ if var is _sentinel:
99
+ var = self._et_registry[name] = ContextVar(name)
100
+ var.set(value)
101
+
102
+ def __delattr__(self, name):
103
+ if getattr(self, name, _sentinel) is _sentinel:
104
+ raise AttributeError(f"Attribute not set: {name}")
105
+ setattr(self, name, _sentinel)
106
+
107
+ def __call__(self, callable_):
108
+ @wraps(callable_)
109
+ def wrapper(*args, **kw):
110
+ return self._run(callable_, *args, **kw)
111
+
112
+ return wrapper
113
+
114
+ def _ensure_api_ready(self):
115
+ if not self._ctypes_initialized:
116
+ ctypes.pythonapi.PyContext_Enter.argtypes = [ctypes.py_object]
117
+ ctypes.pythonapi.PyContext_Exit.argtypes = [ctypes.py_object]
118
+ ctypes.pythonapi.PyContext_Enter.restype = ctypes.c_int32
119
+ ctypes.pythonapi.PyContext_Exit.restype = ctypes.c_int32
120
+ self.__class__._ctypes_initialized = True
121
+
122
+ def _get_ctx_key(self):
123
+ key_thread = threading.current_thread()
124
+ try:
125
+ key_task = asyncio.current_task()
126
+ except RuntimeError:
127
+ key_task = None
128
+ return (key_thread, key_task)
129
+
130
+ def _enter_ctx(self, new_ctx):
131
+ if pypy:
132
+ prev_ctx = _get_contextvar_context()
133
+ _set_contextvar_context(new_ctx)
134
+ return prev_ctx
135
+ self._ensure_api_ready()
136
+ result = ctypes.pythonapi.PyContext_Enter(new_ctx)
137
+ if result != 0:
138
+ raise RuntimeError(f"Something went wrong entering context {new_ctx}")
139
+ return None
140
+
141
+ def _exit_ctx(self, current_ctx, prev_ctx):
142
+ if pypy:
143
+ _set_contextvar_context(prev_ctx)
144
+ return
145
+ result = ctypes.pythonapi.PyContext_Exit(current_ctx)
146
+ if result != 0:
147
+ raise RuntimeError(f"Something went wrong exiting context {current_ctx}")
148
+
149
+ def __enter__(self):
150
+ new_ctx = copy_context()
151
+ prev_ctx = self._enter_ctx(new_ctx)
152
+ with self._et_lock:
153
+ self._et_stack.setdefault(self._get_ctx_key(), []).append(
154
+ (new_ctx, prev_ctx)
155
+ )
156
+ return self
157
+
158
+ def __exit__(self, exc_type, exc_value, traceback):
159
+ key = self._get_ctx_key()
160
+ with self._et_lock:
161
+ current_ctx, prev_ctx = self._et_stack[key].pop()
162
+ if not self._et_stack[key]:
163
+ self._et_stack.pop(key)
164
+ self._exit_ctx(current_ctx, prev_ctx)
165
+
166
+ def _run(self, callable_, *args, **kw):
167
+ """Runs callable with an isolated context
168
+ no need to decorate the target callable
169
+ """
170
+ new_context = copy_context()
171
+ result = new_context.run(callable_, *args, **kw)
172
+ if inspect.isawaitable(result):
173
+ result = self._awaitable_wrapper(result, new_context)
174
+ elif inspect.isgenerator(result):
175
+ result = self._generator_wrapper(result, new_context)
176
+ elif inspect.isasyncgen(result):
177
+ result = self._async_generator_wrapper(result, new_context)
178
+ # raise NotImplementedError("NativeContextLocal doesn't yet work with async generators")
179
+ return result
180
+
181
+ @staticmethod
182
+ def _generator_wrapper(generator, ctx_copy):
183
+ value = None
184
+ while True:
185
+ try:
186
+ if value is None:
187
+ value = yield ctx_copy.run(next, generator)
188
+ else:
189
+ value = yield ctx_copy.run(generator.send, value)
190
+ except StopIteration as stop:
191
+ return stop.value
192
+ except Exception as exc:
193
+ # for debugging times: this will be hard without a break here!
194
+ # print(exc)
195
+ try:
196
+ value = ctx_copy.run(generator.throw, exc)
197
+ except StopIteration as stop:
198
+ return stop.value
199
+
200
+ if sys.version_info >= (3, 11):
201
+
202
+ async def _awaitable_wrapper(self, coro, ctx_copy):
203
+ def trampoline():
204
+ return asyncio.create_task(coro, context=ctx_copy)
205
+
206
+ return await ctx_copy.run(trampoline)
207
+
208
+ else:
209
+
210
+ async def _awaitable_wrapper(self, coro, ctx_copy):
211
+ from ._future_task import FutureTask
212
+
213
+ loop = asyncio.get_running_loop()
214
+
215
+ def trampoline():
216
+ return FutureTask(coro, loop=loop, context=ctx_copy)
217
+
218
+ return await ctx_copy.run(trampoline)
219
+
220
+ ## this fails in spetacular and inovative ways!
221
+ # async def _awaitable_wrapper(self, coro, ctx_copy, force_context=True):
222
+ # if force_context:
223
+ # try:
224
+ # self._enter_ctx(ctx_copy)
225
+ # result = await coro
226
+ # finally:
227
+ # self._exit_ctx(ctx_copy)
228
+ # return result
229
+ # else:
230
+ # return await coro
231
+
232
+ async def _awaitable_wrapper2(self, coro, ctx_copy):
233
+ raise NotImplementedError(
234
+ """This code will only work with Python versions > 3.11. Please use `ContextLocal(backend="python")` for Python version 3.8 - 3.10"""
235
+ )
236
+
237
+ async def _async_generator_wrapper(self, generator, ctx_copy):
238
+ value = None
239
+ while True:
240
+ try:
241
+ if value is None:
242
+ async_res = ctx_copy.run(anext, generator)
243
+ else:
244
+ async_res = ctx_copy.run(generator.asend, value)
245
+ value = yield await self._awaitable_wrapper(async_res, ctx_copy)
246
+ except StopAsyncIteration:
247
+ break
248
+ except Exception as exc:
249
+ # for debugging times: this will be hard without a break here!
250
+ # print("*" * 50 , exc)
251
+ try:
252
+ async_res = ctx_copy.run(generator.athrow, exc)
253
+ value = yield await self._awaitable_wrapper(async_res, ctx_copy)
254
+ except StopAsyncIteration:
255
+ break
256
+
257
+ def __dir__(self):
258
+ return list(
259
+ key
260
+ for key, value in self._et_registry.items()
261
+ if value.get() is not _sentinel
262
+ )
@@ -0,0 +1,51 @@
1
+ from collections.abc import MutableMapping
2
+
3
+ from .base import ContextLocal
4
+ from .contextlocal import PyContextLocal
5
+ from .contextlocal_native import NativeContextLocal
6
+
7
+
8
+ class ContextMap(MutableMapping, ContextLocal):
9
+ """Works the same as PyContextLocal,
10
+ but uses the mapping interface instead of dealing with instance attributes.
11
+
12
+ Ideal, as for most map uses, when the keys depend on data rather than
13
+ hardcoded state variables
14
+ """
15
+
16
+ _backend_registry = {}
17
+
18
+ def __init__(self, *, backend=None, **kwargs):
19
+ super().__init__()
20
+ for key, value in kwargs.items():
21
+ self[key] = value
22
+
23
+ def __getitem__(self, name):
24
+ try:
25
+ return self.__getattr__(name)
26
+ except AttributeError:
27
+ raise KeyError(name)
28
+
29
+ def __setitem__(self, name, value):
30
+ setattr(self, name, value)
31
+
32
+ def __delitem__(self, name):
33
+ try:
34
+ delattr(self, name)
35
+ except AttributeError:
36
+ raise KeyError(name)
37
+
38
+ def __iter__(self):
39
+ return iter(dir(self))
40
+
41
+ def __len__(self):
42
+ return len(dir(self))
43
+
44
+
45
+ class PyContextMap(PyContextLocal, ContextMap):
46
+ _backend_key = "python"
47
+ _BASEDIST = 1
48
+
49
+
50
+ class NativeContextMap(NativeContextLocal, ContextMap):
51
+ _backend_key = "native"
@@ -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,10 @@
1
+ extracontext/__init__.py,sha256=zAVHqTFP5Tl5hKVP0psbER4QIEUKfxXFiJjrQADvdWE,316
2
+ extracontext/_future_task.py,sha256=PEPWIJDdpLvvu90r-rD5btHnnyVTJ1dghiggURIbTW8,1898
3
+ extracontext/base.py,sha256=6TGVBdkZFr4vW18milFkOmzsw049wGuP9lyymBt5wqc,664
4
+ extracontext/contextlocal.py,sha256=EQnR-6vWU7JyREeefxlMQC7zGAo5RV9qq_oTynHutZI,9843
5
+ extracontext/contextlocal_native.py,sha256=uE67YmO-i40vd89AWOR67_AnnHW1milTPycZuGrNmKM,8771
6
+ extracontext/mapping.py,sha256=oNmgLJnE-8_L46qZwpLEwJTIdX1ZRWg5FzlzepKaBf0,1295
7
+ python_extracontext-1.0.0b1.dist-info/METADATA,sha256=atM3ybXCr-k7byEwiiVdf-7xS5GoZRPwWxSDWTN8u50,9361
8
+ python_extracontext-1.0.0b1.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
9
+ python_extracontext-1.0.0b1.dist-info/top_level.txt,sha256=r0JvexrESeoDk32GNGKs1pSnl-SA_3YiMrDcGIT952A,13
10
+ python_extracontext-1.0.0b1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (72.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ extracontext