| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -28,6 +28,7 @@ | |||
| 28 | 28 | 'changes', | |
| 29 | 29 | 'glossary_search', | |
| 30 | 30 | 'lexers', | |
| 31 | + 'pydoc_topics', | ||
| 31 | 32 | 'pyspecific', | |
| 32 | 33 | 'sphinx.ext.coverage', | |
| 33 | 34 | 'sphinx.ext.doctest', | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,16 @@ | |||
| 1 | + Pending removal in Python 3.18 | ||
| 2 | + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
| 3 | + | ||
| 4 | + * Deprecated private functions (:gh:`128863`): | ||
| 5 | + | ||
| 6 | + * :c:func:`!_PyBytes_Join`: use :c:func:`PyBytes_Join`. | ||
| 7 | + * :c:func:`!_PyDict_GetItemStringWithError`: use :c:func:`PyDict_GetItemStringRef`. | ||
| 8 | + * :c:func:`!_PyDict_Pop()`: :c:func:`PyDict_Pop`. | ||
| 9 | + * :c:func:`!_PyThreadState_UncheckedGet`: use :c:func:`PyThreadState_GetUnchecked`. | ||
| 10 | + * :c:func:`!_PyUnicode_AsString`: use :c:func:`PyUnicode_AsUTF8`. | ||
| 11 | + * :c:func:`!_Py_HashPointer`: use :c:func:`Py_HashPointer`. | ||
| 12 | + * :c:func:`!_Py_fopen_obj`: use :c:func:`Py_fopen`. | ||
| 13 | + | ||
| 14 | + The `pythoncapi-compat project | ||
| 15 | + <https://github.com/python/pythoncapi-compat/>`__ can be used to get these | ||
| 16 | + new public functions on Python 3.13 and older. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,145 @@ | |||
| 1 | + .. currentmodule:: asyncio | ||
| 2 | + | ||
| 3 | + | ||
| 4 | + .. _asyncio-graph: | ||
| 5 | + | ||
| 6 | + ======================== | ||
| 7 | + Call Graph Introspection | ||
| 8 | + ======================== | ||
| 9 | + | ||
| 10 | + **Source code:** :source:`Lib/asyncio/graph.py` | ||
| 11 | + | ||
| 12 | + ------------------------------------- | ||
| 13 | + | ||
| 14 | + asyncio has powerful runtime call graph introspection utilities | ||
| 15 | + to trace the entire call graph of a running *coroutine* or *task*, or | ||
| 16 | + a suspended *future*. These utilities and the underlying machinery | ||
| 17 | + can be used from within a Python program or by external profilers | ||
| 18 | + and debuggers. | ||
| 19 | + | ||
| 20 | + .. versionadded:: next | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + .. function:: print_call_graph(future=None, /, *, file=None, depth=1, limit=None) | ||
| 24 | + | ||
| 25 | + Print the async call graph for the current task or the provided | ||
| 26 | + :class:`Task` or :class:`Future`. | ||
| 27 | + | ||
| 28 | + This function prints entries starting from the top frame and going | ||
| 29 | + down towards the invocation point. | ||
| 30 | + | ||
| 31 | + The function receives an optional *future* argument. | ||
| 32 | + If not passed, the current running task will be used. | ||
| 33 | + | ||
| 34 | + If the function is called on *the current task*, the optional | ||
| 35 | + keyword-only *depth* argument can be used to skip the specified | ||
| 36 | + number of frames from top of the stack. | ||
| 37 | + | ||
| 38 | + If the optional keyword-only *limit* argument is provided, each call stack | ||
| 39 | + in the resulting graph is truncated to include at most ``abs(limit)`` | ||
| 40 | + entries. If *limit* is positive, the entries left are the closest to | ||
| 41 | + the invocation point. If *limit* is negative, the topmost entries are | ||
| 42 | + left. If *limit* is omitted or ``None``, all entries are present. | ||
| 43 | + If *limit* is ``0``, the call stack is not printed at all, only | ||
| 44 | + "awaited by" information is printed. | ||
| 45 | + | ||
| 46 | + If *file* is omitted or ``None``, the function will print | ||
| 47 | + to :data:`sys.stdout`. | ||
| 48 | + | ||
| 49 | + **Example:** | ||
| 50 | + | ||
| 51 | + The following Python code: | ||
| 52 | + | ||
| 53 | + .. code-block:: python | ||
| 54 | + | ||
| 55 | + import asyncio | ||
| 56 | + | ||
| 57 | + async def test(): | ||
| 58 | + asyncio.print_call_graph() | ||
| 59 | + | ||
| 60 | + async def main(): | ||
| 61 | + async with asyncio.TaskGroup() as g: | ||
| 62 | + g.create_task(test()) | ||
| 63 | + | ||
| 64 | + asyncio.run(main()) | ||
| 65 | + | ||
| 66 | + will print:: | ||
| 67 | + | ||
| 68 | + * Task(name='Task-2', id=0x1039f0fe0) | ||
| 69 | + + Call stack: | ||
| 70 | + | File 't2.py', line 4, in async test() | ||
| 71 | + + Awaited by: | ||
| 72 | + * Task(name='Task-1', id=0x103a5e060) | ||
| 73 | + + Call stack: | ||
| 74 | + | File 'taskgroups.py', line 107, in async TaskGroup.__aexit__() | ||
| 75 | + | File 't2.py', line 7, in async main() | ||
| 76 | + | ||
| 77 | + .. function:: format_call_graph(future=None, /, *, depth=1, limit=None) | ||
| 78 | + | ||
| 79 | + Like :func:`print_call_graph`, but returns a string. | ||
| 80 | + If *future* is ``None`` and there's no current task, | ||
| 81 | + the function returns an empty string. | ||
| 82 | + | ||
| 83 | + | ||
| 84 | + .. function:: capture_call_graph(future=None, /, *, depth=1, limit=None) | ||
| 85 | + | ||
| 86 | + Capture the async call graph for the current task or the provided | ||
| 87 | + :class:`Task` or :class:`Future`. | ||
| 88 | + | ||
| 89 | + The function receives an optional *future* argument. | ||
| 90 | + If not passed, the current running task will be used. If there's no | ||
| 91 | + current task, the function returns ``None``. | ||
| 92 | + | ||
| 93 | + If the function is called on *the current task*, the optional | ||
| 94 | + keyword-only *depth* argument can be used to skip the specified | ||
| 95 | + number of frames from top of the stack. | ||
| 96 | + | ||
| 97 | + Returns a ``FutureCallGraph`` data class object: | ||
| 98 | + | ||
| 99 | + * ``FutureCallGraph(future, call_stack, awaited_by)`` | ||
| 100 | + | ||
| 101 | + Where *future* is a reference to a :class:`Future` or | ||
| 102 | + a :class:`Task` (or their subclasses.) | ||
| 103 | + | ||
| 104 | + ``call_stack`` is a tuple of ``FrameCallGraphEntry`` objects. | ||
| 105 | + | ||
| 106 | + ``awaited_by`` is a tuple of ``FutureCallGraph`` objects. | ||
| 107 | + | ||
| 108 | + * ``FrameCallGraphEntry(frame)`` | ||
| 109 | + | ||
| 110 | + Where *frame* is a frame object of a regular Python function | ||
| 111 | + in the call stack. | ||
| 112 | + | ||
| 113 | + | ||
| 114 | + Low level utility functions | ||
| 115 | + =========================== | ||
| 116 | + | ||
| 117 | + To introspect an async call graph asyncio requires cooperation from | ||
| 118 | + control flow structures, such as :func:`shield` or :class:`TaskGroup`. | ||
| 119 | + Any time an intermediate :class:`Future` object with low-level APIs like | ||
| 120 | + :meth:`Future.add_done_callback() <asyncio.Future.add_done_callback>` is | ||
| 121 | + involved, the following two functions should be used to inform asyncio | ||
| 122 | + about how exactly such intermediate future objects are connected with | ||
| 123 | + the tasks they wrap or control. | ||
| 124 | + | ||
| 125 | + | ||
| 126 | + .. function:: future_add_to_awaited_by(future, waiter, /) | ||
| 127 | + | ||
| 128 | + Record that *future* is awaited on by *waiter*. | ||
| 129 | + | ||
| 130 | + Both *future* and *waiter* must be instances of | ||
| 131 | + :class:`Future` or :class:`Task` or their subclasses, | ||
| 132 | + otherwise the call would have no effect. | ||
| 133 | + | ||
| 134 | + A call to ``future_add_to_awaited_by()`` must be followed by an | ||
| 135 | + eventual call to the :func:`future_discard_from_awaited_by` function | ||
| 136 | + with the same arguments. | ||
| 137 | + | ||
| 138 | + | ||
| 139 | + .. function:: future_discard_from_awaited_by(future, waiter, /) | ||
| 140 | + | ||
| 141 | + Record that *future* is no longer awaited on by *waiter*. | ||
| 142 | + | ||
| 143 | + Both *future* and *waiter* must be instances of | ||
| 144 | + :class:`Future` or :class:`Task` or their subclasses, otherwise | ||
| 145 | + the call would have no effect. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -99,6 +99,7 @@ You can experiment with an ``asyncio`` concurrent context in the :term:`REPL`: | |||
| 99 | 99 | asyncio-subprocess.rst | |
| 100 | 100 | asyncio-queue.rst | |
| 101 | 101 | asyncio-exceptions.rst | |
| 102 | + asyncio-graph.rst | ||
| 102 | 103 | ||
| 103 | 104 | .. toctree:: | |
| 104 | 105 | :caption: Low-level APIs | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -150,6 +150,12 @@ attributes (see :ref:`import-mod-attrs` for module attributes): | |||
| 150 | 150 | | | f_locals | local namespace seen by | | |
| 151 | 151 | | | | this frame | | |
| 152 | 152 | +-----------------+-------------------+---------------------------+ | |
| 153 | + | | f_generator | returns the generator or | | ||
| 154 | + | | | coroutine object that | | ||
| 155 | + | | | owns this frame, or | | ||
| 156 | + | | | ``None`` if the frame is | | ||
| 157 | + | | | of a regular function | | ||
| 158 | + +-----------------+-------------------+---------------------------+ | ||
| 153 | 159 | | | f_trace | tracing function for this | | |
| 154 | 160 | | | | frame, or ``None`` | | |
| 155 | 161 | +-----------------+-------------------+---------------------------+ | |
@@ -310,6 +316,10 @@ attributes (see :ref:`import-mod-attrs` for module attributes): | |||
| 310 | 316 | ||
| 311 | 317 | Add ``__builtins__`` attribute to functions. | |
| 312 | 318 | ||
| 319 | + .. versionchanged:: next | ||
| 320 | + | ||
| 321 | + Add ``f_generator`` attribute to frames. | ||
| 322 | + | ||
| 313 | 323 | .. function:: getmembers(object[, predicate]) | |
| 314 | 324 | ||
| 315 | 325 | Return all the members of an object in a list of ``(name, value)`` | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,187 @@ | |||
| 1 | + """Support for building "topic help" for pydoc.""" | ||
| 2 | + | ||
| 3 | + from __future__ import annotations | ||
| 4 | + | ||
| 5 | + from time import asctime | ||
| 6 | + from typing import TYPE_CHECKING | ||
| 7 | + | ||
| 8 | + from sphinx.builders.text import TextBuilder | ||
| 9 | + from sphinx.util import logging | ||
| 10 | + from sphinx.util.display import status_iterator | ||
| 11 | + from sphinx.util.docutils import new_document | ||
| 12 | + from sphinx.writers.text import TextTranslator | ||
| 13 | + | ||
| 14 | + if TYPE_CHECKING: | ||
| 15 | + from collections.abc import Sequence, Set | ||
| 16 | + | ||
| 17 | + from sphinx.application import Sphinx | ||
| 18 | + from sphinx.util.typing import ExtensionMetadata | ||
| 19 | + | ||
| 20 | + logger = logging.getLogger(__name__) | ||
| 21 | + | ||
| 22 | + _PYDOC_TOPIC_LABELS: Sequence[str] = sorted({ | ||
| 23 | + "assert", | ||
| 24 | + "assignment", | ||
| 25 | + "assignment-expressions", | ||
| 26 | + "async", | ||
| 27 | + "atom-identifiers", | ||
| 28 | + "atom-literals", | ||
| 29 | + "attribute-access", | ||
| 30 | + "attribute-references", | ||
| 31 | + "augassign", | ||
| 32 | + "await", | ||
| 33 | + "binary", | ||
| 34 | + "bitwise", | ||
| 35 | + "bltin-code-objects", | ||
| 36 | + "bltin-ellipsis-object", | ||
| 37 | + "bltin-null-object", | ||
| 38 | + "bltin-type-objects", | ||
| 39 | + "booleans", | ||
| 40 | + "break", | ||
| 41 | + "callable-types", | ||
| 42 | + "calls", | ||
| 43 | + "class", | ||
| 44 | + "comparisons", | ||
| 45 | + "compound", | ||
| 46 | + "context-managers", | ||
| 47 | + "continue", | ||
| 48 | + "conversions", | ||
| 49 | + "customization", | ||
| 50 | + "debugger", | ||
| 51 | + "del", | ||
| 52 | + "dict", | ||
| 53 | + "dynamic-features", | ||
| 54 | + "else", | ||
| 55 | + "exceptions", | ||
| 56 | + "execmodel", | ||
| 57 | + "exprlists", | ||
| 58 | + "floating", | ||
| 59 | + "for", | ||
| 60 | + "formatstrings", | ||
| 61 | + "function", | ||
| 62 | + "global", | ||
| 63 | + "id-classes", | ||
| 64 | + "identifiers", | ||
| 65 | + "if", | ||
| 66 | + "imaginary", | ||
| 67 | + "import", | ||
| 68 | + "in", | ||
| 69 | + "integers", | ||
| 70 | + "lambda", | ||
| 71 | + "lists", | ||
| 72 | + "naming", | ||
| 73 | + "nonlocal", | ||
| 74 | + "numbers", | ||
| 75 | + "numeric-types", | ||
| 76 | + "objects", | ||
| 77 | + "operator-summary", | ||
| 78 | + "pass", | ||
| 79 | + "power", | ||
| 80 | + "raise", | ||
| 81 | + "return", | ||
| 82 | + "sequence-types", | ||
| 83 | + "shifting", | ||
| 84 | + "slicings", | ||
| 85 | + "specialattrs", | ||
| 86 | + "specialnames", | ||
| 87 | + "string-methods", | ||
| 88 | + "strings", | ||
| 89 | + "subscriptions", | ||
| 90 | + "truth", | ||
| 91 | + "try", | ||
| 92 | + "types", | ||
| 93 | + "typesfunctions", | ||
| 94 | + "typesmapping", | ||
| 95 | + "typesmethods", | ||
| 96 | + "typesmodules", | ||
| 97 | + "typesseq", | ||
| 98 | + "typesseq-mutable", | ||
| 99 | + "unary", | ||
| 100 | + "while", | ||
| 101 | + "with", | ||
| 102 | + "yield", | ||
| 103 | + }) | ||
| 104 | + | ||
| 105 | + | ||
| 106 | + class PydocTopicsBuilder(TextBuilder): | ||
| 107 | + name = "pydoc-topics" | ||
| 108 | + | ||
| 109 | + def init(self) -> None: | ||
| 110 | + super().init() | ||
| 111 | + self.topics: dict[str, str] = {} | ||
| 112 | + | ||
| 113 | + def get_outdated_docs(self) -> str: | ||
| 114 | + # Return a string describing what an update build will build. | ||
| 115 | + return "all pydoc topics" | ||
| 116 | + | ||
| 117 | + def write_documents(self, _docnames: Set[str]) -> None: | ||
| 118 | + env = self.env | ||
| 119 | + | ||
| 120 | + labels: dict[str, tuple[str, str, str]] | ||
| 121 | + labels = env.domains.standard_domain.labels | ||
| 122 | + | ||
| 123 | + # docname -> list of (topic_label, label_id) pairs | ||
| 124 | + doc_labels: dict[str, list[tuple[str, str]]] = {} | ||
| 125 | + for topic_label in _PYDOC_TOPIC_LABELS: | ||
| 126 | + try: | ||
| 127 | + docname, label_id, _section_name = labels[topic_label] | ||
| 128 | + except KeyError: | ||
| 129 | + logger.warning("label %r not in documentation", topic_label) | ||
| 130 | + continue | ||
| 131 | + doc_labels.setdefault(docname, []).append((topic_label, label_id)) | ||
| 132 | + | ||
| 133 | + for docname, label_ids in status_iterator( | ||
| 134 | + doc_labels.items(), | ||
| 135 | + "building topics... ", | ||
| 136 | + length=len(doc_labels), | ||
| 137 | + stringify_func=_display_labels, | ||
| 138 | + ): | ||
| 139 | + doctree = env.get_and_resolve_doctree(docname, builder=self) | ||
| 140 | + doc_ids = doctree.ids | ||
| 141 | + for topic_label, label_id in label_ids: | ||
| 142 | + document = new_document("<section node>") | ||
| 143 | + document.append(doc_ids[label_id]) | ||
| 144 | + visitor = TextTranslator(document, builder=self) | ||
| 145 | + document.walkabout(visitor) | ||
| 146 | + self.topics[topic_label] = visitor.body | ||
| 147 | + | ||
| 148 | + def finish(self) -> None: | ||
| 149 | + topics_repr = "\n".join( | ||
| 150 | + f" '{topic}': {_repr(self.topics[topic])}," | ||
| 151 | + for topic in sorted(self.topics) | ||
| 152 | + ) | ||
| 153 | + topics = f"""\ | ||
| 154 | + # Autogenerated by Sphinx on {asctime()} | ||
| 155 | + # as part of the release process. | ||
| 156 | + | ||
| 157 | + topics = {{ | ||
| 158 | + {topics_repr} | ||
| 159 | + }} | ||
| 160 | + """ | ||
| 161 | + self.outdir.joinpath("topics.py").write_text(topics, encoding="utf-8") | ||
| 162 | + | ||
| 163 | + | ||
| 164 | + def _display_labels(item: tuple[str, Sequence[tuple[str, str]]]) -> str: | ||
| 165 | + _docname, label_ids = item | ||
| 166 | + labels = [name for name, _id in label_ids] | ||
| 167 | + if len(labels) > 4: | ||
| 168 | + return f"{labels[0]}, {labels[1]}, ..., {labels[-2]}, {labels[-1]}" | ||
| 169 | + return ", ".join(labels) | ||
| 170 | + | ||
| 171 | + | ||
| 172 | + def _repr(text: str, /) -> str: | ||
| 173 | + """Return a triple-single-quoted representation of text.""" | ||
| 174 | + if "'''" not in text: | ||
| 175 | + return f"r'''{text}'''" | ||
| 176 | + text = text.replace("\\", "\\\\").replace("'''", r"\'\'\'") | ||
| 177 | + return f"'''{text}'''" | ||
| 178 | + | ||
| 179 | + | ||
| 180 | + def setup(app: Sphinx) -> ExtensionMetadata: | ||
| 181 | + app.add_builder(PydocTopicsBuilder) | ||
| 182 | + | ||
| 183 | + return { | ||
| 184 | + "version": "1.0", | ||
| 185 | + "parallel_read_safe": True, | ||
| 186 | + "parallel_write_safe": True, | ||
| 187 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments