@@ -110,6 +110,13 @@ The ``tasklet`` class
110
110
:meth: `tasklet.setup `. The difference is that when providing them to
111
111
:meth: `tasklet.bind `, the tasklet is not made runnable yet.
112
112
113
+ .. versionadded :: 3.7.6
114
+
115
+ If *func * is not :data: `None `, this method also sets the
116
+ :class: `~contextvars.Context ` object of this tasklet to the
117
+ :class: `~contextvars.Context ` object of the current tasklet.
118
+ Therefore it is usually not required to set the context explicitly.
119
+
113
120
*func * can be :data: `None ` when providing arguments, in which case a previous call
114
121
to :meth: `tasklet.bind ` must have provided the function.
115
122
@@ -344,6 +351,61 @@ The ``tasklet`` class
344
351
# Implement unsafe logic here.
345
352
t.set_ignore_nesting(old_value)
346
353
354
+ .. method :: tasklet.set_context(context)
355
+
356
+ .. versionadded :: 3.7.6
357
+
358
+ Set the :class: `~contextvars.Context ` object to be used while this tasklet runs.
359
+
360
+ Every tasklet has a private context attribute.
361
+ When the tasklet runs, this context becomes the current context of the thread.
362
+
363
+ :param context: the context to be set
364
+ :type context: :class: `contextvars.Context `
365
+ :return: the tasklet itself
366
+ :rtype: :class: `tasklet `
367
+ :raises RuntimeError: if the tasklet is bound to a foreign thread and is current or scheduled.
368
+ :raises RuntimeError: if called from within :meth: `contextvars.Context.run `.
369
+
370
+ .. note ::
371
+
372
+ The methods :meth: `__init__ `, :meth: `bind ` and :meth: `__setstate__ ` also set the context
373
+ of the tasklet they are called on to the context of the current tasklet. Therefore it is
374
+ usually not required to set the context explicitly.
375
+
376
+ .. note ::
377
+ This method has been added on a provisional basis (see :pep: `411 `
378
+ for details.)
379
+
380
+ .. method :: tasklet.context_run(callable, \*args, \*\*kwargs)
381
+
382
+ .. versionadded :: 3.7.6
383
+
384
+ Execute ``callable(*args, **kwargs) `` in the context object of the tasklet
385
+ the contest_run method is called on. Return the result of the
386
+ execution or propagate an exception if one occurred.
387
+ This method is roughly equivalent following pseudo code::
388
+
389
+ def context_run(self, callable, *args, **kwargs):
390
+ saved_context = stackless.current._internal_get_context()
391
+ stackless.current.set_context(self._internal_get_context())
392
+ try:
393
+ return callable(*args, **kw)
394
+ finally:
395
+ stackless.current.set_context(saved_context)
396
+
397
+ See also :meth: `contextvars.Context.run ` for additional information.
398
+ Use this method with care, because it lets you manipulate the context of
399
+ another tasklet. Often it is sufficient to use a copy of the context
400
+ instead of the original object::
401
+
402
+ copied_context = tasklet.context_run(contextvars.copy_context)
403
+ copied_context.run(...)
404
+
405
+ .. note ::
406
+ This method has been added on a provisional basis (see :pep: `411 `
407
+ for details.)
408
+
347
409
.. method :: tasklet.__del__()
348
410
349
411
.. versionadded :: 3.7
@@ -365,6 +427,13 @@ The ``tasklet`` class
365
427
366
428
See :meth: `object.__setstate__ `.
367
429
430
+ .. versionadded :: 3.7.6
431
+
432
+ If the tasklet becomes alive through this call and if *state * does not contain
433
+ a :class: `~contextvars.Context ` object, then :meth: `~__setstate__ ` also sets
434
+ the :class: `~contextvars.Context ` object of the
435
+ tasklet to the :class: `~contextvars.Context ` object of the current tasklet.
436
+
368
437
:param state: the state as given by ``__reduce_ex__(...)[2] ``
369
438
:type state: :class: `tuple `
370
439
:return: self
@@ -423,6 +492,18 @@ The following attributes allow checking of user set situations:
423
492
This attribute is ``True `` while this tasklet is within a
424
493
:meth: `tasklet.set_ignore_nesting ` block
425
494
495
+ .. attribute :: tasklet.context_id
496
+
497
+ .. versionadded :: 3.7.6
498
+
499
+ This attribute is the :func: `id ` of the :class: `~contextvars.Context ` object to be used while this tasklet runs.
500
+ It is intended mostly for debugging.
501
+
502
+ .. note ::
503
+ This attribute has been added on a provisional basis (see :pep: `411 `
504
+ for details.)
505
+
506
+
426
507
The following attributes allow identification of tasklet place:
427
508
428
509
.. attribute :: tasklet.is_current
@@ -511,3 +592,72 @@ state transitions these functions are roughly equivalent to the following
511
592
512
593
def schedule_remove():
513
594
stackless.current.next.switch()
595
+
596
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
597
+ Tasklets and Context Variables
598
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
599
+
600
+ .. versionadded :: 3.7.6
601
+
602
+ Version 3.7 of the |PPL | adds context variables, see module :mod: `contextvars `.
603
+ Usually they are used in connection with
604
+ :mod: `asyncio `, but they are a useful concept for |SLP | too.
605
+ Using context variables and multiple tasklets together didn't work well in |SLP | versions 3.7.0 to
606
+ 3.7.5, because all tasklets of a given thread shared the same context.
607
+
608
+ Starting with version 3.7.6 |SLP | adds explicit support for context variables.
609
+ Design requirements were:
610
+
611
+ 1. Be fully compatible with |CPY | and its design decisions.
612
+ 2. Be fully compatible with previous applications of |SLP |, which are unaware of context variables.
613
+ 3. Automatically share a context between related tasklets. This way a tasklet, that needs to set
614
+ a context variable, can delegate this duty to a sub-tasklet without the need to manage the
615
+ context of the sub-tasklet manually.
616
+ 4. Enable the integration of tasklet-based co-routines into the :mod: `asyncio ` framework.
617
+ This is an obvious application which involves context variables and tasklets. See
618
+ `slp-coroutine <https://pypi.org/project/slp-coroutine >`_ for an example.
619
+
620
+ Now each tasklet object has it own private context attribute. The design goals have some consequences:
621
+
622
+ * The active :class: `~contextvars.Context ` object of a thread (as defined by the |PPL |)
623
+ is the context of the :attr: `~stackless.current ` tasklet. This implies that a tasklet switch,
624
+ switches the active context of the thread.
625
+
626
+ * In accordance with the design decisions made in :pep: `567 ` the context of a tasklet can't be
627
+ accessed directly [#f1 ]_, but you can use the method :meth: `tasklet.context_run ` to run arbitrary code
628
+ in this context. For instance ``tasklet.context_run(contextvars.copy_context()) `` returns a copy
629
+ of the context.
630
+ The attribute :attr: `tasklet.context_id ` can be used to test, if two tasklets share the context.
631
+
632
+ * If you use the C-API, the context attribute of a tasklet is stored in the field *context * of the structure
633
+ :c:type: `PyTaskletObject ` or :c:type: `PyThreadState `. This field is is either undefined (``NULL ``) or a pointer to a
634
+ :class: `~contextvars.Context ` object.
635
+ A tasklet, whose *context * is ``NULL `` **must ** behave identically to a tasklet, whose context is an
636
+ empty :class: `~contextvars.Context ` object [#f2 ]_. Therefore the |PY | API provides no way to distinguish
637
+ both states. Whenever the context of a tasklet is to be shared with another tasklet and `tasklet->context `
638
+ is initially `NULL `, it must be set to a newly created :class: `~contextvars.Context ` object beforehand.
639
+ This affects the methods :meth: `~tasklet.context_run `, :meth: `~tasklet.__init__ `, :meth: `~tasklet.bind `
640
+ and :meth: `~tasklet.__setstate__ ` as well as the attribute :attr: `tasklet.context_id `.
641
+
642
+ * If the state of a tasklet changes from *not alive * to *bound * or to *alive * (methods :meth: `~tasklet.__init__ `,
643
+ :meth: `~tasklet.bind ` or :meth: `~tasklet.__setstate__ `), the context
644
+ of the tasklet is set to the currently active context. This way a newly initialized tasklet automatically
645
+ shares the context of its creator.
646
+
647
+ * The :mod: `contextvars ` implementation of |CPY | imposes several restrictions on |SLP |. Especially the sanity checks in
648
+ :c:func: `PyContext_Enter ` and :c:func: `PyContext_Exit ` make it impossible to replace the current context within
649
+ the execution of the method :meth: `contextvars.Context.run `. In that case |SLP | raises :exc: `RuntimeError `.
650
+
651
+ .. note ::
652
+ Context support has been added on a provisional basis (see :pep: `411 ` for details.)
653
+
654
+ .. rubric :: Footnotes
655
+
656
+ .. [#f1 ] Not exactly true. The return value of :meth: `tasklet.__reduce_ex__ ` can contain references to class
657
+ :class: `contextvars.Context `, but it is strongly discouraged, to use them for any other purpose
658
+ than pickling.
659
+
660
+ .. [#f2 ] Setting a context variable to a non default value changes the value of the field *context * from ``NULL ``
661
+ to a pointer to a newly created :class: `~contextvars.Context ` object. This can happen anytime in a
662
+ library call. Therefore any difference between an undefined context and an empty context causes ill defined
663
+ behavior.
0 commit comments