Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-greenlet for openSUSE:Factory 
checked in at 2024-01-12 23:44:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-greenlet (Old)
 and      /work/SRC/openSUSE:Factory/.python-greenlet.new.21961 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-greenlet"

Fri Jan 12 23:44:40 2024 rev:48 rq:1138145 version:3.0.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-greenlet/python-greenlet.changes  
2024-01-09 20:48:40.602089627 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-greenlet.new.21961/python-greenlet.changes   
    2024-01-12 23:44:59.769652238 +0100
@@ -1,0 +2,10 @@
+Wed Jan 10 22:14:16 UTC 2024 - Ben Greiner <c...@bnavigator.de>
+
+- Update to 3.0.3
+  * Python 3.12: Restore the full ability to walk the stack of a
+    suspended greenlet; previously only the innermost frame was
+    exposed. See issue 388. Fix by Joshua Oreman in PR 393.
+- Disable building the docs: Now requires the furo theme, which is
+  not available.
+
+-------------------------------------------------------------------

Old:
----
  greenlet-3.0.2.tar.gz

New:
----
  greenlet-3.0.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-greenlet.spec ++++++
--- /var/tmp/diff_new_pack.pIyojo/_old  2024-01-12 23:45:00.457677420 +0100
+++ /var/tmp/diff_new_pack.pIyojo/_new  2024-01-12 23:45:00.457677420 +0100
@@ -17,9 +17,12 @@
 #
 
 
+# Requires python-furo
+%bcond_with docs
+
 %{?sle15_python_module_pythons}
 Name:           python-greenlet
-Version:        3.0.2
+Version:        3.0.3
 Release:        0
 Summary:        Lightweight in-process concurrent programming
 License:        MIT
@@ -27,7 +30,7 @@
 URL:            https://github.com/python-greenlet/greenlet
 Source0:        
https://files.pythonhosted.org/packages/source/g/greenlet/greenlet-%{version}.tar.gz
 Source9:        python-greenlet-rpmlintrc
-BuildRequires:  %{python_module devel}
+BuildRequires:  %{python_module devel >= 3.7}
 BuildRequires:  %{python_module objgraph}
 BuildRequires:  %{python_module pip}
 BuildRequires:  %{python_module psutil}
@@ -36,7 +39,10 @@
 BuildRequires:  c++_compiler
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
+%if %{with docs}
 BuildRequires:  python3-Sphinx
+BuildRequires:  python3-furo
+%endif
 %python_subpackages
 
 %description
@@ -56,13 +62,16 @@
 
 %prep
 %autosetup -p1 -n greenlet-%{version}
+sed -i '1{/env python/d}' src/greenlet/tests/test_version.py
 
 %build
 export CFLAGS="%{optflags} -fno-tree-dominator-opts -fno-strict-aliasing"
 %pyproject_wheel
 
+%if %{with docs}
 export PYTHONPATH=$PWD/src
 cd docs && make html && rm _build/html/.buildinfo
+%endif
 
 %install
 %pyproject_install
@@ -76,9 +85,12 @@
 
 %files %{python_files}
 %doc AUTHORS CHANGES.rst README.rst
+%if %{with docs}
 %doc docs/_build/html/
+%endif
 %license LICENSE*
-%{python_sitearch}/greenlet*
+%{python_sitearch}/greenlet
+%{python_sitearch}/greenlet-%{version}.dist-info
 
 %files %{python_files devel}
 %doc AUTHORS

++++++ greenlet-3.0.2.tar.gz -> greenlet-3.0.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/.github/workflows/tests.yml 
new/greenlet-3.0.3/.github/workflows/tests.yml
--- old/greenlet-3.0.2/.github/workflows/tests.yml      2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/.github/workflows/tests.yml      2023-12-21 
22:57:40.000000000 +0100
@@ -89,6 +89,14 @@
     - name: Doctest
       run: |
         sphinx-build -b doctest -d docs/_build/doctrees2 docs 
docs/_build/doctest2
+    - name: Lint
+      if: matrix.python-version == '3.10' && startsWith(runner.os, 'Linux')
+      # We only need to do this on one version.
+      # We do this here rather than a separate job to avoid the compilation 
overhead.
+      run: |
+        pip install -U pylint
+        python -m pylint --rcfile=.pylintrc greenlet
+
     - name: Publish package to PyPI (mac)
       # We cannot 'uses: pypa/gh-action-pypi-publish@v1.4.1' because
       # that's apparently a container action, and those don't run on
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/.gitignore 
new/greenlet-3.0.3/.gitignore
--- old/greenlet-3.0.2/.gitignore       2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/.gitignore       1970-01-01 01:00:00.000000000 +0100
@@ -1,14 +0,0 @@
-*.so
-*.pyd
-*.pyc
-*.pyo
-build/
-dist/
-.tox/
-wheelhouse/
-greenlet.egg-info/
-/docs/_build
-__pycache__/
-/.ropeproject/
-/MANIFEST
-benchmarks/*.json
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/.pylintrc new/greenlet-3.0.3/.pylintrc
--- old/greenlet-3.0.2/.pylintrc        2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/.pylintrc        2023-12-21 22:57:40.000000000 +0100
@@ -1,9 +1,85 @@
 [MASTER]
-load-plugins=pylint.extensions.bad_builtin
+load-plugins=pylint.extensions.bad_builtin,
+   pylint.extensions.code_style,
+   pylint.extensions.dict_init_mutate,
+   pylint.extensions.dunder,
+   pylint.extensions.comparison_placement,
+   pylint.extensions.confusing_elif,
+   pylint.extensions.for_any_all,
+   pylint.extensions.consider_refactoring_into_while_condition,
+   pylint.extensions.check_elif,
+   pylint.extensions.eq_without_hash,
+   pylint.extensions.overlapping_exceptions,
+
+#    pylint.extensions.comparetozero,
+# Takes out ``if x == 0:`` and wants you to write ``if not x:``
+# but in many cases, the == 0 is actually much more clear.
+
+#    pylint.extensions.mccabe,
+# We have too many too-complex methods. We should enable this and fix them
+# one by one.
+
+#    pylint.extensions.redefined_variable_type,
+# We use that pattern during initialization.
+
+# magic_value wants you to not use arbitrary strings and numbers
+# inline in the code. But it's overzealous and has way too many false
+# positives. Trust people to do the most readable thing.
+#   pylint.extensions.magic_value
+
+# Empty comment would be good, except it detects blank lines within
+# a single comment block.
+#
+# Those are often used to separate paragraphs, like here.
+#   pylint.extensions.empty_comment,
+
+# consider_ternary_expression is a nice check, but is also overzealous.
+# Trust the human to do the readable thing.
+#    pylint.extensions.consider_ternary_expression,
+
+# redefined_loop_name tends to catch us with things like
+# for name in (a, b, c): name = name + '_column' ...
+#    pylint.extensions.redefined_loop_name,
+
+# This wants you to turn ``x in (1, 2)`` into ``x in {1, 2}``.
+# They both result in the LOAD_CONST bytecode, one a tuple one a
+# frozenset. In theory a set lookup using hashing is faster than
+# a linear scan of a tuple; but if the tuple is small, it can often
+# actually be faster to scan the tuple.
+#   pylint.extensions.set_membership,
+
 # Fix zope.cachedescriptors.property.Lazy; the property-classes doesn't seem to
 # do anything.
 # 
https://stackoverflow.com/questions/51160955/pylint-how-to-specify-a-self-defined-property-decorator-with-property-classes
-init-hook = "import astroid.bases; 
astroid.bases.POSSIBLE_PROPERTIES.add('Lazy')"
+# For releases prior to 2.14.2, this needs to be a one-line, quoted string. 
After that,
+# a multi-line string.
+# - Make zope.cachedescriptors.property.Lazy look like a property;
+#   fixes pylint thinking it is a method.
+# - Run in Pure Python mode (ignore C extensions that respect this);
+#   fixes some issues with zope.interface, like IFoo.providedby(ob)
+#   claiming not to have the right number of parameters...except no, it does 
not.
+init-hook =
+    import astroid.bases
+    astroid.bases.POSSIBLE_PROPERTIES.add('Lazy')
+    astroid.bases.POSSIBLE_PROPERTIES.add('LazyOnClass')
+    astroid.bases.POSSIBLE_PROPERTIES.add('readproperty')
+    astroid.bases.POSSIBLE_PROPERTIES.add('non_overridable')
+    import os
+    os.environ['PURE_PYTHON'] = ("1")
+    # Ending on a quoted string
+    # breaks pylint 2.14.5 (it strips the trailing quote. This is
+    # probably because it tries to handle one-line quoted strings as well as 
multi-blocks).
+    # The parens around it fix the issue.
+
+extension-pkg-whitelist=greenlet._greenlet
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+# gevent: The changes for Python 3.7 in _ssl3.py lead to infinite recursion
+# in pylint 2.3.1/astroid 2.2.5 in that file unless we this this to 1
+# from the default of 100.
+limit-inference-results=1
 
 [MESSAGES CONTROL]
 
@@ -49,31 +125,55 @@
 # Pylint 2.4 adds self-assigning-variable. But we do *that* to avoid 
unused-import when we
 # "export" the variable and don't have a __all__.
 # Pylint 2.6+ adds some python-3-only things that don't apply: 
raise-missing-from, super-with-arguments, consider-using-f-string, 
redundant-u-string-prefix
+# unnecessary-lambda-assignment: New check introduced in v2.14.0
+# unnecessary-dunder-call: New check introduced in v2.14.0
+# consider-using-assignment-expr: wants you to use the walrus operator.
+#   It hits way too much and its not clear they would be improvements.
+# confusing-consecutive-elif: Are they though?
 disable=wrong-import-position,
     wrong-import-order,
     missing-docstring,
     ungrouped-imports,
     invalid-name,
+    protected-access,
     too-few-public-methods,
+    exec-used,
     global-statement,
+    multiple-statements,
     locally-disabled,
+    cyclic-import,
     too-many-arguments,
+    redefined-builtin,
     useless-suppression,
     duplicate-code,
+    undefined-all-variable,
+    inconsistent-return-statements,
+    useless-return,
     useless-object-inheritance,
     import-outside-toplevel,
     self-assigning-variable,
-    consider-using-f-string
+    raise-missing-from,
+    super-with-arguments,
+    consider-using-f-string,
+    consider-using-assignment-expr,
+    redundant-u-string-prefix,
+    unnecessary-lambda-assignment,
+    unnecessary-dunder-call,
+    use-dict-literal,
+    confusing-consecutive-elif,
+
 
+enable=consider-using-augmented-assign
 
 [FORMAT]
-max-line-length=100
+# duplicated from setup.cfg
+max-line-length=160
 max-module-lines=1100
 
 [MISCELLANEOUS]
 # List of note tags to take in consideration, separated by a comma.
 #notes=FIXME,XXX,TODO
-# Disable that, we don't want them to fail the lint CI job.
+# Disable that, we don't want them in the report (???)
 notes=
 
 [VARIABLES]
@@ -85,14 +185,8 @@
 # List of members which are set dynamically and missed by pylint inference
 # system, and so shouldn't trigger E1101 when accessed. Python regular
 # expressions are accepted.
-generated-members=REQUEST,acl_users,aq_parent,providedBy
-
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-# XXX: deprecated in 2.14; replaced with ignored-checks-for-mixins.
-# The defaults for that value seem to be what we want
-#ignore-mixin-members=yes
+# gevent: this is helpful for py3/py2 code.
+generated-members=exc_clear
 
 # List of classes names for which member attributes should not be checked
 # (useful for classes with attributes dynamically set). This can work
@@ -104,25 +198,18 @@
 # (useful for modules/projects where namespaces are manipulated during runtime
 # and thus existing member attributes cannot be deduced by static analysis. It
 # supports qualified module names, as well as Unix pattern matching.
-#ignored-modules=gevent._corecffi,gevent.os,os,greenlet,threading,gevent.libev.corecffi,gevent.socket,gevent.core,gevent.testing.support
+ignored-modules=gevent._corecffi,gevent.os,os,greenlet,threading,gevent.libev.corecffi,gevent.socket,gevent.core,gevent.testing.support
 
 [DESIGN]
 max-attributes=12
 max-parents=10
 
 [BASIC]
+bad-functions=input
 # Prospector turns ot unsafe-load-any-extension by default, but
 # pylint leaves it off. This is the proximal cause of the
 # undefined-all-variable crash.
 unsafe-load-any-extension = yes
-property-classes=zope.cachedescriptors.property.Lazy,zope.cachedescriptors.property.Cached
-extension-pkg-allow-list=greenlet._greenlet
-
-[CLASSES]
-# List of interface methods to ignore, separated by a comma. This is used for
-# instance to not check methods defines in Zope's Interface base class.
-
-
 
 # Local Variables:
 # mode: conf
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/.readthedocs.yml 
new/greenlet-3.0.3/.readthedocs.yml
--- old/greenlet-3.0.2/.readthedocs.yml 2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/.readthedocs.yml 2023-12-21 22:57:40.000000000 +0100
@@ -7,11 +7,20 @@
 
 # Build documentation in the docs/ directory with Sphinx
 sphinx:
+  builder: html
   configuration: docs/conf.py
 
-# Optionally set the version of Python and requirements required to build your 
docs
+
+# Set the version of Python and requirements required to build your
+# docs
+
+build:
+  # os is required for some reason
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+
 python:
-  version: 3
   install:
     - method: pip
       path: .
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/CHANGES.rst 
new/greenlet-3.0.3/CHANGES.rst
--- old/greenlet-3.0.2/CHANGES.rst      2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/CHANGES.rst      2023-12-21 22:57:40.000000000 +0100
@@ -2,6 +2,15 @@
  Changes
 =========
 
+3.0.3 (2023-12-21)
+==================
+
+- Python 3.12: Restore the full ability to walk the stack of a suspended
+  greenlet; previously only the innermost frame was exposed. See `issue 388
+  <https://github.com/python-greenlet/greenlet/issues/388>`_. Fix by
+  Joshua Oreman in `PR 393
+  <https://github.com/python-greenlet/greenlet/pull/393/>`_.
+
 3.0.2 (2023-12-08)
 ==================
 
@@ -236,7 +245,7 @@
 ====================
 
 Platforms
-~~~~~~~~~
+---------
 
 - Add experimental, untested support for 64-bit Windows on ARM using
   MSVC. See `PR 271 <https://github.com/python-greenlet/greenlet/pull/271>`_.
@@ -386,7 +395,7 @@
 - (Documentation) Publish the change log to https://greenlet.readthedocs.io
 
 Supported Platforms
-~~~~~~~~~~~~~~~~~~~
+-------------------
 
 - Drop support for Python 2.4, 2.5, 2.6, 3.0, 3.1, 3.2 and 3.4.
   The project metadata now includes the ``python_requires`` data to
@@ -396,7 +405,7 @@
   <https://github.com/python-greenlet/greenlet/pull/197>`_.
 
 Packaging Changes
-~~~~~~~~~~~~~~~~~
+-----------------
 
 - Require setuptools to build from source.
 - Stop asking setuptools to build both .tar.gz and .zip
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/PKG-INFO new/greenlet-3.0.3/PKG-INFO
--- old/greenlet-3.0.2/PKG-INFO 2023-12-08 20:53:34.632305900 +0100
+++ new/greenlet-3.0.3/PKG-INFO 2023-12-21 22:57:41.091845500 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: greenlet
-Version: 3.0.2
+Version: 3.0.3
 Summary: Lightweight in-process concurrent programming
 Home-page: https://greenlet.readthedocs.io/
 Author: Alexey Borzenkov
@@ -31,14 +31,11 @@
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Requires-Python: >=3.7
 Description-Content-Type: text/x-rst
+Provides-Extra: docs
+Provides-Extra: test
 License-File: LICENSE
 License-File: LICENSE.PSF
 License-File: AUTHORS
-Provides-Extra: docs
-Requires-Dist: Sphinx; extra == "docs"
-Provides-Extra: test
-Requires-Dist: objgraph; extra == "test"
-Requires-Dist: psutil; extra == "test"
 
 .. This file is included into docs/history.rst
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/benchmarks/chain.py 
new/greenlet-3.0.3/benchmarks/chain.py
--- old/greenlet-3.0.2/benchmarks/chain.py      2023-12-08 20:53:34.000000000 
+0100
+++ new/greenlet-3.0.3/benchmarks/chain.py      2023-12-21 22:57:40.000000000 
+0100
@@ -5,10 +5,24 @@
 along.
 """
 
+import os
 import pyperf
 import greenlet
 
-
+# This is obsolete now, we always expose frames for Python 3.12.
+# See https://github.com/python-greenlet/greenlet/pull/393/
+# for a complete discussion of performance.
+EXPOSE_FRAMES = 'EXPOSE_FRAMES' in os.environ
+
+# Exposing
+# 100 frames Mean +- std dev: 5.62 us +- 0.10 us
+# 200 frames Mean +- std dev: 14.0 us +- 0.6 us
+# 300 frames Mean +- std dev: 22.7 us +- 0.4 us
+#
+# Non-exposing
+# 100 frames Mean +- std dev: 3.64 us +- 0.06 us -> 1.54/1.98us
+# 200 frames Mean +- std dev: 9.49 us +- 0.13 us -> 1.47/4.51us
+# 300 frames Mean +- std dev: 15.7 us +- 0.3 us  -> 1.45/7us
 
 def link(next_greenlet):
     value = greenlet.getcurrent().parent.switch()
@@ -23,6 +37,7 @@
         start_node = greenlet.getcurrent()
         for _ in range(CHAIN_GREENLET_COUNT):
             g = greenlet.greenlet(link)
+            g.gr_frames_always_exposed = EXPOSE_FRAMES
             g.switch(start_node)
             start_node = g
         x = start_node.switch(0)
@@ -51,7 +66,8 @@
     return end - begin
 
 SWITCH_INNER_LOOPS = 10000
-def bm_switch(loops):
+def bm_switch_shallow(loops):
+    # pylint:disable=attribute-defined-outside-init
     class G(greenlet.greenlet):
         other = None
         def run(self):
@@ -60,15 +76,63 @@
                 o.switch()
 
     begin = pyperf.perf_counter()
+
     for _ in range(loops):
         gl1 = G()
         gl2 = G()
+        gl1.gr_frames_always_exposed = EXPOSE_FRAMES
+        gl2.gr_frames_always_exposed = EXPOSE_FRAMES
         gl1.other = gl2
         gl2.other = gl1
         gl1.switch()
+
+        gl1.switch()
+        gl2.switch()
+        gl1.other = gl2.other = None
+        assert gl1.dead
+        assert gl2.dead
+
     end = pyperf.perf_counter()
     return end - begin
 
+def bm_switch_deep(loops, _MAX_DEPTH=200):
+    # pylint:disable=attribute-defined-outside-init
+    class G(greenlet.greenlet):
+        other = None
+        def run(self):
+            for _ in range(SWITCH_INNER_LOOPS):
+                self.recur_then_switch()
+
+        def recur_then_switch(self, depth=_MAX_DEPTH):
+            if not depth:
+                self.other.switch()
+            else:
+                self.recur_then_switch(depth - 1)
+
+    begin = pyperf.perf_counter()
+
+    for _ in range(loops):
+        gl1 = G()
+        gl2 = G()
+        gl1.gr_frames_always_exposed = EXPOSE_FRAMES
+        gl2.gr_frames_always_exposed = EXPOSE_FRAMES
+        gl1.other = gl2
+        gl2.other = gl1
+        gl1.switch()
+
+        gl1.switch()
+        gl2.switch()
+        gl1.other = gl2.other = None
+        assert gl1.dead
+        assert gl2.dead
+
+    end = pyperf.perf_counter()
+    return end - begin
+
+def bm_switch_deeper(loops):
+    return bm_switch_deep(loops, 400)
+
+
 CREATE_INNER_LOOPS = 10
 def bm_create(loops):
     gl = greenlet.greenlet
@@ -87,8 +151,58 @@
     end = pyperf.perf_counter()
     return end - begin
 
+
+
+
+def _bm_recur_frame(loops, RECUR_DEPTH):
+
+    def recur(depth):
+        if not depth:
+            return greenlet.getcurrent().parent.switch(greenlet.getcurrent())
+        return recur(depth - 1)
+
+
+    begin = pyperf.perf_counter()
+    for _ in range(loops):
+
+        for _ in range(CHAIN_GREENLET_COUNT):
+            g = greenlet.greenlet(recur)
+            g.gr_frames_always_exposed = EXPOSE_FRAMES
+            g2 = g.switch(RECUR_DEPTH)
+            assert g2 is g, (g2, g)
+            f = g2.gr_frame
+            assert f is not None, "frame is none"
+            count = 0
+            while f:
+                count += 1
+                f = f.f_back
+            # This assertion fails with the released versions of greenlet
+            # on Python 3.12
+            #assert count == RECUR_DEPTH + 1, (count, RECUR_DEPTH)
+            # Switch back so it can be collected; otherwise they build
+            # up forever.
+            g.switch()
+            # fall off the end of it and back to us.
+            del g
+            del g2
+            del f
+
+
+    end = pyperf.perf_counter()
+    return end - begin
+
+def bm_recur_frame_2(loops):
+    return _bm_recur_frame(loops, 2)
+
+def bm_recur_frame_20(loops):
+    return _bm_recur_frame(loops, 20)
+
+def bm_recur_frame_200(loops):
+    return _bm_recur_frame(loops, 200)
+
 if __name__ == '__main__':
     runner = pyperf.Runner()
+
     runner.bench_time_func(
         'create a greenlet',
         bm_create,
@@ -96,12 +210,23 @@
     )
 
     runner.bench_time_func(
-        'switch between two greenlets',
-        bm_switch,
+        'switch between two greenlets (shallow)',
+        bm_switch_shallow,
         inner_loops=SWITCH_INNER_LOOPS
     )
 
     runner.bench_time_func(
+        'switch between two greenlets (deep)',
+        bm_switch_deep,
+        inner_loops=SWITCH_INNER_LOOPS
+    )
+
+    runner.bench_time_func(
+        'switch between two greenlets (deeper)',
+        bm_switch_deeper,
+        inner_loops=SWITCH_INNER_LOOPS
+    )
+    runner.bench_time_func(
         'getcurrent single thread',
         bm_getcurrent,
         inner_loops=GETCURRENT_INNER_LOOPS
@@ -110,3 +235,17 @@
         'chain(%s)' % CHAIN_GREENLET_COUNT,
         bm_chain,
     )
+
+    runner.bench_time_func(
+        'read 2 nested frames',
+        bm_recur_frame_2,
+    )
+
+    runner.bench_time_func(
+        'read 20 nested frames',
+        bm_recur_frame_20,
+    )
+    runner.bench_time_func(
+        'read 200 nested frames',
+        bm_recur_frame_200,
+    )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/docs/_static/custom.css 
new/greenlet-3.0.3/docs/_static/custom.css
--- old/greenlet-3.0.2/docs/_static/custom.css  1970-01-01 01:00:00.000000000 
+0100
+++ new/greenlet-3.0.3/docs/_static/custom.css  2023-12-21 22:57:40.000000000 
+0100
@@ -0,0 +1,99 @@
+/* Font definitions */
+@font-face {
+  font-family: 'JetBrains Mono';
+  src: 
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold-Italic.woff2')
 format('woff2'),
+    
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold-Italic.woff')
 format('woff');
+  font-weight: 700;
+  font-style: italic;
+  font-display: swap;
+}
+
+@font-face {
+  font-family: 'JetBrains Mono';
+  src: 
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff2')
 format('woff2'),
+    
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold.woff')
 format('woff');
+  font-weight: 700;
+  font-style: normal;
+  font-display: swap;
+}
+
+@font-face {
+  font-family: 'JetBrains Mono';
+  src: 
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold-Italic.woff2')
 format('woff2'),
+    
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold-Italic.woff')
 format('woff');
+  font-weight: 800;
+  font-style: italic;
+  font-display: swap;
+}
+
+@font-face {
+  font-family: 'JetBrains Mono';
+  src: 
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold.woff2')
 format('woff2'),
+    
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold.woff')
 format('woff');
+  font-weight: 800;
+  font-style: normal;
+  font-display: swap;
+}
+
+@font-face {
+  font-family: 'JetBrains Mono';
+  src: 
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Italic.woff2')
 format('woff2'),
+    
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Italic.woff')
 format('woff');
+  font-weight: 400;
+  font-style: italic;
+  font-display: swap;
+}
+
+@font-face {
+  font-family: 'JetBrains Mono';
+  src: 
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium-Italic.woff2')
 format('woff2'),
+    
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium-Italic.woff')
 format('woff');
+  font-weight: 500;
+  font-style: italic;
+  font-display: swap;
+}
+
+@font-face {
+  font-family: 'JetBrains Mono';
+  src: 
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium.woff2')
 format('woff2'),
+    
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium.woff')
 format('woff');
+  font-weight: 500;
+  font-style: normal;
+  font-display: swap;
+}
+
+@font-face {
+  font-family: 'JetBrains Mono';
+  src: 
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2')
 format('woff2'),
+    
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff')
 format('woff');
+  font-weight: 400;
+  font-style: normal;
+  font-display: swap;
+}
+
+
+article {
+/* Furo theme makes this 1.5 which uses soo much space */
+    line-height: 1.1;
+}
+
+a {
+    text-decoration: none;
+}
+
+.admonition-opinion p.admonition-title {
+    background-color: rgba(255, 150, 235, 0.44);
+}
+
+div.admonition-opinion.admonition {
+    border-left: .2rem solid rgba(255, 150, 235, 0.44);
+}
+
+
+.admonition-design-options p.admonition-title {
+    background-color: rgba(173, 28, 237, 0.44);
+}
+
+div.admonition-design-options.admonition {
+    border-left: .2rem solid rgba(173, 28, 237, 0.44);
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/docs/api.rst 
new/greenlet-3.0.3/docs/api.rst
--- old/greenlet-3.0.2/docs/api.rst     2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/docs/api.rst     2023-12-21 22:57:40.000000000 +0100
@@ -32,7 +32,6 @@
 
    .. autoattribute:: gr_context
 
-
       The :class:`contextvars.Context` in which ``g`` will run.
       Writable; defaults to ``None``, reflecting that a greenlet
       starts execution in an empty context unless told otherwise.
@@ -56,6 +55,17 @@
       for suspended greenlets; it is None if the greenlet is dead, not
       yet started, or currently executing.
 
+      .. note:: Greenlet stack introspection is fragile on CPython 3.12
+         and later. The frame objects of a suspended greenlet are not safe
+         to access as-is, but must be adjusted by the greenlet package in
+         order to make traversing ``f_back`` links not crash the interpreter,
+         and restored to their original state when resuming the
+         greenlet. The intent is to handle this transparently, but it
+         does introduce additional overhead to switching greenlets,
+         and there may be obscure usage patterns that can still crash
+         the interpreter; if you find one of these, please report it
+         to the maintainer.
+
    .. autoattribute:: parent
 
       The parent greenlet. This is writable, but it is not allowed to create
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/docs/conf.py 
new/greenlet-3.0.3/docs/conf.py
--- old/greenlet-3.0.2/docs/conf.py     2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/docs/conf.py     2023-12-21 22:57:40.000000000 +0100
@@ -17,9 +17,17 @@
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #sys.path.insert(0, os.path.abspath('.'))
-import pkg_resources
 sys.path.append(os.path.abspath('../src/'))
-rqmt = pkg_resources.require('greenlet')[0]
+try:
+    from importlib import metadata
+except ImportError:
+    # Building the docs on 3.7. Which we don't do,
+    # except for running doctests.
+    glet_version = '0.0.0'
+else:
+    glet_version = metadata.version('greenlet')
+
+
 # -- General configuration 
-----------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
@@ -58,9 +66,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = '%s.%s' % tuple(rqmt.version.split('.')[:2])
+version = glet_version
 # The full version, including alpha/beta/rc tags.
-release = rqmt.version
+release = version
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -101,7 +109,23 @@
 
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
-html_theme = 'default'
+
+html_theme = "furo"
+html_css_files = [
+    'custom.css',
+]
+
+html_theme_options = {
+    "sidebar_hide_name": True, # Because we show a logo
+
+    'light_css_variables': {
+        "color-brand-primary": "#7c9a5e",
+        "color-brand-content": "#7c9a5e",
+        "color-foreground-border": "#b7d897",
+        'font-stack': '"SF Pro",-apple-system,BlinkMacSystemFont,"Segoe 
UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
+        'font-stack--monospace': '"JetBrainsMono", "JetBrains Mono", 
"JetBrains Mono Regular", "JetBrainsMono-Regular", ui-monospace, profont, 
monospace',
+    },
+}
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
@@ -130,7 +154,7 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = []  # ['_static']
+html_static_path = ['_static']
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
@@ -252,8 +276,9 @@
 #texinfo_show_urls = 'footnote'
 
 intersphinx_mapping = {
-    'https://docs.python.org/': None,
-    'https://www.gevent.org/': None,
+    'python': ('https://docs.python.org/', None),
+    'gevent': ('https://www.gevent.org/', None),
+
 }
 
 
@@ -266,7 +291,7 @@
     #'members': None,
     'show-inheritance': None,
 }
-autodoc_member_order = 'bysource'
+autodoc_member_order = 'groupwise'
 autoclass_content = 'both'
 
 extlinks = {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/setup.py new/greenlet-3.0.3/setup.py
--- old/greenlet-3.0.2/setup.py 2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/setup.py 2023-12-21 22:57:40.000000000 +0100
@@ -247,6 +247,7 @@
     extras_require={
         'docs': [
             'Sphinx',
+            'furo',
         ],
         'test': [
             'objgraph',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/TGreenlet.cpp 
new/greenlet-3.0.3/src/greenlet/TGreenlet.cpp
--- old/greenlet-3.0.2/src/greenlet/TGreenlet.cpp       2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/TGreenlet.cpp       2023-12-21 
22:57:40.000000000 +0100
@@ -168,6 +168,7 @@
         current->exception_state << tstate;
         this->python_state.will_switch_from(tstate);
         switching_thread_state = this;
+        current->expose_frames();
     }
     assert(this->args() || PyErr_Occurred());
     // If this is the first switch into a greenlet, this will
@@ -606,5 +607,108 @@
     return this->stack_state.active() && !this->python_state.top_frame();
 }
 
+#if GREENLET_PY312
+void GREENLET_NOINLINE(Greenlet::expose_frames)()
+{
+    if (!this->python_state.top_frame()) {
+        return;
+    }
+
+    _PyInterpreterFrame* last_complete_iframe = nullptr;
+    _PyInterpreterFrame* iframe = this->python_state.top_frame()->f_frame;
+    while (iframe) {
+        // We must make a copy before looking at the iframe contents,
+        // since iframe might point to a portion of the greenlet's C stack
+        // that was spilled when switching greenlets.
+        _PyInterpreterFrame iframe_copy;
+        this->stack_state.copy_from_stack(&iframe_copy, iframe, 
sizeof(*iframe));
+        if (!_PyFrame_IsIncomplete(&iframe_copy)) {
+            // If the iframe were OWNED_BY_CSTACK then it would always be
+            // incomplete. Since it's not incomplete, it's not on the C stack
+            // and we can access it through the original `iframe` pointer
+            // directly.  This is important since GetFrameObject might
+            // lazily _create_ the frame object and we don't want the
+            // interpreter to lose track of it.
+            assert(iframe_copy.owner != FRAME_OWNED_BY_CSTACK);
+
+            // We really want to just write:
+            //     PyFrameObject* frame = _PyFrame_GetFrameObject(iframe);
+            // but _PyFrame_GetFrameObject calls _PyFrame_MakeAndSetFrameObject
+            // which is not a visible symbol in libpython. The easiest
+            // way to get a public function to call it is using
+            // PyFrame_GetBack, which is defined as follows:
+            //     assert(frame != NULL);
+            //     assert(!_PyFrame_IsIncomplete(frame->f_frame));
+            //     PyFrameObject *back = frame->f_back;
+            //     if (back == NULL) {
+            //         _PyInterpreterFrame *prev = frame->f_frame->previous;
+            //         prev = _PyFrame_GetFirstComplete(prev);
+            //         if (prev) {
+            //             back = _PyFrame_GetFrameObject(prev);
+            //         }
+            //     }
+            //     return (PyFrameObject*)Py_XNewRef(back);
+            if (!iframe->frame_obj) {
+                PyFrameObject dummy_frame;
+                _PyInterpreterFrame dummy_iframe;
+                dummy_frame.f_back = nullptr;
+                dummy_frame.f_frame = &dummy_iframe;
+                // force the iframe to be considered complete without
+                // needing to check its code object:
+                dummy_iframe.owner = FRAME_OWNED_BY_GENERATOR;
+                dummy_iframe.previous = iframe;
+                assert(!_PyFrame_IsIncomplete(&dummy_iframe));
+                // Drop the returned reference immediately; the iframe
+                // continues to hold a strong reference
+                Py_XDECREF(PyFrame_GetBack(&dummy_frame));
+                assert(iframe->frame_obj);
+            }
+
+            // This is a complete frame, so make the last one of those we saw
+            // point at it, bypassing any incomplete frames (which may have
+            // been on the C stack) in between the two. We're overwriting
+            // last_complete_iframe->previous and need that to be reversible,
+            // so we store the original previous ptr in the frame object
+            // (which we must have created on a previous iteration through
+            // this loop). The frame object has a bunch of storage that is
+            // only used when its iframe is OWNED_BY_FRAME_OBJECT, which only
+            // occurs when the frame object outlives the frame's execution,
+            // which can't have happened yet because the frame is currently
+            // executing as far as the interpreter is concerned. So, we can
+            // reuse it for our own purposes.
+            assert(iframe->owner == FRAME_OWNED_BY_THREAD
+                   || iframe->owner == FRAME_OWNED_BY_GENERATOR);
+            if (last_complete_iframe) {
+                assert(last_complete_iframe->frame_obj);
+                memcpy(&last_complete_iframe->frame_obj->_f_frame_data[0],
+                       &last_complete_iframe->previous, sizeof(void *));
+                last_complete_iframe->previous = iframe;
+            }
+            last_complete_iframe = iframe;
+        }
+        // Frames that are OWNED_BY_FRAME_OBJECT are linked via the
+        // frame's f_back while all others are linked via the iframe's
+        // previous ptr. Since all the frames we traverse are running
+        // as far as the interpreter is concerned, we don't have to
+        // worry about the OWNED_BY_FRAME_OBJECT case.
+        iframe = iframe_copy.previous;
+    }
+
+    // Give the outermost complete iframe a null previous pointer to
+    // account for any potential incomplete/C-stack iframes between it
+    // and the actual top-of-stack
+    if (last_complete_iframe) {
+        assert(last_complete_iframe->frame_obj);
+        memcpy(&last_complete_iframe->frame_obj->_f_frame_data[0],
+               &last_complete_iframe->previous, sizeof(void *));
+        last_complete_iframe->previous = nullptr;
+    }
+}
+#else
+void Greenlet::expose_frames()
+{
+
+}
+#endif
 
 }; // namespace greenlet
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/TPythonState.cpp 
new/greenlet-3.0.3/src/greenlet/TPythonState.cpp
--- old/greenlet-3.0.2/src/greenlet/TPythonState.cpp    2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/TPythonState.cpp    2023-12-21 
22:57:40.000000000 +0100
@@ -25,9 +25,6 @@
     ,datastack_top(nullptr)
     ,datastack_limit(nullptr)
 #endif
-#if GREENLET_PY312
-    ,_prev_frame(nullptr)
-#endif
 {
 #if GREENLET_USE_CFRAME
     /*
@@ -147,12 +144,6 @@
                         // reference.
     this->_top_frame.steal(frame);
   #if GREENLET_PY312
-    if (frame) {
-        this->_prev_frame = frame->f_frame->previous;
-        frame->f_frame->previous = nullptr;
-    }
-  #endif
-  #if GREENLET_PY312
     this->trash_delete_nesting = tstate->trash.delete_nesting;
   #else // not 312
     this->trash_delete_nesting = tstate->trash_delete_nesting;
@@ -164,6 +155,28 @@
 #endif // GREENLET_PY311
 }
 
+#if GREENLET_PY312
+void GREENLET_NOINLINE(PythonState::unexpose_frames)()
+{
+    if (!this->top_frame()) {
+        return;
+    }
+
+    // See GreenletState::expose_frames() and the comment on 
frames_were_exposed
+    // for more information about this logic.
+    _PyInterpreterFrame *iframe = this->_top_frame->f_frame;
+    while (iframe != nullptr) {
+        _PyInterpreterFrame *prev_exposed = iframe->previous;
+        assert(iframe->frame_obj);
+        memcpy(&iframe->previous, &iframe->frame_obj->_f_frame_data[0],
+               sizeof(void *));
+        iframe = prev_exposed;
+    }
+}
+#else
+void PythonState::unexpose_frames()
+{}
+#endif
 
 void PythonState::operator>>(PyThreadState *const tstate) noexcept
 {
@@ -187,13 +200,7 @@
   #if GREENLET_PY312
     tstate->py_recursion_remaining = tstate->py_recursion_limit - 
this->py_recursion_depth;
     tstate->c_recursion_remaining = C_RECURSION_LIMIT - 
this->c_recursion_depth;
-    // We're just going to throw this object away anyway, go ahead and
-    // do it now.
-    PyFrameObject* frame = this->_top_frame.relinquish_ownership();
-    if (frame && frame->f_frame) {
-      frame->f_frame->previous = this->_prev_frame;
-    }
-    this->_prev_frame = nullptr;
+    this->unexpose_frames();
   #else // \/ 3.11
     tstate->recursion_remaining = tstate->recursion_limit - 
this->recursion_depth;
   #endif // GREENLET_PY312
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/TStackState.cpp 
new/greenlet-3.0.3/src/greenlet/TStackState.cpp
--- old/greenlet-3.0.2/src/greenlet/TStackState.cpp     2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/TStackState.cpp     2023-12-21 
22:57:40.000000000 +0100
@@ -226,6 +226,39 @@
     }
 }
 
+void StackState::copy_from_stack(void* vdest, const void* vsrc, size_t n) const
+{
+    char* dest = static_cast<char*>(vdest);
+    const char* src = static_cast<const char*>(vsrc);
+    if (src + n <= this->_stack_start
+        || src >= this->_stack_start + this->_stack_saved
+        || this->_stack_saved == 0) {
+        // Nothing we're copying was spilled from the stack
+        memcpy(dest, src, n);
+        return;
+    }
+
+    if (src < this->_stack_start) {
+        // Copy the part before the saved stack.
+        // We know src + n > _stack_start due to the test above.
+        const size_t nbefore = this->_stack_start - src;
+        memcpy(dest, src, nbefore);
+        dest += nbefore;
+        src += nbefore;
+        n -= nbefore;
+    }
+    // We know src >= _stack_start after the before-copy, and
+    // src < _stack_start + _stack_saved due to the first if condition
+    size_t nspilled = std::min<size_t>(n, this->_stack_start + 
this->_stack_saved - src);
+    memcpy(dest, this->stack_copy + (src - this->_stack_start), nspilled);
+    dest += nspilled;
+    src += nspilled;
+    n -= nspilled;
+    if (n > 0) {
+        // Copy the part after the saved stack
+        memcpy(dest, src, n);
+    }
+}
 
 }; // namespace greenlet
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/__init__.py 
new/greenlet-3.0.3/src/greenlet/__init__.py
--- old/greenlet-3.0.2/src/greenlet/__init__.py 2023-12-08 20:53:34.000000000 
+0100
+++ new/greenlet-3.0.3/src/greenlet/__init__.py 2023-12-21 22:57:40.000000000 
+0100
@@ -25,7 +25,7 @@
 ###
 # Metadata
 ###
-__version__ = '3.0.2'
+__version__ = '3.0.3'
 from ._greenlet import _C_API # pylint:disable=no-name-in-module
 
 ###
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/greenlet.cpp 
new/greenlet-3.0.3/src/greenlet/greenlet.cpp
--- old/greenlet-3.0.2/src/greenlet/greenlet.cpp        2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/greenlet.cpp        2023-12-21 
22:57:40.000000000 +0100
@@ -762,6 +762,7 @@
     return top_frame.acquire_or_None();
 }
 
+
 static PyObject*
 green_getstate(PyGreenlet* self)
 {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/greenlet_greenlet.hpp 
new/greenlet-3.0.3/src/greenlet/greenlet_greenlet.hpp
--- old/greenlet-3.0.2/src/greenlet/greenlet_greenlet.hpp       2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/greenlet_greenlet.hpp       2023-12-21 
22:57:40.000000000 +0100
@@ -117,11 +117,20 @@
         PyObject** datastack_top;
         PyObject** datastack_limit;
 #endif
-#if GREENLET_PY312
-        _PyInterpreterFrame* _prev_frame;
-#endif
+        // The PyInterpreterFrame list on 3.12+ contains some entries that are
+        // on the C stack, which can't be directly accessed while a greenlet is
+        // suspended. In order to keep greenlet gr_frame introspection working,
+        // we adjust stack switching to rewrite the interpreter frame list
+        // to skip these C-stack frames; we call this "exposing" the greenlet's
+        // frames because it makes them valid to work with in Python. Then when
+        // the greenlet is resumed we need to remember to reverse the operation
+        // we did. The C-stack frames are "entry frames" which are a low-level
+        // interpreter detail; they're not needed for introspection, but do
+        // need to be present for the eval loop to work.
+        void unexpose_frames();
 
     public:
+
         PythonState();
         // You can use this for testing whether we have a frame
         // or not. It returns const so they can't modify it.
@@ -137,6 +146,7 @@
 #if GREENLET_USE_CFRAME
         void set_new_cframe(_PyCFrame& frame) noexcept;
 #endif
+
         inline void may_switch_away() noexcept;
         inline void will_switch_from(PyThreadState *const origin_tstate) 
noexcept;
         void did_finish(PyThreadState* tstate) noexcept;
@@ -186,6 +196,14 @@
 #ifdef GREENLET_USE_STDIO
         friend std::ostream& operator<<(std::ostream& os, const StackState& s);
 #endif
+
+        // Fill in [dest, dest + n) with the values that would be at
+        // [src, src + n) while this greenlet is running. This is like memcpy
+        // except that if the greenlet is suspended it accounts for the portion
+        // of the greenlet's stack that was spilled to the heap. `src` may
+        // be on this greenlet's stack, or on the heap, but not on a different
+        // greenlet's stack.
+        void copy_from_stack(void* dest, const void* src, size_t n) const;
     };
 #ifdef GREENLET_USE_STDIO
     std::ostream& operator<<(std::ostream& os, const StackState& s);
@@ -377,6 +395,19 @@
         // was running in was known to have exited.
         void deallocing_greenlet_in_thread(const ThreadState* current_state);
 
+        // Must be called on 3.12+ before exposing a suspended greenlet's
+        // frames to user code. This rewrites the linked list of interpreter
+        // frames to skip the ones that are being stored on the C stack (which
+        // can't be safely accessed while the greenlet is suspended because
+        // that stack space might be hosting a different greenlet), and
+        // sets PythonState::frames_were_exposed so we remember to restore
+        // the original list before resuming the greenlet. The C-stack frames
+        // are a low-level interpreter implementation detail; while they're
+        // important to the bytecode eval loop, they're superfluous for
+        // introspection purposes.
+        void expose_frames();
+
+
         // TODO: Figure out how to make these non-public.
         inline void slp_restore_state() noexcept;
         inline int slp_save_state(char *const stackref) noexcept;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/greenlet-3.0.2/src/greenlet/tests/_test_extension_cpp.cpp 
new/greenlet-3.0.3/src/greenlet/tests/_test_extension_cpp.cpp
--- old/greenlet-3.0.2/src/greenlet/tests/_test_extension_cpp.cpp       
2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/tests/_test_extension_cpp.cpp       
2023-12-21 22:57:40.000000000 +0100
@@ -100,6 +100,15 @@
     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)
@@ -173,6 +182,12 @@
      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}
 };
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/tests/leakcheck.py 
new/greenlet-3.0.3/src/greenlet/tests/leakcheck.py
--- old/greenlet-3.0.2/src/greenlet/tests/leakcheck.py  2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/tests/leakcheck.py  2023-12-21 
22:57:40.000000000 +0100
@@ -220,6 +220,7 @@
 
     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:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/greenlet-3.0.2/src/greenlet/tests/test_contextvars.py 
new/greenlet-3.0.3/src/greenlet/tests/test_contextvars.py
--- old/greenlet-3.0.2/src/greenlet/tests/test_contextvars.py   2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/tests/test_contextvars.py   2023-12-21 
22:57:40.000000000 +0100
@@ -47,6 +47,7 @@
             callback()
 
     def _test_context(self, propagate_by):
+        # pylint:disable=too-many-branches
         ID_VAR.set(0)
 
         callback = getcurrent().switch
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/tests/test_gc.py 
new/greenlet-3.0.3/src/greenlet/tests/test_gc.py
--- old/greenlet-3.0.2/src/greenlet/tests/test_gc.py    2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/tests/test_gc.py    2023-12-21 
22:57:40.000000000 +0100
@@ -23,7 +23,7 @@
 
     def test_circular_greenlet(self):
         class circular_greenlet(greenlet.greenlet):
-            pass
+            self = None
         o = circular_greenlet()
         o.self = o
         o = weakref.ref(o)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/greenlet-3.0.2/src/greenlet/tests/test_generator_nested.py 
new/greenlet-3.0.3/src/greenlet/tests/test_generator_nested.py
--- old/greenlet-3.0.2/src/greenlet/tests/test_generator_nested.py      
2023-12-08 20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/tests/test_generator_nested.py      
2023-12-21 22:57:40.000000000 +0100
@@ -149,7 +149,7 @@
         # XXX Test to make sure we are working as a generator expression
 
     def test_genlet_simple(self):
-        for g in [g1, g2, g3]:
+        for g in g1, g2, g3:
             seen = []
             for _ in range(3):
                 for j in g(5, seen):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/tests/test_greenlet.py 
new/greenlet-3.0.3/src/greenlet/tests/test_greenlet.py
--- old/greenlet-3.0.2/src/greenlet/tests/test_greenlet.py      2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/tests/test_greenlet.py      2023-12-21 
22:57:40.000000000 +0100
@@ -18,6 +18,12 @@
 # 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
@@ -412,7 +418,7 @@
         class mygreenlet(RawGreenlet):
             def __getattribute__(self, name):
                 try:
-                    raise Exception()
+                    raise Exception # pylint:disable=broad-exception-raised
                 except: # pylint:disable=bare-except
                     pass
                 return RawGreenlet.__getattribute__(self, name)
@@ -840,23 +846,76 @@
         self.assertEqual(seen, [42, 24])
 
     def test_can_access_f_back_of_suspended_greenlet(self):
-        # On Python 3.12, they added a ->previous field to
-        # _PyInterpreterFrame that has to be cleared when a frame is inactive.
-        # If we got that wrong, this immediately crashes.
+        # 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 Hub():
-            main.switch()
+        def outer():
+            inner()
 
-        hub = RawGreenlet(Hub)
+        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)
+        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):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/tests/test_throw.py 
new/greenlet-3.0.3/src/greenlet/tests/test_throw.py
--- old/greenlet-3.0.2/src/greenlet/tests/test_throw.py 2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/tests/test_throw.py 2023-12-21 
22:57:40.000000000 +0100
@@ -67,8 +67,7 @@
                 main.switch("f1 ready to catch")
             except IndexError:
                 return "caught"
-            else:
-                return "normal exit"
+            return "normal exit"
 
         def f2():
             main.switch("from f2")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet/tests/test_weakref.py 
new/greenlet-3.0.3/src/greenlet/tests/test_weakref.py
--- old/greenlet-3.0.2/src/greenlet/tests/test_weakref.py       2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet/tests/test_weakref.py       2023-12-21 
22:57:40.000000000 +0100
@@ -1,6 +1,6 @@
 import gc
 import weakref
-import unittest
+
 
 import greenlet
 from . import TestCase
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet.egg-info/PKG-INFO 
new/greenlet-3.0.3/src/greenlet.egg-info/PKG-INFO
--- old/greenlet-3.0.2/src/greenlet.egg-info/PKG-INFO   2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet.egg-info/PKG-INFO   2023-12-21 
22:57:41.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: greenlet
-Version: 3.0.2
+Version: 3.0.3
 Summary: Lightweight in-process concurrent programming
 Home-page: https://greenlet.readthedocs.io/
 Author: Alexey Borzenkov
@@ -31,14 +31,11 @@
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Requires-Python: >=3.7
 Description-Content-Type: text/x-rst
+Provides-Extra: docs
+Provides-Extra: test
 License-File: LICENSE
 License-File: LICENSE.PSF
 License-File: AUTHORS
-Provides-Extra: docs
-Requires-Dist: Sphinx; extra == "docs"
-Provides-Extra: test
-Requires-Dist: objgraph; extra == "test"
-Requires-Dist: psutil; extra == "test"
 
 .. This file is included into docs/history.rst
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet.egg-info/SOURCES.txt 
new/greenlet-3.0.3/src/greenlet.egg-info/SOURCES.txt
--- old/greenlet-3.0.2/src/greenlet.egg-info/SOURCES.txt        2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet.egg-info/SOURCES.txt        2023-12-21 
22:57:41.000000000 +0100
@@ -1,5 +1,4 @@
 .clang-format
-.gitignore
 .pylintrc
 .readthedocs.yml
 AUTHORS
@@ -37,6 +36,7 @@
 docs/python_threads.rst
 docs/switching.rst
 docs/tracing.rst
+docs/_static/custom.css
 src/greenlet/TBrokenGreenlet.cpp
 src/greenlet/TExceptionState.cpp
 src/greenlet/TGreenlet.cpp
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.0.2/src/greenlet.egg-info/requires.txt 
new/greenlet-3.0.3/src/greenlet.egg-info/requires.txt
--- old/greenlet-3.0.2/src/greenlet.egg-info/requires.txt       2023-12-08 
20:53:34.000000000 +0100
+++ new/greenlet-3.0.3/src/greenlet.egg-info/requires.txt       2023-12-21 
22:57:41.000000000 +0100
@@ -1,6 +1,7 @@
 
 [docs]
 Sphinx
+furo
 
 [test]
 objgraph

Reply via email to