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