https://github.com/python/cpython/commit/2f2c2ced6b2aff74b41115d45e1713266f3e94fe
commit: 2f2c2ced6b2aff74b41115d45e1713266f3e94fe
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-19T11:38:52+03:00
summary:

gh-155263: Add the --list option to Argument Clinic (GH-155264)

It prints the modules, classes and functions which Argument Clinic
defines in the specified files, each function with its signature.

files:
A Misc/NEWS.d/next/Tools-Demos/2026-08-06-08-32-53.gh-issue-155263.RCcjtk.rst
M Lib/test/test_clinic.py
M Tools/clinic/libclinic/cli.py
M Tools/clinic/libclinic/dsl_parser.py
M Tools/clinic/libclinic/function.py

diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index 3bf654371b7364..d3e4454348d023 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -3681,6 +3681,163 @@ def test_cli_converters_no_converters(self):
                 f.write("/*[clinic input]\n[clinic start generated code]*/\n")
             self.assertEqual(self.expect_success("--converters", fn), "")
 
+    LIST_CODE = dedent("""
+        /*[clinic input]
+        func
+            a: int
+            /
+
+        Docstring.
+        [clinic start generated code]*/
+
+        /*[clinic input]
+        cloned = func
+        [clinic start generated code]*/
+
+        /*[clinic input]
+        module m
+        class m.C "void *" ""
+        class m.C.D "void *" ""
+        [clinic start generated code]*/
+
+        /*[clinic input]
+        m.C.meth
+            self: self(type="void *")
+            a: object
+            [
+            b: object
+            ]
+            /
+
+        Docstring.
+        [clinic start generated code]*/
+
+        /*[clinic input]
+        @classmethod
+        m.C.__new__
+            a: object
+
+        Docstring.
+        [clinic start generated code]*/
+
+        /*[clinic input]
+        @getter
+        m.C.prop
+        [clinic start generated code]*/
+
+        /*[clinic input]
+        @setter
+        m.C.prop
+        [clinic start generated code]*/
+
+        /*[clinic input]
+        m.C.D.meth
+            self: self(type="void *")
+
+        Docstring.
+        [clinic start generated code]*/
+    """)
+
+    def make_list_file(self, tmp_dir):
+        fn = os.path.join(tmp_dir, "test.c")
+        with open(fn, "w", encoding="utf-8") as f:
+            f.write(self.LIST_CODE)
+        return fn
+
+    LIST_OUTPUT = [
+        "  func($module, a, /)",
+        "  cloned($module, a, /)",
+        "  module m",
+        "    class m.C",
+        # A signature with an option group is only for the docstring.
+        "      m.C.meth(a, [b])",
+        "      m.C(a)",
+        "      getter m.C.prop",
+        "      setter m.C.prop",
+        "      class m.C.D",
+        "        m.C.D.meth($self, /)",
+    ]
+
+    def test_cli_list(self):
+        with os_helper.temp_dir() as tmp_dir:
+            fn = self.make_list_file(tmp_dir)
+            pre_mtime = os.stat(fn).st_mtime_ns
+            out = self.expect_success("--list", fn)
+            self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
+            # Nothing is written.
+            with open(fn, encoding="utf-8") as f:
+                self.assertEqual(f.read(), self.LIST_CODE)
+            self.assertEqual(os.stat(fn).st_mtime_ns, pre_mtime)
+            self.assertEqual(os.listdir(tmp_dir), ["test.c"])
+
+    def test_cli_list_no_clinic_block(self):
+        with os_helper.temp_dir() as tmp_dir:
+            fn = os.path.join(tmp_dir, "test.c")
+            with open(fn, "w", encoding="utf-8") as f:
+                f.write("int x;\n")
+            self.assertEqual(self.expect_success("--list", fn), "")
+
+    def test_cli_list_no_definitions(self):
+        with os_helper.temp_dir() as tmp_dir:
+            fn = os.path.join(tmp_dir, "test.c")
+            with open(fn, "w", encoding="utf-8") as f:
+                f.write("/*[clinic input]\n[clinic start generated code]*/\n")
+            self.assertEqual(self.expect_success("--list", fn), "")
+
+    def test_cli_list_make(self):
+        with os_helper.temp_dir() as tmp_dir:
+            fn = self.make_list_file(tmp_dir)
+            out = self.expect_success("--list", "--make", "--srcdir", tmp_dir)
+            self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
+            self.assertEqual(os.listdir(tmp_dir), ["test.c"])
+
+    def test_cli_list_verbose(self):
+        with os_helper.temp_dir() as tmp_dir:
+            fn = self.make_list_file(tmp_dir)
+            # The progress does not mix with the report.
+            out, err, code = self.run_clinic("-v", "--list", fn)
+            self.assertEqual(code, 0)
+            self.assertEqual(err.splitlines(), [fn])
+            self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
+
+    def test_cli_list_checksum_mismatch(self):
+        with os_helper.temp_dir() as tmp_dir:
+            fn = self.make_list_file(tmp_dir)
+            with open(fn, "a", encoding="utf-8") as f:
+                f.write("/*[clinic end generated code: "
+                        "output=0123456789abcdef input=fedcba9876543210]*/\n")
+            _, err = self.expect_failure("--list", fn)
+            self.assertIn("Checksum mismatch!", err)
+            # The check is skipped with --force.
+            out = self.expect_success("-f", "--list", fn)
+            self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
+            self.assertEqual(os.listdir(tmp_dir), ["test.c"])
+
+    def test_cli_list_external(self):
+        # A file which uses getters, setters and nested classes.
+        source = support.findfile('clinic.test.c')
+        out = self.expect_success("--list", source)
+        lines = out.splitlines()
+        self.assertEqual(lines[0], source)
+        for line in ("  class Test",
+                     "    getter Test.property",
+                     "    setter Test.property",
+                     "    Test.class_method($type, /)",
+                     "  module m",
+                     "    class m.T"):
+            with self.subTest(line=line):
+                self.assertIn(line, lines)
+
+    def test_cli_fail_list_and_dry_run(self):
+        for opt in "--dry-run", "--diff":
+            with self.subTest(opt=opt):
+                _, err = self.expect_failure("--list", opt, "test.c")
+                self.assertIn("can't use --dry-run or --diff with --list", err)
+
+    def test_cli_fail_list_and_converters(self):
+        _, err = self.expect_failure("--list", "--converters", "test.c")
+        self.assertIn("can't use --converters with --list", err)
+
     def test_cli_fail_directory(self):
         with os_helper.temp_dir() as tmp_dir:
             subdir = os.path.join(tmp_dir, "test.c")
diff --git 
a/Misc/NEWS.d/next/Tools-Demos/2026-08-06-08-32-53.gh-issue-155263.RCcjtk.rst 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-06-08-32-53.gh-issue-155263.RCcjtk.rst
new file mode 100644
index 00000000000000..969efa1af808f5
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-06-08-32-53.gh-issue-155263.RCcjtk.rst
@@ -0,0 +1,3 @@
+Add the ``--list`` option to Argument Clinic.
+It prints the modules, classes and functions which Argument Clinic defines in
+the specified files, each function with its signature.
diff --git a/Tools/clinic/libclinic/cli.py b/Tools/clinic/libclinic/cli.py
index 290fc3a6e59408..9629c173177945 100644
--- a/Tools/clinic/libclinic/cli.py
+++ b/Tools/clinic/libclinic/cli.py
@@ -22,6 +22,9 @@
     return_converters, ReturnConverterType)
 from libclinic.clanguage import CLanguage
 from libclinic.app import Clinic
+from libclinic.dsl_parser import render_text_signature
+from libclinic.function import (
+    Class, Definition, Module, GETTER, SETTER, walk_definitions)
 
 
 # TODO:
@@ -54,7 +57,7 @@ def parse_file(
         output: str | None = None,
         verify: bool = True,
         writer: libclinic.FileWriter | None = None,
-) -> None:
+) -> Clinic | None:
     if not output:
         output = filename
     if writer is None:
@@ -78,7 +81,7 @@ def parse_file(
     # exit quickly if there are no clinic markers in the file
     find_start_re = BlockParser("", language).find_start_re
     if not find_start_re.search(raw):
-        return
+        return None
 
     if LIMITED_CAPI_REGEX.search(raw):
         limited_capi = True
@@ -97,6 +100,31 @@ def parse_file(
     writer.update_times(output,
                         [fn for fn, _ in files if fn != output],
                         any(changed for _, changed in files))
+    return clinic
+
+
+def format_definition(depth: int, name: str, definition: Definition) -> str:
+    indent = "  " * (depth + 1)
+    if isinstance(definition, Module):
+        return f"{indent}module {name}"
+    if isinstance(definition, Class):
+        return f"{indent}class {name}"
+    if definition.kind is GETTER:
+        return f"{indent}getter {name}"
+    if definition.kind is SETTER:
+        return f"{indent}setter {name}"
+    signature = render_text_signature(definition, definition.render_parameters,
+                                      name=name, line_width=None)
+    return indent + signature
+
+
+def print_definitions(clinic: Clinic) -> None:
+    """Print the modules, classes and functions defined in the parsed file."""
+    lines = [format_definition(depth, name, definition)
+             for depth, name, definition in walk_definitions(clinic)]
+    if lines:
+        print(clinic.filename)
+        print("\n".join(lines))
 
 
 def create_cli() -> argparse.ArgumentParser:
@@ -126,6 +154,10 @@ def create_cli() -> argparse.ArgumentParser:
                                "and return converters; if files are "
                                "specified, print only the converters "
                                "which they define"))
+    cmdline.add_argument("--list", action='store_true',
+                         help=("don't write any file, only list the modules, "
+                               "classes and functions which the specified "
+                               "files define, with their signatures"))
     cmdline.add_argument("--make", action='store_true',
                          help="walk --srcdir to run over all relevant files")
     cmdline.add_argument("--srcdir", type=str, default=os.curdir,
@@ -252,7 +284,7 @@ def run_clinic(parser: argparse.ArgumentParser, ns: 
argparse.Namespace) -> None:
     dry_run = ns.dry_run or ns.diff
     # The report is written to the standard output, so the progress
     # is written to the standard error stream to not mix them.
-    verbose_file = sys.stderr if dry_run else sys.stdout
+    verbose_file = sys.stderr if dry_run or ns.list else sys.stdout
 
     filenames: Iterable[str]
     if ns.make:
@@ -268,6 +300,12 @@ def run_clinic(parser: argparse.ArgumentParser, ns: 
argparse.Namespace) -> None:
             parser.error("can't use -o with multiple filenames")
         filenames = ns.filename
 
+    if ns.list:
+        if dry_run:
+            parser.error("can't use --dry-run or --diff with --list")
+        if ns.converters:
+            parser.error("can't use --converters with --list")
+
     if ns.converters:
         if dry_run:
             parser.error("can't use --dry-run or --diff with --converters")
@@ -280,20 +318,22 @@ def run_clinic(parser: argparse.ArgumentParser, ns: 
argparse.Namespace) -> None:
         builtin_legacy_converters = dict(legacy_converters)
         builtin_return_converters = dict(return_converters)
 
-    writer = libclinic.FileWriter(dry_run=dry_run or ns.converters)
+    writer = libclinic.FileWriter(dry_run=dry_run or ns.converters or ns.list)
     for filename in filenames:
         if ns.verbose:
             print(filename, file=verbose_file)
-        parse_file(filename, output=ns.output,
-                   verify=not ns.force, limited_capi=ns.limited_capi,
-                   writer=writer)
+        clinic = parse_file(filename, output=ns.output,
+                            verify=not ns.force, limited_capi=ns.limited_capi,
+                            writer=writer)
+        if ns.list and clinic is not None:
+            print_definitions(clinic)
 
     if ns.converters:
         print_converters(
             defined_in_files(converters, builtin_converters),
             defined_in_files(legacy_converters, builtin_legacy_converters),
             defined_in_files(return_converters, builtin_return_converters))
-    else:
+    elif not ns.list:
         report_changes(writer, diff=ns.diff)
 
 
diff --git a/Tools/clinic/libclinic/dsl_parser.py 
b/Tools/clinic/libclinic/dsl_parser.py
index 75924dcc05a750..b241f58711e68a 100644
--- a/Tools/clinic/libclinic/dsl_parser.py
+++ b/Tools/clinic/libclinic/dsl_parser.py
@@ -1396,184 +1396,10 @@ def state_function_docstring(self, line: str) -> None:
     def format_docstring_signature(
         f: Function, parameters: list[Parameter]
     ) -> str:
-        lines = []
-        lines.append(f.displayname)
-        if f.forced_text_signature:
-            lines.append(f.forced_text_signature)
-        elif f.kind in ACCESSORS:
-            # @getter and @setter do not need signatures like a method or a 
function.
-            return ''
-        else:
-            lines.append('(')
-
-            # populate "right_bracket_count" field for every parameter
-            assert parameters, "We should always have a self parameter. " + 
repr(f)
-            assert isinstance(parameters[0].converter, self_converter)
-            # self is always positional-only.
-            assert parameters[0].is_positional_only()
-            assert parameters[0].right_bracket_count == 0
-            positional_only = True
-            for p in parameters[1:]:
-                if not p.is_positional_only():
-                    positional_only = False
-                else:
-                    assert positional_only
-                if positional_only:
-                    p.right_bracket_count = p.group_depth
-                else:
-                    # don't put any right brackets around non-positional-only 
parameters, ever.
-                    p.right_bracket_count = 0
-
-            right_bracket_count = 0
-            last_group = 0
-
-            def fix_right_bracket_count(desired: int, group: int = 0) -> str:
-                nonlocal right_bracket_count, last_group
-                s = ''
-                if (group != last_group and right_bracket_count and
-                    ((desired >= right_bracket_count) if group < 0 else
-                     (desired <= right_bracket_count))):
-                    # The group is not nested in the previous group,
-                    # close the brackets of the latter first.
-                    s += ']' * right_bracket_count
-                    right_bracket_count = 0
-                last_group = group
-                while right_bracket_count < desired:
-                    s += '['
-                    right_bracket_count += 1
-                while right_bracket_count > desired:
-                    s += ']'
-                    right_bracket_count -= 1
-                return s
-
-            need_slash = False
-            added_slash = False
-            need_a_trailing_slash = False
-
-            # we only need a trailing slash:
-            #   * if this is not a "docstring_only" signature
-            #   * and if the last *shown* parameter is
-            #     positional only
-            if not f.docstring_only:
-                for p in reversed(parameters):
-                    if not p.converter.show_in_signature:
-                        continue
-                    if p.is_positional_only():
-                        need_a_trailing_slash = True
-                    break
-
-
-            added_star = False
-
-            first_parameter = True
-            last_p = parameters[-1]
-            line_length = len(''.join(lines))
-            indent = " " * line_length
-            def add_parameter(text: str) -> None:
-                nonlocal line_length
-                nonlocal first_parameter
-                if first_parameter:
-                    s = text
-                    first_parameter = False
-                else:
-                    s = ' ' + text
-                    if line_length + len(s) >= 72:
-                        lines.extend(["\n", indent])
-                        line_length = len(indent)
-                        s = text
-                line_length += len(s)
-                lines.append(s)
-
-            for p in parameters:
-                if not p.converter.show_in_signature:
-                    continue
-                assert p.name
-
-                is_self = isinstance(p.converter, self_converter)
-                if is_self and f.docstring_only:
-                    # this isn't a real machine-parsable signature,
-                    # so let's not print the "self" parameter
-                    continue
-
-                if p.is_positional_only():
-                    need_slash = not f.docstring_only
-                elif need_slash and not (added_slash or 
p.is_positional_only()):
-                    added_slash = True
-                    add_parameter('/,')
-
-                if p.is_keyword_only() and not added_star:
-                    added_star = True
-                    add_parameter('*,')
-
-                p_lines = [fix_right_bracket_count(p.right_bracket_count,
-                                                   p.group)]
-
-                if isinstance(p.converter, self_converter):
-                    # annotate first parameter as being a "self".
-                    #
-                    # if inspect.Signature gets this function,
-                    # and it's already bound, the self parameter
-                    # will be stripped off.
-                    #
-                    # if it's not bound, it should be marked
-                    # as positional-only.
-                    #
-                    # note: we don't print "self" for __init__,
-                    # because this isn't actually the signature
-                    # for __init__.  (it can't be, __init__ doesn't
-                    # have a docstring.)  if this is an __init__
-                    # (or __new__), then this signature is for
-                    # calling the class to construct a new instance.
-                    p_lines.append('$')
-
-                if p.is_vararg():
-                    p_lines.append("*")
-                    added_star = True
-                if p.is_var_keyword():
-                    p_lines.append("**")
-
-                name = p.converter.signature_name or p.name
-                p_lines.append(name)
-
-                if not p.is_variable_length() and p.converter.is_optional():
-                    p_lines.append('=')
-                    value = p.converter.py_default
-                    if not value:
-                        value = repr(p.converter.default)
-                    p_lines.append(value)
-
-                if (p != last_p) or need_a_trailing_slash:
-                    p_lines.append(',')
-
-                p_output = "".join(p_lines)
-                add_parameter(p_output)
-
-            lines.append(fix_right_bracket_count(0))
-            if need_a_trailing_slash:
-                add_parameter('/')
-            lines.append(')')
-
-        # PEP 8 says:
-        #
-        #     The Python standard library will not use function annotations
-        #     as that would result in a premature commitment to a particular
-        #     annotation style. Instead, the annotations are left for users
-        #     to discover and experiment with useful annotation styles.
-        #
-        # therefore this is commented out:
-        #
-        # if f.return_converter.py_default:
-        #     lines.append(' -> ')
-        #     lines.append(f.return_converter.py_default)
-
-        if not f.docstring_only:
-            lines.append("\n" + libclinic.SIG_END_MARKER + "\n")
-
-        signature_line = "".join(lines)
-
-        # now fix up the places where the brackets look wrong
-        return signature_line.replace(', ]', ',] ')
-
+        signature = render_text_signature(f, parameters)
+        if signature and not f.docstring_only:
+            signature += "\n" + libclinic.SIG_END_MARKER + "\n"
+        return signature
     @staticmethod
     def format_docstring_parameters(params: list[Parameter]) -> str:
         """Create substitution text for {parameters}"""
@@ -1715,3 +1541,191 @@ def do_post_block_processing_cleanup(self, lineno: int) 
-> None:
                 exc.lineno = lineno
             exc.filename = self.clinic.filename
             raise
+
+
+def render_text_signature(
+    f: Function,
+    parameters: list[Parameter],
+    *,
+    name: str | None = None,
+    line_width: int | None = 72,
+) -> str:
+    """Render the text signature of the function.
+
+    *name* replaces the name of the function.  *line_width* is the width
+    at which the signature is wrapped, None disables wrapping.
+    """
+    lines = []
+    lines.append(f.displayname if name is None else name)
+    if f.forced_text_signature:
+        lines.append(f.forced_text_signature)
+    elif f.kind in ACCESSORS:
+        # @getter and @setter do not need signatures like a method or a 
function.
+        return ''
+    else:
+        lines.append('(')
+
+        # populate "right_bracket_count" field for every parameter
+        assert parameters, "We should always have a self parameter. " + repr(f)
+        assert isinstance(parameters[0].converter, self_converter)
+        # self is always positional-only.
+        assert parameters[0].is_positional_only()
+        assert parameters[0].right_bracket_count == 0
+        positional_only = True
+        for p in parameters[1:]:
+            if not p.is_positional_only():
+                positional_only = False
+            else:
+                assert positional_only
+            if positional_only:
+                p.right_bracket_count = p.group_depth
+            else:
+                # don't put any right brackets around non-positional-only 
parameters, ever.
+                p.right_bracket_count = 0
+
+        right_bracket_count = 0
+        last_group = 0
+
+        def fix_right_bracket_count(desired: int, group: int = 0) -> str:
+            nonlocal right_bracket_count, last_group
+            s = ''
+            if (group != last_group and right_bracket_count and
+                ((desired >= right_bracket_count) if group < 0 else
+                 (desired <= right_bracket_count))):
+                # The group is not nested in the previous group,
+                # close the brackets of the latter first.
+                s += ']' * right_bracket_count
+                right_bracket_count = 0
+            last_group = group
+            while right_bracket_count < desired:
+                s += '['
+                right_bracket_count += 1
+            while right_bracket_count > desired:
+                s += ']'
+                right_bracket_count -= 1
+            return s
+
+        need_slash = False
+        added_slash = False
+        need_a_trailing_slash = False
+
+        # we only need a trailing slash:
+        #   * if this is not a "docstring_only" signature
+        #   * and if the last *shown* parameter is
+        #     positional only
+        if not f.docstring_only:
+            for p in reversed(parameters):
+                if not p.converter.show_in_signature:
+                    continue
+                if p.is_positional_only():
+                    need_a_trailing_slash = True
+                break
+
+
+        added_star = False
+
+        first_parameter = True
+        last_p = parameters[-1]
+        line_length = len(''.join(lines))
+        indent = " " * line_length
+        def add_parameter(text: str) -> None:
+            nonlocal line_length
+            nonlocal first_parameter
+            if first_parameter:
+                s = text
+                first_parameter = False
+            else:
+                s = ' ' + text
+                if line_width is not None and line_length + len(s) >= 
line_width:
+                    lines.extend(["\n", indent])
+                    line_length = len(indent)
+                    s = text
+            line_length += len(s)
+            lines.append(s)
+
+        for p in parameters:
+            if not p.converter.show_in_signature:
+                continue
+            assert p.name
+
+            is_self = isinstance(p.converter, self_converter)
+            if is_self and f.docstring_only:
+                # this isn't a real machine-parsable signature,
+                # so let's not print the "self" parameter
+                continue
+
+            if p.is_positional_only():
+                need_slash = not f.docstring_only
+            elif need_slash and not (added_slash or p.is_positional_only()):
+                added_slash = True
+                add_parameter('/,')
+
+            if p.is_keyword_only() and not added_star:
+                added_star = True
+                add_parameter('*,')
+
+            p_lines = [fix_right_bracket_count(p.right_bracket_count,
+                                               p.group)]
+
+            if isinstance(p.converter, self_converter):
+                # annotate first parameter as being a "self".
+                #
+                # if inspect.Signature gets this function,
+                # and it's already bound, the self parameter
+                # will be stripped off.
+                #
+                # if it's not bound, it should be marked
+                # as positional-only.
+                #
+                # note: we don't print "self" for __init__,
+                # because this isn't actually the signature
+                # for __init__.  (it can't be, __init__ doesn't
+                # have a docstring.)  if this is an __init__
+                # (or __new__), then this signature is for
+                # calling the class to construct a new instance.
+                p_lines.append('$')
+
+            if p.is_vararg():
+                p_lines.append("*")
+                added_star = True
+            if p.is_var_keyword():
+                p_lines.append("**")
+
+            name = p.converter.signature_name or p.name
+            p_lines.append(name)
+
+            if not p.is_variable_length() and p.converter.is_optional():
+                p_lines.append('=')
+                value = p.converter.py_default
+                if not value:
+                    value = repr(p.converter.default)
+                p_lines.append(value)
+
+            if (p != last_p) or need_a_trailing_slash:
+                p_lines.append(',')
+
+            p_output = "".join(p_lines)
+            add_parameter(p_output)
+
+        lines.append(fix_right_bracket_count(0))
+        if need_a_trailing_slash:
+            add_parameter('/')
+        lines.append(')')
+
+    # PEP 8 says:
+    #
+    #     The Python standard library will not use function annotations
+    #     as that would result in a premature commitment to a particular
+    #     annotation style. Instead, the annotations are left for users
+    #     to discover and experiment with useful annotation styles.
+    #
+    # therefore this is commented out:
+    #
+    # if f.return_converter.py_default:
+    #     lines.append(' -> ')
+    #     lines.append(f.return_converter.py_default)
+
+    signature_line = "".join(lines)
+
+    # now fix up the places where the brackets look wrong
+    return signature_line.replace(', ]', ',] ')
diff --git a/Tools/clinic/libclinic/function.py 
b/Tools/clinic/libclinic/function.py
index 58b61c6f822196..af858f2e40e75e 100644
--- a/Tools/clinic/libclinic/function.py
+++ b/Tools/clinic/libclinic/function.py
@@ -282,6 +282,36 @@ def render_docstring(self) -> str:
 
 ParamTuple = tuple["Parameter", ...]
 
+Definition = Module | Class | Function
+
+
+def walk_definitions(
+    parent: Clinic | Module | Class,
+    prefix: str = '',
+    depth: int = 0,
+) -> Iterator[tuple[int, str, Definition]]:
+    """Yield (depth, dotted name, definition) for every nested definition.
+
+    The name of a module is already fully qualified, but the name of
+    a class is not, hence the prefix.
+    """
+    for function in parent.functions:
+        if function.kind.new_or_init:
+            # __new__() and __init__() are called as the class itself.
+            name = prefix
+        else:
+            name = f'{prefix}.{function.name}' if prefix else function.name
+        yield depth, name, function
+    for cls in parent.classes.values():
+        name = f'{prefix}.{cls.name}' if prefix else cls.name
+        yield depth, name, cls
+        yield from walk_definitions(cls, name, depth + 1)
+    if not isinstance(parent, Class):
+        # Only a module can contain modules.
+        for module in parent.modules.values():
+            yield depth, module.name, module
+            yield from walk_definitions(module, module.name, depth + 1)
+
 
 def permute_left_option_groups(
     l: Sequence[Iterable[Parameter]]

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to