Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 27 additions & 12 deletions .github/workflows/reusable-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,6 @@ jobs:
--fail-if-regression \
--fail-if-improved \
--fail-if-new-news-nit
- name: 'Build EPUB documentation'
continue-on-error: true
run: |
set -Eeuo pipefail
make -C Doc/ PYTHON=../python SPHINXOPTS="--quiet" epub
pip install epubcheck
epubcheck Doc/build/epub/Python.epub &> Doc/epubcheck.txt
- name: 'Check for fatal errors in EPUB'
if: github.event_name == 'pull_request'
continue-on-error: true # until gh-136155 is fixed
run: |
python Doc/tools/check-epub.py

# Run "doctest" on HEAD as new syntax doesn't exist in the latest stable release
doctest:
Expand Down Expand Up @@ -114,3 +102,30 @@ jobs:
# Use "xvfb-run" since some doctest tests open GUI windows
- name: 'Run documentation doctest'
run: xvfb-run make -C Doc/ PYTHON=../python SPHINXERRORHANDLING="--fail-on-warning" doctest

check-epub:
name: 'Check EPUB'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: 'Set up Python'
uses: actions/setup-python@v5
with:
python-version: '3'
cache: 'pip'
cache-dependency-path: 'Doc/requirements.txt'
- name: 'Install build dependencies'
run: |
make -C Doc/ venv
python -m pip install epubcheck
- name: 'Build EPUB documentation'
run: make -C Doc/ PYTHON=../python epub
- name: 'Run epubcheck'
continue-on-error: true
run: epubcheck Doc/build/epub/Python.epub &> Doc/epubcheck.txt
- run: cat Doc/epubcheck.txt
- name: 'Check for fatal errors in EPUB'
run: python Doc/tools/check-epub.py
2 changes: 1 addition & 1 deletion Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ are always available. They are listed here in alphabetical order.
``pow(base, exp) % mod``). The two-argument form ``pow(base, exp)`` is
equivalent to using the power operator: ``base**exp``.

The arguments must have numeric types. With mixed operand types, the
When arguments are builtin numeric types with mixed operand types, the
coercion rules for binary arithmetic operators apply. For :class:`int`
operands, the result has the same type as the operands (after coercion)
unless the second argument is negative; in that case, all arguments are
Expand Down
10 changes: 5 additions & 5 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ provides backports of these new features to older versions of Python.

.. seealso::

`"Typing cheat sheet" <https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html>`_
`Typing cheat sheet <https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html>`_
A quick overview of type hints (hosted at the mypy docs)

"Type System Reference" section of `the mypy docs <https://mypy.readthedocs.io/en/stable/index.html>`_
Type System Reference section of `the mypy docs <https://mypy.readthedocs.io/en/stable/index.html>`_
The Python typing system is standardised via PEPs, so this reference
should broadly apply to most Python type checkers. (Some parts may still
be specific to mypy.)

`"Static Typing with Python" <https://typing.python.org/en/latest/>`_
`Static Typing with Python <https://typing.python.org/en/latest/>`_
Type-checker-agnostic documentation written by the community detailing
type system features, useful typing related tools and typing best
practices.
Expand All @@ -64,7 +64,7 @@ Specification for the Python Type System
========================================

The canonical, up-to-date specification of the Python type system can be
found at `"Specification for the Python type system" <https://typing.python.org/en/latest/spec/index.html>`_.
found at `Specification for the Python type system <https://typing.python.org/en/latest/spec/index.html>`_.

.. _type-aliases:

Expand Down Expand Up @@ -2573,7 +2573,7 @@ types.
at runtime as soon as the class has been created. Monkey-patching
attributes onto a runtime-checkable protocol will still work, but will
have no impact on :func:`isinstance` checks comparing objects to the
protocol. See :ref:`"What's new in Python 3.12" <whatsnew-typing-py312>`
protocol. See :ref:`What's new in Python 3.12 <whatsnew-typing-py312>`
for more details.


Expand Down
36 changes: 21 additions & 15 deletions Doc/tools/check-epub.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import sys
from pathlib import Path

CPYTHON_ROOT = Path(
__file__, # cpython/Doc/tools/check-epub.py
'..', # cpython/Doc/tools
'..', # cpython/Doc
'..', # cpython
).resolve()
EPUBCHECK_PATH = CPYTHON_ROOT / 'Doc' / 'epubcheck.txt'

def main() -> int:
wrong_directory_msg = "Must run this script from the repo root"
if not Path("Doc").exists() or not Path("Doc").is_dir():
raise RuntimeError(wrong_directory_msg)

with Path("Doc/epubcheck.txt").open(encoding="UTF-8") as f:
messages = [message.split(" - ") for message in f.read().splitlines()]

fatal_errors = [message for message in messages if message[0] == "FATAL"]
def main() -> int:
lines = EPUBCHECK_PATH.read_text(encoding='utf-8').splitlines()
fatal_errors = [line for line in lines if line.startswith('FATAL')]

if fatal_errors:
print("\nError: must not contain fatal errors:\n")
for error in fatal_errors:
print(" - ".join(error))
err_count = len(fatal_errors)
s = 's' * (err_count != 1)
print()
print(f'Error: epubcheck reported {err_count} fatal error{s}:')
print()
print('\n'.join(fatal_errors))
return 1

return len(fatal_errors)
print('Success: no fatal errors found.')
return 0


if __name__ == "__main__":
sys.exit(main())
if __name__ == '__main__':
raise SystemExit(main())
1 change: 1 addition & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ struct _ceval_runtime_state {
struct trampoline_api_st trampoline_api;
FILE *map_file;
Py_ssize_t persist_after_fork;
_PyFrameEvalFunction prev_eval_frame;
#else
int _not_used;
#endif
Expand Down
26 changes: 16 additions & 10 deletions InternalDocs/garbage_collector.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,15 +329,16 @@ Once the GC knows the list of unreachable objects, a very delicate process start
with the objective of completely destroying these objects. Roughly, the process
follows these steps in order:

1. Handle and clear weak references (if any). Weak references to unreachable objects
are set to `None`. If the weak reference has an associated callback, the callback
is enqueued to be called once the clearing of weak references is finished. We only
invoke callbacks for weak references that are themselves reachable. If both the weak
reference and the pointed-to object are unreachable we do not execute the callback.
This is partly for historical reasons: the callback could resurrect an unreachable
object and support for weak references predates support for object resurrection.
Ignoring the weak reference's callback is fine because both the object and the weakref
are going away, so it's legitimate to say the weak reference is going away first.
1. Handle weak references with callbacks (if any). If the weak reference has
an associated callback, the callback is enqueued to be called after the weak
reference is cleared. We only invoke callbacks for weak references that
are themselves reachable. If both the weak reference and the pointed-to
object are unreachable we do not execute the callback. This is partly for
historical reasons: the callback could resurrect an unreachable object
and support for weak references predates support for object resurrection.
Ignoring the weak reference's callback is fine because both the object and
the weakref are going away, so it's legitimate to say the weak reference is
going away first.
2. If an object has legacy finalizers (`tp_del` slot) move it to the
`gc.garbage` list.
3. Call the finalizers (`tp_finalize` slot) and mark the objects as already
Expand All @@ -346,7 +347,12 @@ follows these steps in order:
4. Deal with resurrected objects. If some objects have been resurrected, the GC
finds the new subset of objects that are still unreachable by running the cycle
detection algorithm again and continues with them.
5. Call the `tp_clear` slot of every object so all internal links are broken and
5. Clear any weak references that still refer to unreachable objects. The
`wr_object` attribute for these weakrefs are set to `None`. Note that some
of these weak references maybe have been newly created during the running of
finalizers in step 3. Also, clear any weak references that are part of the
unreachable set.
6. Call the `tp_clear` slot of every object so all internal links are broken and
the reference counts fall to 0, triggering the destruction of all unreachable
objects.

Expand Down
46 changes: 30 additions & 16 deletions Lib/test/test_finalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def test_simple(self):
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors([])
self.assertIs(wr(), None)
self.assertIsNone(wr())
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors([])
Expand All @@ -188,12 +188,12 @@ def test_simple_resurrect(self):
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors(ids)
self.assertIsNot(wr(), None)
self.assertIsNotNone(wr())
self.clear_survivors()
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors([])
self.assertIs(wr(), None)
self.assertIsNone(wr())

@support.cpython_only
def test_non_gc(self):
Expand Down Expand Up @@ -265,7 +265,7 @@ def test_simple(self):
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors([])
self.assertIs(wr(), None)
self.assertIsNone(wr())
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors([])
Expand All @@ -276,19 +276,24 @@ def test_simple_resurrect(self):
s = SelfCycleResurrector()
ids = [id(s)]
wr = weakref.ref(s)
wrc = weakref.ref(s, lambda x: None)
del s
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors(ids)
# XXX is this desirable?
self.assertIs(wr(), None)
# This used to be None because weakrefs were cleared before
# calling finalizers. Now they are cleared after.
self.assertIsNotNone(wr())
# A weakref with a callback is still cleared before calling
# finalizers.
self.assertIsNone(wrc())
# When trying to destroy the object a second time, __del__
# isn't called anymore (and the object isn't resurrected).
self.clear_survivors()
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors([])
self.assertIs(wr(), None)
self.assertIsNone(wr())

def test_simple_suicide(self):
# Test the GC is able to deal with an object that kills its last
Expand All @@ -301,11 +306,11 @@ def test_simple_suicide(self):
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors([])
self.assertIs(wr(), None)
self.assertIsNone(wr())
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors([])
self.assertIs(wr(), None)
self.assertIsNone(wr())


class ChainedBase:
Expand Down Expand Up @@ -378,18 +383,27 @@ def check_non_resurrecting_chain(self, classes):

def check_resurrecting_chain(self, classes):
N = len(classes)
def dummy_callback(ref):
pass
with SimpleBase.test():
nodes = self.build_chain(classes)
N = len(nodes)
ids = [id(s) for s in nodes]
survivor_ids = [id(s) for s in nodes if isinstance(s, SimpleResurrector)]
wrs = [weakref.ref(s) for s in nodes]
wrcs = [weakref.ref(s, dummy_callback) for s in nodes]
del nodes
gc.collect()
self.assert_del_calls(ids)
self.assert_survivors(survivor_ids)
# XXX desirable?
self.assertEqual([wr() for wr in wrs], [None] * N)
for wr in wrs:
# These values used to be None because weakrefs were cleared
# before calling finalizers. Now they are cleared after.
self.assertIsNotNone(wr())
for wr in wrcs:
# Weakrefs with callbacks are still cleared before calling
# finalizers.
self.assertIsNone(wr())
self.clear_survivors()
gc.collect()
self.assert_del_calls(ids)
Expand Down Expand Up @@ -491,7 +505,7 @@ def test_legacy(self):
self.assert_del_calls(ids)
self.assert_tp_del_calls(ids)
self.assert_survivors([])
self.assertIs(wr(), None)
self.assertIsNone(wr())
gc.collect()
self.assert_del_calls(ids)
self.assert_tp_del_calls(ids)
Expand All @@ -507,13 +521,13 @@ def test_legacy_resurrect(self):
self.assert_tp_del_calls(ids)
self.assert_survivors(ids)
# weakrefs are cleared before tp_del is called.
self.assertIs(wr(), None)
self.assertIsNone(wr())
self.clear_survivors()
gc.collect()
self.assert_del_calls(ids)
self.assert_tp_del_calls(ids * 2)
self.assert_survivors(ids)
self.assertIs(wr(), None)
self.assertIsNone(wr())

def test_legacy_self_cycle(self):
# Self-cycles with legacy finalizers end up in gc.garbage.
Expand All @@ -527,11 +541,11 @@ def test_legacy_self_cycle(self):
self.assert_tp_del_calls([])
self.assert_survivors([])
self.assert_garbage(ids)
self.assertIsNot(wr(), None)
self.assertIsNotNone(wr())
# Break the cycle to allow collection
gc.garbage[0].ref = None
self.assert_garbage([])
self.assertIs(wr(), None)
self.assertIsNone(wr())


if __name__ == "__main__":
Expand Down
Loading
Loading