summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-11-11 08:11:34 -0600
committerJason Madden <jamadden@gmail.com>2020-11-11 08:11:34 -0600
commit1f83f62ce5ce9c5a2f2c58f548263360dc78b130 (patch)
tree6c0f642b0a22ed384a97a5902bf3e5907807cbe3
parent2a0b220ac8fe434c8161d3c8c285ad0d0bac3d0b (diff)
downloadgreenlet-1f83f62ce5ce9c5a2f2c58f548263360dc78b130.tar.gz
Minor docs tweaks; add change note; whitespace in greenlet.
-rw-r--r--CHANGES.rst8
-rw-r--r--docs/greenlet.rst151
-rw-r--r--greenlet.c44
-rw-r--r--tests/test_contextvars.py491
4 files changed, 359 insertions, 335 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index d7bc4a6..2ee0828 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -6,9 +6,11 @@
==================
- Add the ability to set a greenlet's PEP 567 contextvars context
- directly, by assigning to the greenlet's ``gr_context`` attribute. This
- restores support for some patterns of using greenlets atop an async
- environment that became more challenging in 0.4.17.
+ directly, by assigning to the greenlet's ``gr_context`` attribute.
+ This restores support for some patterns of using greenlets atop an
+ async environment that became more challenging in 0.4.17. Thanks to
+ Joshua Oreman, Mike bayer, and Fantix King, among others. See `PR
+ 198 <https://github.com/python-greenlet/greenlet/pull/198/>`_.
- (Packaging) Require setuptools to build from source.
- (Packaging) Stop asking setuptools to build both .tar.gz and .zip
sdists. PyPI has standardized on .tar.gz for all platforms.
diff --git a/docs/greenlet.rst b/docs/greenlet.rst
index f6202c9..6ae6dcc 100644
--- a/docs/greenlet.rst
+++ b/docs/greenlet.rst
@@ -13,20 +13,20 @@ Motivation
==========
The "greenlet" package is a spin-off of `Stackless`_, a version of CPython
-that supports micro-threads called "tasklets". Tasklets run
+that supports micro-threads called "tasklets". Tasklets run
pseudo-concurrently (typically in a single or a few OS-level threads) and
are synchronized with data exchanges on "channels".
A "greenlet", on the other hand, is a still more primitive notion of
micro-thread with no implicit scheduling; coroutines, in other words.
This is useful when you want to
-control exactly when your code runs. You can build custom scheduled
+control exactly when your code runs. You can build custom scheduled
micro-threads on top of greenlet; however, it seems that greenlets are
useful on their own as a way to make advanced control flow structures.
For example, we can recreate generators; the difference with Python's own
generators is that our generators can call nested functions and the nested
-functions can yield values too. (Additionally, you don't need a "yield"
-keyword. See the example in ``test/test_generator.py``).
+functions can yield values too. (Additionally, you don't need a "yield"
+keyword. See the example in ``test/test_generator.py``).
Greenlets are provided as a C extension module for the regular unmodified
interpreter.
@@ -37,7 +37,7 @@ Example
-------
Let's consider a system controlled by a terminal-like console, where the user
-types commands. Assume that the input comes character by character. In such
+types commands. Assume that the input comes character by character. In such
a system, there will typically be a loop like the following one::
def process_commands(*args):
@@ -52,11 +52,11 @@ a system, there will typically be a loop like the following one::
break # stop the command loop
process_command(line)
-Now assume that you want to plug this program into a GUI. Most GUI toolkits
-are event-based. They will invoke a call-back for each character the user
-presses. [Replace "GUI" with "XML expat parser" if that rings more bells to
+Now assume that you want to plug this program into a GUI. Most GUI toolkits
+are event-based. They will invoke a call-back for each character the user
+presses. [Replace "GUI" with "XML expat parser" if that rings more bells to
you ``:-)``] In this setting, it is difficult to implement the
-read_next_char() function needed by the code above. We have two incompatible
+read_next_char() function needed by the code above. We have two incompatible
functions::
def event_keydown(key):
@@ -65,8 +65,8 @@ functions::
def read_next_char():
?? should wait for the next event_keydown() call
-You might consider doing that with threads. Greenlets are an alternate
-solution that don't have the related locking and shutdown problems. You
+You might consider doing that with threads. Greenlets are an alternate
+solution that don't have the related locking and shutdown problems. You
start the process_commands() function in its own, separate greenlet, and
then you exchange the keypresses with it as follows::
@@ -88,7 +88,7 @@ then you exchange the keypresses with it as follows::
In this example, the execution flow is: when read_next_char() is called, it
is part of the g_processor greenlet, so when it switches to its parent
-greenlet, it resumes execution in the top-level main loop (the GUI). When
+greenlet, it resumes execution in the top-level main loop (the GUI). When
the GUI calls event_keydown(), it switches to g_processor, which means that
the execution jumps back wherever it was suspended in that greenlet -- in
this case, to the switch() instruction in read_next_char() -- and the ``key``
@@ -97,7 +97,7 @@ read_next_char().
Note that read_next_char() will be suspended and resumed with its call stack
preserved, so that it will itself return to different positions in
-process_commands() depending on where it was originally called from. This
+process_commands() depending on where it was originally called from. This
allows the logic of the program to be kept in a nice control-flow way; we
don't have to completely rewrite process_commands() to turn it into a state
machine.
@@ -109,20 +109,20 @@ Usage
Introduction
------------
-A "greenlet" is a small independent pseudo-thread. Think about it as a
+A "greenlet" is a small independent pseudo-thread. Think about it as a
small stack of frames; the outermost (bottom) frame is the initial
function you called, and the innermost frame is the one in which the
-greenlet is currently paused. You work with greenlets by creating a
-number of such stacks and jumping execution between them. Jumps are never
+greenlet is currently paused. You work with greenlets by creating a
+number of such stacks and jumping execution between them. Jumps are never
implicit: a greenlet must choose to jump to another greenlet, which will
cause the former to suspend and the latter to resume where it was
-suspended. Jumping between greenlets is called "switching".
+suspended. Jumping between greenlets is called "switching".
When you create a greenlet, it gets an initially empty stack; when you
first switch to it, it starts to run a specified function, which may call
-other functions, switch out of the greenlet, etc. When eventually the
+other functions, switch out of the greenlet, etc. When eventually the
outermost function finishes its execution, the greenlet's stack becomes
-empty again and the greenlet is "dead". Greenlets can also die of an
+empty again and the greenlet is "dead". Greenlets can also die of an
uncaught exception.
For example::
@@ -146,26 +146,26 @@ For example::
The last line jumps to test1, which prints 12, jumps to test2, prints 56,
jumps back into test1, prints 34; and then test1 finishes and gr1 dies.
At this point, the execution comes back to the original ``gr1.switch()``
-call. Note that 78 is never printed.
+call. Note that 78 is never printed.
Parents
-------
-Let's see where execution goes when a greenlet dies. Every greenlet has a
-"parent" greenlet. The parent greenlet is initially the one in which the
-greenlet was created (this can be changed at any time). The parent is
-where execution continues when a greenlet dies. This way, greenlets are
-organized in a tree. Top-level code that doesn't run in a user-created
+Let's see where execution goes when a greenlet dies. Every greenlet has a
+"parent" greenlet. The parent greenlet is initially the one in which the
+greenlet was created (this can be changed at any time). The parent is
+where execution continues when a greenlet dies. This way, greenlets are
+organized in a tree. Top-level code that doesn't run in a user-created
greenlet runs in the implicit "main" greenlet, which is the root of the
tree.
In the above example, both gr1 and gr2 have the main greenlet as a parent.
Whenever one of them dies, the execution comes back to "main".
-Uncaught exceptions are propagated into the parent, too. For example, if
+Uncaught exceptions are propagated into the parent, too. For example, if
the above test2() contained a typo, it would generate a NameError that
would kill gr2, and the exception would go back directly into "main".
-The traceback would show test2, but not test1. Remember, switches are not
+The traceback would show test2, but not test1. Remember, switches are not
calls, but transfer of execution between parallel "stack containers", and
the "parent" defines which stack logically comes "below" the current one.
@@ -176,7 +176,7 @@ Instantiation
operations:
``greenlet(run=None, parent=None)``
- Create a new greenlet object (without running it). ``run`` is the
+ Create a new greenlet object (without running it). ``run`` is the
callable to invoke, and ``parent`` is the parent greenlet, which
defaults to the current greenlet.
@@ -188,7 +188,7 @@ operations:
This special exception does not propagate to the parent greenlet; it
can be used to kill a single greenlet.
-The ``greenlet`` type can be subclassed, too. A greenlet runs by calling
+The ``greenlet`` type can be subclassed, too. A greenlet runs by calling
its ``run`` attribute, which is normally set when the greenlet is
created; but for subclasses it also makes sense to define a ``run`` method
instead of giving a ``run`` argument to the constructor.
@@ -199,9 +199,9 @@ Switching
Switches between greenlets occur when the method switch() of a greenlet is
called, in which case execution jumps to the greenlet whose switch() is
called, or when a greenlet dies, in which case execution jumps to the
-parent greenlet. During a switch, an object or an exception is "sent" to
+parent greenlet. During a switch, an object or an exception is "sent" to
the target greenlet; this can be used as a convenient way to pass
-information between greenlets. For example::
+information between greenlets. For example::
def test1(x, y):
z = gr2.switch(x+y)
@@ -216,7 +216,7 @@ information between greenlets. For example::
gr1.switch("hello", " world")
This prints "hello world" and 42, with the same order of execution as the
-previous example. Note that the arguments of test1() and test2() are not
+previous example. Note that the arguments of test1() and test2() are not
provided when the greenlet is created, but only the first time someone
switches to it.
@@ -224,29 +224,29 @@ Here are the precise rules for sending objects around:
``g.switch(*args, **kwargs)``
Switches execution to the greenlet ``g``, sending it the given
- arguments. As a special case, if ``g`` did not start yet, then it
+ arguments. As a special case, if ``g`` did not start yet, then it
will start to run now.
Dying greenlet
If a greenlet's ``run()`` finishes, its return value is the object
- sent to its parent. If ``run()`` terminates with an exception, the
+ sent to its parent. If ``run()`` terminates with an exception, the
exception is propagated to its parent (unless it is a
``greenlet.GreenletExit`` exception, in which case the exception
object is caught and *returned* to the parent).
Apart from the cases described above, the target greenlet normally
receives the object as the return value of the call to ``switch()`` in
-which it was previously suspended. Indeed, although a call to
+which it was previously suspended. Indeed, although a call to
``switch()`` does not return immediately, it will still return at some
-point in the future, when some other greenlet switches back. When this
+point in the future, when some other greenlet switches back. When this
occurs, then execution resumes just after the ``switch()`` where it was
suspended, and the ``switch()`` itself appears to return the object that
-was just sent. This means that ``x = g.switch(y)`` will send the object
+was just sent. This means that ``x = g.switch(y)`` will send the object
``y`` to ``g``, and will later put the (unrelated) object that some
(unrelated) greenlet passes back to us into ``x``.
Note that any attempt to switch to a dead greenlet actually goes to the
-dead greenlet's parent, or its parent's parent, and so on. (The final
+dead greenlet's parent, or its parent's parent, and so on. (The final
parent is the "main" greenlet, which is never dead.)
Context variables
@@ -263,7 +263,7 @@ A new greenlet's context is initially empty, i.e., all
:class:`~contextvars.ContextVar`\s have their default values. This
matches the behavior of a new thread, but differs from that of a new
:class:`asyncio.Task`, which inherits a copy of the context that was
-active when it was spawned. You can assign to a greenlet's
+active when it was spawned. You can assign to a greenlet's
``gr_context`` attribute to change the context that it will use. For
example::
@@ -317,57 +317,64 @@ restrictions.
You can access and change a greenlet's context almost no matter what
state the greenlet is in. It can be dead, not yet started, or
suspended (on any thread), or running (on the current thread only).
-Accessing or modifying ``gr_context`` of a greenlet running on a different
-thread raises :exc:`ValueError`.
+Accessing or modifying ``gr_context`` of a greenlet running on a
+different thread raises :exc:`ValueError`.
+
+.. warning::
+
+ Changing the ``gr_context`` after a greenlet has begun
+ running is not recommended for reasons outlined below.
Once a greenlet has started running, ``gr_context`` tracks its
*current* context: the one that would be active if you switched to the
-greenlet right now. This may not be the same as the value of
+greenlet right now. This may not be the same as the value of
``gr_context`` before the greenlet started running. One potential
difference occurs if a greenlet running in the default empty context
(represented as ``None``) sets any context variables: a new
:class:`~contextvars.Context` will be implicitly created to hold them,
which will be reflected in ``gr_context``. Another one occurs if a
-greenlet makes a call to ``Context.run(some_inner,
-func)``: its ``gr_context`` will be ``some_inner`` until ``func()``
-returns.
-
-.. warning:: Assigning to ``gr_context`` of an active greenlet that
- might be inside a call to :meth:`Context.run()
- <contextvars.Context.run>` is not recommended, because
- :meth:`~contextvars.Context.run` will raise an exception if the
- current context when it exits doesn't match the context that it set
- upon entry. The safest thing to do is set ``gr_context`` once,
- before starting the greenlet; then there's no potential conflict
- with :meth:`Context.run() <contextvars.Context.run>` calls.
+greenlet makes a call to ``Context.run(some_inner, func)``: its
+``gr_context`` will be ``some_inner`` until ``func()`` returns.
+
+.. warning::
+
+ Assigning to ``gr_context`` of an active greenlet that might be
+ inside a call to :meth:`Context.run() <contextvars.Context.run>` is
+ not recommended, because :meth:`~contextvars.Context.run` will
+ raise an exception if the current context when it exits doesn't
+ match the context that it set upon entry. The safest thing to do is
+ set ``gr_context`` once, before starting the greenlet; then there's
+ no potential conflict with :meth:`Context.run()
+ <contextvars.Context.run>` calls.
Methods and attributes of greenlets
-----------------------------------
``g.switch(*args, **kwargs)``
- Switches execution to the greenlet ``g``. See above.
+ Switches execution to the greenlet ``g``. See above.
``g.run``
- The callable that ``g`` will run when it starts. After ``g`` started,
- this attribute no longer exists.
+ The callable that ``g`` will run when it starts. After ``g``
+ started, this attribute no longer exists.
``g.parent``
- The parent greenlet. This is writable, but it is not allowed to
- create cycles of parents.
+ The parent greenlet. This is writable, but it is not allowed to create
+ cycles of parents.
``g.gr_frame``
The frame that was active in this greenlet when it most recently
called ``some_other_greenlet.switch()``, and that will resume
execution when ``g.switch()`` is next called. The remainder of the
greenlet's stack can be accessed by following the frame objects'
- ``f_back`` attributes. ``gr_frame`` is non-None only for suspended
- greenlets; it is None if the greenlet is dead, not yet started,
- or currently executing.
+ ``f_back`` attributes. ``gr_frame`` is non-None only for suspended
+ greenlets; it is None if the greenlet is dead, not yet started, or
+ currently executing.
``g.gr_context``
The :class:`contextvars.Context` in which ``g`` will
run. Writable; defaults to ``None``, reflecting that a greenlet
starts execution in an empty context unless told otherwise.
+ Generally, this should only be set once, before a greenlet begins running.
Accessing or modifying this attribute raises :exc:`AttributeError`
on Python versions 3.6 and earlier (which don't natively support the
`contextvars` module) or if ``greenlet`` was built without
@@ -381,9 +388,9 @@ Methods and attributes of greenlets
``g.throw([typ, [val, [tb]]])``
Switches execution to the greenlet ``g``, but immediately raises the
- given exception in ``g``. If no argument is provided, the exception
- defaults to ``greenlet.GreenletExit``. The normal exception
- propagation rules apply, as described above. Note that calling this
+ given exception in ``g``. If no argument is provided, the exception
+ defaults to ``greenlet.GreenletExit``. The normal exception
+ propagation rules apply, as described above. Note that calling this
method is almost equivalent to the following::
def raiser():
@@ -399,7 +406,7 @@ Greenlets and Python threads
----------------------------
Greenlets can be combined with Python threads; in this case, each thread
-contains an independent "main" greenlet with a tree of sub-greenlets. It
+contains an independent "main" greenlet with a tree of sub-greenlets. It
is not possible to mix or switch between greenlets belonging to different
threads.
@@ -408,12 +415,12 @@ Garbage-collecting live greenlets
If all the references to a greenlet object go away (including the
references from the parent attribute of other greenlets), then there is no
-way to ever switch back to this greenlet. In this case, a GreenletExit
-exception is generated into the greenlet. This is the only case where a
-greenlet receives the execution asynchronously. This gives
+way to ever switch back to this greenlet. In this case, a GreenletExit
+exception is generated into the greenlet. This is the only case where a
+greenlet receives the execution asynchronously. This gives
``try:finally:`` blocks a chance to clean up resources held by the
-greenlet. This feature also enables a programming style in which
-greenlets are infinite loops waiting for data and processing it. Such
+greenlet. This feature also enables a programming style in which
+greenlets are infinite loops waiting for data and processing it. Such
loops are automatically interrupted when the last reference to the
greenlet goes away.
@@ -422,7 +429,7 @@ reference to it stored somewhere; just catching and ignoring the
GreenletExit is likely to lead to an infinite loop.
Greenlets do not participate in garbage collection; cycles involving data
-that is present in a greenlet's frames will not be detected. Storing
+that is present in a greenlet's frames will not be detected. Storing
references to other greenlets cyclically may lead to leaks.
Tracing support
diff --git a/greenlet.c b/greenlet.c
index 720f8db..aad4ba0 100644
--- a/greenlet.c
+++ b/greenlet.c
@@ -1347,9 +1347,9 @@ static int green_setparent(PyGreenlet* self, PyObject* nparent, void* c)
}
#ifdef Py_CONTEXT_H
-#define GREENLET_NO_CONTEXTVARS_REASON "this build of greenlet"
+#define GREENLET_NO_CONTEXTVARS_REASON "This build of greenlet"
#else
-#define GREENLET_NO_CONTEXTVARS_REASON "this Python interpreter"
+#define GREENLET_NO_CONTEXTVARS_REASON "This Python interpreter"
#endif
static PyObject* green_getcontext(PyGreenlet* self, void* c)
@@ -1366,12 +1366,15 @@ static PyObject* green_getcontext(PyGreenlet* self, void* c)
not the greenlet object. */
if (self == ts_current) {
result = tstate->context;
- } else {
- PyErr_SetString(PyExc_ValueError, "cannot get context of a "
- "greenlet that is running in a different thread");
+ }
+ else {
+ PyErr_SetString(PyExc_ValueError,
+ "cannot get context of a "
+ "greenlet that is running in a different thread");
return NULL;
}
- } else {
+ }
+ else {
/* Greenlet is not running: just return context. */
result = self->context;
}
@@ -1381,8 +1384,9 @@ static PyObject* green_getcontext(PyGreenlet* self, void* c)
Py_INCREF(result);
return result;
#else
- PyErr_SetString(PyExc_AttributeError, GREENLET_NO_CONTEXTVARS_REASON
- " does not support context variables");
+ PyErr_SetString(PyExc_AttributeError,
+ GREENLET_NO_CONTEXTVARS_REASON
+ " does not support context variables");
return NULL;
#endif
}
@@ -1402,9 +1406,11 @@ static int green_setcontext(PyGreenlet* self, PyObject* nctx, void* c)
if (nctx == Py_None) {
/* "Empty context" is stored as NULL, not None. */
nctx = NULL;
- } else if (!PyContext_CheckExact(nctx)) {
- PyErr_SetString(PyExc_TypeError, "greenlet context must be a "
- "contextvars.Context or None");
+ }
+ else if (!PyContext_CheckExact(nctx)) {
+ PyErr_SetString(PyExc_TypeError,
+ "greenlet context must be a "
+ "contextvars.Context or None");
return -1;
}
tstate = PyThreadState_GET();
@@ -1416,12 +1422,15 @@ static int green_setcontext(PyGreenlet* self, PyObject* nctx, void* c)
tstate->context = nctx;
tstate->context_ver++;
Py_XINCREF(nctx);
- } else {
- PyErr_SetString(PyExc_ValueError, "cannot set context of a "
- "greenlet that is running in a different thread");
+ }
+ else {
+ PyErr_SetString(PyExc_ValueError,
+ "cannot set context of a "
+ "greenlet that is running in a different thread");
return -1;
}
- } else {
+ }
+ else {
/* Greenlet is not running: just set context. */
octx = self->context;
self->context = nctx;
@@ -1430,8 +1439,9 @@ static int green_setcontext(PyGreenlet* self, PyObject* nctx, void* c)
Py_XDECREF(octx);
return 0;
#else
- PyErr_SetString(PyExc_AttributeError, GREENLET_NO_CONTEXTVARS_REASON
- " does not support context variables");
+ PyErr_SetString(PyExc_AttributeError,
+ GREENLET_NO_CONTEXTVARS_REASON
+ " does not support context variables");
return -1;
#endif
}
diff --git a/tests/test_contextvars.py b/tests/test_contextvars.py
index d4766e5..02873c5 100644
--- a/tests/test_contextvars.py
+++ b/tests/test_contextvars.py
@@ -1,260 +1,265 @@
+import unittest
+import gc
+import sys
+
from functools import partial
+
from greenlet import greenlet
from greenlet import getcurrent
from greenlet import GREENLET_USE_CONTEXT_VARS
-import unittest
-import gc
-import sys
-if GREENLET_USE_CONTEXT_VARS:
+try:
from contextvars import Context
from contextvars import ContextVar
from contextvars import copy_context
-
- class ContextVarsTests(unittest.TestCase):
- def _new_ctx_run(self, *args, **kwargs):
- return copy_context().run(*args, **kwargs)
-
- def _increment(self, greenlet_id, ctx_var, callback, counts, expect):
- if expect is None:
- self.assertIsNone(ctx_var.get())
- else:
- self.assertEqual(ctx_var.get(), expect)
- ctx_var.set(greenlet_id)
- for i in range(2):
- counts[ctx_var.get()] += 1
- callback()
-
- def _test_context(self, propagate_by):
- id_var = ContextVar("id", default=None)
- id_var.set(0)
-
- callback = getcurrent().switch
- counts = dict((i, 0) for i in range(5))
-
- lets = [
- greenlet(partial(
- partial(
- copy_context().run,
- self._increment
- ) if propagate_by == "run" else self._increment,
- greenlet_id=i,
- ctx_var=id_var,
- callback=callback,
- counts=counts,
- expect=(
- i - 1 if propagate_by == "share" else
- 0 if propagate_by in ("set", "run") else None
- )
- ))
- for i in range(1, 5)
- ]
-
+except ImportError:
+ Context = ContextVar = copy_context = None
+
+@unittest.skipUnless(GREENLET_USE_CONTEXT_VARS, "ContextVar not supported")
+class ContextVarsTests(unittest.TestCase):
+ def _new_ctx_run(self, *args, **kwargs):
+ return copy_context().run(*args, **kwargs)
+
+ def _increment(self, greenlet_id, ctx_var, callback, counts, expect):
+ if expect is None:
+ self.assertIsNone(ctx_var.get())
+ else:
+ self.assertEqual(ctx_var.get(), expect)
+ ctx_var.set(greenlet_id)
+ for _ in range(2):
+ counts[ctx_var.get()] += 1
+ callback()
+
+ def _test_context(self, propagate_by):
+ id_var = ContextVar("id", default=None)
+ id_var.set(0)
+
+ callback = getcurrent().switch
+ counts = dict((i, 0) for i in range(5))
+
+ lets = [
+ greenlet(partial(
+ partial(
+ copy_context().run,
+ self._increment
+ ) if propagate_by == "run" else self._increment,
+ greenlet_id=i,
+ ctx_var=id_var,
+ callback=callback,
+ counts=counts,
+ expect=(
+ i - 1 if propagate_by == "share" else
+ 0 if propagate_by in ("set", "run") else None
+ )
+ ))
+ for i in range(1, 5)
+ ]
+
+ for let in lets:
+ if propagate_by == "set":
+ let.gr_context = copy_context()
+ elif propagate_by == "share":
+ let.gr_context = getcurrent().gr_context
+
+ for i in range(2):
+ counts[id_var.get()] += 1
for let in lets:
- if propagate_by == "set":
- let.gr_context = copy_context()
- elif propagate_by == "share":
- let.gr_context = getcurrent().gr_context
-
- for i in range(2):
- counts[id_var.get()] += 1
- for let in lets:
- let.switch()
-
- if propagate_by == "run":
- # Must leave each context.run() in reverse order of entry
- for let in reversed(lets):
- let.switch()
- else:
- # No context.run(), so fine to exit in any order.
- for let in lets:
- let.switch()
-
+ let.switch()
+
+ if propagate_by == "run":
+ # Must leave each context.run() in reverse order of entry
+ for let in reversed(lets):
+ let.switch()
+ else:
+ # No context.run(), so fine to exit in any order.
for let in lets:
- self.assertTrue(let.dead)
- # When using run(), we leave the run() as the greenlet dies,
- # and there's no context "underneath". When not using run(),
- # gr_context still reflects the context the greenlet was
- # running in.
- self.assertEqual(let.gr_context is None, propagate_by == "run")
-
- if propagate_by == "share":
- self.assertEqual(counts, {0: 1, 1: 1, 2: 1, 3: 1, 4: 6})
- else:
- self.assertEqual(set(counts.values()), set([2]))
-
- def test_context_propagated_by_context_run(self):
- self._new_ctx_run(self._test_context, "run")
-
- def test_context_propagated_by_setting_attribute(self):
- self._new_ctx_run(self._test_context, "set")
-
- def test_context_not_propagated(self):
- self._new_ctx_run(self._test_context, None)
-
- def test_context_shared(self):
- self._new_ctx_run(self._test_context, "share")
-
- def test_break_ctxvars(self):
- let1 = greenlet(copy_context().run)
- let2 = greenlet(copy_context().run)
- let1.switch(getcurrent().switch)
- let2.switch(getcurrent().switch)
- # Since let2 entered the current context and let1 exits its own, the
- # interpreter emits:
- # RuntimeError: cannot exit context: thread state references a different context object
- let1.switch()
-
- def test_not_broken_if_using_attribute_instead_of_context_run(self):
- let1 = greenlet(getcurrent().switch)
- let2 = greenlet(getcurrent().switch)
- let1.gr_context = copy_context()
- let2.gr_context = copy_context()
- let1.switch()
- let2.switch()
- let1.switch()
- let2.switch()
-
- def test_context_assignment_while_running(self):
- id_var = ContextVar("id", default=None)
-
- def target():
- self.assertIsNone(id_var.get())
- self.assertIsNone(gr.gr_context)
-
- # Context is created on first use
- id_var.set(1)
- self.assertIsInstance(gr.gr_context, Context)
- self.assertEqual(id_var.get(), 1)
- self.assertEqual(gr.gr_context[id_var], 1)
-
- # Clearing the context makes it get re-created as another
- # empty context when next used
- old_context = gr.gr_context
- gr.gr_context = None # assign None while running
- self.assertIsNone(id_var.get())
- self.assertIsNone(gr.gr_context)
- id_var.set(2)
- self.assertIsInstance(gr.gr_context, Context)
- self.assertEqual(id_var.get(), 2)
- self.assertEqual(gr.gr_context[id_var], 2)
-
- new_context = gr.gr_context
- getcurrent().parent.switch((old_context, new_context))
- # parent switches us back to old_context
-
- self.assertEqual(id_var.get(), 1)
- gr.gr_context = new_context # assign non-None while running
- self.assertEqual(id_var.get(), 2)
-
- getcurrent().parent.switch()
- # parent switches us back to no context
- self.assertIsNone(id_var.get())
- self.assertIsNone(gr.gr_context)
- gr.gr_context = old_context
- self.assertEqual(id_var.get(), 1)
-
- getcurrent().parent.switch()
- # parent switches us back to no context
- self.assertIsNone(id_var.get())
- self.assertIsNone(gr.gr_context)
-
- gr = greenlet(target)
-
- with self.assertRaisesRegex(AttributeError, "can't delete attr"):
- del gr.gr_context
+ let.switch()
+
+ for let in lets:
+ self.assertTrue(let.dead)
+ # When using run(), we leave the run() as the greenlet dies,
+ # and there's no context "underneath". When not using run(),
+ # gr_context still reflects the context the greenlet was
+ # running in.
+ self.assertEqual(let.gr_context is None, propagate_by == "run")
+
+ if propagate_by == "share":
+ self.assertEqual(counts, {0: 1, 1: 1, 2: 1, 3: 1, 4: 6})
+ else:
+ self.assertEqual(set(counts.values()), set([2]))
+
+ def test_context_propagated_by_context_run(self):
+ self._new_ctx_run(self._test_context, "run")
+
+ def test_context_propagated_by_setting_attribute(self):
+ self._new_ctx_run(self._test_context, "set")
+
+ def test_context_not_propagated(self):
+ self._new_ctx_run(self._test_context, None)
+
+ def test_context_shared(self):
+ self._new_ctx_run(self._test_context, "share")
+
+ def test_break_ctxvars(self):
+ let1 = greenlet(copy_context().run)
+ let2 = greenlet(copy_context().run)
+ let1.switch(getcurrent().switch)
+ let2.switch(getcurrent().switch)
+ # Since let2 entered the current context and let1 exits its own, the
+ # interpreter emits:
+ # RuntimeError: cannot exit context: thread state references a different context object
+ let1.switch()
+
+ def test_not_broken_if_using_attribute_instead_of_context_run(self):
+ let1 = greenlet(getcurrent().switch)
+ let2 = greenlet(getcurrent().switch)
+ let1.gr_context = copy_context()
+ let2.gr_context = copy_context()
+ let1.switch()
+ let2.switch()
+ let1.switch()
+ let2.switch()
+
+ def test_context_assignment_while_running(self):
+ id_var = ContextVar("id", default=None)
+
+ def target():
+ self.assertIsNone(id_var.get())
+ self.assertIsNone(gr.gr_context)
+ # Context is created on first use
+ id_var.set(1)
+ self.assertIsInstance(gr.gr_context, Context)
+ self.assertEqual(id_var.get(), 1)
+ self.assertEqual(gr.gr_context[id_var], 1)
+
+ # Clearing the context makes it get re-created as another
+ # empty context when next used
+ old_context = gr.gr_context
+ gr.gr_context = None # assign None while running
+ self.assertIsNone(id_var.get())
self.assertIsNone(gr.gr_context)
- old_context, new_context = gr.switch()
- self.assertIs(new_context, gr.gr_context)
- self.assertEqual(old_context[id_var], 1)
- self.assertEqual(new_context[id_var], 2)
- self.assertEqual(new_context.run(id_var.get), 2)
- gr.gr_context = old_context # assign non-None while suspended
- gr.switch()
- self.assertIs(gr.gr_context, new_context)
- gr.gr_context = None # assign None while suspended
- gr.switch()
- self.assertIs(gr.gr_context, old_context)
- gr.gr_context = None
- gr.switch()
+ id_var.set(2)
+ self.assertIsInstance(gr.gr_context, Context)
+ self.assertEqual(id_var.get(), 2)
+ self.assertEqual(gr.gr_context[id_var], 2)
+
+ new_context = gr.gr_context
+ getcurrent().parent.switch((old_context, new_context))
+ # parent switches us back to old_context
+
+ self.assertEqual(id_var.get(), 1)
+ gr.gr_context = new_context # assign non-None while running
+ self.assertEqual(id_var.get(), 2)
+
+ getcurrent().parent.switch()
+ # parent switches us back to no context
+ self.assertIsNone(id_var.get())
self.assertIsNone(gr.gr_context)
+ gr.gr_context = old_context
+ self.assertEqual(id_var.get(), 1)
- # Make sure there are no reference leaks
- gr = None
- gc.collect()
- self.assertEqual(sys.getrefcount(old_context), 2)
- self.assertEqual(sys.getrefcount(new_context), 2)
-
- def test_context_assignment_different_thread(self):
- import threading
-
- ctx = Context()
- var = ContextVar("var", default=None)
- is_running = threading.Event()
- should_suspend = threading.Event()
- did_suspend = threading.Event()
- should_exit = threading.Event()
- holder = []
-
- def greenlet_in_thread_fn():
- var.set(1)
- is_running.set()
- should_suspend.wait()
- var.set(2)
- getcurrent().parent.switch()
- holder.append(var.get())
-
- def thread_fn():
- gr = greenlet(greenlet_in_thread_fn)
- gr.gr_context = ctx
- holder.append(gr)
- gr.switch()
- did_suspend.set()
- should_exit.wait()
- gr.switch()
-
- thread = threading.Thread(target=thread_fn, daemon=True)
- thread.start()
- is_running.wait()
- gr = holder[0]
-
- # Can't access or modify context if the greenlet is running
- # in a different thread
- with self.assertRaisesRegex(ValueError, "running in a different"):
- gr.gr_context
- with self.assertRaisesRegex(ValueError, "running in a different"):
- gr.gr_context = None
-
- should_suspend.set()
- did_suspend.wait()
-
- # OK to access and modify context if greenlet is suspended
- self.assertIs(gr.gr_context, ctx)
- self.assertEqual(gr.gr_context[var], 2)
- gr.gr_context = None
+ getcurrent().parent.switch()
+ # parent switches us back to no context
+ self.assertIsNone(id_var.get())
+ self.assertIsNone(gr.gr_context)
+
+ gr = greenlet(target)
+
+ with self.assertRaisesRegex(AttributeError, "can't delete attr"):
+ del gr.gr_context
+
+ self.assertIsNone(gr.gr_context)
+ old_context, new_context = gr.switch()
+ self.assertIs(new_context, gr.gr_context)
+ self.assertEqual(old_context[id_var], 1)
+ self.assertEqual(new_context[id_var], 2)
+ self.assertEqual(new_context.run(id_var.get), 2)
+ gr.gr_context = old_context # assign non-None while suspended
+ gr.switch()
+ self.assertIs(gr.gr_context, new_context)
+ gr.gr_context = None # assign None while suspended
+ gr.switch()
+ self.assertIs(gr.gr_context, old_context)
+ gr.gr_context = None
+ gr.switch()
+ self.assertIsNone(gr.gr_context)
+
+ # Make sure there are no reference leaks
+ gr = None
+ gc.collect()
+ self.assertEqual(sys.getrefcount(old_context), 2)
+ self.assertEqual(sys.getrefcount(new_context), 2)
+
+ def test_context_assignment_different_thread(self):
+ import threading
+
+ ctx = Context()
+ var = ContextVar("var", default=None)
+ is_running = threading.Event()
+ should_suspend = threading.Event()
+ did_suspend = threading.Event()
+ should_exit = threading.Event()
+ holder = []
+
+ def greenlet_in_thread_fn():
+ var.set(1)
+ is_running.set()
+ should_suspend.wait()
+ var.set(2)
+ getcurrent().parent.switch()
+ holder.append(var.get())
+
+ def thread_fn():
+ gr = greenlet(greenlet_in_thread_fn)
+ gr.gr_context = ctx
+ holder.append(gr)
+ gr.switch()
+ did_suspend.set()
+ should_exit.wait()
+ gr.switch()
- should_exit.set()
- thread.join()
+ thread = threading.Thread(target=thread_fn, daemon=True)
+ thread.start()
+ is_running.wait()
+ gr = holder[0]
- self.assertEqual(holder, [gr, None])
+ # Can't access or modify context if the greenlet is running
+ # in a different thread
+ with self.assertRaisesRegex(ValueError, "running in a different"):
+ getattr(gr, 'gr_context')
+ with self.assertRaisesRegex(ValueError, "running in a different"):
+ gr.gr_context = None
- # Context can still be accessed/modified when greenlet is dead:
- self.assertIsNone(gr.gr_context)
- gr.gr_context = ctx
- self.assertIs(gr.gr_context, ctx)
-
-else:
- # no contextvars support
- class NoContextVarsTests(unittest.TestCase):
- def test_contextvars_errors(self):
- let1 = greenlet(getcurrent().switch)
- with self.assertRaises(AttributeError):
- let1.gr_context
- with self.assertRaises(AttributeError):
- let1.gr_context = None
- let1.switch()
- with self.assertRaises(AttributeError):
- let1.gr_context
- with self.assertRaises(AttributeError):
- let1.gr_context = None
+ should_suspend.set()
+ did_suspend.wait()
+
+ # OK to access and modify context if greenlet is suspended
+ self.assertIs(gr.gr_context, ctx)
+ self.assertEqual(gr.gr_context[var], 2)
+ gr.gr_context = None
+
+ should_exit.set()
+ thread.join()
+
+ self.assertEqual(holder, [gr, None])
+
+ # Context can still be accessed/modified when greenlet is dead:
+ self.assertIsNone(gr.gr_context)
+ gr.gr_context = ctx
+ self.assertIs(gr.gr_context, ctx)
+
+@unittest.skipIf(GREENLET_USE_CONTEXT_VARS, "ContextVar supported")
+class NoContextVarsTests(unittest.TestCase):
+ def test_contextvars_errors(self):
+ let1 = greenlet(getcurrent().switch)
+ self.assertFalse(hasattr(let1, 'gr_context'))
+ with self.assertRaises(AttributeError):
+ getattr(let1, 'gr_context')
+ with self.assertRaises(AttributeError):
+ let1.gr_context = None
+ let1.switch()
+ with self.assertRaises(AttributeError):
+ getattr(let1, 'gr_context')
+ with self.assertRaises(AttributeError):
+ let1.gr_context = None