about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/greenlet/tests
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/greenlet/tests')
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/__init__.py240
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension.c231
-rwxr-xr-x.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension.cpython-312-x86_64-linux-gnu.sobin0 -> 37168 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension_cpp.cpp226
-rwxr-xr-x.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension_cpp.cpython-312-x86_64-linux-gnu.sobin0 -> 57800 bytes
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/fail_clearing_run_switches.py47
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/fail_cpp_exception.py33
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/fail_initialstub_already_started.py78
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/fail_slp_switch.py29
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_three_greenlets.py44
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_three_greenlets2.py55
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_two_greenlets.py41
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/leakcheck.py319
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_contextvars.py310
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_cpp.py73
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_extension_interface.py115
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_gc.py86
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_generator.py59
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_generator_nested.py168
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet.py1324
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py187
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_leaks.py443
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_stack_saved.py19
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_throw.py128
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_tracing.py291
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_version.py41
-rw-r--r--.venv/lib/python3.12/site-packages/greenlet/tests/test_weakref.py35
27 files changed, 4622 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/__init__.py b/.venv/lib/python3.12/site-packages/greenlet/tests/__init__.py
new file mode 100644
index 00000000..e69392e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/__init__.py
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for greenlet.
+
+"""
+import os
+import sys
+import unittest
+
+from gc import collect
+from gc import get_objects
+from threading import active_count as active_thread_count
+from time import sleep
+from time import time
+
+import psutil
+
+from greenlet import greenlet as RawGreenlet
+from greenlet import getcurrent
+
+from greenlet._greenlet import get_pending_cleanup_count
+from greenlet._greenlet import get_total_main_greenlets
+
+from . import leakcheck
+
+PY312 = sys.version_info[:2] >= (3, 12)
+PY313 = sys.version_info[:2] >= (3, 13)
+
+WIN = sys.platform.startswith("win")
+RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS')
+RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS
+RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR')
+RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR
+RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX')
+
+class TestCaseMetaClass(type):
+    # wrap each test method with
+    # a) leak checks
+    def __new__(cls, classname, bases, classDict):
+        # pylint and pep8 fight over what this should be called (mcs or cls).
+        # pylint gets it right, but we can't scope disable pep8, so we go with
+        # its convention.
+        # pylint: disable=bad-mcs-classmethod-argument
+        check_totalrefcount = True
+
+        # Python 3: must copy, we mutate the classDict. Interestingly enough,
+        # it doesn't actually error out, but under 3.6 we wind up wrapping
+        # and re-wrapping the same items over and over and over.
+        for key, value in list(classDict.items()):
+            if key.startswith('test') and callable(value):
+                classDict.pop(key)
+                if check_totalrefcount:
+                    value = leakcheck.wrap_refcount(value)
+                classDict[key] = value
+        return type.__new__(cls, classname, bases, classDict)
+
+
+class TestCase(TestCaseMetaClass(
+        "NewBase",
+        (unittest.TestCase,),
+        {})):
+
+    cleanup_attempt_sleep_duration = 0.001
+    cleanup_max_sleep_seconds = 1
+
+    def wait_for_pending_cleanups(self,
+                                  initial_active_threads=None,
+                                  initial_main_greenlets=None):
+        initial_active_threads = initial_active_threads or self.threads_before_test
+        initial_main_greenlets = initial_main_greenlets or self.main_greenlets_before_test
+        sleep_time = self.cleanup_attempt_sleep_duration
+        # NOTE: This is racy! A Python-level thread object may be dead
+        # and gone, but the C thread may not yet have fired its
+        # destructors and added to the queue. There's no particular
+        # way to know that's about to happen. We try to watch the
+        # Python threads to make sure they, at least, have gone away.
+        # Counting the main greenlets, which we can easily do deterministically,
+        # also helps.
+
+        # Always sleep at least once to let other threads run
+        sleep(sleep_time)
+        quit_after = time() + self.cleanup_max_sleep_seconds
+        # TODO: We could add an API that calls us back when a particular main greenlet is deleted?
+        # It would have to drop the GIL
+        while (
+                get_pending_cleanup_count()
+                or active_thread_count() > initial_active_threads
+                or (not self.expect_greenlet_leak
+                    and get_total_main_greenlets() > initial_main_greenlets)):
+            sleep(sleep_time)
+            if time() > quit_after:
+                print("Time limit exceeded.")
+                print("Threads: Waiting for only", initial_active_threads,
+                      "-->", active_thread_count())
+                print("MGlets : Waiting for only", initial_main_greenlets,
+                      "-->", get_total_main_greenlets())
+                break
+        collect()
+
+    def count_objects(self, kind=list, exact_kind=True):
+        # pylint:disable=unidiomatic-typecheck
+        # Collect the garbage.
+        for _ in range(3):
+            collect()
+        if exact_kind:
+            return sum(
+                1
+                for x in get_objects()
+                if type(x) is kind
+            )
+        # instances
+        return sum(
+            1
+            for x in get_objects()
+            if isinstance(x, kind)
+        )
+
+    greenlets_before_test = 0
+    threads_before_test = 0
+    main_greenlets_before_test = 0
+    expect_greenlet_leak = False
+
+    def count_greenlets(self):
+        """
+        Find all the greenlets and subclasses tracked by the GC.
+        """
+        return self.count_objects(RawGreenlet, False)
+
+    def setUp(self):
+        # Ensure the main greenlet exists, otherwise the first test
+        # gets a false positive leak
+        super().setUp()
+        getcurrent()
+        self.threads_before_test = active_thread_count()
+        self.main_greenlets_before_test = get_total_main_greenlets()
+        self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test)
+        self.greenlets_before_test = self.count_greenlets()
+
+    def tearDown(self):
+        if getattr(self, 'skipTearDown', False):
+            return
+
+        self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test)
+        super().tearDown()
+
+    def get_expected_returncodes_for_aborted_process(self):
+        import signal
+        # The child should be aborted in an unusual way. On POSIX
+        # platforms, this is done with abort() and signal.SIGABRT,
+        # which is reflected in a negative return value; however, on
+        # Windows, even though we observe the child print "Fatal
+        # Python error: Aborted" and in older versions of the C
+        # runtime "This application has requested the Runtime to
+        # terminate it in an unusual way," it always has an exit code
+        # of 3. This is interesting because 3 is the error code for
+        # ERROR_PATH_NOT_FOUND; BUT: the C runtime abort() function
+        # also uses this code.
+        #
+        # If we link to the static C library on Windows, the error
+        # code changes to '0xc0000409' (hex(3221226505)), which
+        # apparently is STATUS_STACK_BUFFER_OVERRUN; but "What this
+        # means is that nowadays when you get a
+        # STATUS_STACK_BUFFER_OVERRUN, it doesn’t actually mean that
+        # there is a stack buffer overrun. It just means that the
+        # application decided to terminate itself with great haste."
+        #
+        #
+        # On windows, we've also seen '0xc0000005' (hex(3221225477)).
+        # That's "Access Violation"
+        #
+        # See
+        # https://devblogs.microsoft.com/oldnewthing/20110519-00/?p=10623
+        # and
+        # https://docs.microsoft.com/en-us/previous-versions/k089yyh0(v=vs.140)?redirectedfrom=MSDN
+        # and
+        # https://devblogs.microsoft.com/oldnewthing/20190108-00/?p=100655
+        expected_exit = (
+            -signal.SIGABRT,
+            # But beginning on Python 3.11, the faulthandler
+            # that prints the C backtraces sometimes segfaults after
+            # reporting the exception but before printing the stack.
+            # This has only been seen on linux/gcc.
+            -signal.SIGSEGV,
+        ) if not WIN else (
+            3,
+            0xc0000409,
+            0xc0000005,
+        )
+        return expected_exit
+
+    def get_process_uss(self):
+        """
+        Return the current process's USS in bytes.
+
+        uss is available on Linux, macOS, Windows. Also known as
+        "Unique Set Size", this is the memory which is unique to a
+        process and which would be freed if the process was terminated
+        right now.
+
+        If this is not supported by ``psutil``, this raises the
+        :exc:`unittest.SkipTest` exception.
+        """
+        try:
+            return psutil.Process().memory_full_info().uss
+        except AttributeError as e:
+            raise unittest.SkipTest("uss not supported") from e
+
+    def run_script(self, script_name, show_output=True):
+        import subprocess
+        script = os.path.join(
+            os.path.dirname(__file__),
+            script_name,
+        )
+
+        try:
+            return subprocess.check_output([sys.executable, script],
+                                           encoding='utf-8',
+                                           stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError as ex:
+            if show_output:
+                print('-----')
+                print('Failed to run script', script)
+                print('~~~~~')
+                print(ex.output)
+                print('------')
+            raise
+
+
+    def assertScriptRaises(self, script_name, exitcodes=None):
+        import subprocess
+        with self.assertRaises(subprocess.CalledProcessError) as exc:
+            output = self.run_script(script_name, show_output=False)
+            __traceback_info__ = output
+            # We're going to fail the assertion if we get here, at least
+            # preserve the output in the traceback.
+
+        if exitcodes is None:
+            exitcodes = self.get_expected_returncodes_for_aborted_process()
+        self.assertIn(exc.exception.returncode, exitcodes)
+        return exc.exception
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension.c b/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension.c
new file mode 100644
index 00000000..05e81c03
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension.c
@@ -0,0 +1,231 @@
+/* This is a set of functions used by test_extension_interface.py to test the
+ * Greenlet C API.
+ */
+
+#include "../greenlet.h"
+
+#ifndef Py_RETURN_NONE
+#    define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None
+#endif
+
+#define TEST_MODULE_NAME "_test_extension"
+
+static PyObject*
+test_switch(PyObject* self, PyObject* greenlet)
+{
+    PyObject* result = NULL;
+
+    if (greenlet == NULL || !PyGreenlet_Check(greenlet)) {
+        PyErr_BadArgument();
+        return NULL;
+    }
+
+    result = PyGreenlet_Switch((PyGreenlet*)greenlet, NULL, NULL);
+    if (result == NULL) {
+        if (!PyErr_Occurred()) {
+            PyErr_SetString(PyExc_AssertionError,
+                            "greenlet.switch() failed for some reason.");
+        }
+        return NULL;
+    }
+    Py_INCREF(result);
+    return result;
+}
+
+static PyObject*
+test_switch_kwargs(PyObject* self, PyObject* args, PyObject* kwargs)
+{
+    PyGreenlet* g = NULL;
+    PyObject* result = NULL;
+
+    PyArg_ParseTuple(args, "O!", &PyGreenlet_Type, &g);
+
+    if (g == NULL || !PyGreenlet_Check(g)) {
+        PyErr_BadArgument();
+        return NULL;
+    }
+
+    result = PyGreenlet_Switch(g, NULL, kwargs);
+    if (result == NULL) {
+        if (!PyErr_Occurred()) {
+            PyErr_SetString(PyExc_AssertionError,
+                            "greenlet.switch() failed for some reason.");
+        }
+        return NULL;
+    }
+    Py_XINCREF(result);
+    return result;
+}
+
+static PyObject*
+test_getcurrent(PyObject* self)
+{
+    PyGreenlet* g = PyGreenlet_GetCurrent();
+    if (g == NULL || !PyGreenlet_Check(g) || !PyGreenlet_ACTIVE(g)) {
+        PyErr_SetString(PyExc_AssertionError,
+                        "getcurrent() returned an invalid greenlet");
+        Py_XDECREF(g);
+        return NULL;
+    }
+    Py_DECREF(g);
+    Py_RETURN_NONE;
+}
+
+static PyObject*
+test_setparent(PyObject* self, PyObject* arg)
+{
+    PyGreenlet* current;
+    PyGreenlet* greenlet = NULL;
+
+    if (arg == NULL || !PyGreenlet_Check(arg)) {
+        PyErr_BadArgument();
+        return NULL;
+    }
+    if ((current = PyGreenlet_GetCurrent()) == NULL) {
+        return NULL;
+    }
+    greenlet = (PyGreenlet*)arg;
+    if (PyGreenlet_SetParent(greenlet, current)) {
+        Py_DECREF(current);
+        return NULL;
+    }
+    Py_DECREF(current);
+    if (PyGreenlet_Switch(greenlet, NULL, NULL) == NULL) {
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+static PyObject*
+test_new_greenlet(PyObject* self, PyObject* callable)
+{
+    PyObject* result = NULL;
+    PyGreenlet* greenlet = PyGreenlet_New(callable, NULL);
+
+    if (!greenlet) {
+        return NULL;
+    }
+
+    result = PyGreenlet_Switch(greenlet, NULL, NULL);
+    Py_CLEAR(greenlet);
+    if (result == NULL) {
+        return NULL;
+    }
+
+    Py_INCREF(result);
+    return result;
+}
+
+static PyObject*
+test_raise_dead_greenlet(PyObject* self)
+{
+    PyErr_SetString(PyExc_GreenletExit, "test GreenletExit exception.");
+    return NULL;
+}
+
+static PyObject*
+test_raise_greenlet_error(PyObject* self)
+{
+    PyErr_SetString(PyExc_GreenletError, "test greenlet.error exception");
+    return NULL;
+}
+
+static PyObject*
+test_throw(PyObject* self, PyGreenlet* g)
+{
+    const char msg[] = "take that sucka!";
+    PyObject* msg_obj = Py_BuildValue("s", msg);
+    PyGreenlet_Throw(g, PyExc_ValueError, msg_obj, NULL);
+    Py_DECREF(msg_obj);
+    if (PyErr_Occurred()) {
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+static PyObject*
+test_throw_exact(PyObject* self, PyObject* args)
+{
+    PyGreenlet* g = NULL;
+    PyObject* typ = NULL;
+    PyObject* val = NULL;
+    PyObject* tb = NULL;
+
+    if (!PyArg_ParseTuple(args, "OOOO:throw", &g, &typ, &val, &tb)) {
+        return NULL;
+    }
+
+    PyGreenlet_Throw(g, typ, val, tb);
+    if (PyErr_Occurred()) {
+        return NULL;
+    }
+    Py_RETURN_NONE;
+}
+
+static PyMethodDef test_methods[] = {
+    {"test_switch",
+     (PyCFunction)test_switch,
+     METH_O,
+     "Switch to the provided greenlet sending provided arguments, and \n"
+     "return the results."},
+    {"test_switch_kwargs",
+     (PyCFunction)test_switch_kwargs,
+     METH_VARARGS | METH_KEYWORDS,
+     "Switch to the provided greenlet sending the provided keyword args."},
+    {"test_getcurrent",
+     (PyCFunction)test_getcurrent,
+     METH_NOARGS,
+     "Test PyGreenlet_GetCurrent()"},
+    {"test_setparent",
+     (PyCFunction)test_setparent,
+     METH_O,
+     "Se the parent of the provided greenlet and switch to it."},
+    {"test_new_greenlet",
+     (PyCFunction)test_new_greenlet,
+     METH_O,
+     "Test PyGreenlet_New()"},
+    {"test_raise_dead_greenlet",
+     (PyCFunction)test_raise_dead_greenlet,
+     METH_NOARGS,
+     "Just raise greenlet.GreenletExit"},
+    {"test_raise_greenlet_error",
+     (PyCFunction)test_raise_greenlet_error,
+     METH_NOARGS,
+     "Just raise greenlet.error"},
+    {"test_throw",
+     (PyCFunction)test_throw,
+     METH_O,
+     "Throw a ValueError at the provided greenlet"},
+    {"test_throw_exact",
+     (PyCFunction)test_throw_exact,
+     METH_VARARGS,
+     "Throw exactly the arguments given at the provided greenlet"},
+    {NULL, NULL, 0, NULL}
+};
+
+
+#define INITERROR return NULL
+
+static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT,
+                                       TEST_MODULE_NAME,
+                                       NULL,
+                                       0,
+                                       test_methods,
+                                       NULL,
+                                       NULL,
+                                       NULL,
+                                       NULL};
+
+PyMODINIT_FUNC
+PyInit__test_extension(void)
+{
+    PyObject* module = NULL;
+    module = PyModule_Create(&moduledef);
+
+    if (module == NULL) {
+        return NULL;
+    }
+
+    PyGreenlet_Import();
+    return module;
+}
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension.cpython-312-x86_64-linux-gnu.so b/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension.cpython-312-x86_64-linux-gnu.so
new file mode 100755
index 00000000..4413bee7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension.cpython-312-x86_64-linux-gnu.so
Binary files differdiff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension_cpp.cpp b/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension_cpp.cpp
new file mode 100644
index 00000000..5cbe6a76
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension_cpp.cpp
@@ -0,0 +1,226 @@
+/* This is a set of functions used to test C++ exceptions are not
+ * broken during greenlet switches
+ */
+
+#include "../greenlet.h"
+#include "../greenlet_compiler_compat.hpp"
+#include <exception>
+#include <stdexcept>
+
+struct exception_t {
+    int depth;
+    exception_t(int depth) : depth(depth) {}
+};
+
+/* Functions are called via pointers to prevent inlining */
+static void (*p_test_exception_throw_nonstd)(int depth);
+static void (*p_test_exception_throw_std)();
+static PyObject* (*p_test_exception_switch_recurse)(int depth, int left);
+
+static void
+test_exception_throw_nonstd(int depth)
+{
+    throw exception_t(depth);
+}
+
+static void
+test_exception_throw_std()
+{
+    throw std::runtime_error("Thrown from an extension.");
+}
+
+static PyObject*
+test_exception_switch_recurse(int depth, int left)
+{
+    if (left > 0) {
+        return p_test_exception_switch_recurse(depth, left - 1);
+    }
+
+    PyObject* result = NULL;
+    PyGreenlet* self = PyGreenlet_GetCurrent();
+    if (self == NULL)
+        return NULL;
+
+    try {
+        if (PyGreenlet_Switch(PyGreenlet_GET_PARENT(self), NULL, NULL) == NULL) {
+            Py_DECREF(self);
+            return NULL;
+        }
+        p_test_exception_throw_nonstd(depth);
+        PyErr_SetString(PyExc_RuntimeError,
+                        "throwing C++ exception didn't work");
+    }
+    catch (const exception_t& e) {
+        if (e.depth != depth)
+            PyErr_SetString(PyExc_AssertionError, "depth mismatch");
+        else
+            result = PyLong_FromLong(depth);
+    }
+    catch (...) {
+        PyErr_SetString(PyExc_RuntimeError, "unexpected C++ exception");
+    }
+
+    Py_DECREF(self);
+    return result;
+}
+
+/* test_exception_switch(int depth)
+ * - recurses depth times
+ * - switches to parent inside try/catch block
+ * - throws an exception that (expected to be caught in the same function)
+ * - verifies depth matches (exceptions shouldn't be caught in other greenlets)
+ */
+static PyObject*
+test_exception_switch(PyObject* UNUSED(self), PyObject* args)
+{
+    int depth;
+    if (!PyArg_ParseTuple(args, "i", &depth))
+        return NULL;
+    return p_test_exception_switch_recurse(depth, depth);
+}
+
+
+static PyObject*
+py_test_exception_throw_nonstd(PyObject* self, PyObject* args)
+{
+    if (!PyArg_ParseTuple(args, ""))
+        return NULL;
+    p_test_exception_throw_nonstd(0);
+    PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw");
+    return NULL;
+}
+
+static PyObject*
+py_test_exception_throw_std(PyObject* self, PyObject* args)
+{
+    if (!PyArg_ParseTuple(args, ""))
+        return NULL;
+    p_test_exception_throw_std();
+    PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw");
+    return NULL;
+}
+
+static PyObject*
+py_test_call(PyObject* self, PyObject* arg)
+{
+    PyObject* noargs = PyTuple_New(0);
+    PyObject* ret = PyObject_Call(arg, noargs, nullptr);
+    Py_DECREF(noargs);
+    return ret;
+}
+
+
+
+/* test_exception_switch_and_do_in_g2(g2func)
+ * - creates new greenlet g2 to run g2func
+ * - switches to g2 inside try/catch block
+ * - verifies that no exception has been caught
+ *
+ * it is used together with test_exception_throw to verify that unhandled
+ * exceptions thrown in one greenlet do not propagate to other greenlet nor
+ * segfault the process.
+ */
+static PyObject*
+test_exception_switch_and_do_in_g2(PyObject* self, PyObject* args)
+{
+    PyObject* g2func = NULL;
+    PyObject* result = NULL;
+
+    if (!PyArg_ParseTuple(args, "O", &g2func))
+        return NULL;
+    PyGreenlet* g2 = PyGreenlet_New(g2func, NULL);
+    if (!g2) {
+        return NULL;
+    }
+
+    try {
+        result = PyGreenlet_Switch(g2, NULL, NULL);
+        if (!result) {
+            return NULL;
+        }
+    }
+    catch (const exception_t& e) {
+        /* if we are here the memory can be already corrupted and the program
+         * might crash before below py-level exception might become printed.
+         * -> print something to stderr to make it clear that we had entered
+         *    this catch block.
+         * See comments in inner_bootstrap()
+         */
+#if defined(WIN32) || defined(_WIN32)
+        fprintf(stderr, "C++ exception unexpectedly caught in g1\n");
+        PyErr_SetString(PyExc_AssertionError, "C++ exception unexpectedly caught in g1");
+        Py_XDECREF(result);
+        return NULL;
+#else
+        throw;
+#endif
+    }
+
+    Py_XDECREF(result);
+    Py_RETURN_NONE;
+}
+
+static PyMethodDef test_methods[] = {
+    {"test_exception_switch",
+     (PyCFunction)&test_exception_switch,
+     METH_VARARGS,
+     "Switches to parent twice, to test exception handling and greenlet "
+     "switching."},
+    {"test_exception_switch_and_do_in_g2",
+     (PyCFunction)&test_exception_switch_and_do_in_g2,
+     METH_VARARGS,
+     "Creates new greenlet g2 to run g2func and switches to it inside try/catch "
+     "block. Used together with test_exception_throw to verify that unhandled "
+     "C++ exceptions thrown in a greenlet doe not corrupt memory."},
+    {"test_exception_throw_nonstd",
+     (PyCFunction)&py_test_exception_throw_nonstd,
+     METH_VARARGS,
+     "Throws non-standard C++ exception. Calling this function directly should abort the process."
+    },
+    {"test_exception_throw_std",
+     (PyCFunction)&py_test_exception_throw_std,
+     METH_VARARGS,
+     "Throws standard C++ exception. Calling this function directly should abort the process."
+    },
+    {"test_call",
+     (PyCFunction)&py_test_call,
+     METH_O,
+     "Call the given callable. Unlike calling it directly, this creates a "
+     "new C-level stack frame, which may be helpful in testing."
+    },
+    {NULL, NULL, 0, NULL}
+};
+
+
+static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT,
+                                       "greenlet.tests._test_extension_cpp",
+                                       NULL,
+                                       0,
+                                       test_methods,
+                                       NULL,
+                                       NULL,
+                                       NULL,
+                                       NULL};
+
+PyMODINIT_FUNC
+PyInit__test_extension_cpp(void)
+{
+    PyObject* module = NULL;
+
+    module = PyModule_Create(&moduledef);
+
+    if (module == NULL) {
+        return NULL;
+    }
+
+    PyGreenlet_Import();
+    if (_PyGreenlet_API == NULL) {
+        return NULL;
+    }
+
+    p_test_exception_throw_nonstd = test_exception_throw_nonstd;
+    p_test_exception_throw_std = test_exception_throw_std;
+    p_test_exception_switch_recurse = test_exception_switch_recurse;
+
+    return module;
+}
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension_cpp.cpython-312-x86_64-linux-gnu.so b/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension_cpp.cpython-312-x86_64-linux-gnu.so
new file mode 100755
index 00000000..e7b30ca4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/_test_extension_cpp.cpython-312-x86_64-linux-gnu.so
Binary files differdiff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/fail_clearing_run_switches.py b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_clearing_run_switches.py
new file mode 100644
index 00000000..6dd1492f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_clearing_run_switches.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+If we have a run callable passed to the constructor or set as an
+attribute, but we don't actually use that (because ``__getattribute__``
+or the like interferes), then when we clear callable before beginning
+to run, there's an opportunity for Python code to run.
+
+"""
+import greenlet
+
+g = None
+main = greenlet.getcurrent()
+
+results = []
+
+class RunCallable:
+
+    def __del__(self):
+        results.append(('RunCallable', '__del__'))
+        main.switch('from RunCallable')
+
+
+class G(greenlet.greenlet):
+
+    def __getattribute__(self, name):
+        if name == 'run':
+            results.append(('G.__getattribute__', 'run'))
+            return run_func
+        return object.__getattribute__(self, name)
+
+
+def run_func():
+    results.append(('run_func', 'enter'))
+
+
+g = G(RunCallable())
+# Try to start G. It will get to the point where it deletes
+# its run callable C++ variable in inner_bootstrap. That triggers
+# the __del__ method, which switches back to main before g
+# actually even starts running.
+x = g.switch()
+results.append(('main: g.switch()', x))
+# In the C++ code, this results in g->g_switch() appearing to return, even though
+# it has yet to run.
+print('In main with', x, flush=True)
+g.switch()
+print('RESULTS', results)
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/fail_cpp_exception.py b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_cpp_exception.py
new file mode 100644
index 00000000..fa4dc2eb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_cpp_exception.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+"""
+Helper for testing a C++ exception throw aborts the process.
+
+Takes one argument, the name of the function in :mod:`_test_extension_cpp` to call.
+"""
+import sys
+import greenlet
+from greenlet.tests import _test_extension_cpp
+print('fail_cpp_exception is running')
+
+def run_unhandled_exception_in_greenlet_aborts():
+    def _():
+        _test_extension_cpp.test_exception_switch_and_do_in_g2(
+            _test_extension_cpp.test_exception_throw_nonstd
+        )
+    g1 = greenlet.greenlet(_)
+    g1.switch()
+
+
+func_name = sys.argv[1]
+try:
+    func = getattr(_test_extension_cpp, func_name)
+except AttributeError:
+    if func_name == run_unhandled_exception_in_greenlet_aborts.__name__:
+        func = run_unhandled_exception_in_greenlet_aborts
+    elif func_name == 'run_as_greenlet_target':
+        g = greenlet.greenlet(_test_extension_cpp.test_exception_throw_std)
+        func = g.switch
+    else:
+        raise
+print('raising', func, flush=True)
+func()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/fail_initialstub_already_started.py b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_initialstub_already_started.py
new file mode 100644
index 00000000..c1a44efd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_initialstub_already_started.py
@@ -0,0 +1,78 @@
+"""
+Testing initialstub throwing an already started exception.
+"""
+
+import greenlet
+
+a = None
+b = None
+c = None
+main = greenlet.getcurrent()
+
+# If we switch into a dead greenlet,
+# we go looking for its parents.
+# if a parent is not yet started, we start it.
+
+results = []
+
+def a_run(*args):
+    #results.append('A')
+    results.append(('Begin A', args))
+
+
+def c_run():
+    results.append('Begin C')
+    b.switch('From C')
+    results.append('C done')
+
+class A(greenlet.greenlet): pass
+
+class B(greenlet.greenlet):
+    doing_it = False
+    def __getattribute__(self, name):
+        if name == 'run' and not self.doing_it:
+            assert greenlet.getcurrent() is c
+            self.doing_it = True
+            results.append('Switch to b from B.__getattribute__ in '
+                           + type(greenlet.getcurrent()).__name__)
+            b.switch()
+            results.append('B.__getattribute__ back from main in '
+                           + type(greenlet.getcurrent()).__name__)
+        if name == 'run':
+            name = '_B_run'
+        return object.__getattribute__(self, name)
+
+    def _B_run(self, *arg):
+        results.append(('Begin B', arg))
+        results.append('_B_run switching to main')
+        main.switch('From B')
+
+class C(greenlet.greenlet):
+    pass
+a = A(a_run)
+b = B(parent=a)
+c = C(c_run, b)
+
+# Start a child; while running, it will start B,
+# but starting B will ALSO start B.
+result = c.switch()
+results.append(('main from c', result))
+
+# Switch back to C, which was in the middle of switching
+# already. This will throw the ``GreenletStartedWhileInPython``
+# exception, which results in parent A getting started (B is finished)
+c.switch()
+
+results.append(('A dead?', a.dead, 'B dead?', b.dead, 'C dead?', c.dead))
+
+# A and B should both be dead now.
+assert a.dead
+assert b.dead
+assert not c.dead
+
+result = c.switch()
+results.append(('main from c.2', result))
+# Now C is dead
+assert c.dead
+
+print("RESULTS:", results)
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/fail_slp_switch.py b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_slp_switch.py
new file mode 100644
index 00000000..09905269
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_slp_switch.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+"""
+A test helper for seeing what happens when slp_switch()
+fails.
+"""
+# pragma: no cover
+
+import greenlet
+
+
+print('fail_slp_switch is running', flush=True)
+
+runs = []
+def func():
+    runs.append(1)
+    greenlet.getcurrent().parent.switch()
+    runs.append(2)
+    greenlet.getcurrent().parent.switch()
+    runs.append(3)
+
+g = greenlet._greenlet.UnswitchableGreenlet(func)
+g.switch()
+assert runs == [1]
+g.switch()
+assert runs == [1, 2]
+g.force_slp_switch_error = True
+
+# This should crash.
+g.switch()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_three_greenlets.py b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_three_greenlets.py
new file mode 100644
index 00000000..e151b19a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_three_greenlets.py
@@ -0,0 +1,44 @@
+"""
+Uses a trace function to switch greenlets at unexpected times.
+
+In the trace function, we switch from the current greenlet to another
+greenlet, which switches
+"""
+import greenlet
+
+g1 = None
+g2 = None
+
+switch_to_g2 = False
+
+def tracefunc(*args):
+    print('TRACE', *args)
+    global switch_to_g2
+    if switch_to_g2:
+        switch_to_g2 = False
+        g2.switch()
+    print('\tLEAVE TRACE', *args)
+
+def g1_run():
+    print('In g1_run')
+    global switch_to_g2
+    switch_to_g2 = True
+    from_parent = greenlet.getcurrent().parent.switch()
+    print('Return to g1_run')
+    print('From parent', from_parent)
+
+def g2_run():
+    #g1.switch()
+    greenlet.getcurrent().parent.switch()
+
+greenlet.settrace(tracefunc)
+
+g1 = greenlet.greenlet(g1_run)
+g2 = greenlet.greenlet(g2_run)
+
+# This switch didn't actually finish!
+# And if it did, it would raise TypeError
+# because g1_run() doesn't take any arguments.
+g1.switch(1)
+print('Back in main')
+g1.switch(2)
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_three_greenlets2.py b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_three_greenlets2.py
new file mode 100644
index 00000000..1f6b66bc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_three_greenlets2.py
@@ -0,0 +1,55 @@
+"""
+Like fail_switch_three_greenlets, but the call into g1_run would actually be
+valid.
+"""
+import greenlet
+
+g1 = None
+g2 = None
+
+switch_to_g2 = True
+
+results = []
+
+def tracefunc(*args):
+    results.append(('trace', args[0]))
+    print('TRACE', *args)
+    global switch_to_g2
+    if switch_to_g2:
+        switch_to_g2 = False
+        g2.switch('g2 from tracefunc')
+    print('\tLEAVE TRACE', *args)
+
+def g1_run(arg):
+    results.append(('g1 arg', arg))
+    print('In g1_run')
+    from_parent = greenlet.getcurrent().parent.switch('from g1_run')
+    results.append(('g1 from parent', from_parent))
+    return 'g1 done'
+
+def g2_run(arg):
+    #g1.switch()
+    results.append(('g2 arg', arg))
+    parent = greenlet.getcurrent().parent.switch('from g2_run')
+    global switch_to_g2
+    switch_to_g2 = False
+    results.append(('g2 from parent', parent))
+    return 'g2 done'
+
+
+greenlet.settrace(tracefunc)
+
+g1 = greenlet.greenlet(g1_run)
+g2 = greenlet.greenlet(g2_run)
+
+x = g1.switch('g1 from main')
+results.append(('main g1', x))
+print('Back in main', x)
+x = g1.switch('g2 from main')
+results.append(('main g2', x))
+print('back in amain again', x)
+x = g1.switch('g1 from main 2')
+results.append(('main g1.2', x))
+x = g2.switch()
+results.append(('main g2.2', x))
+print("RESULTS:", results)
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_two_greenlets.py b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_two_greenlets.py
new file mode 100644
index 00000000..3e52345a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/fail_switch_two_greenlets.py
@@ -0,0 +1,41 @@
+"""
+Uses a trace function to switch greenlets at unexpected times.
+
+In the trace function, we switch from the current greenlet to another
+greenlet, which switches
+"""
+import greenlet
+
+g1 = None
+g2 = None
+
+switch_to_g2 = False
+
+def tracefunc(*args):
+    print('TRACE', *args)
+    global switch_to_g2
+    if switch_to_g2:
+        switch_to_g2 = False
+        g2.switch()
+    print('\tLEAVE TRACE', *args)
+
+def g1_run():
+    print('In g1_run')
+    global switch_to_g2
+    switch_to_g2 = True
+    greenlet.getcurrent().parent.switch()
+    print('Return to g1_run')
+    print('Falling off end of g1_run')
+
+def g2_run():
+    g1.switch()
+    print('Falling off end of g2')
+
+greenlet.settrace(tracefunc)
+
+g1 = greenlet.greenlet(g1_run)
+g2 = greenlet.greenlet(g2_run)
+
+g1.switch()
+print('Falling off end of main')
+g2.switch()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/leakcheck.py b/.venv/lib/python3.12/site-packages/greenlet/tests/leakcheck.py
new file mode 100644
index 00000000..a5152fb2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/leakcheck.py
@@ -0,0 +1,319 @@
+# Copyright (c) 2018 gevent community
+# Copyright (c) 2021 greenlet community
+#
+# This was originally part of gevent's test suite. The main author
+# (Jason Madden) vendored a copy of it into greenlet.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+from __future__ import print_function
+
+import os
+import sys
+import gc
+
+from functools import wraps
+import unittest
+
+
+import objgraph
+
+# graphviz 0.18 (Nov 7 2021), available only on Python 3.6 and newer,
+# has added type hints (sigh). It wants to use ``typing.Literal`` for
+# some stuff, but that's only available on Python 3.9+. If that's not
+# found, it creates a ``unittest.mock.MagicMock`` object and annotates
+# with that. These are GC'able objects, and doing almost *anything*
+# with them results in an explosion of objects. For example, trying to
+# compare them for equality creates new objects. This causes our
+# leakchecks to fail, with reports like:
+#
+# greenlet.tests.leakcheck.LeakCheckError: refcount increased by [337, 1333, 343, 430, 530, 643, 769]
+# _Call          1820      +546
+# dict           4094       +76
+# MagicProxy      585       +73
+# tuple          2693       +66
+# _CallList        24        +3
+# weakref        1441        +1
+# function       5996        +1
+# type            736        +1
+# cell            592        +1
+# MagicMock         8        +1
+#
+# To avoid this, we *could* filter this type of object out early. In
+# principle it could leak, but we don't use mocks in greenlet, so it
+# doesn't leak from us. However, a further issue is that ``MagicMock``
+# objects have subobjects that are also GC'able, like ``_Call``, and
+# those create new mocks of their own too. So we'd have to filter them
+# as well, and they're not public. That's OK, we can workaround the
+# problem by being very careful to never compare by equality or other
+# user-defined operators, only using object identity or other builtin
+# functions.
+
+RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS')
+RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS
+RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR')
+RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR
+RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX')
+SKIP_LEAKCHECKS = RUNNING_ON_MANYLINUX or os.environ.get('GREENLET_SKIP_LEAKCHECKS')
+SKIP_FAILING_LEAKCHECKS = os.environ.get('GREENLET_SKIP_FAILING_LEAKCHECKS')
+ONLY_FAILING_LEAKCHECKS = os.environ.get('GREENLET_ONLY_FAILING_LEAKCHECKS')
+
+def ignores_leakcheck(func):
+    """
+    Ignore the given object during leakchecks.
+
+    Can be applied to a method, in which case the method will run, but
+    will not be subject to leak checks.
+
+    If applied to a class, the entire class will be skipped during leakchecks. This
+    is intended to be used for classes that are very slow and cause problems such as
+    test timeouts; typically it will be used for classes that are subclasses of a base
+    class and specify variants of behaviour (such as pool sizes).
+    """
+    func.ignore_leakcheck = True
+    return func
+
+def fails_leakcheck(func):
+    """
+    Mark that the function is known to leak.
+    """
+    func.fails_leakcheck = True
+    if SKIP_FAILING_LEAKCHECKS:
+        func = unittest.skip("Skipping known failures")(func)
+    return func
+
+class LeakCheckError(AssertionError):
+    pass
+
+if hasattr(sys, 'getobjects'):
+    # In a Python build with ``--with-trace-refs``, make objgraph
+    # trace *all* the objects, not just those that are tracked by the
+    # GC
+    class _MockGC(object):
+        def get_objects(self):
+            return sys.getobjects(0) # pylint:disable=no-member
+        def __getattr__(self, name):
+            return getattr(gc, name)
+    objgraph.gc = _MockGC()
+    fails_strict_leakcheck = fails_leakcheck
+else:
+    def fails_strict_leakcheck(func):
+        """
+        Decorator for a function that is known to fail when running
+        strict (``sys.getobjects()``) leakchecks.
+
+        This type of leakcheck finds all objects, even those, such as
+        strings, which are not tracked by the garbage collector.
+        """
+        return func
+
+class ignores_types_in_strict_leakcheck(object):
+    def __init__(self, types):
+        self.types = types
+    def __call__(self, func):
+        func.leakcheck_ignore_types = self.types
+        return func
+
+class _RefCountChecker(object):
+
+    # Some builtin things that we ignore
+    # XXX: Those things were ignored by gevent, but they're important here,
+    # presumably.
+    IGNORED_TYPES = () #(tuple, dict, types.FrameType, types.TracebackType)
+
+    def __init__(self, testcase, function):
+        self.testcase = testcase
+        self.function = function
+        self.deltas = []
+        self.peak_stats = {}
+        self.ignored_types = ()
+
+        # The very first time we are called, we have already been
+        # self.setUp() by the test runner, so we don't need to do it again.
+        self.needs_setUp = False
+
+    def _include_object_p(self, obj):
+        # pylint:disable=too-many-return-statements
+        #
+        # See the comment block at the top. We must be careful to
+        # avoid invoking user-defined operations.
+        if obj is self:
+            return False
+        kind = type(obj)
+        # ``self._include_object_p == obj`` returns NotImplemented
+        # for non-function objects, which causes the interpreter
+        # to try to reverse the order of arguments...which leads
+        # to the explosion of mock objects. We don't want that, so we implement
+        # the check manually.
+        if kind == type(self._include_object_p):
+            try:
+                # pylint:disable=not-callable
+                exact_method_equals = self._include_object_p.__eq__(obj)
+            except AttributeError:
+                # Python 2.7 methods may only have __cmp__, and that raises a
+                # TypeError for non-method arguments
+                # pylint:disable=no-member
+                exact_method_equals = self._include_object_p.__cmp__(obj) == 0
+
+            if exact_method_equals is not NotImplemented and exact_method_equals:
+                return False
+
+        # Similarly, we need to check identity in our __dict__ to avoid mock explosions.
+        for x in self.__dict__.values():
+            if obj is x:
+                return False
+
+
+        if kind in self.ignored_types or kind in self.IGNORED_TYPES:
+            return False
+
+        return True
+
+    def _growth(self):
+        return objgraph.growth(limit=None, peak_stats=self.peak_stats,
+                               filter=self._include_object_p)
+
+    def _report_diff(self, growth):
+        if not growth:
+            return "<Unable to calculate growth>"
+
+        lines = []
+        width = max(len(name) for name, _, _ in growth)
+        for name, count, delta in growth:
+            lines.append('%-*s%9d %+9d' % (width, name, count, delta))
+
+        diff = '\n'.join(lines)
+        return diff
+
+
+    def _run_test(self, args, kwargs):
+        gc_enabled = gc.isenabled()
+        gc.disable()
+
+        if self.needs_setUp:
+            self.testcase.setUp()
+            self.testcase.skipTearDown = False
+        try:
+            self.function(self.testcase, *args, **kwargs)
+        finally:
+            self.testcase.tearDown()
+            self.testcase.doCleanups()
+            self.testcase.skipTearDown = True
+            self.needs_setUp = True
+            if gc_enabled:
+                gc.enable()
+
+    def _growth_after(self):
+        # Grab post snapshot
+        # pylint:disable=no-member
+        if 'urlparse' in sys.modules:
+            sys.modules['urlparse'].clear_cache()
+        if 'urllib.parse' in sys.modules:
+            sys.modules['urllib.parse'].clear_cache()
+
+        return self._growth()
+
+    def _check_deltas(self, growth):
+        # Return false when we have decided there is no leak,
+        # true if we should keep looping, raises an assertion
+        # if we have decided there is a leak.
+
+        deltas = self.deltas
+        if not deltas:
+            # We haven't run yet, no data, keep looping
+            return True
+
+        if gc.garbage:
+            raise LeakCheckError("Generated uncollectable garbage %r" % (gc.garbage,))
+
+
+        # the following configurations are classified as "no leak"
+        # [0, 0]
+        # [x, 0, 0]
+        # [... a, b, c, d]  where a+b+c+d = 0
+        #
+        # the following configurations are classified as "leak"
+        # [... z, z, z]  where z > 0
+
+        if deltas[-2:] == [0, 0] and len(deltas) in (2, 3):
+            return False
+
+        if deltas[-3:] == [0, 0, 0]:
+            return False
+
+        if len(deltas) >= 4 and sum(deltas[-4:]) == 0:
+            return False
+
+        if len(deltas) >= 3 and deltas[-1] > 0 and deltas[-1] == deltas[-2] and deltas[-2] == deltas[-3]:
+            diff = self._report_diff(growth)
+            raise LeakCheckError('refcount increased by %r\n%s' % (deltas, diff))
+
+        # OK, we don't know for sure yet. Let's search for more
+        if sum(deltas[-3:]) <= 0 or sum(deltas[-4:]) <= 0 or deltas[-4:].count(0) >= 2:
+            # this is suspicious, so give a few more runs
+            limit = 11
+        else:
+            limit = 7
+        if len(deltas) >= limit:
+            raise LeakCheckError('refcount increased by %r\n%s'
+                                 % (deltas,
+                                    self._report_diff(growth)))
+
+        # We couldn't decide yet, keep going
+        return True
+
+    def __call__(self, args, kwargs):
+        for _ in range(3):
+            gc.collect()
+
+        expect_failure = getattr(self.function, 'fails_leakcheck', False)
+        if expect_failure:
+            self.testcase.expect_greenlet_leak = True
+        self.ignored_types = getattr(self.function, "leakcheck_ignore_types", ())
+
+        # Capture state before; the incremental will be
+        # updated by each call to _growth_after
+        growth = self._growth()
+
+        try:
+            while self._check_deltas(growth):
+                self._run_test(args, kwargs)
+
+                growth = self._growth_after()
+
+                self.deltas.append(sum((stat[2] for stat in growth)))
+        except LeakCheckError:
+            if not expect_failure:
+                raise
+        else:
+            if expect_failure:
+                raise LeakCheckError("Expected %s to leak but it did not." % (self.function,))
+
+def wrap_refcount(method):
+    if getattr(method, 'ignore_leakcheck', False) or SKIP_LEAKCHECKS:
+        return method
+
+    @wraps(method)
+    def wrapper(self, *args, **kwargs): # pylint:disable=too-many-branches
+        if getattr(self, 'ignore_leakcheck', False):
+            raise unittest.SkipTest("This class ignored during leakchecks")
+        if ONLY_FAILING_LEAKCHECKS and not getattr(method, 'fails_leakcheck', False):
+            raise unittest.SkipTest("Only running tests that fail leakchecks.")
+        return _RefCountChecker(self, method)(args, kwargs)
+
+    return wrapper
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_contextvars.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_contextvars.py
new file mode 100644
index 00000000..9a16f671
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_contextvars.py
@@ -0,0 +1,310 @@
+from __future__ import print_function
+
+import gc
+import sys
+import unittest
+
+from functools import partial
+from unittest import skipUnless
+from unittest import skipIf
+
+from greenlet import greenlet
+from greenlet import getcurrent
+from . import TestCase
+
+
+try:
+    from contextvars import Context
+    from contextvars import ContextVar
+    from contextvars import copy_context
+    # From the documentation:
+    #
+    # Important: Context Variables should be created at the top module
+    # level and never in closures. Context objects hold strong
+    # references to context variables which prevents context variables
+    # from being properly garbage collected.
+    ID_VAR = ContextVar("id", default=None)
+    VAR_VAR = ContextVar("var", default=None)
+    ContextVar = None
+except ImportError:
+    Context = ContextVar = copy_context = None
+
+# We don't support testing if greenlet's built-in context var support is disabled.
+@skipUnless(Context is not None, "ContextVar not supported")
+class ContextVarsTests(TestCase):
+    def _new_ctx_run(self, *args, **kwargs):
+        return copy_context().run(*args, **kwargs)
+
+    def _increment(self, greenlet_id, callback, counts, expect):
+        ctx_var = ID_VAR
+        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):
+        # pylint:disable=too-many-branches
+        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,
+                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:
+                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()
+
+        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.
+            if propagate_by == 'run':
+                self.assertIsNone(let.gr_context)
+            else:
+                self.assertIsNotNone(let.gr_context)
+
+
+        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):
+        # pylint:disable=too-many-statements
+        ID_VAR.set(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 context attribute"):
+            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
+        VAR_VAR.set(None)
+        ctx = Context()
+
+        is_running = threading.Event()
+        should_suspend = threading.Event()
+        did_suspend = threading.Event()
+        should_exit = threading.Event()
+        holder = []
+
+        def greenlet_in_thread_fn():
+            VAR_VAR.set(1)
+            is_running.set()
+            should_suspend.wait(10)
+            VAR_VAR.set(2)
+            getcurrent().parent.switch()
+            holder.append(VAR_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(10)
+            gr.switch()
+            del gr
+            greenlet() # trigger cleanup
+
+        thread = threading.Thread(target=thread_fn, daemon=True)
+        thread.start()
+        is_running.wait(10)
+        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"):
+            getattr(gr, 'gr_context')
+        with self.assertRaisesRegex(ValueError, "running in a different"):
+            gr.gr_context = None
+
+        should_suspend.set()
+        did_suspend.wait(10)
+
+        # OK to access and modify context if greenlet is suspended
+        self.assertIs(gr.gr_context, ctx)
+        self.assertEqual(gr.gr_context[VAR_VAR], 2)
+        gr.gr_context = None
+
+        should_exit.set()
+        thread.join(10)
+
+        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)
+
+        # Otherwise we leak greenlets on some platforms.
+        # XXX: Should be able to do this automatically
+        del holder[:]
+        gr = None
+        thread = None
+
+    def test_context_assignment_wrong_type(self):
+        g = greenlet()
+        with self.assertRaisesRegex(TypeError,
+                                    "greenlet context must be a contextvars.Context or None"):
+            g.gr_context = self
+
+
+@skipIf(Context is not None, "ContextVar supported")
+class NoContextVarsTests(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
+
+        del let1
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_cpp.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_cpp.py
new file mode 100644
index 00000000..2d0cc9c9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_cpp.py
@@ -0,0 +1,73 @@
+from __future__ import print_function
+from __future__ import absolute_import
+
+import subprocess
+import unittest
+
+import greenlet
+from . import _test_extension_cpp
+from . import TestCase
+from . import WIN
+
+class CPPTests(TestCase):
+    def test_exception_switch(self):
+        greenlets = []
+        for i in range(4):
+            g = greenlet.greenlet(_test_extension_cpp.test_exception_switch)
+            g.switch(i)
+            greenlets.append(g)
+        for i, g in enumerate(greenlets):
+            self.assertEqual(g.switch(), i)
+
+    def _do_test_unhandled_exception(self, target):
+        import os
+        import sys
+        script = os.path.join(
+            os.path.dirname(__file__),
+            'fail_cpp_exception.py',
+        )
+        args = [sys.executable, script, target.__name__ if not isinstance(target, str) else target]
+        __traceback_info__ = args
+        with self.assertRaises(subprocess.CalledProcessError) as exc:
+            subprocess.check_output(
+                args,
+                encoding='utf-8',
+                stderr=subprocess.STDOUT
+            )
+
+        ex = exc.exception
+        expected_exit = self.get_expected_returncodes_for_aborted_process()
+        self.assertIn(ex.returncode, expected_exit)
+        self.assertIn('fail_cpp_exception is running', ex.output)
+        return ex.output
+
+
+    def test_unhandled_nonstd_exception_aborts(self):
+        # verify that plain unhandled throw aborts
+        self._do_test_unhandled_exception(_test_extension_cpp.test_exception_throw_nonstd)
+
+    def test_unhandled_std_exception_aborts(self):
+        # verify that plain unhandled throw aborts
+        self._do_test_unhandled_exception(_test_extension_cpp.test_exception_throw_std)
+
+    @unittest.skipIf(WIN, "XXX: This does not crash on Windows")
+    # Meaning the exception is getting lost somewhere...
+    def test_unhandled_std_exception_as_greenlet_function_aborts(self):
+        # verify that plain unhandled throw aborts
+        output = self._do_test_unhandled_exception('run_as_greenlet_target')
+        self.assertIn(
+            # We really expect this to be prefixed with "greenlet: Unhandled C++ exception:"
+            # as added by our handler for std::exception (see TUserGreenlet.cpp), but
+            # that's not correct everywhere --- our handler never runs before std::terminate
+            # gets called (for example, on arm32).
+            'Thrown from an extension.',
+            output
+        )
+
+    def test_unhandled_exception_in_greenlet_aborts(self):
+        # verify that unhandled throw called in greenlet aborts too
+        self._do_test_unhandled_exception('run_unhandled_exception_in_greenlet_aborts')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_extension_interface.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_extension_interface.py
new file mode 100644
index 00000000..34b66567
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_extension_interface.py
@@ -0,0 +1,115 @@
+from __future__ import print_function
+from __future__ import absolute_import
+
+import sys
+
+import greenlet
+from . import _test_extension
+from . import TestCase
+
+# pylint:disable=c-extension-no-member
+
+class CAPITests(TestCase):
+    def test_switch(self):
+        self.assertEqual(
+            50, _test_extension.test_switch(greenlet.greenlet(lambda: 50)))
+
+    def test_switch_kwargs(self):
+        def adder(x, y):
+            return x * y
+        g = greenlet.greenlet(adder)
+        self.assertEqual(6, _test_extension.test_switch_kwargs(g, x=3, y=2))
+
+    def test_setparent(self):
+        # pylint:disable=disallowed-name
+        def foo():
+            def bar():
+                greenlet.getcurrent().parent.switch()
+
+                # This final switch should go back to the main greenlet, since
+                # the test_setparent() function in the C extension should have
+                # reparented this greenlet.
+                greenlet.getcurrent().parent.switch()
+                raise AssertionError("Should never have reached this code")
+            child = greenlet.greenlet(bar)
+            child.switch()
+            greenlet.getcurrent().parent.switch(child)
+            greenlet.getcurrent().parent.throw(
+                AssertionError("Should never reach this code"))
+        foo_child = greenlet.greenlet(foo).switch()
+        self.assertEqual(None, _test_extension.test_setparent(foo_child))
+
+    def test_getcurrent(self):
+        _test_extension.test_getcurrent()
+
+    def test_new_greenlet(self):
+        self.assertEqual(-15, _test_extension.test_new_greenlet(lambda: -15))
+
+    def test_raise_greenlet_dead(self):
+        self.assertRaises(
+            greenlet.GreenletExit, _test_extension.test_raise_dead_greenlet)
+
+    def test_raise_greenlet_error(self):
+        self.assertRaises(
+            greenlet.error, _test_extension.test_raise_greenlet_error)
+
+    def test_throw(self):
+        seen = []
+
+        def foo():         # pylint:disable=disallowed-name
+            try:
+                greenlet.getcurrent().parent.switch()
+            except ValueError:
+                seen.append(sys.exc_info()[1])
+            except greenlet.GreenletExit:
+                raise AssertionError
+        g = greenlet.greenlet(foo)
+        g.switch()
+        _test_extension.test_throw(g)
+        self.assertEqual(len(seen), 1)
+        self.assertTrue(
+            isinstance(seen[0], ValueError),
+            "ValueError was not raised in foo()")
+        self.assertEqual(
+            str(seen[0]),
+            'take that sucka!',
+            "message doesn't match")
+
+    def test_non_traceback_param(self):
+        with self.assertRaises(TypeError) as exc:
+            _test_extension.test_throw_exact(
+                greenlet.getcurrent(),
+                Exception,
+                Exception(),
+                self
+            )
+        self.assertEqual(str(exc.exception),
+                         "throw() third argument must be a traceback object")
+
+    def test_instance_of_wrong_type(self):
+        with self.assertRaises(TypeError) as exc:
+            _test_extension.test_throw_exact(
+                greenlet.getcurrent(),
+                Exception(),
+                BaseException(),
+                None,
+            )
+
+        self.assertEqual(str(exc.exception),
+                         "instance exception may not have a separate value")
+
+    def test_not_throwable(self):
+        with self.assertRaises(TypeError) as exc:
+            _test_extension.test_throw_exact(
+                greenlet.getcurrent(),
+                "abc",
+                None,
+                None,
+            )
+        self.assertEqual(str(exc.exception),
+                         "exceptions must be classes, or instances, not str")
+
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_gc.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_gc.py
new file mode 100644
index 00000000..994addb9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_gc.py
@@ -0,0 +1,86 @@
+import gc
+
+import weakref
+
+import greenlet
+
+
+from . import TestCase
+from .leakcheck import fails_leakcheck
+# These only work with greenlet gc support
+# which is no longer optional.
+assert greenlet.GREENLET_USE_GC
+
+class GCTests(TestCase):
+    def test_dead_circular_ref(self):
+        o = weakref.ref(greenlet.greenlet(greenlet.getcurrent).switch())
+        gc.collect()
+        if o() is not None:
+            import sys
+            print("O IS NOT NONE.", sys.getrefcount(o()))
+        self.assertIsNone(o())
+        self.assertFalse(gc.garbage, gc.garbage)
+
+    def test_circular_greenlet(self):
+        class circular_greenlet(greenlet.greenlet):
+            self = None
+        o = circular_greenlet()
+        o.self = o
+        o = weakref.ref(o)
+        gc.collect()
+        self.assertIsNone(o())
+        self.assertFalse(gc.garbage, gc.garbage)
+
+    def test_inactive_ref(self):
+        class inactive_greenlet(greenlet.greenlet):
+            def __init__(self):
+                greenlet.greenlet.__init__(self, run=self.run)
+
+            def run(self):
+                pass
+        o = inactive_greenlet()
+        o = weakref.ref(o)
+        gc.collect()
+        self.assertIsNone(o())
+        self.assertFalse(gc.garbage, gc.garbage)
+
+    @fails_leakcheck
+    def test_finalizer_crash(self):
+        # This test is designed to crash when active greenlets
+        # are made garbage collectable, until the underlying
+        # problem is resolved. How does it work:
+        # - order of object creation is important
+        # - array is created first, so it is moved to unreachable first
+        # - we create a cycle between a greenlet and this array
+        # - we create an object that participates in gc, is only
+        #   referenced by a greenlet, and would corrupt gc lists
+        #   on destruction, the easiest is to use an object with
+        #   a finalizer
+        # - because array is the first object in unreachable it is
+        #   cleared first, which causes all references to greenlet
+        #   to disappear and causes greenlet to be destroyed, but since
+        #   it is still live it causes a switch during gc, which causes
+        #   an object with finalizer to be destroyed, which causes stack
+        #   corruption and then a crash
+
+        class object_with_finalizer(object):
+            def __del__(self):
+                pass
+        array = []
+        parent = greenlet.getcurrent()
+        def greenlet_body():
+            greenlet.getcurrent().object = object_with_finalizer()
+            try:
+                parent.switch()
+            except greenlet.GreenletExit:
+                print("Got greenlet exit!")
+            finally:
+                del greenlet.getcurrent().object
+        g = greenlet.greenlet(greenlet_body)
+        g.array = array
+        array.append(g)
+        g.switch()
+        del array
+        del g
+        greenlet.getcurrent()
+        gc.collect()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_generator.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_generator.py
new file mode 100644
index 00000000..ca4a644b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_generator.py
@@ -0,0 +1,59 @@
+
+from greenlet import greenlet
+
+from . import TestCase
+
+class genlet(greenlet):
+    parent = None
+    def __init__(self, *args, **kwds):
+        self.args = args
+        self.kwds = kwds
+
+    def run(self):
+        fn, = self.fn
+        fn(*self.args, **self.kwds)
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        self.parent = greenlet.getcurrent()
+        result = self.switch()
+        if self:
+            return result
+
+        raise StopIteration
+
+    next = __next__
+
+
+def Yield(value):
+    g = greenlet.getcurrent()
+    while not isinstance(g, genlet):
+        if g is None:
+            raise RuntimeError('yield outside a genlet')
+        g = g.parent
+    g.parent.switch(value)
+
+
+def generator(func):
+    class Generator(genlet):
+        fn = (func,)
+    return Generator
+
+# ____________________________________________________________
+
+
+class GeneratorTests(TestCase):
+    def test_generator(self):
+        seen = []
+
+        def g(n):
+            for i in range(n):
+                seen.append(i)
+                Yield(i)
+        g = generator(g)
+        for _ in range(3):
+            for j in g(5):
+                seen.append(j)
+        self.assertEqual(seen, 3 * [0, 0, 1, 1, 2, 2, 3, 3, 4, 4])
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_generator_nested.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_generator_nested.py
new file mode 100644
index 00000000..8d752a63
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_generator_nested.py
@@ -0,0 +1,168 @@
+
+from greenlet import greenlet
+from . import TestCase
+from .leakcheck import fails_leakcheck
+
+class genlet(greenlet):
+    parent = None
+    def __init__(self, *args, **kwds):
+        self.args = args
+        self.kwds = kwds
+        self.child = None
+
+    def run(self):
+        # Note the function is packed in a tuple
+        # to avoid creating a bound method for it.
+        fn, = self.fn
+        fn(*self.args, **self.kwds)
+
+    def __iter__(self):
+        return self
+
+    def set_child(self, child):
+        self.child = child
+
+    def __next__(self):
+        if self.child:
+            child = self.child
+            while child.child:
+                tmp = child
+                child = child.child
+                tmp.child = None
+
+            result = child.switch()
+        else:
+            self.parent = greenlet.getcurrent()
+            result = self.switch()
+
+        if self:
+            return result
+
+        raise StopIteration
+
+    next = __next__
+
+def Yield(value, level=1):
+    g = greenlet.getcurrent()
+
+    while level != 0:
+        if not isinstance(g, genlet):
+            raise RuntimeError('yield outside a genlet')
+        if level > 1:
+            g.parent.set_child(g)
+        g = g.parent
+        level -= 1
+
+    g.switch(value)
+
+
+def Genlet(func):
+    class TheGenlet(genlet):
+        fn = (func,)
+    return TheGenlet
+
+# ____________________________________________________________
+
+
+def g1(n, seen):
+    for i in range(n):
+        seen.append(i + 1)
+        yield i
+
+
+def g2(n, seen):
+    for i in range(n):
+        seen.append(i + 1)
+        Yield(i)
+
+g2 = Genlet(g2)
+
+
+def nested(i):
+    Yield(i)
+
+
+def g3(n, seen):
+    for i in range(n):
+        seen.append(i + 1)
+        nested(i)
+g3 = Genlet(g3)
+
+
+def a(n):
+    if n == 0:
+        return
+    for ii in ax(n - 1):
+        Yield(ii)
+    Yield(n)
+ax = Genlet(a)
+
+
+def perms(l):
+    if len(l) > 1:
+        for e in l:
+            # No syntactical sugar for generator expressions
+            x = [Yield([e] + p) for p in perms([x for x in l if x != e])]
+            assert x
+    else:
+        Yield(l)
+perms = Genlet(perms)
+
+
+def gr1(n):
+    for ii in range(1, n):
+        Yield(ii)
+        Yield(ii * ii, 2)
+
+gr1 = Genlet(gr1)
+
+
+def gr2(n, seen):
+    for ii in gr1(n):
+        seen.append(ii)
+
+gr2 = Genlet(gr2)
+
+
+class NestedGeneratorTests(TestCase):
+    def test_layered_genlets(self):
+        seen = []
+        for ii in gr2(5, seen):
+            seen.append(ii)
+        self.assertEqual(seen, [1, 1, 2, 4, 3, 9, 4, 16])
+
+    @fails_leakcheck
+    def test_permutations(self):
+        gen_perms = perms(list(range(4)))
+        permutations = list(gen_perms)
+        self.assertEqual(len(permutations), 4 * 3 * 2 * 1)
+        self.assertIn([0, 1, 2, 3], permutations)
+        self.assertIn([3, 2, 1, 0], permutations)
+        res = []
+        for ii in zip(perms(list(range(4))), perms(list(range(3)))):
+            res.append(ii)
+        self.assertEqual(
+            res,
+            [([0, 1, 2, 3], [0, 1, 2]), ([0, 1, 3, 2], [0, 2, 1]),
+             ([0, 2, 1, 3], [1, 0, 2]), ([0, 2, 3, 1], [1, 2, 0]),
+             ([0, 3, 1, 2], [2, 0, 1]), ([0, 3, 2, 1], [2, 1, 0])])
+        # XXX Test to make sure we are working as a generator expression
+
+    def test_genlet_simple(self):
+        for g in g1, g2, g3:
+            seen = []
+            for _ in range(3):
+                for j in g(5, seen):
+                    seen.append(j)
+            self.assertEqual(seen, 3 * [1, 0, 2, 1, 3, 2, 4, 3, 5, 4])
+
+    def test_genlet_bad(self):
+        try:
+            Yield(10)
+        except RuntimeError:
+            pass
+
+    def test_nested_genlets(self):
+        seen = []
+        for ii in ax(5):
+            seen.append(ii)
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet.py
new file mode 100644
index 00000000..c4aabea7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet.py
@@ -0,0 +1,1324 @@
+import gc
+import sys
+import time
+import threading
+import unittest
+
+from abc import ABCMeta
+from abc import abstractmethod
+
+import greenlet
+from greenlet import greenlet as RawGreenlet
+from . import TestCase
+from . import RUNNING_ON_MANYLINUX
+from . import PY313
+from .leakcheck import fails_leakcheck
+
+
+# We manually manage locks in many tests
+# pylint:disable=consider-using-with
+# pylint:disable=too-many-public-methods
+# This module is quite large.
+# TODO: Refactor into separate test files. For example,
+# put all the regression tests that used to produce
+# crashes in test_greenlet_no_crash; put tests that DO deliberately crash
+# the interpreter into test_greenlet_crash.
+# pylint:disable=too-many-lines
+
+class SomeError(Exception):
+    pass
+
+
+def fmain(seen):
+    try:
+        greenlet.getcurrent().parent.switch()
+    except:
+        seen.append(sys.exc_info()[0])
+        raise
+    raise SomeError
+
+
+def send_exception(g, exc):
+    # note: send_exception(g, exc)  can be now done with  g.throw(exc).
+    # the purpose of this test is to explicitly check the propagation rules.
+    def crasher(exc):
+        raise exc
+    g1 = RawGreenlet(crasher, parent=g)
+    g1.switch(exc)
+
+
+class TestGreenlet(TestCase):
+
+    def _do_simple_test(self):
+        lst = []
+
+        def f():
+            lst.append(1)
+            greenlet.getcurrent().parent.switch()
+            lst.append(3)
+        g = RawGreenlet(f)
+        lst.append(0)
+        g.switch()
+        lst.append(2)
+        g.switch()
+        lst.append(4)
+        self.assertEqual(lst, list(range(5)))
+
+    def test_simple(self):
+        self._do_simple_test()
+
+    def test_switch_no_run_raises_AttributeError(self):
+        g = RawGreenlet()
+        with self.assertRaises(AttributeError) as exc:
+            g.switch()
+
+        self.assertIn("run", str(exc.exception))
+
+    def test_throw_no_run_raises_AttributeError(self):
+        g = RawGreenlet()
+        with self.assertRaises(AttributeError) as exc:
+            g.throw(SomeError)
+
+        self.assertIn("run", str(exc.exception))
+
+    def test_parent_equals_None(self):
+        g = RawGreenlet(parent=None)
+        self.assertIsNotNone(g)
+        self.assertIs(g.parent, greenlet.getcurrent())
+
+    def test_run_equals_None(self):
+        g = RawGreenlet(run=None)
+        self.assertIsNotNone(g)
+        self.assertIsNone(g.run)
+
+    def test_two_children(self):
+        lst = []
+
+        def f():
+            lst.append(1)
+            greenlet.getcurrent().parent.switch()
+            lst.extend([1, 1])
+        g = RawGreenlet(f)
+        h = RawGreenlet(f)
+        g.switch()
+        self.assertEqual(len(lst), 1)
+        h.switch()
+        self.assertEqual(len(lst), 2)
+        h.switch()
+        self.assertEqual(len(lst), 4)
+        self.assertEqual(h.dead, True)
+        g.switch()
+        self.assertEqual(len(lst), 6)
+        self.assertEqual(g.dead, True)
+
+    def test_two_recursive_children(self):
+        lst = []
+
+        def f():
+            lst.append('b')
+            greenlet.getcurrent().parent.switch()
+
+        def g():
+            lst.append('a')
+            g = RawGreenlet(f)
+            g.switch()
+            lst.append('c')
+
+        g = RawGreenlet(g)
+        self.assertEqual(sys.getrefcount(g), 2)
+        g.switch()
+        self.assertEqual(lst, ['a', 'b', 'c'])
+        # Just the one in this frame, plus the one on the stack we pass to the function
+        self.assertEqual(sys.getrefcount(g), 2)
+
+    def test_threads(self):
+        success = []
+
+        def f():
+            self._do_simple_test()
+            success.append(True)
+        ths = [threading.Thread(target=f) for i in range(10)]
+        for th in ths:
+            th.start()
+        for th in ths:
+            th.join(10)
+        self.assertEqual(len(success), len(ths))
+
+    def test_exception(self):
+        seen = []
+        g1 = RawGreenlet(fmain)
+        g2 = RawGreenlet(fmain)
+        g1.switch(seen)
+        g2.switch(seen)
+        g2.parent = g1
+
+        self.assertEqual(seen, [])
+        #with self.assertRaises(SomeError):
+        #    p("***Switching back")
+        #    g2.switch()
+        # Creating this as a bound method can reveal bugs that
+        # are hidden on newer versions of Python that avoid creating
+        # bound methods for direct expressions; IOW, don't use the `with`
+        # form!
+        self.assertRaises(SomeError, g2.switch)
+        self.assertEqual(seen, [SomeError])
+
+        value = g2.switch()
+        self.assertEqual(value, ())
+        self.assertEqual(seen, [SomeError])
+
+        value = g2.switch(25)
+        self.assertEqual(value, 25)
+        self.assertEqual(seen, [SomeError])
+
+
+    def test_send_exception(self):
+        seen = []
+        g1 = RawGreenlet(fmain)
+        g1.switch(seen)
+        self.assertRaises(KeyError, send_exception, g1, KeyError)
+        self.assertEqual(seen, [KeyError])
+
+    def test_dealloc(self):
+        seen = []
+        g1 = RawGreenlet(fmain)
+        g2 = RawGreenlet(fmain)
+        g1.switch(seen)
+        g2.switch(seen)
+        self.assertEqual(seen, [])
+        del g1
+        gc.collect()
+        self.assertEqual(seen, [greenlet.GreenletExit])
+        del g2
+        gc.collect()
+        self.assertEqual(seen, [greenlet.GreenletExit, greenlet.GreenletExit])
+
+    def test_dealloc_catches_GreenletExit_throws_other(self):
+        def run():
+            try:
+                greenlet.getcurrent().parent.switch()
+            except greenlet.GreenletExit:
+                raise SomeError from None
+
+        g = RawGreenlet(run)
+        g.switch()
+        # Destroying the only reference to the greenlet causes it
+        # to get GreenletExit; when it in turn raises, even though we're the parent
+        # we don't get the exception, it just gets printed.
+        # When we run on 3.8 only, we can use sys.unraisablehook
+        oldstderr = sys.stderr
+        from io import StringIO
+        stderr = sys.stderr = StringIO()
+        try:
+            del g
+        finally:
+            sys.stderr = oldstderr
+
+        v = stderr.getvalue()
+        self.assertIn("Exception", v)
+        self.assertIn('ignored', v)
+        self.assertIn("SomeError", v)
+
+
+    @unittest.skipIf(
+        PY313 and RUNNING_ON_MANYLINUX,
+        "Sometimes flaky (getting one GreenletExit in the second list)"
+        # Probably due to funky timing interactions?
+        # TODO: FIXME Make that work.
+    )
+
+    def test_dealloc_other_thread(self):
+        seen = []
+        someref = []
+
+        bg_glet_created_running_and_no_longer_ref_in_bg = threading.Event()
+        fg_ref_released = threading.Event()
+        bg_should_be_clear = threading.Event()
+        ok_to_exit_bg_thread = threading.Event()
+
+        def f():
+            g1 = RawGreenlet(fmain)
+            g1.switch(seen)
+            someref.append(g1)
+            del g1
+            gc.collect()
+
+            bg_glet_created_running_and_no_longer_ref_in_bg.set()
+            fg_ref_released.wait(3)
+
+            RawGreenlet()   # trigger release
+            bg_should_be_clear.set()
+            ok_to_exit_bg_thread.wait(3)
+            RawGreenlet() # One more time
+
+        t = threading.Thread(target=f)
+        t.start()
+        bg_glet_created_running_and_no_longer_ref_in_bg.wait(10)
+
+        self.assertEqual(seen, [])
+        self.assertEqual(len(someref), 1)
+        del someref[:]
+        gc.collect()
+        # g1 is not released immediately because it's from another thread
+        self.assertEqual(seen, [])
+        fg_ref_released.set()
+        bg_should_be_clear.wait(3)
+        try:
+            self.assertEqual(seen, [greenlet.GreenletExit])
+        finally:
+            ok_to_exit_bg_thread.set()
+            t.join(10)
+            del seen[:]
+            del someref[:]
+
+    def test_frame(self):
+        def f1():
+            f = sys._getframe(0) # pylint:disable=protected-access
+            self.assertEqual(f.f_back, None)
+            greenlet.getcurrent().parent.switch(f)
+            return "meaning of life"
+        g = RawGreenlet(f1)
+        frame = g.switch()
+        self.assertTrue(frame is g.gr_frame)
+        self.assertTrue(g)
+
+        from_g = g.switch()
+        self.assertFalse(g)
+        self.assertEqual(from_g, 'meaning of life')
+        self.assertEqual(g.gr_frame, None)
+
+    def test_thread_bug(self):
+        def runner(x):
+            g = RawGreenlet(lambda: time.sleep(x))
+            g.switch()
+        t1 = threading.Thread(target=runner, args=(0.2,))
+        t2 = threading.Thread(target=runner, args=(0.3,))
+        t1.start()
+        t2.start()
+        t1.join(10)
+        t2.join(10)
+
+    def test_switch_kwargs(self):
+        def run(a, b):
+            self.assertEqual(a, 4)
+            self.assertEqual(b, 2)
+            return 42
+        x = RawGreenlet(run).switch(a=4, b=2)
+        self.assertEqual(x, 42)
+
+    def test_switch_kwargs_to_parent(self):
+        def run(x):
+            greenlet.getcurrent().parent.switch(x=x)
+            greenlet.getcurrent().parent.switch(2, x=3)
+            return x, x ** 2
+        g = RawGreenlet(run)
+        self.assertEqual({'x': 3}, g.switch(3))
+        self.assertEqual(((2,), {'x': 3}), g.switch())
+        self.assertEqual((3, 9), g.switch())
+
+    def test_switch_to_another_thread(self):
+        data = {}
+        created_event = threading.Event()
+        done_event = threading.Event()
+
+        def run():
+            data['g'] = RawGreenlet(lambda: None)
+            created_event.set()
+            done_event.wait(10)
+        thread = threading.Thread(target=run)
+        thread.start()
+        created_event.wait(10)
+        with self.assertRaises(greenlet.error):
+            data['g'].switch()
+        done_event.set()
+        thread.join(10)
+        # XXX: Should handle this automatically
+        data.clear()
+
+    def test_exc_state(self):
+        def f():
+            try:
+                raise ValueError('fun')
+            except: # pylint:disable=bare-except
+                exc_info = sys.exc_info()
+                RawGreenlet(h).switch()
+                self.assertEqual(exc_info, sys.exc_info())
+
+        def h():
+            self.assertEqual(sys.exc_info(), (None, None, None))
+
+        RawGreenlet(f).switch()
+
+    def test_instance_dict(self):
+        def f():
+            greenlet.getcurrent().test = 42
+        def deldict(g):
+            del g.__dict__
+        def setdict(g, value):
+            g.__dict__ = value
+        g = RawGreenlet(f)
+        self.assertEqual(g.__dict__, {})
+        g.switch()
+        self.assertEqual(g.test, 42)
+        self.assertEqual(g.__dict__, {'test': 42})
+        g.__dict__ = g.__dict__
+        self.assertEqual(g.__dict__, {'test': 42})
+        self.assertRaises(TypeError, deldict, g)
+        self.assertRaises(TypeError, setdict, g, 42)
+
+    def test_running_greenlet_has_no_run(self):
+        has_run = []
+        def func():
+            has_run.append(
+                hasattr(greenlet.getcurrent(), 'run')
+            )
+
+        g = RawGreenlet(func)
+        g.switch()
+        self.assertEqual(has_run, [False])
+
+    def test_deepcopy(self):
+        import copy
+        self.assertRaises(TypeError, copy.copy, RawGreenlet())
+        self.assertRaises(TypeError, copy.deepcopy, RawGreenlet())
+
+    def test_parent_restored_on_kill(self):
+        hub = RawGreenlet(lambda: None)
+        main = greenlet.getcurrent()
+        result = []
+        def worker():
+            try:
+                # Wait to be killed by going back to the test.
+                main.switch()
+            except greenlet.GreenletExit:
+                # Resurrect and switch to parent
+                result.append(greenlet.getcurrent().parent)
+                result.append(greenlet.getcurrent())
+                hub.switch()
+        g = RawGreenlet(worker, parent=hub)
+        g.switch()
+        # delete the only reference, thereby raising GreenletExit
+        del g
+        self.assertTrue(result)
+        self.assertIs(result[0], main)
+        self.assertIs(result[1].parent, hub)
+        # Delete them, thereby breaking the cycle between the greenlet
+        # and the frame, which otherwise would never be collectable
+        # XXX: We should be able to automatically fix this.
+        del result[:]
+        hub = None
+        main = None
+
+    def test_parent_return_failure(self):
+        # No run causes AttributeError on switch
+        g1 = RawGreenlet()
+        # Greenlet that implicitly switches to parent
+        g2 = RawGreenlet(lambda: None, parent=g1)
+        # AttributeError should propagate to us, no fatal errors
+        with self.assertRaises(AttributeError):
+            g2.switch()
+
+    def test_throw_exception_not_lost(self):
+        class mygreenlet(RawGreenlet):
+            def __getattribute__(self, name):
+                try:
+                    raise Exception # pylint:disable=broad-exception-raised
+                except: # pylint:disable=bare-except
+                    pass
+                return RawGreenlet.__getattribute__(self, name)
+        g = mygreenlet(lambda: None)
+        self.assertRaises(SomeError, g.throw, SomeError())
+
+    @fails_leakcheck
+    def _do_test_throw_to_dead_thread_doesnt_crash(self, wait_for_cleanup=False):
+        result = []
+        def worker():
+            greenlet.getcurrent().parent.switch()
+
+        def creator():
+            g = RawGreenlet(worker)
+            g.switch()
+            result.append(g)
+            if wait_for_cleanup:
+                # Let this greenlet eventually be cleaned up.
+                g.switch()
+                greenlet.getcurrent()
+        t = threading.Thread(target=creator)
+        t.start()
+        t.join(10)
+        del t
+        # But, depending on the operating system, the thread
+        # deallocator may not actually have run yet! So we can't be
+        # sure about the error message unless we wait.
+        if wait_for_cleanup:
+            self.wait_for_pending_cleanups()
+        with self.assertRaises(greenlet.error) as exc:
+            result[0].throw(SomeError)
+
+        if not wait_for_cleanup:
+            s = str(exc.exception)
+            self.assertTrue(
+                s == "cannot switch to a different thread (which happens to have exited)"
+                or 'Cannot switch' in s
+            )
+        else:
+            self.assertEqual(
+                str(exc.exception),
+                "cannot switch to a different thread (which happens to have exited)",
+            )
+
+        if hasattr(result[0].gr_frame, 'clear'):
+            # The frame is actually executing (it thinks), we can't clear it.
+            with self.assertRaises(RuntimeError):
+                result[0].gr_frame.clear()
+        # Unfortunately, this doesn't actually clear the references, they're in the
+        # fast local array.
+        if not wait_for_cleanup:
+            # f_locals has no clear method in Python 3.13
+            if hasattr(result[0].gr_frame.f_locals, 'clear'):
+                result[0].gr_frame.f_locals.clear()
+        else:
+            self.assertIsNone(result[0].gr_frame)
+
+        del creator
+        worker = None
+        del result[:]
+        # XXX: we ought to be able to automatically fix this.
+        # See issue 252
+        self.expect_greenlet_leak = True # direct us not to wait for it to go away
+
+    @fails_leakcheck
+    def test_throw_to_dead_thread_doesnt_crash(self):
+        self._do_test_throw_to_dead_thread_doesnt_crash()
+
+    def test_throw_to_dead_thread_doesnt_crash_wait(self):
+        self._do_test_throw_to_dead_thread_doesnt_crash(True)
+
+    @fails_leakcheck
+    def test_recursive_startup(self):
+        class convoluted(RawGreenlet):
+            def __init__(self):
+                RawGreenlet.__init__(self)
+                self.count = 0
+            def __getattribute__(self, name):
+                if name == 'run' and self.count == 0:
+                    self.count = 1
+                    self.switch(43)
+                return RawGreenlet.__getattribute__(self, name)
+            def run(self, value):
+                while True:
+                    self.parent.switch(value)
+        g = convoluted()
+        self.assertEqual(g.switch(42), 43)
+        # Exits the running greenlet, otherwise it leaks
+        # XXX: We should be able to automatically fix this
+        #g.throw(greenlet.GreenletExit)
+        #del g
+        self.expect_greenlet_leak = True
+
+    def test_threaded_updatecurrent(self):
+        # released when main thread should execute
+        lock1 = threading.Lock()
+        lock1.acquire()
+        # released when another thread should execute
+        lock2 = threading.Lock()
+        lock2.acquire()
+        class finalized(object):
+            def __del__(self):
+                # happens while in green_updatecurrent() in main greenlet
+                # should be very careful not to accidentally call it again
+                # at the same time we must make sure another thread executes
+                lock2.release()
+                lock1.acquire()
+                # now ts_current belongs to another thread
+        def deallocator():
+            greenlet.getcurrent().parent.switch()
+        def fthread():
+            lock2.acquire()
+            greenlet.getcurrent()
+            del g[0]
+            lock1.release()
+            lock2.acquire()
+            greenlet.getcurrent()
+            lock1.release()
+        main = greenlet.getcurrent()
+        g = [RawGreenlet(deallocator)]
+        g[0].bomb = finalized()
+        g[0].switch()
+        t = threading.Thread(target=fthread)
+        t.start()
+        # let another thread grab ts_current and deallocate g[0]
+        lock2.release()
+        lock1.acquire()
+        # this is the corner stone
+        # getcurrent() will notice that ts_current belongs to another thread
+        # and start the update process, which would notice that g[0] should
+        # be deallocated, and that will execute an object's finalizer. Now,
+        # that object will let another thread run so it can grab ts_current
+        # again, which would likely crash the interpreter if there's no
+        # check for this case at the end of green_updatecurrent(). This test
+        # passes if getcurrent() returns correct result, but it's likely
+        # to randomly crash if it's not anyway.
+        self.assertEqual(greenlet.getcurrent(), main)
+        # wait for another thread to complete, just in case
+        t.join(10)
+
+    def test_dealloc_switch_args_not_lost(self):
+        seen = []
+        def worker():
+            # wait for the value
+            value = greenlet.getcurrent().parent.switch()
+            # delete all references to ourself
+            del worker[0]
+            initiator.parent = greenlet.getcurrent().parent
+            # switch to main with the value, but because
+            # ts_current is the last reference to us we
+            # return here immediately, where we resurrect ourself.
+            try:
+                greenlet.getcurrent().parent.switch(value)
+            finally:
+                seen.append(greenlet.getcurrent())
+        def initiator():
+            return 42 # implicitly falls thru to parent
+
+        worker = [RawGreenlet(worker)]
+
+        worker[0].switch() # prime worker
+        initiator = RawGreenlet(initiator, worker[0])
+        value = initiator.switch()
+        self.assertTrue(seen)
+        self.assertEqual(value, 42)
+
+    def test_tuple_subclass(self):
+        # The point of this test is to see what happens when a custom
+        # tuple subclass is used as an object passed directly to the C
+        # function ``green_switch``; part of ``green_switch`` checks
+        # the ``len()`` of the ``args`` tuple, and that can call back
+        # into Python. Here, when it calls back into Python, we
+        # recursively enter ``green_switch`` again.
+
+        # This test is really only relevant on Python 2. The builtin
+        # `apply` function directly passes the given args tuple object
+        # to the underlying function, whereas the Python 3 version
+        # unpacks and repacks into an actual tuple. This could still
+        # happen using the C API on Python 3 though. We should write a
+        # builtin version of apply() ourself.
+        def _apply(func, a, k):
+            func(*a, **k)
+
+        class mytuple(tuple):
+            def __len__(self):
+                greenlet.getcurrent().switch()
+                return tuple.__len__(self)
+        args = mytuple()
+        kwargs = dict(a=42)
+        def switchapply():
+            _apply(greenlet.getcurrent().parent.switch, args, kwargs)
+        g = RawGreenlet(switchapply)
+        self.assertEqual(g.switch(), kwargs)
+
+    def test_abstract_subclasses(self):
+        AbstractSubclass = ABCMeta(
+            'AbstractSubclass',
+            (RawGreenlet,),
+            {'run': abstractmethod(lambda self: None)})
+
+        class BadSubclass(AbstractSubclass):
+            pass
+
+        class GoodSubclass(AbstractSubclass):
+            def run(self):
+                pass
+
+        GoodSubclass() # should not raise
+        self.assertRaises(TypeError, BadSubclass)
+
+    def test_implicit_parent_with_threads(self):
+        if not gc.isenabled():
+            return # cannot test with disabled gc
+        N = gc.get_threshold()[0]
+        if N < 50:
+            return # cannot test with such a small N
+        def attempt():
+            lock1 = threading.Lock()
+            lock1.acquire()
+            lock2 = threading.Lock()
+            lock2.acquire()
+            recycled = [False]
+            def another_thread():
+                lock1.acquire() # wait for gc
+                greenlet.getcurrent() # update ts_current
+                lock2.release() # release gc
+            t = threading.Thread(target=another_thread)
+            t.start()
+            class gc_callback(object):
+                def __del__(self):
+                    lock1.release()
+                    lock2.acquire()
+                    recycled[0] = True
+            class garbage(object):
+                def __init__(self):
+                    self.cycle = self
+                    self.callback = gc_callback()
+            l = []
+            x = range(N*2)
+            current = greenlet.getcurrent()
+            g = garbage()
+            for _ in x:
+                g = None # lose reference to garbage
+                if recycled[0]:
+                    # gc callback called prematurely
+                    t.join(10)
+                    return False
+                last = RawGreenlet()
+                if recycled[0]:
+                    break # yes! gc called in green_new
+                l.append(last) # increase allocation counter
+            else:
+                # gc callback not called when expected
+                gc.collect()
+                if recycled[0]:
+                    t.join(10)
+                return False
+            self.assertEqual(last.parent, current)
+            for g in l:
+                self.assertEqual(g.parent, current)
+            return True
+        for _ in range(5):
+            if attempt():
+                break
+
+    def test_issue_245_reference_counting_subclass_no_threads(self):
+        # https://github.com/python-greenlet/greenlet/issues/245
+        # Before the fix, this crashed pretty reliably on
+        # Python 3.10, at least on macOS; but much less reliably on other
+        # interpreters (memory layout must have changed).
+        # The threaded test crashed more reliably on more interpreters.
+        from greenlet import getcurrent
+        from greenlet import GreenletExit
+
+        class Greenlet(RawGreenlet):
+            pass
+
+        initial_refs = sys.getrefcount(Greenlet)
+        # This has to be an instance variable because
+        # Python 2 raises a SyntaxError if we delete a local
+        # variable referenced in an inner scope.
+        self.glets = [] # pylint:disable=attribute-defined-outside-init
+
+        def greenlet_main():
+            try:
+                getcurrent().parent.switch()
+            except GreenletExit:
+                self.glets.append(getcurrent())
+
+        # Before the
+        for _ in range(10):
+            Greenlet(greenlet_main).switch()
+
+        del self.glets
+        self.assertEqual(sys.getrefcount(Greenlet), initial_refs)
+
+    @unittest.skipIf(
+        PY313 and RUNNING_ON_MANYLINUX,
+        "The manylinux images appear to hang on this test on 3.13rc2"
+        # Or perhaps I just got tired of waiting for the 450s timeout.
+        # Still, it shouldn't take anywhere near that long. Does not reproduce in
+        # Ubuntu images, on macOS or Windows.
+    )
+    def test_issue_245_reference_counting_subclass_threads(self):
+        # https://github.com/python-greenlet/greenlet/issues/245
+        from threading import Thread
+        from threading import Event
+
+        from greenlet import getcurrent
+
+        class MyGreenlet(RawGreenlet):
+            pass
+
+        glets = []
+        ref_cleared = Event()
+
+        def greenlet_main():
+            getcurrent().parent.switch()
+
+        def thread_main(greenlet_running_event):
+            mine = MyGreenlet(greenlet_main)
+            glets.append(mine)
+            # The greenlets being deleted must be active
+            mine.switch()
+            # Don't keep any reference to it in this thread
+            del mine
+            # Let main know we published our greenlet.
+            greenlet_running_event.set()
+            # Wait for main to let us know the references are
+            # gone and the greenlet objects no longer reachable
+            ref_cleared.wait(10)
+            # The creating thread must call getcurrent() (or a few other
+            # greenlet APIs) because that's when the thread-local list of dead
+            # greenlets gets cleared.
+            getcurrent()
+
+        # We start with 3 references to the subclass:
+        # - This module
+        # - Its __mro__
+        # - The __subclassess__ attribute of greenlet
+        # - (If we call gc.get_referents(), we find four entries, including
+        #   some other tuple ``(greenlet)`` that I'm not sure about but must be part
+        #   of the machinery.)
+        #
+        # On Python 3.10 it's often enough to just run 3 threads; on Python 2.7,
+        # more threads are needed, and the results are still
+        # non-deterministic. Presumably the memory layouts are different
+        initial_refs = sys.getrefcount(MyGreenlet)
+        thread_ready_events = []
+        for _ in range(
+                initial_refs + 45
+        ):
+            event = Event()
+            thread = Thread(target=thread_main, args=(event,))
+            thread_ready_events.append(event)
+            thread.start()
+
+
+        for done_event in thread_ready_events:
+            done_event.wait(10)
+
+
+        del glets[:]
+        ref_cleared.set()
+        # Let any other thread run; it will crash the interpreter
+        # if not fixed (or silently corrupt memory and we possibly crash
+        # later).
+        self.wait_for_pending_cleanups()
+        self.assertEqual(sys.getrefcount(MyGreenlet), initial_refs)
+
+    def test_falling_off_end_switches_to_unstarted_parent_raises_error(self):
+        def no_args():
+            return 13
+
+        parent_never_started = RawGreenlet(no_args)
+
+        def leaf():
+            return 42
+
+        child = RawGreenlet(leaf, parent_never_started)
+
+        # Because the run function takes to arguments
+        with self.assertRaises(TypeError):
+            child.switch()
+
+    def test_falling_off_end_switches_to_unstarted_parent_works(self):
+        def one_arg(x):
+            return (x, 24)
+
+        parent_never_started = RawGreenlet(one_arg)
+
+        def leaf():
+            return 42
+
+        child = RawGreenlet(leaf, parent_never_started)
+
+        result = child.switch()
+        self.assertEqual(result, (42, 24))
+
+    def test_switch_to_dead_greenlet_with_unstarted_perverse_parent(self):
+        class Parent(RawGreenlet):
+            def __getattribute__(self, name):
+                if name == 'run':
+                    raise SomeError
+
+
+        parent_never_started = Parent()
+        seen = []
+        child = RawGreenlet(lambda: seen.append(42), parent_never_started)
+        # Because we automatically start the parent when the child is
+        # finished
+        with self.assertRaises(SomeError):
+            child.switch()
+
+        self.assertEqual(seen, [42])
+
+        with self.assertRaises(SomeError):
+            child.switch()
+        self.assertEqual(seen, [42])
+
+    def test_switch_to_dead_greenlet_reparent(self):
+        seen = []
+        parent_never_started = RawGreenlet(lambda: seen.append(24))
+        child = RawGreenlet(lambda: seen.append(42))
+
+        child.switch()
+        self.assertEqual(seen, [42])
+
+        child.parent = parent_never_started
+        # This actually is the same as switching to the parent.
+        result = child.switch()
+        self.assertIsNone(result)
+        self.assertEqual(seen, [42, 24])
+
+    def test_can_access_f_back_of_suspended_greenlet(self):
+        # This tests our frame rewriting to work around Python 3.12+ having
+        # some interpreter frames on the C stack. It will crash in the absence
+        # of that logic.
+        main = greenlet.getcurrent()
+
+        def outer():
+            inner()
+
+        def inner():
+            main.switch(sys._getframe(0))
+
+        hub = RawGreenlet(outer)
+        # start it
+        hub.switch()
+
+        # start another greenlet to make sure we aren't relying on
+        # anything in `hub` still being on the C stack
+        unrelated = RawGreenlet(lambda: None)
+        unrelated.switch()
+
+        # now it is suspended
+        self.assertIsNotNone(hub.gr_frame)
+        self.assertEqual(hub.gr_frame.f_code.co_name, "inner")
+        self.assertIsNotNone(hub.gr_frame.f_back)
+        self.assertEqual(hub.gr_frame.f_back.f_code.co_name, "outer")
+        # The next line is what would crash
+        self.assertIsNone(hub.gr_frame.f_back.f_back)
+
+    def test_get_stack_with_nested_c_calls(self):
+        from functools import partial
+        from . import _test_extension_cpp
+
+        def recurse(v):
+            if v > 0:
+                return v * _test_extension_cpp.test_call(partial(recurse, v - 1))
+            return greenlet.getcurrent().parent.switch()
+
+        gr = RawGreenlet(recurse)
+        gr.switch(5)
+        frame = gr.gr_frame
+        for i in range(5):
+            self.assertEqual(frame.f_locals["v"], i)
+            frame = frame.f_back
+        self.assertEqual(frame.f_locals["v"], 5)
+        self.assertIsNone(frame.f_back)
+        self.assertEqual(gr.switch(10), 1200)  # 1200 = 5! * 10
+
+    def test_frames_always_exposed(self):
+        # On Python 3.12 this will crash if we don't set the
+        # gr_frames_always_exposed attribute. More background:
+        # https://github.com/python-greenlet/greenlet/issues/388
+        main = greenlet.getcurrent()
+
+        def outer():
+            inner(sys._getframe(0))
+
+        def inner(frame):
+            main.switch(frame)
+
+        gr = RawGreenlet(outer)
+        frame = gr.switch()
+
+        # Do something else to clobber the part of the C stack used by `gr`,
+        # so we can't skate by on "it just happened to still be there"
+        unrelated = RawGreenlet(lambda: None)
+        unrelated.switch()
+
+        self.assertEqual(frame.f_code.co_name, "outer")
+        # The next line crashes on 3.12 if we haven't exposed the frames.
+        self.assertIsNone(frame.f_back)
+
+
+class TestGreenletSetParentErrors(TestCase):
+    def test_threaded_reparent(self):
+        data = {}
+        created_event = threading.Event()
+        done_event = threading.Event()
+
+        def run():
+            data['g'] = RawGreenlet(lambda: None)
+            created_event.set()
+            done_event.wait(10)
+
+        def blank():
+            greenlet.getcurrent().parent.switch()
+
+        thread = threading.Thread(target=run)
+        thread.start()
+        created_event.wait(10)
+        g = RawGreenlet(blank)
+        g.switch()
+        with self.assertRaises(ValueError) as exc:
+            g.parent = data['g']
+        done_event.set()
+        thread.join(10)
+
+        self.assertEqual(str(exc.exception), "parent cannot be on a different thread")
+
+    def test_unexpected_reparenting(self):
+        another = []
+        def worker():
+            g = RawGreenlet(lambda: None)
+            another.append(g)
+            g.switch()
+        t = threading.Thread(target=worker)
+        t.start()
+        t.join(10)
+        # The first time we switch (running g_initialstub(), which is
+        # when we look up the run attribute) we attempt to change the
+        # parent to one from another thread (which also happens to be
+        # dead). ``g_initialstub()`` should detect this and raise a
+        # greenlet error.
+        #
+        # EXCEPT: With the fix for #252, this is actually detected
+        # sooner, when setting the parent itself. Prior to that fix,
+        # the main greenlet from the background thread kept a valid
+        # value for ``run_info``, and appeared to be a valid parent
+        # until we actually started the greenlet. But now that it's
+        # cleared, this test is catching whether ``green_setparent``
+        # can detect the dead thread.
+        #
+        # Further refactoring once again changes this back to a greenlet.error
+        #
+        # We need to wait for the cleanup to happen, but we're
+        # deliberately leaking a main greenlet here.
+        self.wait_for_pending_cleanups(initial_main_greenlets=self.main_greenlets_before_test + 1)
+
+        class convoluted(RawGreenlet):
+            def __getattribute__(self, name):
+                if name == 'run':
+                    self.parent = another[0] # pylint:disable=attribute-defined-outside-init
+                return RawGreenlet.__getattribute__(self, name)
+        g = convoluted(lambda: None)
+        with self.assertRaises(greenlet.error) as exc:
+            g.switch()
+        self.assertEqual(str(exc.exception),
+                         "cannot switch to a different thread (which happens to have exited)")
+        del another[:]
+
+    def test_unexpected_reparenting_thread_running(self):
+        # Like ``test_unexpected_reparenting``, except the background thread is
+        # actually still alive.
+        another = []
+        switched_to_greenlet = threading.Event()
+        keep_main_alive = threading.Event()
+        def worker():
+            g = RawGreenlet(lambda: None)
+            another.append(g)
+            g.switch()
+            switched_to_greenlet.set()
+            keep_main_alive.wait(10)
+        class convoluted(RawGreenlet):
+            def __getattribute__(self, name):
+                if name == 'run':
+                    self.parent = another[0] # pylint:disable=attribute-defined-outside-init
+                return RawGreenlet.__getattribute__(self, name)
+
+        t = threading.Thread(target=worker)
+        t.start()
+
+        switched_to_greenlet.wait(10)
+        try:
+            g = convoluted(lambda: None)
+
+            with self.assertRaises(greenlet.error) as exc:
+                g.switch()
+            self.assertIn("Cannot switch to a different thread", str(exc.exception))
+            self.assertIn("Expected", str(exc.exception))
+            self.assertIn("Current", str(exc.exception))
+        finally:
+            keep_main_alive.set()
+            t.join(10)
+            # XXX: Should handle this automatically.
+            del another[:]
+
+    def test_cannot_delete_parent(self):
+        worker = RawGreenlet(lambda: None)
+        self.assertIs(worker.parent, greenlet.getcurrent())
+
+        with self.assertRaises(AttributeError) as exc:
+            del worker.parent
+        self.assertEqual(str(exc.exception), "can't delete attribute")
+
+    def test_cannot_delete_parent_of_main(self):
+        with self.assertRaises(AttributeError) as exc:
+            del greenlet.getcurrent().parent
+        self.assertEqual(str(exc.exception), "can't delete attribute")
+
+
+    def test_main_greenlet_parent_is_none(self):
+        # assuming we're in a main greenlet here.
+        self.assertIsNone(greenlet.getcurrent().parent)
+
+    def test_set_parent_wrong_types(self):
+        def bg():
+            # Go back to main.
+            greenlet.getcurrent().parent.switch()
+
+        def check(glet):
+            for p in None, 1, self, "42":
+                with self.assertRaises(TypeError) as exc:
+                    glet.parent = p
+
+                self.assertEqual(
+                    str(exc.exception),
+                    "GreenletChecker: Expected any type of greenlet, not " + type(p).__name__)
+
+        # First, not running
+        g = RawGreenlet(bg)
+        self.assertFalse(g)
+        check(g)
+
+        # Then when running.
+        g.switch()
+        self.assertTrue(g)
+        check(g)
+
+        # Let it finish
+        g.switch()
+
+
+    def test_trivial_cycle(self):
+        glet = RawGreenlet(lambda: None)
+        with self.assertRaises(ValueError) as exc:
+            glet.parent = glet
+        self.assertEqual(str(exc.exception), "cyclic parent chain")
+
+    def test_trivial_cycle_main(self):
+        # This used to produce a ValueError, but we catch it earlier than that now.
+        with self.assertRaises(AttributeError) as exc:
+            greenlet.getcurrent().parent = greenlet.getcurrent()
+        self.assertEqual(str(exc.exception), "cannot set the parent of a main greenlet")
+
+    def test_deeper_cycle(self):
+        g1 = RawGreenlet(lambda: None)
+        g2 = RawGreenlet(lambda: None)
+        g3 = RawGreenlet(lambda: None)
+
+        g1.parent = g2
+        g2.parent = g3
+        with self.assertRaises(ValueError) as exc:
+            g3.parent = g1
+        self.assertEqual(str(exc.exception), "cyclic parent chain")
+
+
+class TestRepr(TestCase):
+
+    def assertEndsWith(self, got, suffix):
+        self.assertTrue(got.endswith(suffix), (got, suffix))
+
+    def test_main_while_running(self):
+        r = repr(greenlet.getcurrent())
+        self.assertEndsWith(r, " current active started main>")
+
+    def test_main_in_background(self):
+        main = greenlet.getcurrent()
+        def run():
+            return repr(main)
+
+        g = RawGreenlet(run)
+        r = g.switch()
+        self.assertEndsWith(r, ' suspended active started main>')
+
+    def test_initial(self):
+        r = repr(RawGreenlet())
+        self.assertEndsWith(r, ' pending>')
+
+    def test_main_from_other_thread(self):
+        main = greenlet.getcurrent()
+
+        class T(threading.Thread):
+            original_main = thread_main = None
+            main_glet = None
+            def run(self):
+                self.original_main = repr(main)
+                self.main_glet = greenlet.getcurrent()
+                self.thread_main = repr(self.main_glet)
+
+        t = T()
+        t.start()
+        t.join(10)
+
+        self.assertEndsWith(t.original_main, ' suspended active started main>')
+        self.assertEndsWith(t.thread_main, ' current active started main>')
+        # give the machinery time to notice the death of the thread,
+        # and clean it up. Note that we don't use
+        # ``expect_greenlet_leak`` or wait_for_pending_cleanups,
+        # because at this point we know we have an extra greenlet
+        # still reachable.
+        for _ in range(3):
+            time.sleep(0.001)
+
+        # In the past, main greenlets, even from dead threads, never
+        # really appear dead. We have fixed that, and we also report
+        # that the thread is dead in the repr. (Do this multiple times
+        # to make sure that we don't self-modify and forget our state
+        # in the C++ code).
+        for _ in range(3):
+            self.assertTrue(t.main_glet.dead)
+            r = repr(t.main_glet)
+            self.assertEndsWith(r, ' (thread exited) dead>')
+
+    def test_dead(self):
+        g = RawGreenlet(lambda: None)
+        g.switch()
+        self.assertEndsWith(repr(g), ' dead>')
+        self.assertNotIn('suspended', repr(g))
+        self.assertNotIn('started', repr(g))
+        self.assertNotIn('active', repr(g))
+
+    def test_formatting_produces_native_str(self):
+        # https://github.com/python-greenlet/greenlet/issues/218
+        # %s formatting on Python 2 was producing unicode, not str.
+
+        g_dead = RawGreenlet(lambda: None)
+        g_not_started = RawGreenlet(lambda: None)
+        g_cur = greenlet.getcurrent()
+
+        for g in g_dead, g_not_started, g_cur:
+
+            self.assertIsInstance(
+                '%s' % (g,),
+                str
+            )
+            self.assertIsInstance(
+                '%r' % (g,),
+                str,
+            )
+
+
+class TestMainGreenlet(TestCase):
+    # Tests some implementation details, and relies on some
+    # implementation details.
+
+    def _check_current_is_main(self):
+        # implementation detail
+        assert 'main' in repr(greenlet.getcurrent())
+
+        t = type(greenlet.getcurrent())
+        assert 'main' not in repr(t)
+        return t
+
+    def test_main_greenlet_type_can_be_subclassed(self):
+        main_type = self._check_current_is_main()
+        subclass = type('subclass', (main_type,), {})
+        self.assertIsNotNone(subclass)
+
+    def test_main_greenlet_is_greenlet(self):
+        self._check_current_is_main()
+        self.assertIsInstance(greenlet.getcurrent(), RawGreenlet)
+
+
+
+class TestBrokenGreenlets(TestCase):
+    # Tests for things that used to, or still do, terminate the interpreter.
+    # This often means doing unsavory things.
+
+    def test_failed_to_initialstub(self):
+        def func():
+            raise AssertionError("Never get here")
+
+
+        g = greenlet._greenlet.UnswitchableGreenlet(func)
+        g.force_switch_error = True
+
+        with self.assertRaisesRegex(SystemError,
+                                    "Failed to switch stacks into a greenlet for the first time."):
+            g.switch()
+
+    def test_failed_to_switch_into_running(self):
+        runs = []
+        def func():
+            runs.append(1)
+            greenlet.getcurrent().parent.switch()
+            runs.append(2)
+            greenlet.getcurrent().parent.switch()
+            runs.append(3) # pragma: no cover
+
+        g = greenlet._greenlet.UnswitchableGreenlet(func)
+        g.switch()
+        self.assertEqual(runs, [1])
+        g.switch()
+        self.assertEqual(runs, [1, 2])
+        g.force_switch_error = True
+
+        with self.assertRaisesRegex(SystemError,
+                                    "Failed to switch stacks into a running greenlet."):
+            g.switch()
+
+        # If we stopped here, we would fail the leakcheck, because we've left
+        # the ``inner_bootstrap()`` C frame and its descendents hanging around,
+        # which have a bunch of Python references. They'll never get cleaned up
+        # if we don't let the greenlet finish.
+        g.force_switch_error = False
+        g.switch()
+        self.assertEqual(runs, [1, 2, 3])
+
+    def test_failed_to_slp_switch_into_running(self):
+        ex = self.assertScriptRaises('fail_slp_switch.py')
+
+        self.assertIn('fail_slp_switch is running', ex.output)
+        self.assertIn(ex.returncode, self.get_expected_returncodes_for_aborted_process())
+
+    def test_reentrant_switch_two_greenlets(self):
+        # Before we started capturing the arguments in g_switch_finish, this could crash.
+        output = self.run_script('fail_switch_two_greenlets.py')
+        self.assertIn('In g1_run', output)
+        self.assertIn('TRACE', output)
+        self.assertIn('LEAVE TRACE', output)
+        self.assertIn('Falling off end of main', output)
+        self.assertIn('Falling off end of g1_run', output)
+        self.assertIn('Falling off end of g2', output)
+
+    def test_reentrant_switch_three_greenlets(self):
+        # On debug builds of greenlet, this used to crash with an assertion error;
+        # on non-debug versions, it ran fine (which it should not do!).
+        # Now it always crashes correctly with a TypeError
+        ex = self.assertScriptRaises('fail_switch_three_greenlets.py', exitcodes=(1,))
+
+        self.assertIn('TypeError', ex.output)
+        self.assertIn('positional arguments', ex.output)
+
+    def test_reentrant_switch_three_greenlets2(self):
+        # This actually passed on debug and non-debug builds. It
+        # should probably have been triggering some debug assertions
+        # but it didn't.
+        #
+        # I think the fixes for the above test also kicked in here.
+        output = self.run_script('fail_switch_three_greenlets2.py')
+        self.assertIn(
+            "RESULTS: [('trace', 'switch'), "
+            "('trace', 'switch'), ('g2 arg', 'g2 from tracefunc'), "
+            "('trace', 'switch'), ('main g1', 'from g2_run'), ('trace', 'switch'), "
+            "('g1 arg', 'g1 from main'), ('trace', 'switch'), ('main g2', 'from g1_run'), "
+            "('trace', 'switch'), ('g1 from parent', 'g1 from main 2'), ('trace', 'switch'), "
+            "('main g1.2', 'g1 done'), ('trace', 'switch'), ('g2 from parent', ()), "
+            "('trace', 'switch'), ('main g2.2', 'g2 done')]",
+            output
+        )
+
+    def test_reentrant_switch_GreenletAlreadyStartedInPython(self):
+        output = self.run_script('fail_initialstub_already_started.py')
+
+        self.assertIn(
+            "RESULTS: ['Begin C', 'Switch to b from B.__getattribute__ in C', "
+            "('Begin B', ()), '_B_run switching to main', ('main from c', 'From B'), "
+            "'B.__getattribute__ back from main in C', ('Begin A', (None,)), "
+            "('A dead?', True, 'B dead?', True, 'C dead?', False), "
+            "'C done', ('main from c.2', None)]",
+            output
+        )
+
+    def test_reentrant_switch_run_callable_has_del(self):
+        output = self.run_script('fail_clearing_run_switches.py')
+        self.assertIn(
+             "RESULTS ["
+            "('G.__getattribute__', 'run'), ('RunCallable', '__del__'), "
+            "('main: g.switch()', 'from RunCallable'), ('run_func', 'enter')"
+            "]",
+            output
+        )
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py
new file mode 100644
index 00000000..c1fc1374
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_greenlet_trash.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for greenlets interacting with the CPython trash can API.
+
+The CPython trash can API is not designed to be re-entered from a
+single thread. But this can happen using greenlets, if something
+during the object deallocation process switches greenlets, and this second
+greenlet then causes the trash can to get entered again. Here, we do this
+very explicitly, but in other cases (like gevent) it could be arbitrarily more
+complicated: for example, a weakref callback might try to acquire a lock that's
+already held by another greenlet; that would allow a greenlet switch to occur.
+
+See https://github.com/gevent/gevent/issues/1909
+
+This test is fragile and relies on details of the CPython
+implementation (like most of the rest of this package):
+
+    - We enter the trashcan and deferred deallocation after
+      ``_PyTrash_UNWIND_LEVEL`` calls. This constant, defined in
+      CPython's object.c, is generally 50. That's basically how many objects are required to
+      get us into the deferred deallocation situation.
+
+    - The test fails by hitting an ``assert()`` in object.c; if the
+      build didn't enable assert, then we don't catch this.
+
+    - If the test fails in that way, the interpreter crashes.
+"""
+from __future__ import print_function, absolute_import, division
+
+import unittest
+
+
+class TestTrashCanReEnter(unittest.TestCase):
+
+    def test_it(self):
+        try:
+            # pylint:disable-next=no-name-in-module
+            from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=unused-import
+        except ImportError:
+            import sys
+            # Python 3.13 has not "trash delete nesting" anymore (but "delete later")
+            assert sys.version_info[:2] >= (3, 13)
+            self.skipTest("get_tstate_trash_delete_nesting is not available.")
+
+        # Try several times to trigger it, because it isn't 100%
+        # reliable.
+        for _ in range(10):
+            self.check_it()
+
+    def check_it(self): # pylint:disable=too-many-statements
+        import greenlet
+        from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=no-name-in-module
+        main = greenlet.getcurrent()
+
+        assert get_tstate_trash_delete_nesting() == 0
+
+        # We expect to be in deferred deallocation after this many
+        # deallocations have occurred. TODO: I wish we had a better way to do
+        # this --- that was before get_tstate_trash_delete_nesting; perhaps
+        # we can use that API to do better?
+        TRASH_UNWIND_LEVEL = 50
+        # How many objects to put in a container; it's the container that
+        # queues objects for deferred deallocation.
+        OBJECTS_PER_CONTAINER = 500
+
+        class Dealloc: # define the class here because we alter class variables each time we run.
+            """
+            An object with a ``__del__`` method. When it starts getting deallocated
+            from a deferred trash can run, it switches greenlets, allocates more objects
+            which then also go in the trash can. If we don't save state appropriately,
+            nesting gets out of order and we can crash the interpreter.
+            """
+
+            #: Has our deallocation actually run and switched greenlets?
+            #: When it does, this will be set to the current greenlet. This should
+            #: be happening in the main greenlet, so we check that down below.
+            SPAWNED = False
+
+            #: Has the background greenlet run?
+            BG_RAN = False
+
+            BG_GLET = None
+
+            #: How many of these things have ever been allocated.
+            CREATED = 0
+
+            #: How many of these things have ever been deallocated.
+            DESTROYED = 0
+
+            #: How many were destroyed not in the main greenlet. There should always
+            #: be some.
+            #: If the test is broken or things change in the trashcan implementation,
+            #: this may not be correct.
+            DESTROYED_BG = 0
+
+            def __init__(self, sequence_number):
+                """
+                :param sequence_number: The ordinal of this object during
+                   one particular creation run. This is used to detect (guess, really)
+                   when we have entered the trash can's deferred deallocation.
+                """
+                self.i = sequence_number
+                Dealloc.CREATED += 1
+
+            def __del__(self):
+                if self.i == TRASH_UNWIND_LEVEL and not self.SPAWNED:
+                    Dealloc.SPAWNED = greenlet.getcurrent()
+                    other = Dealloc.BG_GLET = greenlet.greenlet(background_greenlet)
+                    x = other.switch()
+                    assert x == 42
+                    # It's important that we don't switch back to the greenlet,
+                    # we leave it hanging there in an incomplete state. But we don't let it
+                    # get collected, either. If we complete it now, while we're still
+                    # in the scope of the initial trash can, things work out and we
+                    # don't see the problem. We need this greenlet to complete
+                    # at some point in the future, after we've exited this trash can invocation.
+                    del other
+                elif self.i == 40 and greenlet.getcurrent() is not main:
+                    Dealloc.BG_RAN = True
+                    try:
+                        main.switch(42)
+                    except greenlet.GreenletExit as ex:
+                        # We expect this; all references to us go away
+                        # while we're still running, and we need to finish deleting
+                        # ourself.
+                        Dealloc.BG_RAN = type(ex)
+                        del ex
+
+                # Record the fact that we're dead last of all. This ensures that
+                # we actually get returned too.
+                Dealloc.DESTROYED += 1
+                if greenlet.getcurrent() is not main:
+                    Dealloc.DESTROYED_BG += 1
+
+
+        def background_greenlet():
+            # We direct through a second function, instead of
+            # directly calling ``make_some()``, so that we have complete
+            # control over when these objects are destroyed: we need them
+            # to be destroyed in the context of the background greenlet
+            t = make_some()
+            del t # Triggere deletion.
+
+        def make_some():
+            t = ()
+            i = OBJECTS_PER_CONTAINER
+            while i:
+                # Nest the tuples; it's the recursion that gets us
+                # into trash.
+                t = (Dealloc(i), t)
+                i -= 1
+            return t
+
+
+        some = make_some()
+        self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER)
+        self.assertEqual(Dealloc.DESTROYED, 0)
+
+        # If we're going to crash, it should be on the following line.
+        # We only crash if ``assert()`` is enabled, of course.
+        del some
+
+        # For non-debug builds of CPython, we won't crash. The best we can do is check
+        # the nesting level explicitly.
+        self.assertEqual(0, get_tstate_trash_delete_nesting())
+
+        # Discard this, raising GreenletExit into where it is waiting.
+        Dealloc.BG_GLET = None
+        # The same nesting level maintains.
+        self.assertEqual(0, get_tstate_trash_delete_nesting())
+
+        # We definitely cleaned some up in the background
+        self.assertGreater(Dealloc.DESTROYED_BG, 0)
+
+        # Make sure all the cleanups happened.
+        self.assertIs(Dealloc.SPAWNED, main)
+        self.assertTrue(Dealloc.BG_RAN)
+        self.assertEqual(Dealloc.BG_RAN, greenlet.GreenletExit)
+        self.assertEqual(Dealloc.CREATED, Dealloc.DESTROYED )
+        self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER * 2)
+
+        import gc
+        gc.collect()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_leaks.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_leaks.py
new file mode 100644
index 00000000..ed1fa717
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_leaks.py
@@ -0,0 +1,443 @@
+# -*- coding: utf-8 -*-
+"""
+Testing scenarios that may have leaked.
+"""
+from __future__ import print_function, absolute_import, division
+
+import sys
+import gc
+
+import time
+import weakref
+import threading
+
+
+import greenlet
+from . import TestCase
+from .leakcheck import fails_leakcheck
+from .leakcheck import ignores_leakcheck
+from .leakcheck import RUNNING_ON_MANYLINUX
+
+# pylint:disable=protected-access
+
+assert greenlet.GREENLET_USE_GC # Option to disable this was removed in 1.0
+
+class HasFinalizerTracksInstances(object):
+    EXTANT_INSTANCES = set()
+    def __init__(self, msg):
+        self.msg = sys.intern(msg)
+        self.EXTANT_INSTANCES.add(id(self))
+    def __del__(self):
+        self.EXTANT_INSTANCES.remove(id(self))
+    def __repr__(self):
+        return "<HasFinalizerTracksInstances at 0x%x %r>" % (
+            id(self), self.msg
+        )
+    @classmethod
+    def reset(cls):
+        cls.EXTANT_INSTANCES.clear()
+
+
+class TestLeaks(TestCase):
+
+    def test_arg_refs(self):
+        args = ('a', 'b', 'c')
+        refcount_before = sys.getrefcount(args)
+        # pylint:disable=unnecessary-lambda
+        g = greenlet.greenlet(
+            lambda *args: greenlet.getcurrent().parent.switch(*args))
+        for _ in range(100):
+            g.switch(*args)
+        self.assertEqual(sys.getrefcount(args), refcount_before)
+
+    def test_kwarg_refs(self):
+        kwargs = {}
+        # pylint:disable=unnecessary-lambda
+        g = greenlet.greenlet(
+            lambda **kwargs: greenlet.getcurrent().parent.switch(**kwargs))
+        for _ in range(100):
+            g.switch(**kwargs)
+        self.assertEqual(sys.getrefcount(kwargs), 2)
+
+
+    @staticmethod
+    def __recycle_threads():
+        # By introducing a thread that does sleep we allow other threads,
+        # that have triggered their __block condition, but did not have a
+        # chance to deallocate their thread state yet, to finally do so.
+        # The way it works is by requiring a GIL switch (different thread),
+        # which does a GIL release (sleep), which might do a GIL switch
+        # to finished threads and allow them to clean up.
+        def worker():
+            time.sleep(0.001)
+        t = threading.Thread(target=worker)
+        t.start()
+        time.sleep(0.001)
+        t.join(10)
+
+    def test_threaded_leak(self):
+        gg = []
+        def worker():
+            # only main greenlet present
+            gg.append(weakref.ref(greenlet.getcurrent()))
+        for _ in range(2):
+            t = threading.Thread(target=worker)
+            t.start()
+            t.join(10)
+            del t
+        greenlet.getcurrent() # update ts_current
+        self.__recycle_threads()
+        greenlet.getcurrent() # update ts_current
+        gc.collect()
+        greenlet.getcurrent() # update ts_current
+        for g in gg:
+            self.assertIsNone(g())
+
+    def test_threaded_adv_leak(self):
+        gg = []
+        def worker():
+            # main and additional *finished* greenlets
+            ll = greenlet.getcurrent().ll = []
+            def additional():
+                ll.append(greenlet.getcurrent())
+            for _ in range(2):
+                greenlet.greenlet(additional).switch()
+            gg.append(weakref.ref(greenlet.getcurrent()))
+        for _ in range(2):
+            t = threading.Thread(target=worker)
+            t.start()
+            t.join(10)
+            del t
+        greenlet.getcurrent() # update ts_current
+        self.__recycle_threads()
+        greenlet.getcurrent() # update ts_current
+        gc.collect()
+        greenlet.getcurrent() # update ts_current
+        for g in gg:
+            self.assertIsNone(g())
+
+    def assertClocksUsed(self):
+        used = greenlet._greenlet.get_clocks_used_doing_optional_cleanup()
+        self.assertGreaterEqual(used, 0)
+        # we don't lose the value
+        greenlet._greenlet.enable_optional_cleanup(True)
+        used2 = greenlet._greenlet.get_clocks_used_doing_optional_cleanup()
+        self.assertEqual(used, used2)
+        self.assertGreater(greenlet._greenlet.CLOCKS_PER_SEC, 1)
+
+    def _check_issue251(self,
+                        manually_collect_background=True,
+                        explicit_reference_to_switch=False):
+        # See https://github.com/python-greenlet/greenlet/issues/251
+        # Killing a greenlet (probably not the main one)
+        # in one thread from another thread would
+        # result in leaking a list (the ts_delkey list).
+        # We no longer use lists to hold that stuff, though.
+
+        # For the test to be valid, even empty lists have to be tracked by the
+        # GC
+
+        assert gc.is_tracked([])
+        HasFinalizerTracksInstances.reset()
+        greenlet.getcurrent()
+        greenlets_before = self.count_objects(greenlet.greenlet, exact_kind=False)
+
+        background_glet_running = threading.Event()
+        background_glet_killed = threading.Event()
+        background_greenlets = []
+
+        # XXX: Switching this to a greenlet subclass that overrides
+        # run results in all callers failing the leaktest; that
+        # greenlet instance is leaked. There's a bound method for
+        # run() living on the stack of the greenlet in g_initialstub,
+        # and since we don't manually switch back to the background
+        # greenlet to let it "fall off the end" and exit the
+        # g_initialstub function, it never gets cleaned up. Making the
+        # garbage collector aware of this bound method (making it an
+        # attribute of the greenlet structure and traversing into it)
+        # doesn't help, for some reason.
+        def background_greenlet():
+            # Throw control back to the main greenlet.
+            jd = HasFinalizerTracksInstances("DELETING STACK OBJECT")
+            greenlet._greenlet.set_thread_local(
+                'test_leaks_key',
+                HasFinalizerTracksInstances("DELETING THREAD STATE"))
+            # Explicitly keeping 'switch' in a local variable
+            # breaks this test in all versions
+            if explicit_reference_to_switch:
+                s = greenlet.getcurrent().parent.switch
+                s([jd])
+            else:
+                greenlet.getcurrent().parent.switch([jd])
+
+        bg_main_wrefs = []
+
+        def background_thread():
+            glet = greenlet.greenlet(background_greenlet)
+            bg_main_wrefs.append(weakref.ref(glet.parent))
+
+            background_greenlets.append(glet)
+            glet.switch() # Be sure it's active.
+            # Control is ours again.
+            del glet # Delete one reference from the thread it runs in.
+            background_glet_running.set()
+            background_glet_killed.wait(10)
+
+            # To trigger the background collection of the dead
+            # greenlet, thus clearing out the contents of the list, we
+            # need to run some APIs. See issue 252.
+            if manually_collect_background:
+                greenlet.getcurrent()
+
+
+        t = threading.Thread(target=background_thread)
+        t.start()
+        background_glet_running.wait(10)
+        greenlet.getcurrent()
+        lists_before = self.count_objects(list, exact_kind=True)
+
+        assert len(background_greenlets) == 1
+        self.assertFalse(background_greenlets[0].dead)
+        # Delete the last reference to the background greenlet
+        # from a different thread. This puts it in the background thread's
+        # ts_delkey list.
+        del background_greenlets[:]
+        background_glet_killed.set()
+
+        # Now wait for the background thread to die.
+        t.join(10)
+        del t
+        # As part of the fix for 252, we need to cycle the ceval.c
+        # interpreter loop to be sure it has had a chance to process
+        # the pending call.
+        self.wait_for_pending_cleanups()
+
+        lists_after = self.count_objects(list, exact_kind=True)
+        greenlets_after = self.count_objects(greenlet.greenlet, exact_kind=False)
+
+        # On 2.7, we observe that lists_after is smaller than
+        # lists_before. No idea what lists got cleaned up. All the
+        # Python 3 versions match exactly.
+        self.assertLessEqual(lists_after, lists_before)
+        # On versions after 3.6, we've successfully cleaned up the
+        # greenlet references thanks to the internal "vectorcall"
+        # protocol; prior to that, there is a reference path through
+        # the ``greenlet.switch`` method still on the stack that we
+        # can't reach to clean up. The C code goes through terrific
+        # lengths to clean that up.
+        if not explicit_reference_to_switch \
+           and greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None:
+            # If cleanup was disabled, though, we may not find it.
+            self.assertEqual(greenlets_after, greenlets_before)
+            if manually_collect_background:
+                # TODO: Figure out how to make this work!
+                # The one on the stack is still leaking somehow
+                # in the non-manually-collect state.
+                self.assertEqual(HasFinalizerTracksInstances.EXTANT_INSTANCES, set())
+        else:
+            # The explicit reference prevents us from collecting it
+            # and it isn't always found by the GC either for some
+            # reason. The entire frame is leaked somehow, on some
+            # platforms (e.g., MacPorts builds of Python (all
+            # versions!)), but not on other platforms (the linux and
+            # windows builds on GitHub actions and Appveyor). So we'd
+            # like to write a test that proves that the main greenlet
+            # sticks around, and we can on my machine (macOS 11.6,
+            # MacPorts builds of everything) but we can't write that
+            # same test on other platforms. However, hopefully iteration
+            # done by leakcheck will find it.
+            pass
+
+        if greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None:
+            self.assertClocksUsed()
+
+    def test_issue251_killing_cross_thread_leaks_list(self):
+        self._check_issue251()
+
+    def test_issue251_with_cleanup_disabled(self):
+        greenlet._greenlet.enable_optional_cleanup(False)
+        try:
+            self._check_issue251()
+        finally:
+            greenlet._greenlet.enable_optional_cleanup(True)
+
+    @fails_leakcheck
+    def test_issue251_issue252_need_to_collect_in_background(self):
+        # Between greenlet 1.1.2 and the next version, this was still
+        # failing because the leak of the list still exists when we
+        # don't call a greenlet API before exiting the thread. The
+        # proximate cause is that neither of the two greenlets from
+        # the background thread are actually being destroyed, even
+        # though the GC is in fact visiting both objects. It's not
+        # clear where that leak is? For some reason the thread-local
+        # dict holding it isn't being cleaned up.
+        #
+        # The leak, I think, is in the CPYthon internal function that
+        # calls into green_switch(). The argument tuple is still on
+        # the C stack somewhere and can't be reached? That doesn't
+        # make sense, because the tuple should be collectable when
+        # this object goes away.
+        #
+        # Note that this test sometimes spuriously passes on Linux,
+        # for some reason, but I've never seen it pass on macOS.
+        self._check_issue251(manually_collect_background=False)
+
+    @fails_leakcheck
+    def test_issue251_issue252_need_to_collect_in_background_cleanup_disabled(self):
+        self.expect_greenlet_leak = True
+        greenlet._greenlet.enable_optional_cleanup(False)
+        try:
+            self._check_issue251(manually_collect_background=False)
+        finally:
+            greenlet._greenlet.enable_optional_cleanup(True)
+
+    @fails_leakcheck
+    def test_issue251_issue252_explicit_reference_not_collectable(self):
+        self._check_issue251(
+            manually_collect_background=False,
+            explicit_reference_to_switch=True)
+
+    UNTRACK_ATTEMPTS = 100
+
+    def _only_test_some_versions(self):
+        # We're only looking for this problem specifically on 3.11,
+        # and this set of tests is relatively fragile, depending on
+        # OS and memory management details. So we want to run it on 3.11+
+        # (obviously) but not every older 3.x version in order to reduce
+        # false negatives. At the moment, those false results seem to have
+        # resolved, so we are actually running this on 3.8+
+        assert sys.version_info[0] >= 3
+        if sys.version_info[:2] < (3, 8):
+            self.skipTest('Only observed on 3.11')
+        if RUNNING_ON_MANYLINUX:
+            self.skipTest("Slow and not worth repeating here")
+
+    @ignores_leakcheck
+    # Because we're just trying to track raw memory, not objects, and running
+    # the leakcheck makes an already slow test slower.
+    def test_untracked_memory_doesnt_increase(self):
+        # See https://github.com/gevent/gevent/issues/1924
+        # and https://github.com/python-greenlet/greenlet/issues/328
+        self._only_test_some_versions()
+        def f():
+            return 1
+
+        ITER = 10000
+        def run_it():
+            for _ in range(ITER):
+                greenlet.greenlet(f).switch()
+
+        # Establish baseline
+        for _ in range(3):
+            run_it()
+
+        # uss: (Linux, macOS, Windows): aka "Unique Set Size", this is
+        # the memory which is unique to a process and which would be
+        # freed if the process was terminated right now.
+        uss_before = self.get_process_uss()
+
+        for count in range(self.UNTRACK_ATTEMPTS):
+            uss_before = max(uss_before, self.get_process_uss())
+            run_it()
+
+            uss_after = self.get_process_uss()
+            if uss_after <= uss_before and count > 1:
+                break
+
+        self.assertLessEqual(uss_after, uss_before)
+
+    def _check_untracked_memory_thread(self, deallocate_in_thread=True):
+        self._only_test_some_versions()
+        # Like the above test, but what if there are a bunch of
+        # unfinished greenlets in a thread that dies?
+        # Does it matter if we deallocate in the thread or not?
+        EXIT_COUNT = [0]
+
+        def f():
+            try:
+                greenlet.getcurrent().parent.switch()
+            except greenlet.GreenletExit:
+                EXIT_COUNT[0] += 1
+                raise
+            return 1
+
+        ITER = 10000
+        def run_it():
+            glets = []
+            for _ in range(ITER):
+                # Greenlet starts, switches back to us.
+                # We keep a strong reference to the greenlet though so it doesn't
+                # get a GreenletExit exception.
+                g = greenlet.greenlet(f)
+                glets.append(g)
+                g.switch()
+
+            return glets
+
+        test = self
+
+        class ThreadFunc:
+            uss_before = uss_after = 0
+            glets = ()
+            ITER = 2
+            def __call__(self):
+                self.uss_before = test.get_process_uss()
+
+                for _ in range(self.ITER):
+                    self.glets += tuple(run_it())
+
+                for g in self.glets:
+                    test.assertIn('suspended active', str(g))
+                # Drop them.
+                if deallocate_in_thread:
+                    self.glets = ()
+                self.uss_after = test.get_process_uss()
+
+        # Establish baseline
+        uss_before = uss_after = None
+        for count in range(self.UNTRACK_ATTEMPTS):
+            EXIT_COUNT[0] = 0
+            thread_func = ThreadFunc()
+            t = threading.Thread(target=thread_func)
+            t.start()
+            t.join(30)
+            self.assertFalse(t.is_alive())
+
+            if uss_before is None:
+                uss_before = thread_func.uss_before
+
+            uss_before = max(uss_before, thread_func.uss_before)
+            if deallocate_in_thread:
+                self.assertEqual(thread_func.glets, ())
+                self.assertEqual(EXIT_COUNT[0], ITER * thread_func.ITER)
+
+            del thread_func # Deallocate the greenlets; but this won't raise into them
+            del t
+            if not deallocate_in_thread:
+                self.assertEqual(EXIT_COUNT[0], 0)
+            if deallocate_in_thread:
+                self.wait_for_pending_cleanups()
+
+            uss_after = self.get_process_uss()
+            # See if we achieve a non-growth state at some point. Break when we do.
+            if uss_after <= uss_before and count > 1:
+                break
+
+        self.wait_for_pending_cleanups()
+        uss_after = self.get_process_uss()
+        self.assertLessEqual(uss_after, uss_before, "after attempts %d" % (count,))
+
+    @ignores_leakcheck
+    # Because we're just trying to track raw memory, not objects, and running
+    # the leakcheck makes an already slow test slower.
+    def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_thread(self):
+        self._check_untracked_memory_thread(deallocate_in_thread=True)
+
+    @ignores_leakcheck
+    # Because the main greenlets from the background threads do not exit in a timely fashion,
+    # we fail the object-based leakchecks.
+    def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_main(self):
+        self._check_untracked_memory_thread(deallocate_in_thread=False)
+
+if __name__ == '__main__':
+    __import__('unittest').main()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_stack_saved.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_stack_saved.py
new file mode 100644
index 00000000..b362bf95
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_stack_saved.py
@@ -0,0 +1,19 @@
+import greenlet
+from . import TestCase
+
+
+class Test(TestCase):
+
+    def test_stack_saved(self):
+        main = greenlet.getcurrent()
+        self.assertEqual(main._stack_saved, 0)
+
+        def func():
+            main.switch(main._stack_saved)
+
+        g = greenlet.greenlet(func)
+        x = g.switch()
+        self.assertGreater(x, 0)
+        self.assertGreater(g._stack_saved, 0)
+        g.switch()
+        self.assertEqual(g._stack_saved, 0)
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_throw.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_throw.py
new file mode 100644
index 00000000..f4f9a140
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_throw.py
@@ -0,0 +1,128 @@
+import sys
+
+
+from greenlet import greenlet
+from . import TestCase
+
+def switch(*args):
+    return greenlet.getcurrent().parent.switch(*args)
+
+
+class ThrowTests(TestCase):
+    def test_class(self):
+        def f():
+            try:
+                switch("ok")
+            except RuntimeError:
+                switch("ok")
+                return
+            switch("fail")
+        g = greenlet(f)
+        res = g.switch()
+        self.assertEqual(res, "ok")
+        res = g.throw(RuntimeError)
+        self.assertEqual(res, "ok")
+
+    def test_val(self):
+        def f():
+            try:
+                switch("ok")
+            except RuntimeError:
+                val = sys.exc_info()[1]
+                if str(val) == "ciao":
+                    switch("ok")
+                    return
+            switch("fail")
+
+        g = greenlet(f)
+        res = g.switch()
+        self.assertEqual(res, "ok")
+        res = g.throw(RuntimeError("ciao"))
+        self.assertEqual(res, "ok")
+
+        g = greenlet(f)
+        res = g.switch()
+        self.assertEqual(res, "ok")
+        res = g.throw(RuntimeError, "ciao")
+        self.assertEqual(res, "ok")
+
+    def test_kill(self):
+        def f():
+            switch("ok")
+            switch("fail")
+        g = greenlet(f)
+        res = g.switch()
+        self.assertEqual(res, "ok")
+        res = g.throw()
+        self.assertTrue(isinstance(res, greenlet.GreenletExit))
+        self.assertTrue(g.dead)
+        res = g.throw()    # immediately eaten by the already-dead greenlet
+        self.assertTrue(isinstance(res, greenlet.GreenletExit))
+
+    def test_throw_goes_to_original_parent(self):
+        main = greenlet.getcurrent()
+
+        def f1():
+            try:
+                main.switch("f1 ready to catch")
+            except IndexError:
+                return "caught"
+            return "normal exit"
+
+        def f2():
+            main.switch("from f2")
+
+        g1 = greenlet(f1)
+        g2 = greenlet(f2, parent=g1)
+        with self.assertRaises(IndexError):
+            g2.throw(IndexError)
+        self.assertTrue(g2.dead)
+        self.assertTrue(g1.dead)
+
+        g1 = greenlet(f1)
+        g2 = greenlet(f2, parent=g1)
+        res = g1.switch()
+        self.assertEqual(res, "f1 ready to catch")
+        res = g2.throw(IndexError)
+        self.assertEqual(res, "caught")
+        self.assertTrue(g2.dead)
+        self.assertTrue(g1.dead)
+
+        g1 = greenlet(f1)
+        g2 = greenlet(f2, parent=g1)
+        res = g1.switch()
+        self.assertEqual(res, "f1 ready to catch")
+        res = g2.switch()
+        self.assertEqual(res, "from f2")
+        res = g2.throw(IndexError)
+        self.assertEqual(res, "caught")
+        self.assertTrue(g2.dead)
+        self.assertTrue(g1.dead)
+
+    def test_non_traceback_param(self):
+        with self.assertRaises(TypeError) as exc:
+            greenlet.getcurrent().throw(
+                Exception,
+                Exception(),
+                self
+            )
+        self.assertEqual(str(exc.exception),
+                         "throw() third argument must be a traceback object")
+
+    def test_instance_of_wrong_type(self):
+        with self.assertRaises(TypeError) as exc:
+            greenlet.getcurrent().throw(
+                Exception(),
+                BaseException()
+            )
+
+        self.assertEqual(str(exc.exception),
+                         "instance exception may not have a separate value")
+
+    def test_not_throwable(self):
+        with self.assertRaises(TypeError) as exc:
+            greenlet.getcurrent().throw(
+                "abc"
+            )
+        self.assertEqual(str(exc.exception),
+                         "exceptions must be classes, or instances, not str")
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_tracing.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_tracing.py
new file mode 100644
index 00000000..c044d4b6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_tracing.py
@@ -0,0 +1,291 @@
+from __future__ import print_function
+import sys
+import greenlet
+import unittest
+
+from . import TestCase
+from . import PY312
+
+# https://discuss.python.org/t/cpython-3-12-greenlet-and-tracing-profiling-how-to-not-crash-and-get-correct-results/33144/2
+DEBUG_BUILD_PY312 = (
+    PY312 and hasattr(sys, 'gettotalrefcount'),
+    "Broken on debug builds of Python 3.12"
+)
+
+class SomeError(Exception):
+    pass
+
+class GreenletTracer(object):
+    oldtrace = None
+
+    def __init__(self, error_on_trace=False):
+        self.actions = []
+        self.error_on_trace = error_on_trace
+
+    def __call__(self, *args):
+        self.actions.append(args)
+        if self.error_on_trace:
+            raise SomeError
+
+    def __enter__(self):
+        self.oldtrace = greenlet.settrace(self)
+        return self.actions
+
+    def __exit__(self, *args):
+        greenlet.settrace(self.oldtrace)
+
+
+class TestGreenletTracing(TestCase):
+    """
+    Tests of ``greenlet.settrace()``
+    """
+
+    def test_a_greenlet_tracing(self):
+        main = greenlet.getcurrent()
+        def dummy():
+            pass
+        def dummyexc():
+            raise SomeError()
+
+        with GreenletTracer() as actions:
+            g1 = greenlet.greenlet(dummy)
+            g1.switch()
+            g2 = greenlet.greenlet(dummyexc)
+            self.assertRaises(SomeError, g2.switch)
+
+        self.assertEqual(actions, [
+            ('switch', (main, g1)),
+            ('switch', (g1, main)),
+            ('switch', (main, g2)),
+            ('throw', (g2, main)),
+        ])
+
+    def test_b_exception_disables_tracing(self):
+        main = greenlet.getcurrent()
+        def dummy():
+            main.switch()
+        g = greenlet.greenlet(dummy)
+        g.switch()
+        with GreenletTracer(error_on_trace=True) as actions:
+            self.assertRaises(SomeError, g.switch)
+            self.assertEqual(greenlet.gettrace(), None)
+
+        self.assertEqual(actions, [
+            ('switch', (main, g)),
+        ])
+
+    def test_set_same_tracer_twice(self):
+        # https://github.com/python-greenlet/greenlet/issues/332
+        # Our logic in asserting that the tracefunction should
+        # gain a reference was incorrect if the same tracefunction was set
+        # twice.
+        tracer = GreenletTracer()
+        with tracer:
+            greenlet.settrace(tracer)
+
+
+class PythonTracer(object):
+    oldtrace = None
+
+    def __init__(self):
+        self.actions = []
+
+    def __call__(self, frame, event, arg):
+        # Record the co_name so we have an idea what function we're in.
+        self.actions.append((event, frame.f_code.co_name))
+
+    def __enter__(self):
+        self.oldtrace = sys.setprofile(self)
+        return self.actions
+
+    def __exit__(self, *args):
+        sys.setprofile(self.oldtrace)
+
+def tpt_callback():
+    return 42
+
+class TestPythonTracing(TestCase):
+    """
+    Tests of the interaction of ``sys.settrace()``
+    with greenlet facilities.
+
+    NOTE: Most of this is probably CPython specific.
+    """
+
+    maxDiff = None
+
+    def test_trace_events_trivial(self):
+        with PythonTracer() as actions:
+            tpt_callback()
+        # If we use the sys.settrace instead of setprofile, we get
+        # this:
+
+        # self.assertEqual(actions, [
+        #     ('call', 'tpt_callback'),
+        #     ('call', '__exit__'),
+        # ])
+
+        self.assertEqual(actions, [
+            ('return', '__enter__'),
+            ('call', 'tpt_callback'),
+            ('return', 'tpt_callback'),
+            ('call', '__exit__'),
+            ('c_call', '__exit__'),
+        ])
+
+    def _trace_switch(self, glet):
+        with PythonTracer() as actions:
+            glet.switch()
+        return actions
+
+    def _check_trace_events_func_already_set(self, glet):
+        actions = self._trace_switch(glet)
+        self.assertEqual(actions, [
+            ('return', '__enter__'),
+            ('c_call', '_trace_switch'),
+            ('call', 'run'),
+            ('call', 'tpt_callback'),
+            ('return', 'tpt_callback'),
+            ('return', 'run'),
+            ('c_return', '_trace_switch'),
+            ('call', '__exit__'),
+            ('c_call', '__exit__'),
+        ])
+
+    def test_trace_events_into_greenlet_func_already_set(self):
+        def run():
+            return tpt_callback()
+
+        self._check_trace_events_func_already_set(greenlet.greenlet(run))
+
+    def test_trace_events_into_greenlet_subclass_already_set(self):
+        class X(greenlet.greenlet):
+            def run(self):
+                return tpt_callback()
+        self._check_trace_events_func_already_set(X())
+
+    def _check_trace_events_from_greenlet_sets_profiler(self, g, tracer):
+        g.switch()
+        tpt_callback()
+        tracer.__exit__()
+        self.assertEqual(tracer.actions, [
+            ('return', '__enter__'),
+            ('call', 'tpt_callback'),
+            ('return', 'tpt_callback'),
+            ('return', 'run'),
+            ('call', 'tpt_callback'),
+            ('return', 'tpt_callback'),
+            ('call', '__exit__'),
+            ('c_call', '__exit__'),
+        ])
+
+
+    def test_trace_events_from_greenlet_func_sets_profiler(self):
+        tracer = PythonTracer()
+        def run():
+            tracer.__enter__()
+            return tpt_callback()
+
+        self._check_trace_events_from_greenlet_sets_profiler(greenlet.greenlet(run),
+                                                             tracer)
+
+    def test_trace_events_from_greenlet_subclass_sets_profiler(self):
+        tracer = PythonTracer()
+        class X(greenlet.greenlet):
+            def run(self):
+                tracer.__enter__()
+                return tpt_callback()
+
+        self._check_trace_events_from_greenlet_sets_profiler(X(), tracer)
+
+    @unittest.skipIf(*DEBUG_BUILD_PY312)
+    def test_trace_events_multiple_greenlets_switching(self):
+        tracer = PythonTracer()
+
+        g1 = None
+        g2 = None
+
+        def g1_run():
+            tracer.__enter__()
+            tpt_callback()
+            g2.switch()
+            tpt_callback()
+            return 42
+
+        def g2_run():
+            tpt_callback()
+            tracer.__exit__()
+            tpt_callback()
+            g1.switch()
+
+        g1 = greenlet.greenlet(g1_run)
+        g2 = greenlet.greenlet(g2_run)
+
+        x = g1.switch()
+        self.assertEqual(x, 42)
+        tpt_callback() # ensure not in the trace
+        self.assertEqual(tracer.actions, [
+            ('return', '__enter__'),
+            ('call', 'tpt_callback'),
+            ('return', 'tpt_callback'),
+            ('c_call', 'g1_run'),
+            ('call', 'g2_run'),
+            ('call', 'tpt_callback'),
+            ('return', 'tpt_callback'),
+            ('call', '__exit__'),
+            ('c_call', '__exit__'),
+        ])
+
+    @unittest.skipIf(*DEBUG_BUILD_PY312)
+    def test_trace_events_multiple_greenlets_switching_siblings(self):
+        # Like the first version, but get both greenlets running first
+        # as "siblings" and then establish the tracing.
+        tracer = PythonTracer()
+
+        g1 = None
+        g2 = None
+
+        def g1_run():
+            greenlet.getcurrent().parent.switch()
+            tracer.__enter__()
+            tpt_callback()
+            g2.switch()
+            tpt_callback()
+            return 42
+
+        def g2_run():
+            greenlet.getcurrent().parent.switch()
+
+            tpt_callback()
+            tracer.__exit__()
+            tpt_callback()
+            g1.switch()
+
+        g1 = greenlet.greenlet(g1_run)
+        g2 = greenlet.greenlet(g2_run)
+
+        # Start g1
+        g1.switch()
+        # And it immediately returns control to us.
+        # Start g2
+        g2.switch()
+        # Which also returns. Now kick of the real part of the
+        # test.
+        x = g1.switch()
+        self.assertEqual(x, 42)
+
+        tpt_callback() # ensure not in the trace
+        self.assertEqual(tracer.actions, [
+            ('return', '__enter__'),
+            ('call', 'tpt_callback'),
+            ('return', 'tpt_callback'),
+            ('c_call', 'g1_run'),
+            ('call', 'tpt_callback'),
+            ('return', 'tpt_callback'),
+            ('call', '__exit__'),
+            ('c_call', '__exit__'),
+        ])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_version.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_version.py
new file mode 100644
index 00000000..96c17cf1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_version.py
@@ -0,0 +1,41 @@
+#! /usr/bin/env python
+from __future__ import absolute_import
+from __future__ import print_function
+
+import sys
+import os
+from unittest import TestCase as NonLeakingTestCase
+
+import greenlet
+
+# No reason to run this multiple times under leakchecks,
+# it doesn't do anything.
+class VersionTests(NonLeakingTestCase):
+    def test_version(self):
+        def find_dominating_file(name):
+            if os.path.exists(name):
+                return name
+
+            tried = []
+            here = os.path.abspath(os.path.dirname(__file__))
+            for i in range(10):
+                up = ['..'] * i
+                path = [here] + up + [name]
+                fname = os.path.join(*path)
+                fname = os.path.abspath(fname)
+                tried.append(fname)
+                if os.path.exists(fname):
+                    return fname
+            raise AssertionError("Could not find file " + name + "; checked " + str(tried))
+
+        try:
+            setup_py = find_dominating_file('setup.py')
+        except AssertionError as e:
+            self.skipTest("Unable to find setup.py; must be out of tree. " + str(e))
+
+
+        invoke_setup = "%s %s --version" % (sys.executable, setup_py)
+        with os.popen(invoke_setup) as f:
+            sversion = f.read().strip()
+
+        self.assertEqual(sversion, greenlet.__version__)
diff --git a/.venv/lib/python3.12/site-packages/greenlet/tests/test_weakref.py b/.venv/lib/python3.12/site-packages/greenlet/tests/test_weakref.py
new file mode 100644
index 00000000..05a38a7f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/greenlet/tests/test_weakref.py
@@ -0,0 +1,35 @@
+import gc
+import weakref
+
+
+import greenlet
+from . import TestCase
+
+class WeakRefTests(TestCase):
+    def test_dead_weakref(self):
+        def _dead_greenlet():
+            g = greenlet.greenlet(lambda: None)
+            g.switch()
+            return g
+        o = weakref.ref(_dead_greenlet())
+        gc.collect()
+        self.assertEqual(o(), None)
+
+    def test_inactive_weakref(self):
+        o = weakref.ref(greenlet.greenlet())
+        gc.collect()
+        self.assertEqual(o(), None)
+
+    def test_dealloc_weakref(self):
+        seen = []
+        def worker():
+            try:
+                greenlet.getcurrent().parent.switch()
+            finally:
+                seen.append(g())
+        g = greenlet.greenlet(worker)
+        g.switch()
+        g2 = greenlet.greenlet(lambda: None, g)
+        g = weakref.ref(g2)
+        g2 = None
+        self.assertEqual(seen, [None])