From f660ec37531b5e368a27ba065f73d31ff6fb6680 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 12 Aug 2025 18:16:04 +0100 Subject: [PATCH 1/4] gh-137242: Add Android CI job (#137186) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Russell Keith-Magee Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- .github/workflows/build.yml | 25 ++++++++ Android/README.md | 8 ++- Android/android-env.sh | 2 +- Android/android.py | 122 ++++++++++++++++++++++++++++++------ 4 files changed, 134 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bc2a0e372659db..56f222cd94ab6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -397,6 +397,29 @@ jobs: - name: SSL tests run: ./python Lib/test/ssltests.py + build-android: + name: Android (${{ matrix.arch }}) + needs: build-context + if: needs.build-context.outputs.run-tests == 'true' + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + # Use the same runs-on configuration as build-macos and build-ubuntu. + - arch: aarch64 + runs-on: ${{ github.repository_owner == 'python' && 'ghcr.io/cirruslabs/macos-runner:sonoma' || 'macos-14' }} + - arch: x86_64 + runs-on: ubuntu-24.04 + + runs-on: ${{ matrix.runs-on }} + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Build and test + run: ./Android/android.py ci ${{ matrix.arch }}-linux-android + build-wasi: name: 'WASI' needs: build-context @@ -705,6 +728,7 @@ jobs: - build-ubuntu - build-ubuntu-ssltests-awslc - build-ubuntu-ssltests-openssl + - build-android - build-wasi - test-hypothesis - build-asan @@ -740,6 +764,7 @@ jobs: build-ubuntu, build-ubuntu-ssltests-awslc, build-ubuntu-ssltests-openssl, + build-android, build-wasi, test-hypothesis, build-asan, diff --git a/Android/README.md b/Android/README.md index c42eb627006e6a..9f71aeb934f386 100644 --- a/Android/README.md +++ b/Android/README.md @@ -96,10 +96,12 @@ similar to the `Android` directory of the CPython source tree. ## Testing -The Python test suite can be run on Linux, macOS, or Windows: +The Python test suite can be run on Linux, macOS, or Windows. -* On Linux, the emulator needs access to the KVM virtualization interface, and - a DISPLAY environment variable pointing at an X server. Xvfb is acceptable. +On Linux, the emulator needs access to the KVM virtualization interface. This may +require adding your user to a group, or changing your udev rules. On GitHub +Actions, the test script will do this automatically using the commands shown +[here](https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/). The test suite can usually be run on a device with 2 GB of RAM, but this is borderline, so you may need to increase it to 4 GB. As of Android diff --git a/Android/android-env.sh b/Android/android-env.sh index 7b381a013cf0ba..5859c0eac4a88f 100644 --- a/Android/android-env.sh +++ b/Android/android-env.sh @@ -24,7 +24,7 @@ fail() { # * https://android.googlesource.com/platform/ndk/+/ndk-rXX-release/docs/BuildSystemMaintainers.md # where XX is the NDK version. Do a diff against the version you're upgrading from, e.g.: # https://android.googlesource.com/platform/ndk/+/ndk-r25-release..ndk-r26-release/docs/BuildSystemMaintainers.md -ndk_version=27.2.12479018 +ndk_version=27.3.13750724 ndk=$ANDROID_HOME/ndk/$ndk_version if ! [ -e "$ndk" ]; then diff --git a/Android/android.py b/Android/android.py index e6090aa1d80db0..85874ad9b60f3d 100755 --- a/Android/android.py +++ b/Android/android.py @@ -3,6 +3,7 @@ import asyncio import argparse import os +import platform import re import shlex import shutil @@ -247,7 +248,13 @@ def make_host_python(context): # flags to be duplicated. So we don't use the `host` argument here. os.chdir(host_dir) run(["make", "-j", str(os.cpu_count())]) - run(["make", "install", f"prefix={prefix_dir}"]) + + # The `make install` output is very verbose and rarely useful, so + # suppress it by default. + run( + ["make", "install", f"prefix={prefix_dir}"], + capture_output=not context.verbose, + ) def build_all(context): @@ -266,6 +273,18 @@ def clean_all(context): clean(host) +def setup_ci(): + # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ + if "GITHUB_ACTIONS" in os.environ and platform.system() == "Linux": + run( + ["sudo", "tee", "/etc/udev/rules.d/99-kvm4all.rules"], + input='KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"\n', + text=True, + ) + run(["sudo", "udevadm", "control", "--reload-rules"]) + run(["sudo", "udevadm", "trigger", "--name-match=kvm"]) + + def setup_sdk(): sdkmanager = android_home / ( "cmdline-tools/latest/bin/sdkmanager" @@ -578,6 +597,7 @@ async def gradle_task(context): async def run_testbed(context): + setup_ci() setup_sdk() setup_testbed() @@ -671,11 +691,63 @@ def package(context): else: shutil.copy2(src, dst, follow_symlinks=False) + # Strip debug information. + if not context.debug: + so_files = glob(f"{temp_dir}/**/*.so", recursive=True) + run([android_env(context.host)["STRIP"], *so_files], log=False) + dist_dir = subdir(context.host, "dist", create=True) package_path = shutil.make_archive( f"{dist_dir}/python-{version}-{context.host}", "gztar", temp_dir ) print(f"Wrote {package_path}") + return package_path + + +def ci(context): + for step in [ + configure_build_python, + make_build_python, + configure_host_python, + make_host_python, + package, + ]: + caption = ( + step.__name__.replace("_", " ") + .capitalize() + .replace("python", "Python") + ) + print(f"::group::{caption}") + result = step(context) + if step is package: + package_path = result + print("::endgroup::") + + if ( + "GITHUB_ACTIONS" in os.environ + and (platform.system(), platform.machine()) != ("Linux", "x86_64") + ): + print( + "Skipping tests: GitHub Actions does not support the Android " + "emulator on this platform." + ) + else: + with TemporaryDirectory(prefix=SCRIPT_NAME) as temp_dir: + print("::group::Tests") + # Prove the package is self-contained by using it to run the tests. + shutil.unpack_archive(package_path, temp_dir) + + # Arguments are similar to --fast-ci, but in single-process mode. + launcher_args = ["--managed", "maxVersion", "-v"] + test_args = [ + "--single-process", "--fail-env-changed", "--rerun", "--slowest", + "--verbose3", "-u", "all,-cpu", "--timeout=600" + ] + run( + ["./android.py", "test", *launcher_args, "--", *test_args], + cwd=temp_dir + ) + print("::endgroup::") def env(context): @@ -695,32 +767,40 @@ def parse_args(): parser = argparse.ArgumentParser() subcommands = parser.add_subparsers(dest="subcommand", required=True) + def add_parser(*args, **kwargs): + parser = subcommands.add_parser(*args, **kwargs) + parser.add_argument( + "-v", "--verbose", action="count", default=0, + help="Show verbose output. Use twice to be even more verbose.") + return parser + # Subcommands - build = subcommands.add_parser( + build = add_parser( "build", help="Run configure-build, make-build, configure-host and " "make-host") - configure_build = subcommands.add_parser( + configure_build = add_parser( "configure-build", help="Run `configure` for the build Python") - subcommands.add_parser( + add_parser( "make-build", help="Run `make` for the build Python") - configure_host = subcommands.add_parser( + configure_host = add_parser( "configure-host", help="Run `configure` for Android") - make_host = subcommands.add_parser( + make_host = add_parser( "make-host", help="Run `make` for Android") - subcommands.add_parser("clean", help="Delete all build directories") - subcommands.add_parser("build-testbed", help="Build the testbed app") - test = subcommands.add_parser("test", help="Run the testbed app") - package = subcommands.add_parser("package", help="Make a release package") - env = subcommands.add_parser("env", help="Print environment variables") + add_parser("clean", help="Delete all build directories") + add_parser("build-testbed", help="Build the testbed app") + test = add_parser("test", help="Run the testbed app") + package = add_parser("package", help="Make a release package") + ci = add_parser("ci", help="Run build, package and test") + env = add_parser("env", help="Print environment variables") # Common arguments - for subcommand in build, configure_build, configure_host: + for subcommand in [build, configure_build, configure_host, ci]: subcommand.add_argument( "--clean", action="store_true", default=False, dest="clean", help="Delete the relevant build directories first") - host_commands = [build, configure_host, make_host, package] + host_commands = [build, configure_host, make_host, package, ci] if in_source_tree: host_commands.append(env) for subcommand in host_commands: @@ -728,16 +808,11 @@ def parse_args(): "host", metavar="HOST", choices=HOSTS, help="Host triplet: choices=[%(choices)s]") - for subcommand in build, configure_build, configure_host: + for subcommand in [build, configure_build, configure_host, ci]: subcommand.add_argument("args", nargs="*", help="Extra arguments to pass to `configure`") # Test arguments - test.add_argument( - "-v", "--verbose", action="count", default=0, - help="Show Gradle output, and non-Python logcat messages. " - "Use twice to include high-volume messages which are rarely useful.") - device_group = test.add_mutually_exclusive_group(required=True) device_group.add_argument( "--connected", metavar="SERIAL", help="Run on a connected device. " @@ -765,6 +840,12 @@ def parse_args(): "args", nargs="*", help=f"Arguments to add to sys.argv. " f"Separate them from {SCRIPT_NAME}'s own arguments with `--`.") + # Package arguments. + for subcommand in [package, ci]: + subcommand.add_argument( + "-g", action="store_true", default=False, dest="debug", + help="Include debug information in package") + return parser.parse_args() @@ -788,6 +869,7 @@ def main(): "build-testbed": build_testbed, "test": run_testbed, "package": package, + "ci": ci, "env": env, } @@ -803,6 +885,8 @@ def main(): def print_called_process_error(e): for stream_name in ["stdout", "stderr"]: content = getattr(e, stream_name) + if isinstance(content, bytes): + content = content.decode(*DECODE_ARGS) stream = getattr(sys, stream_name) if content: stream.write(content) From 654b8d936450d94472e26f0b14a9f8d6d249563a Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Tue, 12 Aug 2025 10:32:53 -0700 Subject: [PATCH 2/4] GH-137562: Fix github-issue number for deallocated objects in cache bug (GH-137614) --- Python/gc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/gc.c b/Python/gc.c index 03455e88d5eeb1..79c7476f4a9a74 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -882,7 +882,7 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers) * to invalidate caches (e.g. by PyType_Modified), that clearing has created * a bug. If the weakref to the subclass is cleared before a finalizer is * called, the cache may not be correctly invalidated. That can lead to - * segfaults since the caches can refer to deallocated objects (GH-91636 + * segfaults since the caches can refer to deallocated objects (GH-135552 * is an example). Now, we delay the clear of weakrefs without callbacks * until *after* finalizers have been executed. That means weakrefs without * callbacks are still usable while finalizers are being executed. From 003bd8cc63279b455a7ca5d783ff3623fd2b3804 Mon Sep 17 00:00:00 2001 From: RafaelWO <38643099+RafaelWO@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:28:27 +0200 Subject: [PATCH 3/4] gh-136672: Docs: Move Enum functions and add examples (GH-136791) * Docs: Move Enum functions and add examples When the `Enum` functions `_add_alias_` and `_add_value_alias_` were added in de6bca956432cc852a4a41e2a2cee9cdacd19f35, the documentation for them was done under `EnumType` instead of `Enum`. This change moves them to the docs of the `Enum` class and adds an example for each function. --------- Co-authored-by: Ethan Furman --- Doc/howto/enum.rst | 4 ++-- Doc/library/enum.rst | 53 ++++++++++++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Doc/howto/enum.rst b/Doc/howto/enum.rst index 6441b7aed1eda8..7713aede6d564a 100644 --- a/Doc/howto/enum.rst +++ b/Doc/howto/enum.rst @@ -990,9 +990,9 @@ Supported ``_sunder_`` names from the final class - :meth:`~Enum._generate_next_value_` -- used to get an appropriate value for an enum member; may be overridden -- :meth:`~EnumType._add_alias_` -- adds a new name as an alias to an existing +- :meth:`~Enum._add_alias_` -- adds a new name as an alias to an existing member. -- :meth:`~EnumType._add_value_alias_` -- adds a new value as an alias to an +- :meth:`~Enum._add_value_alias_` -- adds a new value as an alias to an existing member. See `MultiValueEnum`_ for an example. .. note:: diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 2cfc2f4962979f..8875669ffccc37 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -175,6 +175,10 @@ Data Types final *enum*, as well as creating the enum members, properly handling duplicates, providing iteration over the enum class, etc. + .. versionadded:: 3.11 + + Before 3.11 ``EnumType`` was called ``EnumMeta``, which is still available as an alias. + .. method:: EnumType.__call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None) This method is called in two different ways: @@ -206,7 +210,7 @@ Data Types >>> Color.RED.value in Color True - .. versionchanged:: 3.12 + .. versionchanged:: 3.12 Before Python 3.12, a ``TypeError`` is raised if a non-Enum-member is used in a containment check. @@ -251,20 +255,6 @@ Data Types >>> list(reversed(Color)) [, , ] - .. method:: EnumType._add_alias_ - - Adds a new name as an alias to an existing member. Raises a - :exc:`NameError` if the name is already assigned to a different member. - - .. method:: EnumType._add_value_alias_ - - Adds a new value as an alias to an existing member. Raises a - :exc:`ValueError` if the value is already linked with a different member. - - .. versionadded:: 3.11 - - Before 3.11 ``EnumType`` was called ``EnumMeta``, which is still available as an alias. - .. class:: Enum @@ -470,6 +460,30 @@ Data Types .. versionchanged:: 3.12 Added :ref:`enum-dataclass-support` + .. method:: Enum._add_alias_ + + Adds a new name as an alias to an existing member:: + + >>> Color.RED._add_alias_("ERROR") + >>> Color.ERROR + + + Raises a :exc:`NameError` if the name is already assigned to a different member. + + .. versionadded:: 3.13 + + .. method:: Enum._add_value_alias_ + + Adds a new value as an alias to an existing member:: + + >>> Color.RED._add_value_alias_(42) + >>> Color(42) + + + Raises a :exc:`ValueError` if the value is already linked with a different member. + + .. versionadded:: 3.13 + .. class:: IntEnum @@ -879,10 +893,6 @@ Once all the members are created it is no longer used. Supported ``_sunder_`` names """""""""""""""""""""""""""" -- :meth:`~EnumType._add_alias_` -- adds a new name as an alias to an existing - member. -- :meth:`~EnumType._add_value_alias_` -- adds a new value as an alias to an - existing member. - :attr:`~Enum._name_` -- name of the member - :attr:`~Enum._value_` -- value of the member; can be set in ``__new__`` - :meth:`~Enum._missing_` -- a lookup function used when a value is not found; @@ -903,6 +913,11 @@ Supported ``_sunder_`` names For :class:`Flag` classes the next value chosen will be the next highest power-of-two. +- :meth:`~Enum._add_alias_` -- adds a new name as an alias to an existing + member. +- :meth:`~Enum._add_value_alias_` -- adds a new value as an alias to an + existing member. + - While ``_sunder_`` names are generally reserved for the further development of the :class:`Enum` class and can not be used, some are explicitly allowed: From 6baf5524847ef181e901ea5d04d4131132c5a7d5 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:17:35 +0100 Subject: [PATCH 4/4] GH-137623: Begin enforcing docstring length in Argument Clinic (#137624) --- Lib/test/clinic.test.c | 20 +- Modules/clinic/posixmodule.c.h | 6 +- Modules/posixmodule.c | 6 +- .../clinic/libclinic/_overlong_docstrings.py | 299 ++++++++++++++++++ Tools/clinic/libclinic/dsl_parser.py | 23 ++ Tools/clinic/libclinic/function.py | 13 + 6 files changed, 355 insertions(+), 12 deletions(-) create mode 100644 Tools/clinic/libclinic/_overlong_docstrings.py diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c index dc5b4b27a07f99..b0f7e402469ffc 100644 --- a/Lib/test/clinic.test.c +++ b/Lib/test/clinic.test.c @@ -5084,14 +5084,18 @@ Test_an_metho_arg_named_arg_impl(TestObj *self, int arg) Test.__init__ *args: tuple -Varargs init method. For example, nargs is translated to PyTuple_GET_SIZE. +Varargs init method. + +For example, nargs is translated to PyTuple_GET_SIZE. [clinic start generated code]*/ PyDoc_STRVAR(Test___init____doc__, "Test(*args)\n" "--\n" "\n" -"Varargs init method. For example, nargs is translated to PyTuple_GET_SIZE."); +"Varargs init method.\n" +"\n" +"For example, nargs is translated to PyTuple_GET_SIZE."); static int Test___init___impl(TestObj *self, PyObject *args); @@ -5120,21 +5124,25 @@ Test___init__(PyObject *self, PyObject *args, PyObject *kwargs) static int Test___init___impl(TestObj *self, PyObject *args) -/*[clinic end generated code: output=f172425cec373cd6 input=4b8388c4e6baab6f]*/ +/*[clinic end generated code: output=0e5836c40dbc2397 input=a615a4485c0fc3e2]*/ /*[clinic input] @classmethod Test.__new__ *args: tuple -Varargs new method. For example, nargs is translated to PyTuple_GET_SIZE. +Varargs new method. + +For example, nargs is translated to PyTuple_GET_SIZE. [clinic start generated code]*/ PyDoc_STRVAR(Test__doc__, "Test(*args)\n" "--\n" "\n" -"Varargs new method. For example, nargs is translated to PyTuple_GET_SIZE."); +"Varargs new method.\n" +"\n" +"For example, nargs is translated to PyTuple_GET_SIZE."); static PyObject * Test_impl(PyTypeObject *type, PyObject *args); @@ -5162,7 +5170,7 @@ Test(PyTypeObject *type, PyObject *args, PyObject *kwargs) static PyObject * Test_impl(PyTypeObject *type, PyObject *args) -/*[clinic end generated code: output=ee1e8892a67abd4a input=a8259521129cad20]*/ +/*[clinic end generated code: output=e6fba0c8951882fd input=8ce30adb836aeacb]*/ /*[clinic input] diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 8af9e1db781c8f..df4f802ff0bdc9 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -215,8 +215,8 @@ PyDoc_STRVAR(os_access__doc__, " NotImplementedError.\n" "\n" "Note that most operations will use the effective uid/gid, therefore this\n" -" routine can be used in a suid/sgid environment to test if the invoking user\n" -" has the specified access to the path."); +" routine can be used in a suid/sgid environment to test if the invoking\n" +" user has the specified access to the path."); #define OS_ACCESS_METHODDEF \ {"access", _PyCFunction_CAST(os_access), METH_FASTCALL|METH_KEYWORDS, os_access__doc__}, @@ -13419,4 +13419,4 @@ os__emscripten_log(PyObject *module, PyObject *const *args, Py_ssize_t nargs, Py #ifndef OS__EMSCRIPTEN_LOG_METHODDEF #define OS__EMSCRIPTEN_LOG_METHODDEF #endif /* !defined(OS__EMSCRIPTEN_LOG_METHODDEF) */ -/*[clinic end generated code: output=b1e2615384347102 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=23de5d098e2dd73f input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index b1a80788bd8115..76dbb84691db1f 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -3295,15 +3295,15 @@ dir_fd, effective_ids, and follow_symlinks may not be implemented NotImplementedError. Note that most operations will use the effective uid/gid, therefore this - routine can be used in a suid/sgid environment to test if the invoking user - has the specified access to the path. + routine can be used in a suid/sgid environment to test if the invoking + user has the specified access to the path. [clinic start generated code]*/ static int os_access_impl(PyObject *module, path_t *path, int mode, int dir_fd, int effective_ids, int follow_symlinks) -/*[clinic end generated code: output=cf84158bc90b1a77 input=3ffe4e650ee3bf20]*/ +/*[clinic end generated code: output=cf84158bc90b1a77 input=c33565f7584b99e4]*/ { int return_value; diff --git a/Tools/clinic/libclinic/_overlong_docstrings.py b/Tools/clinic/libclinic/_overlong_docstrings.py new file mode 100644 index 00000000000000..5ca335fab2875c --- /dev/null +++ b/Tools/clinic/libclinic/_overlong_docstrings.py @@ -0,0 +1,299 @@ +OVERLONG_SUMMARY = frozenset(( + # Lib/test/ + 'test_preprocessor_guarded_if_e_or_f', + + # Modules/ + '_abc._abc_init', + '_abc._abc_instancecheck', + '_abc._abc_register', + '_abc._abc_subclasscheck', + '_codecs.lookup', + '_ctypes.byref', + '_curses.can_change_color', + '_curses.is_term_resized', + '_curses.mousemask', + '_curses.reset_prog_mode', + '_curses.reset_shell_mode', + '_curses.termname', + '_curses.window.enclose', + '_functools.reduce', + '_gdbm.gdbm.setdefault', + '_hashlib.HMAC.hexdigest', + '_hashlib.openssl_shake_128', + '_hashlib.openssl_shake_256', + '_hashlib.pbkdf2_hmac', + '_hmac.HMAC.hexdigest', + '_interpreters.is_shareable', + '_io._BufferedIOBase.read1', + '_lzma._decode_filter_properties', + '_remote_debugging.RemoteUnwinder.__init__', + '_remote_debugging.RemoteUnwinder.get_all_awaited_by', + '_remote_debugging.RemoteUnwinder.get_async_stack_trace', + '_socket.inet_aton', + '_sre.SRE_Match.expand', + '_sre.SRE_Match.groupdict', + '_sre.SRE_Pattern.finditer', + '_sre.SRE_Pattern.search', + '_sre.SRE_Pattern.sub', + '_sre.SRE_Pattern.subn', + '_ssl._SSLContext.sni_callback', + '_ssl._SSLSocket.pending', + '_ssl._SSLSocket.sendfile', + '_ssl.get_default_verify_paths', + '_ssl.RAND_status', + '_sysconfig.config_vars', + '_testcapi.make_exception_with_doc', + '_testcapi.VectorCallClass.set_vectorcall', + '_tkinter.getbusywaitinterval', + '_tkinter.setbusywaitinterval', + '_tracemalloc.reset_peak', + '_zstd.get_frame_size', + '_zstd.set_parameter_types', + '_zstd.ZstdDecompressor.decompress', + 'array.array.buffer_info', + 'array.array.frombytes', + 'array.array.fromfile', + 'array.array.tobytes', + 'cmath.isfinite', + 'datetime.datetime.strptime', + 'gc.get_objects', + 'itertools.chain.from_iterable', + 'itertools.combinations_with_replacement.__new__', + 'itertools.cycle.__new__', + 'itertools.starmap.__new__', + 'itertools.takewhile.__new__', + 'math.comb', + 'math.perm', + 'os.getresgid', + 'os.lstat', + 'os.pread', + 'os.pwritev', + 'os.sched_getaffinity', + 'os.sched_rr_get_interval', + 'os.timerfd_gettime', + 'os.timerfd_gettime_ns', + 'os.urandom', + 'os.WIFEXITED', + 'os.WTERMSIG', + 'pwd.getpwall', + 'pyexpat.xmlparser.ExternalEntityParserCreate', + 'pyexpat.xmlparser.GetReparseDeferralEnabled', + 'pyexpat.xmlparser.SetParamEntityParsing', + 'pyexpat.xmlparser.UseForeignDTD', + 'readline.redisplay', + 'signal.set_wakeup_fd', + 'unicodedata.UCD.combining', + 'unicodedata.UCD.decomposition', + 'zoneinfo.ZoneInfo.dst', + 'zoneinfo.ZoneInfo.tzname', + 'zoneinfo.ZoneInfo.utcoffset', + + # Objects/ + 'B.zfill', + 'bytearray.count', + 'bytearray.endswith', + 'bytearray.extend', + 'bytearray.find', + 'bytearray.index', + 'bytearray.maketrans', + 'bytearray.rfind', + 'bytearray.rindex', + 'bytearray.rsplit', + 'bytearray.split', + 'bytearray.splitlines', + 'bytearray.startswith', + 'bytes.count', + 'bytes.endswith', + 'bytes.find', + 'bytes.index', + 'bytes.maketrans', + 'bytes.rfind', + 'bytes.rindex', + 'bytes.startswith', + 'code.replace', + 'complex.conjugate', + 'dict.pop', + 'float.as_integer_ratio', + 'frame.f_trace', + 'int.bit_count', + 'OrderedDict.fromkeys', + 'OrderedDict.pop', + 'set.symmetric_difference_update', + 'str.count', + 'str.endswith', + 'str.find', + 'str.index', + 'str.isprintable', + 'str.rfind', + 'str.rindex', + 'str.rsplit', + 'str.split', + 'str.startswith', + 'str.strip', + 'str.swapcase', + 'str.zfill', + + # PC/ + 'msvcrt.kbhit', + + # Python/ + '_jit.is_active', + '_jit.is_available', + '_jit.is_enabled', + 'marshal.dumps', + 'sys._current_exceptions', + 'sys._setprofileallthreads', + 'sys._settraceallthreads', +)) + +OVERLONG_BODY = frozenset(( + # Modules/ + '_bz2.BZ2Decompressor.decompress', + '_curses.color_content', + '_curses.flash', + '_curses.longname', + '_curses.resize_term', + '_curses.use_env', + '_curses.window.border', + '_curses.window.derwin', + '_curses.window.getch', + '_curses.window.getkey', + '_curses.window.inch', + '_curses.window.insch', + '_curses.window.insnstr', + '_curses.window.is_linetouched', + '_curses.window.noutrefresh', + '_curses.window.overlay', + '_curses.window.overwrite', + '_curses.window.refresh', + '_curses.window.scroll', + '_curses.window.subwin', + '_curses.window.touchline', + '_curses_panel.panel.hide', + '_functools.reduce', + '_hashlib.HMAC.hexdigest', + '_hmac.HMAC.hexdigest', + '_interpreters.capture_exception', + '_io._IOBase.seek', + '_io._TextIOBase.detach', + '_io.FileIO.read', + '_io.FileIO.readall', + '_io.FileIO.seek', + '_io.open', + '_io.open_code', + '_lzma.LZMADecompressor.decompress', + '_multibytecodec.MultibyteCodec.decode', + '_multibytecodec.MultibyteCodec.encode', + '_posixsubprocess.fork_exec', + '_remote_debugging.RemoteUnwinder.__init__', + '_remote_debugging.RemoteUnwinder.get_all_awaited_by', + '_remote_debugging.RemoteUnwinder.get_async_stack_trace', + '_remote_debugging.RemoteUnwinder.get_stack_trace', + '_socket.socket.send', + '_sqlite3.Blob.read', + '_sqlite3.Blob.seek', + '_sqlite3.Blob.write', + '_sqlite3.Connection.deserialize', + '_sqlite3.Connection.serialize', + '_sqlite3.Connection.set_progress_handler', + '_sqlite3.Connection.setlimit', + '_ssl._SSLContext.sni_callback', + '_ssl._SSLSocket.context', + '_ssl._SSLSocket.get_channel_binding', + '_ssl._SSLSocket.sendfile', + '_tkinter.setbusywaitinterval', + '_zstd.ZstdCompressor.compress', + '_zstd.ZstdCompressor.flush', + '_zstd.ZstdCompressor.set_pledged_input_size', + '_zstd.ZstdDecompressor.__new__', + '_zstd.ZstdDecompressor.decompress', + '_zstd.ZstdDecompressor.unused_data', + '_zstd.ZstdDict.__new__', + '_zstd.ZstdDict.as_digested_dict', + '_zstd.ZstdDict.as_prefix', + '_zstd.ZstdDict.as_undigested_dict', + 'array.array.byteswap', + 'array.array.fromunicode', + 'array.array.tounicode', + 'binascii.a2b_base64', + 'cmath.isclose', + 'datetime.date.fromtimestamp', + 'datetime.datetime.fromtimestamp', + 'datetime.time.strftime', + 'fcntl.ioctl', + 'fcntl.lockf', + 'gc.freeze', + 'itertools.combinations_with_replacement.__new__', + 'math.nextafter', + 'os.fspath', + 'os.link', + 'os.listdir', + 'os.listxattr', + 'os.lseek', + 'os.mknod', + 'os.preadv', + 'os.pwritev', + 'os.readinto', + 'os.rename', + 'os.replace', + 'os.setxattr', + 'pyexpat.xmlparser.GetInputContext', + 'pyexpat.xmlparser.UseForeignDTD', + 'select.devpoll', + 'select.poll', + 'select.select', + 'signal.setitimer', + 'signal.signal', + 'termios.tcsetwinsize', + 'zlib.Decompress.decompress', + 'zlib.ZlibDecompressor.decompress', + + # Objects/ + 'bytearray.maketrans', + 'bytearray.partition', + 'bytearray.replace', + 'bytearray.rpartition', + 'bytearray.rsplit', + 'bytearray.splitlines', + 'bytearray.strip', + 'bytes.maketrans', + 'bytes.partition', + 'bytes.replace', + 'bytes.rpartition', + 'bytes.rsplit', + 'bytes.splitlines', + 'bytes.strip', + 'float.__getformat__', + 'list.sort', + 'memoryview.tobytes', + 'str.capitalize', + 'str.isalnum', + 'str.isalpha', + 'str.isdecimal', + 'str.isdigit', + 'str.isidentifier', + 'str.islower', + 'str.isnumeric', + 'str.isspace', + 'str.isupper', + 'str.join', + 'str.partition', + 'str.removeprefix', + 'str.replace', + 'str.rpartition', + 'str.splitlines', + 'str.title', + 'str.translate', + + # PC/ + '_wmi.exec_query', + + # Python/ + '__import__', + '_contextvars.ContextVar.get', + '_contextvars.ContextVar.reset', + '_contextvars.ContextVar.set', + '_imp.acquire_lock', + 'marshal.dumps', + 'sys._stats_dump', +)) diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index eca41531f7c8e9..58430df6173fd0 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -14,6 +14,7 @@ from libclinic import ( ClinicError, VersionTuple, fail, warn, unspecified, unknown, NULL) +from libclinic._overlong_docstrings import OVERLONG_SUMMARY, OVERLONG_BODY from libclinic.function import ( Module, Class, Function, Parameter, FunctionKind, @@ -1515,6 +1516,28 @@ def format_docstring(self) -> str: # between it and the {parameters} we're about to add. lines.append('') + # Fail if the summary line is too long. + # Warn if any of the body lines are too long. + # Existing violations are recorded in OVERLONG_{SUMMARY,BODY}. + max_width = f.docstring_line_width + summary_len = len(lines[0]) + max_body = max(map(len, lines[1:])) + if summary_len > max_width: + if f.full_name not in OVERLONG_SUMMARY: + fail(f"Summary line for {f.full_name!r} is too long!\n" + f"The summary line must be no longer than {max_width} characters.") + else: + if f.full_name in OVERLONG_SUMMARY: + warn(f"Remove {f.full_name!r} from OVERLONG_SUMMARY!\n") + + if max_body > max_width: + if f.full_name not in OVERLONG_BODY: + warn(f"Docstring lines for {f.full_name!r} are too long!\n" + f"Lines should be no longer than {max_width} characters.") + else: + if f.full_name in OVERLONG_BODY: + warn(f"Remove {f.full_name!r} from OVERLONG_BODY!\n") + parameters_marker_count = len(f.docstring.split('{parameters}')) - 1 if parameters_marker_count > 1: fail('You may not specify {parameters} more than once in a docstring!') diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index e80e2f5f13f648..4280af0c4c9b49 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -167,6 +167,19 @@ def methoddef_flags(self) -> str | None: flags.append('METH_COEXIST') return '|'.join(flags) + @property + def docstring_line_width(self) -> int: + """Return the maximum line width for docstring lines. + + Pydoc adds indentation when displaying functions and methods. + To keep the total width of within 80 characters, we use a + maximum of 76 characters for global functions and classes, + and 72 characters for methods. + """ + if self.cls is not None and not self.kind.new_or_init: + return 72 + return 76 + def __repr__(self) -> str: return f''