diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0a0100fcb2ebb5..6b8a900f49a23a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -63,14 +63,14 @@ .azure-pipelines/ @AA-Turner # GitHub & related scripts -.github/ @ezio-melotti @hugovk @AA-Turner @webknjaz @itamaro @JacobCoffee -Tools/build/compute-changes.py @AA-Turner @hugovk @webknjaz -Lib/test/test_tools/test_compute_changes.py @AA-Turner @hugovk @webknjaz +.github/ @ezio-melotti @hugovk @AA-Turner @webknjaz @itamaro @JacobCoffee @StanFromIreland +Tools/build/compute-changes.py @AA-Turner @hugovk @webknjaz @StanFromIreland +Lib/test/test_tools/test_compute_changes.py @AA-Turner @hugovk @webknjaz @StanFromIreland Tools/build/verify_ensurepip_wheels.py @AA-Turner @pfmoore @pradyunsg # Pre-commit -.pre-commit-config.yaml @hugovk @JacobCoffee -.ruff.toml @hugovk @AlexWaygood @AA-Turner @JacobCoffee +.pre-commit-config.yaml @hugovk @JacobCoffee @StanFromIreland +.ruff.toml @hugovk @AlexWaygood @AA-Turner @JacobCoffee @StanFromIreland # Patchcheck Tools/patchcheck/ @AA-Turner @itamaro diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 177615621f6b8c..fe1273804a5acb 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -16,7 +16,7 @@ body: Make sure to also search the [CPython issue tracker](https://github.com/python/cpython/issues?q=is%3Aissue+sort%3Acreated-desc) to check that the bug has not already been reported. - type: textarea attributes: - label: "Bug description:" + label: "Bug report" description: > Give a clear and concise description of what happened. Include a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) if possible. diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml index 81ae91e5b0af97..690fe7721fdb8e 100644 --- a/.github/ISSUE_TEMPLATE/crash.yml +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -10,8 +10,9 @@ body: The CPython interpreter is written in a different programming language, C. A "CPython crash" is when Python itself fails, leading to a traceback in the C stack. - type: textarea attributes: - label: What happened? + label: Crash report description: > + Give a clear and concise description of what happened. Include a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) if possible. [Copy and paste code where possible rather than using screenshots](https://meta.stackoverflow.com/a/285557/13990016), and put any code blocks inside triple backticks. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 4361ab2bf827fb..3ecae7112170e0 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -12,7 +12,7 @@ body: Major feature proposals should generally be discussed on [Discourse](https://discuss.python.org/c/ideas/6) before opening a GitHub issue. Wait until it's clear that most people support your idea before filling in this form. - type: textarea attributes: - label: "Proposal:" + label: "Feature or enhancement" description: > Explain your proposal, why it should be implemented, and how it would be used. Add examples, if applicable. diff --git a/.github/workflows/add-issue-header.yml b/.github/workflows/add-issue-header.yml deleted file mode 100644 index 55095f17bb4cce..00000000000000 --- a/.github/workflows/add-issue-header.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Add issue header -# Automatically edits an issue's descriptions with a header, -# one of: -# -# - Bug report -# - Crash report -# - Feature or enhancement - -on: - issues: - types: - # Only ever run once - - opened - -permissions: - contents: read - -jobs: - add-header: - runs-on: ubuntu-slim - permissions: - issues: write - timeout-minutes: 5 - steps: - - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - # language=JavaScript - script: | - // https://devguide.python.org/triage/labels/#type-labels - const HEADERS = new Map([ - ['type-bug', 'Bug report'], - ['type-crash', 'Crash report'], - ['type-feature', 'Feature or enhancement'], - ]); - let issue_data = await github.rest.issues.get({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo - }).then(issue => issue.data); - let header = ''; - for (const label_data of issue_data.labels) { - const label_name = (typeof label_data === 'string') ? label_data : label_data.name; - if (HEADERS.has(label_name)) { - header = HEADERS.get(label_name); - break; - } - } - if (header !== '') { - console.log(`Setting new header: ${header}`); - await github.rest.issues.update({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: `# ${header}\n\n${issue_data.body.replaceAll('\r', '')}` - }); - } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9fbc852455045..9ab0473f4b9c59 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -268,13 +268,12 @@ jobs: # unsupported as it most resembles other 1.1.1-work-a-like ssl APIs # supported by important vendors such as AWS-LC. - { name: openssl, version: 1.1.1w } - - { name: openssl, version: 3.0.22 } - - { name: openssl, version: 3.4.7 } - - { name: openssl, version: 3.5.8 } - - { name: openssl, version: 3.6.4 } - - { name: openssl, version: 4.0.2 } + - { name: openssl, version: 3.4.8 } + - { name: openssl, version: 3.5.9 } + - { name: openssl, version: 3.6.5 } + - { name: openssl, version: 4.0.3 } ## AWS-LC - - { name: aws-lc, version: 5.5.0 } + - { name: aws-lc, version: 5.10.0 } env: SSLLIB_VER: ${{ matrix.ssllib.version }} MULTISSL_DIR: ${{ github.workspace }}/multissl @@ -379,7 +378,7 @@ jobs: needs: build-context if: needs.build-context.outputs.run-ubuntu == 'true' env: - OPENSSL_VER: 3.5.8 + OPENSSL_VER: 3.5.9 PYTHONSTRICTEXTENSIONBUILD: 1 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -493,7 +492,7 @@ jobs: matrix: os: [ubuntu-26.04] env: - OPENSSL_VER: 3.5.8 + OPENSSL_VER: 3.5.9 PYTHONSTRICTEXTENSIONBUILD: 1 ASAN_OPTIONS: detect_leaks=0:allocator_may_return_null=1:handle_segv=0 steps: @@ -587,7 +586,7 @@ jobs: - name: Display build info run: | "$BUILD_DIR/cross-python/bin/python3" -m test.pythoninfo - - name: Run test subset with host build + - name: Run test subset with cross build run: | "$BUILD_DIR/cross-python/bin/python3" -m test test_sysconfig test_site test_embed diff --git a/.github/workflows/reusable-san.yml b/.github/workflows/reusable-san.yml index 7eb3ee919d2892..6b4a43b259638e 100644 --- a/.github/workflows/reusable-san.yml +++ b/.github/workflows/reusable-san.yml @@ -17,7 +17,7 @@ permissions: env: FORCE_COLOR: 1 - OPENSSL_VER: 3.5.8 + OPENSSL_VER: 3.5.9 jobs: build-san-reusable: diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index ba12e82218d1a5..1b10078b8409f4 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -35,7 +35,7 @@ jobs: runs-on: ${{ inputs.os }} timeout-minutes: 60 env: - OPENSSL_VER: 3.5.8 + OPENSSL_VER: 3.5.9 PYTHONSTRICTEXTENSIONBUILD: 1 TERM: linux steps: diff --git a/Doc/builtins/threadsafety.rst b/Doc/builtins/threadsafety.rst index a529f7803affbc..32600a35ef6eea 100644 --- a/Doc/builtins/threadsafety.rst +++ b/Doc/builtins/threadsafety.rst @@ -408,9 +408,9 @@ as operands and always lock both objects: iterate through all the passed iterables and do the following: * :meth:`set.update` and :meth:`set.union` lock both objects only when - the other operand is a :class:`set`, :class:`frozenset`, or :class:`dict`. + the other operand is a :class:`set`, :class:`frozenset`, or :class:`dict`. * :meth:`set.intersection` and :meth:`set.difference` always try to lock - all objects. + all objects. :meth:`set.symmetric_difference` tries to lock both objects. @@ -418,9 +418,9 @@ The update variants of the above methods also have some differences between them: * :meth:`set.difference_update` and :meth:`set.intersection_update` try - to lock all objects one-by-one. + to lock all objects one-by-one. * :meth:`set.symmetric_difference_update` only locks the arguments if it is - of type :class:`set`, :class:`frozenset`, or :class:`dict`. + of type :class:`set`, :class:`frozenset`, or :class:`dict`. The following methods always try to lock both objects: diff --git a/Doc/library/dbm.rst b/Doc/library/dbm.rst index 646981e8692cc5..3e34395e3120c3 100644 --- a/Doc/library/dbm.rst +++ b/Doc/library/dbm.rst @@ -253,6 +253,38 @@ functionality like crash tolerance. A string of characters the *flag* parameter of :meth:`~dbm.gnu.open` supports. +.. data:: GDBM_VERSION_INFO + + A named tuple containing the three components of the GDBM library + version that was used for building the module: + *major*, *minor*, and *patch*. + All values are integers. + The components can also be accessed by name, + so ``dbm.gnu.GDBM_VERSION_INFO[0]`` is equivalent to + ``dbm.gnu.GDBM_VERSION_INFO.major`` and so on. + This may be different from the GDBM library actually used at runtime, + which is available as :const:`gdbm_version_info`. + + .. versionadded:: next + + +.. data:: gdbm_version_info + + A named tuple containing the version of the GDBM library + actually loaded by the interpreter, + with the same fields as :const:`GDBM_VERSION_INFO`. + + .. versionadded:: next + + +.. data:: gdbm_version + + The release string of the GDBM library actually loaded by the interpreter, + like ``'GDBM version 1.26. 30/07/2025'``. + + .. versionadded:: next + + .. function:: open(filename, flag="r", mode=0o666, /) Open a GDBM database and return a :class:`!gdbm` object. @@ -382,7 +414,69 @@ This module can be used with the "classic" NDBM interface or the .. data:: library - Name of the NDBM implementation library used. + Name of the NDBM implementation library used: + ``'GNU gdbm'``, ``'Berkeley DB'`` or ``'ndbm'``. + + .. versionchanged:: next + The value is ``'ndbm'`` for a classic NDBM library. + It was ``'GNU gdbm'`` before. + + +.. data:: GDBM_VERSION_INFO + gdbm_version_info + gdbm_version + + Information about the GDBM library in use, + with the same meaning as the constants of the same names in :mod:`dbm.gnu`. + Only available if :const:`library` is ``'GNU gdbm'``. + + .. versionadded:: next + + +.. data:: BDB_VERSION + + The version string of the Berkeley DB library that was used for building + the module, like ``'Berkeley DB 5.3.28: (September 9, 2013)'``. + This may be different from the Berkeley DB library actually used at runtime, + which is available as :const:`bdb_version`. + Only available if :const:`library` is ``'Berkeley DB'``. + + .. versionadded:: next + + +.. data:: bdb_version + + The version string of the Berkeley DB library actually loaded by the + interpreter. + Only available if :const:`library` is ``'Berkeley DB'``. + + .. versionadded:: next + + +.. data:: BDB_VERSION_INFO + + A named tuple containing the three components of the Berkeley DB library + version that was used for building the module: + *major*, *minor*, and *patch*. + All values are integers. + The components can also be accessed by name, + so ``dbm.ndbm.BDB_VERSION_INFO[0]`` is equivalent to + ``dbm.ndbm.BDB_VERSION_INFO.major`` and so on. + This may be different from the Berkeley DB library actually used at runtime, + which is available as :const:`bdb_version_info`. + Only available if :const:`library` is ``'Berkeley DB'``. + + .. versionadded:: next + + +.. data:: bdb_version_info + + A named tuple containing the version of the Berkeley DB library + actually loaded by the interpreter, + with the same fields as :const:`BDB_VERSION_INFO`. + Only available if :const:`library` is ``'Berkeley DB'``. + + .. versionadded:: next .. function:: open(filename, flag="r", mode=0o666, /) diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index ecd973a070b3df..3b7c30d89236ac 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -1632,6 +1632,46 @@ are also included in the pure Python version for compatibility. .. versionadded:: 3.8.3 +The following constants are only available in the C module. + +.. data:: LIBMPDEC_VERSION + + The version string of the libmpdec library that was used for building + the module. + This may be different from the libmpdec library actually used at runtime, + which is available as :const:`libmpdec_version`. + + .. versionadded:: next + +.. data:: libmpdec_version + + The version string of the libmpdec library actually loaded by the + interpreter. + + .. versionadded:: next + +.. data:: LIBMPDEC_VERSION_INFO + + A named tuple containing the three components of the libmpdec library + version that was used for building the module: + *major*, *minor*, and *micro*. + All values are integers. + The components can also be accessed by name, + so ``decimal.LIBMPDEC_VERSION_INFO[0]`` is equivalent to + ``decimal.LIBMPDEC_VERSION_INFO.major`` and so on. + This may be different from the libmpdec library actually used at runtime, + which is available as :const:`libmpdec_version_info`. + + .. versionadded:: next + +.. data:: libmpdec_version_info + + A named tuple containing the version of the libmpdec library + actually loaded by the interpreter, + with the same fields as :const:`LIBMPDEC_VERSION_INFO`. + + .. versionadded:: next + Rounding modes -------------- diff --git a/Doc/library/pyexpat.rst b/Doc/library/pyexpat.rst index 638b8f2862b7d1..cac861e293ec55 100644 --- a/Doc/library/pyexpat.rst +++ b/Doc/library/pyexpat.rst @@ -60,10 +60,29 @@ This module provides the following exception, type object and data items: like ``'expat_2.8.4'``. +.. data:: VERSION_INFO + + A named tuple containing the three components of the Expat library + version that was used for building the module: + *major*, *minor*, and *micro*. + All values are integers. + The components can also be accessed by name, + so ``xml.parsers.expat.VERSION_INFO[0]`` is equivalent to + ``xml.parsers.expat.VERSION_INFO.major`` and so on. + This may be different from the Expat library actually used at runtime, + which is available as :const:`version_info`. + + .. versionadded:: next + + .. data:: version_info - The version of the Expat library loaded by the interpreter, - as a tuple of three integers: major, minor and micro version. + A named tuple containing the version of the Expat library + loaded by the interpreter, + with the same fields as :const:`VERSION_INFO`. + + .. versionchanged:: next + It is now a named tuple. .. data:: features diff --git a/Doc/library/readline.rst b/Doc/library/readline.rst index 234af8d191e3e3..58471b794df203 100644 --- a/Doc/library/readline.rst +++ b/Doc/library/readline.rst @@ -57,6 +57,38 @@ Readline library in general. .. versionadded:: 3.13 +.. data:: READLINE_VERSION_INFO + + A named tuple containing the two components of the Readline library + version that was used for building the module: *major* and *minor*. + Both values are integers. + The components can also be accessed by name, + so ``readline.READLINE_VERSION_INFO[0]`` is equivalent to + ``readline.READLINE_VERSION_INFO.major`` and so on. + This may be different from the Readline library actually used at runtime, + which is available as :const:`readline_version_info`. + + With the ``editline`` backend, this is the version of the Readline + interface emulated by libedit, not the version of libedit. + + .. versionadded:: next + +.. data:: readline_version_info + + A named tuple containing the version of the Readline library + actually loaded by the interpreter, + with the same fields as :const:`READLINE_VERSION_INFO`. + + .. versionadded:: next + +.. data:: readline_version + + The version string of the Readline library actually loaded by the + interpreter, like ``'8.3'``. + With the ``editline`` backend, this is ``'EditLine wrapper'``. + + .. versionadded:: next + Init file --------- diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 5aa3d2b0adcbf6..130a84ae14230b 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -498,14 +498,40 @@ Module constants The ``named`` DB-API parameter style is also supported. +.. data:: SQLITE_VERSION + + The version string of the SQLite library that was used for building + the module. + This may be different from the SQLite library actually used at runtime, + which is available as :const:`sqlite_version`. + + .. versionadded:: next + .. data:: sqlite_version Version number of the runtime SQLite library as a :class:`string `. +.. data:: SQLITE_VERSION_INFO + + A named tuple containing the three components of the SQLite library + version that was used for building the module: + *major*, *minor*, and *patch*. + All values are integers. + The components can also be accessed by name, + so ``sqlite3.SQLITE_VERSION_INFO[0]`` is equivalent to + ``sqlite3.SQLITE_VERSION_INFO.major`` and so on. + This may be different from the SQLite library actually used at runtime, + which is available as :const:`sqlite_version_info`. + + .. versionadded:: next + .. data:: sqlite_version_info - Version number of the runtime SQLite library as a :class:`tuple` of - :class:`integers `. + A named tuple containing the version of the runtime SQLite library, + with the same fields as :const:`SQLITE_VERSION_INFO`. + + .. versionchanged:: next + It is now a named tuple. .. data:: SQLITE_KEYWORDS diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 66fe6c7aee4862..c38763037d67c5 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -982,14 +982,26 @@ Constants .. data:: OPENSSL_VERSION_INFO - A tuple of five integers representing version information about the - OpenSSL library:: + A named tuple of five integers representing version information about the + OpenSSL library loaded by the interpreter: + *major*, *minor*, *fix*, *patch* and *status*:: >>> ssl.OPENSSL_VERSION_INFO - (1, 0, 2, 11, 15) + ssl.OPENSSL_VERSION_INFO(major=3, minor=0, fix=0, patch=13, status=0) .. versionadded:: 3.2 + .. versionchanged:: next + It is now a named tuple. + +.. data:: OPENSSL_API_VERSION_INFO + + A named tuple containing the version of the OpenSSL library that was used + for building the module, with the same fields as :const:`OPENSSL_VERSION_INFO`. + This may be different from the OpenSSL library actually used at runtime. + + .. versionadded:: next + .. data:: OPENSSL_VERSION_NUMBER The raw version number of the OpenSSL library, as a single integer:: diff --git a/Doc/library/tempfile.rst b/Doc/library/tempfile.rst index 316d82775cf626..8bc1cdb81d65c6 100644 --- a/Doc/library/tempfile.rst +++ b/Doc/library/tempfile.rst @@ -234,6 +234,15 @@ The module defines the following user-callable items: debugging or when you need your cleanup behavior to be conditional based on other logic. + .. warning:: + + Cleanup is not robust against the tree being modified while it is removed. + Files outside of the tree may have their permissions and file flags reset. + + On systems where :data:`shutil.rmtree.avoids_symlink_attacks` is + false, manipulating symbolic links during cleanup + may cause files outside of the tree to be removed. + .. audit-event:: tempfile.mkdtemp fullpath tempfile.TemporaryDirectory .. versionadded:: 3.2 diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 44f688ad0d6422..a6a30928ce83d2 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -6898,3 +6898,32 @@ and :mod:`!tkinter.constants` namespaces. DOTBOX Other option values: ``'underline'``, ``'numeric'`` and ``'dotbox'``. + + +Version information +^^^^^^^^^^^^^^^^^^^ + +.. data:: TCL_VERSION_INFO + TK_VERSION_INFO + + The versions of the Tcl and Tk libraries that were used for building + the :mod:`!_tkinter` module, as named tuples with the same five fields + as :data:`sys.version_info`: *major*, *minor*, *micro*, *releaselevel* + and *serial*. + *releaselevel* is ``'alpha'``, ``'beta'`` or ``'final'``. + Converting them to a string gives the version in the usual Tcl/Tk notation, + for example ``'9.0.3'`` for a final release or ``'9.1b2'`` for a + pre-release. + These may be different from the libraries actually used at runtime, + which are available as :meth:`Misc.info_patchlevel` (the Tcl version) + and the ``tk_patchLevel`` Tcl variable. + + .. versionadded:: next + +.. data:: TkVersion + TclVersion + + The major and minor version of the Tk and Tcl libraries that were used + for building the :mod:`!_tkinter` module, as floats, for example ``8.6``. + Prefer :data:`TK_VERSION_INFO` and :data:`TCL_VERSION_INFO`, which are + more precise and compare correctly. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index c61190822234ea..29bd7dcd749ebc 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -347,6 +347,39 @@ concurrent.futures (Contributed by xzmeng and Serhiy Storchaka in :gh:`108518`.) +decimal +------- + +* Added constants :const:`~decimal.LIBMPDEC_VERSION`, + :const:`~decimal.libmpdec_version`, :const:`~decimal.LIBMPDEC_VERSION_INFO`, + and :const:`~decimal.libmpdec_version_info` in the C implementation, + which provide information about the version of the libmpdec library in use. + (Contributed by Serhiy Storchaka in :gh:`157475`.) + + +dbm.gnu +------- + +* Added constants :const:`~dbm.gnu.GDBM_VERSION_INFO`, + :const:`~dbm.gnu.gdbm_version_info` and :const:`~dbm.gnu.gdbm_version`, + which provide information about the version of the GDBM library in use. + (Contributed by Serhiy Storchaka in :gh:`157478`.) + + +dbm.ndbm +-------- + +* Added constants which provide information about the version of the + underlying library in use: :const:`~dbm.ndbm.GDBM_VERSION_INFO`, + :const:`~dbm.ndbm.gdbm_version_info` and :const:`~dbm.ndbm.gdbm_version` + if it is GDBM, or :const:`~dbm.ndbm.BDB_VERSION`, + :const:`~dbm.ndbm.bdb_version`, :const:`~dbm.ndbm.BDB_VERSION_INFO` and + :const:`~dbm.ndbm.bdb_version_info` if it is Berkeley DB. + :const:`dbm.ndbm.library` is now ``'ndbm'`` instead of ``'GNU gdbm'`` + for a classic NDBM library. + (Contributed by Serhiy Storchaka in :gh:`157480`.) + + difflib ------- @@ -521,6 +554,16 @@ pydoc (Contributed by Serhiy Storchaka in :gh:`153906`.) +readline +-------- + +* Added constants :const:`~readline.READLINE_VERSION_INFO`, + :const:`~readline.readline_version_info` and + :const:`~readline.readline_version`, which provide information about + the version of the Readline library in use. + (Contributed by Serhiy Storchaka in :gh:`157485`.) + + re -- @@ -565,6 +608,22 @@ sqlite3 :exc:`SystemError` or :exc:`ValueError`. (Contributed by Jiseok CHOI in :gh:`150449`.) +* Added constants :const:`~sqlite3.SQLITE_VERSION` and + :const:`~sqlite3.SQLITE_VERSION_INFO` which provide information + about the version of the SQLite library that was used for building the module. + :const:`~sqlite3.sqlite_version_info` is now a named tuple. + (Contributed by Serhiy Storchaka in :gh:`157470`.) + + +ssl +--- + +* :const:`ssl.OPENSSL_VERSION_INFO` is now a named tuple. + Added :const:`~ssl.OPENSSL_API_VERSION_INFO` which provides information + about the version of the OpenSSL library that was used for building + the module. + (Contributed by Serhiy Storchaka in :gh:`157513`.) + symtable -------- @@ -597,6 +656,11 @@ tkinter methods (without recursion) also work on Tk older than 9.1. (Contributed by Serhiy Storchaka in :gh:`151910`.) +* Added constants :const:`~tkinter.TCL_VERSION_INFO` and + :const:`~tkinter.TK_VERSION_INFO`, named tuples with the versions of + the Tcl and Tk libraries that were used for building the module. + (Contributed by Serhiy Storchaka in :gh:`157514`.) + * Added new :class:`!tkinter.Text` methods :meth:`~tkinter.Text.edit_canundo` and :meth:`~tkinter.Text.edit_canredo` which return whether an undo or redo is possible. @@ -790,6 +854,12 @@ xml building a tree for it. (Contributed by Serhiy Storchaka in :gh:`63102`.) +* Added constant :const:`~xml.parsers.expat.VERSION_INFO` in the + :mod:`XML parser ` module, which provides information + about the version of the Expat library that was used for building the module. + :const:`~xml.parsers.expat.version_info` is now a named tuple. + (Contributed by Serhiy Storchaka in :gh:`157473`.) + zipfile ------- diff --git a/Lib/shutil.py b/Lib/shutil.py index ab75ba9da8894b..d6d7806802bd17 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -761,6 +761,7 @@ def _rmtree_safe_fd_step(stack, onexc): # save a call to os.lstat() when walking subdirectories. func, dirfd, path, orig_entry = stack.pop() name = path if orig_entry is None else orig_entry.name + parent_fd = None if func is os.close else dirfd try: if func is os.close: os.close(dirfd) @@ -808,14 +809,14 @@ def _rmtree_safe_fd_step(stack, onexc): except FileNotFoundError: continue except OSError as err: - onexc(os.unlink, fullname, err) + onexc(os.unlink, fullname, err, direntry=entry, dir_fd=topfd) except FileNotFoundError as err: if orig_entry is None or func is os.close: err.filename = path - onexc(func, path, err) + onexc(func, path, err, direntry=orig_entry, dir_fd=parent_fd) except OSError as err: err.filename = path - onexc(func, path, err) + onexc(func, path, err, direntry=orig_entry, dir_fd=parent_fd) _use_fd_functions = ({os.open, os.stat, os.unlink, os.rmdir} <= os.supports_dir_fd and @@ -823,7 +824,8 @@ def _rmtree_safe_fd_step(stack, onexc): os.stat in os.supports_follow_symlinks) _rmtree_impl = _rmtree_safe_fd if _use_fd_functions else _rmtree_unsafe -def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None): +def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None, + _onexc_kwargs=False): """Recursively delete a directory tree. If dir_fd is not None, it should be a file descriptor open to a directory; @@ -846,24 +848,29 @@ def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None): sys.audit("shutil.rmtree", path, dir_fd) if ignore_errors: - def onexc(*args): + def onexc(*args, **kwargs): pass elif onerror is None and onexc is None: - def onexc(*args): + def onexc(*args, **kwargs): raise elif onexc is None: if onerror is None: - def onexc(*args): + def onexc(*args, **kwargs): raise else: # delegate to onerror - def onexc(*args): + def onexc(*args, **kwargs): func, path, exc = args if exc is None: exc_info = None, None, None else: exc_info = type(exc), exc, exc.__traceback__ return onerror(func, path, exc_info) + elif not _onexc_kwargs: + # Only the internal caller in tempfile asks for the extra arguments. + _onexc = onexc + def onexc(func, path, err, **kwargs): + return _onexc(func, path, err) _rmtree_impl(path, dir_fd, onexc) diff --git a/Lib/sqlite3/dbapi2.py b/Lib/sqlite3/dbapi2.py index 0315760516edf8..2054b3a78e3f60 100644 --- a/Lib/sqlite3/dbapi2.py +++ b/Lib/sqlite3/dbapi2.py @@ -46,8 +46,6 @@ def TimestampFromTicks(ticks): return Timestamp(*time.localtime(ticks)[:6]) -sqlite_version_info = tuple([int(x) for x in sqlite_version.split(".")]) - Binary = memoryview collections.abc.Sequence.register(Row) diff --git a/Lib/ssl.py b/Lib/ssl.py index dc957121728f28..44dc0b046f4518 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -103,6 +103,7 @@ import _ssl # if we can't import it, let the error propagate from _ssl import OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_INFO, OPENSSL_VERSION +from _ssl import OPENSSL_API_VERSION_INFO from _ssl import _SSLContext, MemoryBIO, SSLSession from _ssl import ( SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError, diff --git a/Lib/tempfile.py b/Lib/tempfile.py index b5f799f8d68554..ad81c5d3417b4c 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -44,6 +44,7 @@ import shutil as _shutil import errno as _errno from random import Random as _Random +import stat as _stat import sys as _sys import types as _types import weakref as _weakref @@ -274,15 +275,68 @@ def _dont_follow_symlinks(func, path, *args): elif not _os.path.islink(path): func(path, *args) -def _resetperms(path): +def _resetflags(path): try: chflags = _os.chflags except AttributeError: pass else: _dont_follow_symlinks(chflags, path, 0) + +def _resetperms(path): + _resetflags(path) _dont_follow_symlinks(_os.chmod, path, 0o700) +# True if TemporaryDirectory._rmtree() can work relative to open directories +# instead of resolving paths again. +_rmtree_use_dir_fd = ( + {_os.chmod, _os.unlink, _os.lstat} <= _os.supports_dir_fd + and _os.chmod in _os.supports_fd +) + +def _resetperms_fd(dir_fd, path): + # Same as _resetperms(), but for the directory referred to by dir_fd. + if dir_fd is None: + _resetperms(path) + return + _resetflags(path) + _os.chmod(dir_fd, 0o700) + +try: + _nofollow_mode = _os.O_RDONLY | _os.O_NONBLOCK | _os.O_NOFOLLOW +except AttributeError: + _nofollow_mode = None + +def _resetperms_at(name, dir_fd, path): + # Same as _resetperms(), but name is resolved relative to the directory + # file descriptor dir_fd. path is only used for os.chflags(), which + # doesn't support dir_fd or file descriptors. + if dir_fd is None: + _resetperms(path) + return + _resetflags(path) + if _os.chmod in _os.supports_follow_symlinks: + _os.chmod(name, 0o700, dir_fd=dir_fd, follow_symlinks=False) + else: + # dir_fd & follow_symlinks is not supported on this platform. + # Try chmod opening the file with O_NOFOLLOW. + if _nofollow_mode is not None: + try: + fd = _os.open(name, _nofollow_mode, dir_fd=dir_fd) + except OSError: + pass + else: + try: + _os.chmod(fd, 0o700) + finally: + _os.close(fd) + return + # If that did not work, we change by name, which is subject to a race + # condition. + stat = _os.lstat(name, dir_fd=dir_fd) + if not _stat.S_ISLNK(stat.st_mode): + _os.chmod(name, 0o700, dir_fd=dir_fd) + # User visible interfaces. @@ -927,8 +981,12 @@ def __init__(self, suffix=None, prefix=None, dir=None, ignore_errors=self._ignore_cleanup_errors, delete=self._delete) @classmethod - def _rmtree(cls, name, ignore_errors=False, repeated=False): - def onexc(func, path, exc): + def _rmtree(cls, name, ignore_errors=False, repeated=False, dir_fd=None, + fullname=None): + if fullname is None: + fullname = name + + def onexc(func, path, exc, direntry=None, dir_fd=None): # On DragonFly BSD, UF_NOUNLINK removal fails with EISDIR, not EPERM. if isinstance(exc, (PermissionError, IsADirectoryError)): if repeated and path == name: @@ -936,15 +994,30 @@ def onexc(func, path, exc): return raise + # fullpath is path as seen from the working directory + fullpath = fullname + path[len(name):] + # base is path relative to dir_fd, the directory rmtree() + # reached it through, or the whole path when there is none + if dir_fd is None or not _rmtree_use_dir_fd: + base, dir_fd = path, None + elif direntry is None: + base = path + else: + base = direntry.name + try: if path != name: - _resetperms(_os.path.dirname(path)) - _resetperms(path) + # The parent directory of path is the one referred to + # by dir_fd. + _resetperms_fd(dir_fd, _os.path.dirname(fullpath)) + _resetperms_at(base, dir_fd, fullpath) try: - _os.unlink(path) + _os.unlink(base, dir_fd=dir_fd) except IsADirectoryError: - cls._rmtree(path, ignore_errors=ignore_errors) + cls._rmtree(base, ignore_errors=ignore_errors, + repeated=(path == name), + dir_fd=dir_fd, fullname=fullpath) except PermissionError: # The PermissionError handler was originally added for # FreeBSD in directories, but it seems that it is raised @@ -953,21 +1026,27 @@ def onexc(func, path, exc): # raise NotADirectoryError and mask the PermissionError. # So we must re-raise the current PermissionError if # path is not a directory. - if not _os.path.isdir(path) or _os.path.isjunction(path): + if (not _os.path.isdir(fullpath) + or _os.path.isjunction(fullpath)): if ignore_errors: return raise - cls._rmtree(path, ignore_errors=ignore_errors, - repeated=(path == name)) + cls._rmtree(base, ignore_errors=ignore_errors, + repeated=(path == name), + dir_fd=dir_fd, fullname=fullpath) except FileNotFoundError: pass + except OSError: + if ignore_errors: + return + raise elif isinstance(exc, FileNotFoundError): pass else: if not ignore_errors: raise - _shutil.rmtree(name, onexc=onexc) + _shutil.rmtree(name, onexc=onexc, dir_fd=dir_fd, _onexc_kwargs=True) @classmethod def _cleanup(cls, name, warn_message, ignore_errors=False, delete=True): diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 1f0753b9839cdd..100e61a20211c2 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -427,27 +427,13 @@ def collect_readline(info_add): except ImportError: return - def format_attr(attr, value): - if isinstance(value, int): - return "%#x" % value - else: - return value - attributes = ( - "_READLINE_VERSION", - "_READLINE_RUNTIME_VERSION", - "_READLINE_LIBRARY_VERSION", + "backend", + "READLINE_VERSION_INFO", + "readline_version_info", + "readline_version", ) - copy_attributes(info_add, readline, 'readline.%s', attributes, - formatter=format_attr) - - if not hasattr(readline, "_READLINE_LIBRARY_VERSION"): - # _READLINE_LIBRARY_VERSION has been added to CPython 3.7 - doc = getattr(readline, '__doc__', '') - if 'libedit readline' in doc: - info_add('readline.library', 'libedit readline') - elif 'GNU readline' in doc: - info_add('readline.library', 'GNU readline') + copy_attributes(info_add, readline, 'readline.%s', attributes) def run_command(cmd, check=True, **kwargs): @@ -505,7 +491,7 @@ def collect_tkinter(info_add): except ImportError: pass else: - attributes = ('TK_VERSION', 'TCL_VERSION') + attributes = ('TK_PATCH_LEVEL', 'TCL_PATCH_LEVEL') copy_attributes(info_add, _tkinter, 'tkinter.%s', attributes) try: @@ -646,6 +632,7 @@ def format_attr(attr, value): attributes = ( 'OPENSSL_VERSION', 'OPENSSL_VERSION_INFO', + 'OPENSSL_API_VERSION_INFO', 'HAS_SNI', 'OP_ALL', 'OP_NO_TLSv1_1', @@ -701,7 +688,7 @@ def collect_sqlite(info_add): except ImportError: return - attributes = ('sqlite_version',) + attributes = ('SQLITE_VERSION', 'sqlite_version') copy_attributes(info_add, sqlite3, 'sqlite3.%s', attributes) @@ -761,7 +748,7 @@ def collect_expat(info_add): except ImportError: return - attributes = ('EXPAT_VERSION',) + attributes = ('EXPAT_VERSION', 'VERSION_INFO', 'version_info') copy_attributes(info_add, expat, 'expat.%s', attributes) @@ -771,7 +758,7 @@ def collect_decimal(info_add): except ImportError: return - attributes = ('__libmpdec_version__',) + attributes = ('LIBMPDEC_VERSION', 'libmpdec_version') copy_attributes(info_add, _decimal, '_decimal.%s', attributes) @@ -958,13 +945,25 @@ def collect_cc(info_add): info_add('CXX.version', version) +def collect_ndbm(info_add): + try: + import _dbm + except ImportError: + return + + attributes = ('library', 'GDBM_VERSION_INFO', 'gdbm_version', + 'BDB_VERSION', 'bdb_version') + copy_attributes(info_add, _dbm, 'ndbm.%s', attributes) + + def collect_gdbm(info_add): try: - from _gdbm import _GDBM_VERSION + import _gdbm except ImportError: return - info_add('gdbm.GDBM_VERSION', '.'.join(map(str, _GDBM_VERSION))) + attributes = ('GDBM_VERSION_INFO', 'gdbm_version') + copy_attributes(info_add, _gdbm, 'gdbm.%s', attributes) def collect_get_config(info_add): @@ -1380,6 +1379,7 @@ def collect_info(info): collect_expat, collect_fips, collect_gdb, + collect_ndbm, collect_gdbm, collect_get_config, collect_locale, diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py index 733a8ebbf1e5c3..f8487a2b8fb085 100644 --- a/Lib/test/test_capi/test_bytes.py +++ b/Lib/test/test_capi/test_bytes.py @@ -356,18 +356,22 @@ class BaseWriterTest: def create_writer(self, alloc=0, string=b''): raise NotImplementedError + def bytes_equal(self, result, expected): + # Similar to assertEqual(), but check also that the result type + # is RESULT_TYPE. + self.assertEqual(result, expected) + self.assertEqual(type(result), self.RESULT_TYPE) + def test_create(self): # Test PyBytesWriter_Create() writer = self.create_writer() self.assertEqual(writer.get_size(), 0) - self.assertEqual(writer.finish(), b'') + self.bytes_equal(writer.finish(), b'') writer = self.create_writer(3) writer.write(0, b'abc') self.assertEqual(writer.get_size(), 3) - result = writer.finish() - self.assertEqual(result, b'abc') - self.assertEqual(type(result), self.RESULT_TYPE) + self.bytes_equal(writer.finish(), b'abc') @unittest.skipUnless(support.Py_DEBUG, 'need Py_DEBUG') def test_get_data(self): @@ -408,9 +412,7 @@ def test_finish_with_size(self): writer = self.create_writer(10) writer.write(0, b'abc123') self.assertEqual(writer.get_size(), 10) - result = writer.finish_with_size(3) - self.assertEqual(result, b'abc') - self.assertEqual(type(result), self.RESULT_TYPE) + self.bytes_equal(writer.finish_with_size(3), b'abc') # Error if the size is negative writer = self.create_writer(3, ) @@ -428,19 +430,19 @@ def test_write_bytes(self): # Test PyBytesWriter_WriteBytes() writer = self.create_writer() writer.write_bytes(b'Hello World!', -1) - self.assertEqual(writer.finish(), b'Hello World!') + self.bytes_equal(writer.finish(), b'Hello World!') writer = self.create_writer() writer.write_bytes(b'Hello ', -1) writer.write_bytes(b'World! ', 6) - self.assertEqual(writer.finish(), b'Hello World!') + self.bytes_equal(writer.finish(), b'Hello World!') def test_resize(self): # Test PyBytesWriter_Resize() writer = self.create_writer() writer.resize(len(b'hello')) writer.write(0, b'hello') - self.assertEqual(writer.finish(), b'hello') + self.bytes_equal(writer.finish(), b'hello') writer = self.create_writer() writer.resize(0) # noop @@ -451,7 +453,7 @@ def test_resize(self): writer.resize(len(b'number=123'), ) writer.write(len(b'number='), b'123') writer.resize(len(b'number=123')) # noop - self.assertEqual(writer.finish(), b'number=123') + self.bytes_equal(writer.finish(), b'number=123') # Switch from small buffer to large buffer writer = self.create_writer() @@ -460,7 +462,7 @@ def test_resize(self): writer.write(0, b's' * small) writer.resize(large) writer.write(small, b'L' * (large - small)) - self.assertEqual(writer.finish(), + self.bytes_equal(writer.finish(), b's' * small + b'L' * (large - small)) # Make sure that it's possible to write after a resize to zero @@ -469,7 +471,7 @@ def test_resize(self): writer.resize(self.LARGE_BUFFER) writer.resize(0) writer.write_bytes(b'abc', 3) - self.assertEqual(writer.finish(), b'abc') + self.bytes_equal(writer.finish(), b'abc') # invalid size for size in (self.SMALL_BUFFER, self.LARGE_BUFFER): @@ -480,7 +482,7 @@ def test_resize(self): writer.resize(-1) with self.assertRaises((MemoryError, OverflowError)): writer.resize(_testcapi.PY_SSIZE_T_MAX) - self.assertEqual(writer.finish(), b'x' * size) + self.bytes_equal(writer.finish(), b'x' * size) @unittest.skipUnless(support.Py_DEBUG, 'need debug build') def test_resize_canary(self): @@ -495,14 +497,14 @@ def test_resize_canary(self): self.assertEqual(get_data_canary(writer), data + CANARY_BYTE) writer.resize(size - 1) self.assertEqual(get_data_canary(writer), data[:-1] + CANARY_BYTE) - self.assertEqual(writer.finish(), data[:-1]) + self.bytes_equal(writer.finish(), data[:-1]) # Make the buffer empty writer = self.create_writer(size) writer.write(0, data) writer.resize(0) self.assertEqual(get_data_canary(writer), CANARY_BYTE) - self.assertEqual(writer.finish(), b'') + self.bytes_equal(writer.finish(), b'') @support.nomemtest def test_resize_error(self): @@ -516,7 +518,7 @@ def test_resize_error(self): writer.resize(size) suffix = b'still working' writer.write_bytes(suffix, -1) - self.assertEqual(writer.finish(), init + suffix) + self.bytes_equal(writer.finish(), init + suffix) # Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize) # if the new size is smaller than the allocated size @@ -526,7 +528,7 @@ def test_grow(self): writer = self.create_writer(0) writer.grow(len(b'number=123')) writer.write(0, b'number=123') - self.assertEqual(writer.finish(), b'number=123') + self.bytes_equal(writer.finish(), b'number=123') writer = self.create_writer() writer.grow(0) # noop @@ -537,7 +539,7 @@ def test_grow(self): writer.grow(len(b'123'), ) writer.write(len(b'number='), b'123') writer.grow(0) # noop - self.assertEqual(writer.finish(), b'number=123') + self.bytes_equal(writer.finish(), b'number=123') # Switch from small buffer to large buffer writer = self.create_writer() @@ -546,7 +548,7 @@ def test_grow(self): writer.write(0, b's' * small) writer.grow(large - small) writer.write(small, b'L' * (large - small)) - self.assertEqual(writer.finish(), + self.bytes_equal(writer.finish(), b's' * small + b'L' * (large - small)) # invalid size @@ -558,7 +560,7 @@ def test_grow(self): writer.grow(-size - 1) with self.assertRaises(MemoryError): writer.grow(_testcapi.PY_SSIZE_T_MAX) - self.assertEqual(writer.finish(), b'x' * size) + self.bytes_equal(writer.finish(), b'x' * size) @unittest.skipUnless(support.Py_DEBUG, 'need debug build') def test_grow_canary(self): @@ -572,14 +574,14 @@ def test_grow_canary(self): self.assertEqual(get_data_canary(writer), data + CANARY_BYTE) writer.grow(-1) self.assertEqual(get_data_canary(writer), data[:-1] + CANARY_BYTE) - self.assertEqual(writer.finish(), data[:-1]) + self.bytes_equal(writer.finish(), data[:-1]) # Make the buffer empty writer = self.create_writer(size) writer.write(0, data) writer.grow(-size) self.assertEqual(writer.get_data(), b'') - self.assertEqual(writer.finish(), b'') + self.bytes_equal(writer.finish(), b'') @support.nomemtest def test_grow_error(self): @@ -592,7 +594,7 @@ def test_grow_error(self): writer.grow(100) suffix = b'still working' writer.write_bytes(suffix, -1) - self.assertEqual(writer.finish(), init + suffix) + self.bytes_equal(writer.finish(), init + suffix) # Note: PyBytesWriter_Grow() leaves the buffer unchanged (no resize) # if grow is negative. @@ -601,12 +603,27 @@ def test_format_i(self): # Test PyBytesWriter_Format() writer = self.create_writer() writer.format_i(b'x=%i', 123456) - self.assertEqual(writer.finish(), b'x=123456') + self.bytes_equal(writer.finish(), b'x=123456') writer = self.create_writer() writer.format_i(b'x=%i, ', 123) writer.format_i(b'y=%i', 456) - self.assertEqual(writer.finish(), b'x=123, y=456') + self.bytes_equal(writer.finish(), b'x=123, y=456') + + def test_format_s(self): + # Test PyBytesWriter_Format() + writer = self.create_writer() + writer.format_s(b's=%s', b'Hello World') + self.bytes_equal(writer.finish(), b's=Hello World') + + @support.nomemtest + def test_format_s_memory_error(self): + writer = self.create_writer() + s = b'x' * self.LARGE_BUFFER + with self.assertRaises(MemoryError): + _testcapi.call_with_nomemory(0, 0, writer.format_s, b's=%s', s) + writer.write_bytes(b'after', -1) + self.bytes_equal(writer.finish(), b'after') @unittest.skipUnless(support.Py_DEBUG, 'need a Python debug build') def test_canary_byte(self): @@ -697,7 +714,7 @@ def thread_func(writer, LARGE_BUFFER): self.assertEqual(size, 10) self.assertEqual(data, b'x' * 10) - self.assertEqual(writer.finish(), b'x' * 10) + self.bytes_equal(writer.finish(), b'x' * 10) class BytesWriterTest(BaseWriterTest, unittest.TestCase): diff --git a/Lib/test/test_dbm_gnu.py b/Lib/test/test_dbm_gnu.py index 66268c42a300b5..6980a702694803 100644 --- a/Lib/test/test_dbm_gnu.py +++ b/Lib/test/test_dbm_gnu.py @@ -13,12 +13,7 @@ class TestGdbm(unittest.TestCase): @staticmethod def setUpClass(): if support.verbose: - try: - from _gdbm import _GDBM_VERSION as version - except ImportError: - pass - else: - print(f"gdbm version: {version}") + print(f"gdbm version: {gdbm.gdbm_version}") def setUp(self): self.g = None @@ -29,6 +24,35 @@ def tearDown(self): unlink(filename) @cpython_only + def _test_gdbm_version(self, v): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 3) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.patch, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertGreaterEqual(v.major, 1) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + + @unittest.skipUnless(hasattr(gdbm, 'GDBM_VERSION_INFO'), + 'requires gdbm >= 1.9') + def test_gdbm_version(self): + if support.verbose: + print(f'GDBM_VERSION_INFO = {gdbm.GDBM_VERSION_INFO}', flush=True) + print(f'gdbm_version_info = {gdbm.gdbm_version_info}', flush=True) + self._test_gdbm_version(gdbm.GDBM_VERSION_INFO) + self._test_gdbm_version(gdbm.gdbm_version_info) + self.assertEqual(gdbm.GDBM_VERSION_INFO[0], gdbm.gdbm_version_info[0]) + v = gdbm.gdbm_version_info + self.assertIsInstance(gdbm.gdbm_version, str) + self.assertStartsWith(gdbm.gdbm_version, 'GDBM version %d.%d' % v[:2]) + def test_disallow_instantiation(self): # Ensure that the type disallows instantiation (bpo-43916) self.g = gdbm.open(filename, 'c') diff --git a/Lib/test/test_dbm_ndbm.py b/Lib/test/test_dbm_ndbm.py index e0f31c9a9a337d..d86badfdd2726e 100644 --- a/Lib/test/test_dbm_ndbm.py +++ b/Lib/test/test_dbm_ndbm.py @@ -1,3 +1,4 @@ +from test import support from test.support import import_helper from test.support import os_helper import_helper.import_module("dbm.ndbm") #skip if not supported @@ -17,6 +18,53 @@ def tearDown(self): for suffix in ['', '.pag', '.dir', '.db']: os_helper.unlink(self.filename + suffix) + def _test_version_info(self, v): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 3) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.patch, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertGreaterEqual(v.major, 1) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + + def test_library_version(self): + library = dbm.ndbm.library + if support.verbose: + print(f'library = {library!r}', flush=True) + self.assertIsInstance(library, str) + if library == 'GNU gdbm': + prefix = 'GDBM' + elif library == 'Berkeley DB': + prefix = 'BDB' + else: + self.assertEqual(library, 'ndbm') + self.assertNotHasAttr(dbm.ndbm, 'GDBM_VERSION_INFO') + self.assertNotHasAttr(dbm.ndbm, 'BDB_VERSION_INFO') + return + V = getattr(dbm.ndbm, f'{prefix}_VERSION_INFO') + v = getattr(dbm.ndbm, f'{prefix.lower()}_version_info') + version = getattr(dbm.ndbm, f'{prefix.lower()}_version') + if support.verbose: + print(f'{prefix}_VERSION_INFO = {V}', flush=True) + print(f'{prefix.lower()}_version_info = {v}', flush=True) + print(f'{prefix.lower()}_version = {version!r}', flush=True) + self._test_version_info(V) + self._test_version_info(v) + self.assertEqual(V[0], v[0]) + self.assertIsInstance(version, str) + if library == 'GNU gdbm': + self.assertStartsWith(version, 'GDBM version %d.%d' % v[:2]) + else: + self.assertIsInstance(dbm.ndbm.BDB_VERSION, str) + self.assertIn('%d.%d.%d' % v[:3], version) + def test_keys(self): self.d = dbm.ndbm.open(self.filename, 'c') self.assertEqual(self.d.keys(), []) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 7524822632bae7..65bca438078634 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -4534,8 +4534,13 @@ def test_module_attributes(self): self.assertEqual(C.SPEC_VERSION, P.SPEC_VERSION) - self.assertLessEqual(set(dir(C)), set(dir(P))) - self.assertEqual([n for n in dir(C) if n[:2] != '__'], sorted(P.__all__)) + # Information about the libmpdec library, specific to the C module. + libmpdec_names = {'LIBMPDEC_VERSION', 'LIBMPDEC_VERSION_INFO', + 'libmpdec_version', 'libmpdec_version_info'} + self.assertLessEqual(set(dir(C)) - libmpdec_names, set(dir(P))) + self.assertEqual([n for n in dir(C) + if n[:2] != '__' and n not in libmpdec_names], + sorted(P.__all__)) def test_context_attributes(self): @@ -5059,6 +5064,38 @@ def test_constants(self): self.assertEqual(C.DecTraps, C.DecErrors|C.DecOverflow|C.DecUnderflow) +@requires_cdecimal +class CVersion(unittest.TestCase): + """Information about the libmpdec library in _decimal""" + + def _test_libmpdec_version(self, v, string): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 3) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.micro, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.micro) + self.assertGreaterEqual(v.major, 2) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.micro, 0) + self.assertEqual(string, '%d.%d.%d' % v) + + def test_libmpdec_version(self): + if support.verbose: + print(f'LIBMPDEC_VERSION = {C.LIBMPDEC_VERSION}', flush=True) + print(f'libmpdec_version = {C.libmpdec_version}', flush=True) + print(f'LIBMPDEC_VERSION_INFO = {C.LIBMPDEC_VERSION_INFO}', flush=True) + print(f'libmpdec_version_info = {C.libmpdec_version_info}', flush=True) + self._test_libmpdec_version(C.LIBMPDEC_VERSION_INFO, C.LIBMPDEC_VERSION) + self._test_libmpdec_version(C.libmpdec_version_info, C.libmpdec_version) + self.assertEqual(C.LIBMPDEC_VERSION_INFO[0], C.libmpdec_version_info[0]) + self.assertIs(C.libmpdec_version, C.__libmpdec_version__) + @requires_cdecimal class CWhitebox(unittest.TestCase): """Whitebox testing for _decimal""" diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index fc5c7b311934e5..2c4048f4c9f560 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -1393,5 +1393,34 @@ def test_set_maximum_amplification__amplification_not_exceeded(self): self.assertIsNotNone(parser.Parse(payload, True)) +class VersionTest(unittest.TestCase): + + def _test_version_info(self, v): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 3) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.micro, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.micro) + self.assertGreaterEqual(v.major, 2) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.micro, 0) + + def test_version_info(self): + if support.verbose: + print(f'EXPAT_VERSION = {expat.EXPAT_VERSION}', flush=True) + print(f'VERSION_INFO = {expat.VERSION_INFO}', flush=True) + print(f'version_info = {expat.version_info}', flush=True) + self._test_version_info(expat.VERSION_INFO) + self._test_version_info(expat.version_info) + self.assertEqual(expat.EXPAT_VERSION, 'expat_%d.%d.%d' % expat.version_info) + self.assertEqual(expat.VERSION_INFO[0], expat.version_info[0]) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_readline.py b/Lib/test/test_readline.py index 6af26accc13d71..f999ea6cc1affd 100644 --- a/Lib/test/test_readline.py +++ b/Lib/test/test_readline.py @@ -19,21 +19,14 @@ # Skip tests if there is no readline module readline = import_module('readline') -if hasattr(readline, "_READLINE_LIBRARY_VERSION"): - is_editline = ("EditLine wrapper" in readline._READLINE_LIBRARY_VERSION) -else: - is_editline = readline.backend == "editline" +is_editline = readline.backend == "editline" def setUpModule(): if verbose: - # Python implementations other than CPython may not have - # these private attributes - if hasattr(readline, "_READLINE_VERSION"): - print(f"readline version: {readline._READLINE_VERSION:#x}") - print(f"readline runtime version: {readline._READLINE_RUNTIME_VERSION:#x}") - if hasattr(readline, "_READLINE_LIBRARY_VERSION"): - print(f"readline library version: {readline._READLINE_LIBRARY_VERSION!r}") + print(f"readline version: {readline.READLINE_VERSION_INFO}") + print(f"readline runtime version: {readline.readline_version_info}") + print(f"readline library version: {readline.readline_version!r}") print(f"use libedit emulation? {is_editline}") @@ -210,7 +203,36 @@ def test_append_limited_history(self): class TestReadline(unittest.TestCase): - @unittest.skipIf(readline._READLINE_VERSION < 0x0601 and not is_editline, + def _test_readline_version(self, v): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 2) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertGreaterEqual(v.major, 4) + self.assertGreaterEqual(v.minor, 0) + + def test_readline_version(self): + self._test_readline_version(readline.READLINE_VERSION_INFO) + self._test_readline_version(readline.readline_version_info) + self.assertIsInstance(readline.readline_version, str) + if not is_editline: + self.assertEqual(readline.readline_version, + '%d.%d' % readline.readline_version_info) + # Private names kept for backward compatibility. + self.assertEqual(readline._READLINE_VERSION, + readline.READLINE_VERSION_INFO.major << 8 | + readline.READLINE_VERSION_INFO.minor) + self.assertEqual(readline._READLINE_RUNTIME_VERSION, + readline.readline_version_info.major << 8 | + readline.readline_version_info.minor) + self.assertIs(readline._READLINE_LIBRARY_VERSION, + readline.readline_version) + + @unittest.skipIf(readline.READLINE_VERSION_INFO < (6, 1) and not is_editline, "not supported in this library version") def test_init(self): # Issue #19884: Ensure that the ANSI sequence "\033[1034h" is not @@ -366,7 +388,7 @@ def display(substitution, matches, longest_match_length): # See https://cnswww.cns.cwru.edu/php/chet/readline/CHANGES # - editline: history size is broken on OS X 10.11.6. # Newer versions were not tested yet. - @unittest.skipIf(readline._READLINE_VERSION < 0x600, + @unittest.skipIf(readline.READLINE_VERSION_INFO < (6, 0), "this readline version does not support history-size") @unittest.skipIf(is_editline, "editline history size configuration is broken") diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index a7825adf923c19..87b6453fba8024 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -495,6 +495,37 @@ def check_args_to_onexc(self, func, arg, exc): self.assertTrue(isinstance(exc, OSError)) self.errorState = 3 + @os_helper.skip_if_dac_override + @os_helper.skip_unless_working_chmod + @unittest.skipUnless(shutil.rmtree.avoids_symlink_attacks, + 'requires the fd based implementation of rmtree()') + def test_on_exc_kwargs(self): + os.mkdir(TESTFN) + self.addCleanup(shutil.rmtree, TESTFN) + + child_dir_path = os.path.join(TESTFN, 'b') + child_file_path = os.path.join(child_dir_path, 'a') + os.mkdir(child_dir_path) + os_helper.create_empty_file(child_file_path) + old_child_dir_mode = os.stat(child_dir_path).st_mode + # Make unwritable. + new_mode = stat.S_IREAD|stat.S_IEXEC + os.chmod(child_dir_path, new_mode) + + self.addCleanup(os.chmod, child_dir_path, old_child_dir_mode) + + calls = [] + def onexc(func, path, err, direntry=None, dir_fd=None): + calls.append((func, path, err)) + if func is os.unlink: + self.assertEqual(direntry.name, os.path.basename(path)) + self.assertTrue(os.path.samestat( + os.stat(path), os.stat(direntry.name, dir_fd=dir_fd))) + + shutil.rmtree(TESTFN, onexc=onexc, _onexc_kwargs=True) + self.assertIn((os.unlink, child_file_path), + [(func, path) for func, path, err in calls]) + @unittest.skipIf(sys.platform[:6] == 'cygwin', "This test can't be run on Cygwin (issue #1071513).") @os_helper.skip_if_dac_override diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 978227a8651e14..bbb7c1f8b61593 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -31,6 +31,7 @@ import urllib.parse import warnings +from test import support from test.support import ( SHORT_TIMEOUT, check_disallow_instantiation, requires_subprocess ) @@ -88,6 +89,33 @@ def test_programming_error(self): def test_not_supported_error(self): self.assertIsSubclass(sqlite.NotSupportedError, sqlite.DatabaseError) + def _test_sqlite_version(self, v, string): + self.assertIsInstance(v[:], tuple) + self.assertEqual(len(v), 3) + self.assertIsInstance(v[0], int) + self.assertIsInstance(v[1], int) + self.assertIsInstance(v[2], int) + self.assertIsInstance(v.major, int) + self.assertIsInstance(v.minor, int) + self.assertIsInstance(v.patch, int) + self.assertEqual(v[0], v.major) + self.assertEqual(v[1], v.minor) + self.assertEqual(v[2], v.patch) + self.assertGreaterEqual(v.major, 3) + self.assertGreaterEqual(v.minor, 0) + self.assertGreaterEqual(v.patch, 0) + self.assertEqual(string, '%d.%d.%d' % v) + + def test_sqlite_version(self): + if support.verbose: + print(f'SQLITE_VERSION = {sqlite.SQLITE_VERSION}', flush=True) + print(f'sqlite_version = {sqlite.sqlite_version}', flush=True) + print(f'SQLITE_VERSION_INFO = {sqlite.SQLITE_VERSION_INFO}', flush=True) + print(f'sqlite_version_info = {sqlite.sqlite_version_info}', flush=True) + self._test_sqlite_version(sqlite.SQLITE_VERSION_INFO, sqlite.SQLITE_VERSION) + self._test_sqlite_version(sqlite.sqlite_version_info, sqlite.sqlite_version) + self.assertEqual(sqlite.SQLITE_VERSION_INFO[0], sqlite.sqlite_version_info[0]) + def test_module_constants(self): consts = [ "SQLITE_ABORT", diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index abd7710a1d570a..9a6118d94fbbb2 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -545,7 +545,7 @@ def test_parse_cert_CVE_2013_4238(self): (('emailAddress', 'python-dev@python.org'),)) self.assertEqual(p['subject'], subject) self.assertEqual(p['issuer'], subject) - if ssl._OPENSSL_API_VERSION >= (0, 9, 8): + if ssl.OPENSSL_API_VERSION_INFO >= (0, 9, 8): san = (('DNS', 'altnull.python.org\x00example.com'), ('email', 'null@python.org\x00user@example.org'), ('URI', 'http://null.python.org\x00http://example.org'), @@ -601,6 +601,14 @@ def test_openssl_version(self): self.assertIsInstance(n, int) self.assertIsInstance(t, tuple) self.assertIsInstance(s, str) + self.assertEqual(len(t), 5) + self.assertEqual(t, (t.major, t.minor, t.fix, t.patch, t.status)) + a = ssl.OPENSSL_API_VERSION_INFO + self.assertIsInstance(a, tuple) + self.assertEqual(len(a), 5) + self.assertEqual(a, (a.major, a.minor, a.fix, a.patch, a.status)) + self.assertIs(ssl._OPENSSL_API_VERSION, a) + self.assertEqual(a.major, t.major) # Some sanity checks follow # >= 1.1.1 self.assertGreaterEqual(n, 0x10101000) diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index e33cc65e090e3b..cd960ed99117b6 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -14,6 +14,7 @@ import gc import shutil import subprocess +import sysconfig from unittest import mock import unittest @@ -1861,6 +1862,54 @@ def test(target, target_is_directory): new_flags = os.stat(dir1).st_flags self.assertEqual(new_flags, old_flags) + @os_helper.skip_unless_symlink + @os_helper.skip_unless_working_chmod + @support.requires_non_root_user + @unittest.skipIf(support.is_emscripten, 'Fails due to Emscripten bug:' + 'emscripten-core/emscripten#27761') + @unittest.skipUnless(shutil.rmtree.avoids_symlink_attacks, + 'requires the fd based implementation of rmtree()') + def test_cleanup_with_symlink_race(self): + # cleanup() should not operate on files outside of the temporary + # directory when a directory is replaced with a symlink while it + # recovers from a PermissionError (CVE-2026-12345). + with self.do_create(recurse=0) as target: + target_file = os.path.join(target, 'file1') + open(target_file, 'wb').close() + target_mode = os.stat(target_file).st_mode + + d1 = self.do_create(recurse=0) + dir1 = os.path.join(d1.name, 'dir1') + os.mkdir(dir1) + open(os.path.join(dir1, 'file1'), 'wb').close() + # Removing contents of dir1 fails with a PermissionError, and + # dir1 is replaced with a symlink to target at the very moment + # cleanup() starts to recover from that error. + os.chmod(dir1, 0o500) + unlink = os.unlink + def hook(path, *, dir_fd=None): + try: + return unlink(path, dir_fd=dir_fd) + except PermissionError: + if not os.path.islink(dir1): + os.chmod(dir1, 0o700) + os.rename(dir1, dir1 + '_moved') + os.symlink(target, dir1) + raise + try: + with mock.patch('os.unlink', hook): + with contextlib.suppress(OSError): + d1.cleanup() + finally: + if os.path.islink(dir1): + os.unlink(dir1) + os.rename(dir1 + '_moved', dir1) + os.chmod(dir1, 0o700) + d1.cleanup() + + self.assertTrue(os.path.exists(target_file)) + self.assertEqual(os.stat(target_file).st_mode, target_mode) + @support.cpython_only def test_del_on_collection(self): # A TemporaryDirectory is deleted when garbage collected @@ -2033,6 +2082,29 @@ def test_modes(self): d.cleanup() self.assertFalse(os.path.exists(d.name)) + @support.subTests('ignore_errors', (True, False)) + def test_parent_mode_preserved(self, ignore_errors): + # Test that cleanup does not touch the parent directory, + # even if that prevents removal. + for mode in range(8): + mode <<= 6 + with self.subTest(mode=format(mode, '03o')): + outer = self.do_create() + with outer: + d = self.do_create(dir=outer.name, dirs=2, files=2, + ignore_cleanup_errors=ignore_errors) + with d: + os.chmod(outer.name, mode) + orig_mode = os.stat(outer.name).st_mode + try: + d.cleanup() + except PermissionError: + if ignore_errors: + raise + self.assertEqual(os.stat(outer.name).st_mode, orig_mode) + outer.cleanup() + self.assertFalse(os.path.exists(outer.name)) + def check_flags(self, flags): # skip the test if these flags are not supported (ex: FreeBSD 13) filename = os_helper.TESTFN @@ -2070,5 +2142,17 @@ def test_delete_false(self): self.assertTrue(os.path.exists(working_dir)) shutil.rmtree(working_dir) + @unittest.skipUnless( + sysconfig.get_config_var('PY_SUPPORT_TIER') + and sysconfig.get_config_var('PY_SUPPORT_TIER') <= 3, + 'regression test for supported platforms') + @unittest.skipIf(support.MS_WINDOWS, 'dirfd not used on Windows') + @unittest.skipIf(support.is_wasi, 'WASI has no chmod') + def test_cleanup_safe(self): + """Verify that cleanup uses the safer code path""" + # This is a regression test. Feel free to add exceptions for new + # platforms, but don't forget to update the docs. + self.assertTrue(tempfile._rmtree_use_dir_fd) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 5c1eea78669859..b32b58ab4340d2 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -9,6 +9,7 @@ import time import unittest import weakref +import _tkinter import tkinter from tkinter import TclError, ttk import enum @@ -24,6 +25,49 @@ support.requires('gui') +def check_version_info(test, vi): + # The following is almost a copy of tests for sys.version_info. + test.assertIsInstance(vi[:], tuple) + test.assertEqual(len(vi), 5) + test.assertIsInstance(vi[0], int) + test.assertIsInstance(vi[1], int) + test.assertIsInstance(vi[2], int) + test.assertIn(vi[3], ("alpha", "beta", "candidate", "final")) + test.assertIsInstance(vi[4], int) + test.assertIsInstance(vi.major, int) + test.assertIsInstance(vi.minor, int) + test.assertIsInstance(vi.micro, int) + test.assertIn(vi.releaselevel, ("alpha", "beta", "final")) + test.assertIsInstance(vi.serial, int) + test.assertEqual(vi[0], vi.major) + test.assertEqual(vi[1], vi.minor) + test.assertEqual(vi[2], vi.micro) + test.assertEqual(vi[3], vi.releaselevel) + test.assertEqual(vi[4], vi.serial) + test.assertTrue(vi > (1,0,0)) + if vi.releaselevel == 'final': + test.assertEqual(vi.serial, 0) + else: + test.assertEqual(vi.micro, 0) + test.assertStartsWith(str(vi), f'{vi.major}.{vi.minor}') + + +class VersionTest(unittest.TestCase): + + def test_version_info(self): + for vi, version, patchlevel in ( + (tkinter.TCL_VERSION_INFO, _tkinter.TCL_VERSION, _tkinter.TCL_PATCH_LEVEL), + (tkinter.TK_VERSION_INFO, _tkinter.TK_VERSION, _tkinter.TK_PATCH_LEVEL), + ): + with self.subTest(patchlevel=patchlevel): + check_version_info(self, vi) + self.assertEqual(str(vi), patchlevel) + self.assertEqual(f'{vi.major}.{vi.minor}', version) + self.assertEqual(tkinter.TclVersion, + float(f'{tkinter.TCL_VERSION_INFO.major}.' + f'{tkinter.TCL_VERSION_INFO.minor}')) + + class MiscTest(AbstractTkTest, unittest.TestCase): def test_all(self): @@ -830,30 +874,10 @@ def test_info_patchlevel(self): vi = self.root.info_patchlevel() f = tkinter.Frame(self.root) self.assertEqual(f.info_patchlevel(), vi) - # The following is almost a copy of tests for sys.version_info. - self.assertIsInstance(vi[:], tuple) - self.assertEqual(len(vi), 5) - self.assertIsInstance(vi[0], int) - self.assertIsInstance(vi[1], int) - self.assertIsInstance(vi[2], int) - self.assertIn(vi[3], ("alpha", "beta", "candidate", "final")) - self.assertIsInstance(vi[4], int) - self.assertIsInstance(vi.major, int) - self.assertIsInstance(vi.minor, int) - self.assertIsInstance(vi.micro, int) - self.assertIn(vi.releaselevel, ("alpha", "beta", "final")) - self.assertIsInstance(vi.serial, int) - self.assertEqual(vi[0], vi.major) - self.assertEqual(vi[1], vi.minor) - self.assertEqual(vi[2], vi.micro) - self.assertEqual(vi[3], vi.releaselevel) - self.assertEqual(vi[4], vi.serial) - self.assertTrue(vi > (1,0,0)) - if vi.releaselevel == 'final': - self.assertEqual(vi.serial, 0) - else: - self.assertEqual(vi.micro, 0) - self.assertStartsWith(str(vi), f'{vi.major}.{vi.minor}') + check_version_info(self, vi) + # The Tcl library loaded at runtime should be compatible with + # the one used for building the module. + self.assertEqual(vi[:2], tkinter.TCL_VERSION_INFO[:2]) def test_embedded_null(self): widget = tkinter.Entry(self.root) diff --git a/Lib/test/test_tkinter/test_simpledialog.py b/Lib/test/test_tkinter/test_simpledialog.py index 943d6e113ed34c..3da9375158bd02 100644 --- a/Lib/test/test_tkinter/test_simpledialog.py +++ b/Lib/test/test_tkinter/test_simpledialog.py @@ -1,3 +1,4 @@ +import sys import unittest import tkinter from tkinter import messagebox, ttk @@ -245,6 +246,26 @@ def test_go(self): d.root.after(1, lambda: d._buttons[0].invoke()) self.assertEqual(d.go(), 0) + def test_go_foreign_grab(self): + # gh-157676: the grab and the focus can be in a window which was not + # created by tkinter, such as a native message box; they must be + # restored after the dialog. + tk = self.root.tk + tk.call('toplevel', '.foreign') + self.addCleanup(tk.call, 'destroy', '.foreign') + tk.call('wm', 'deiconify', '.foreign') + tk.call('update') + tk.call('grab', 'set', '.foreign') + tk.call('focus', '-force', '.foreign') + d = self.create() + d.root.after(1, lambda: d._buttons[0].invoke()) + self.assertEqual(d.go(), 0) + self.assertEqual(tk.call('grab', 'current', self.root._w), '.foreign') + # On Windows the application can lose the focus when the dialog is + # destroyed, and then "focus" returns an empty string. + if sys.platform != 'win32': + self.assertEqual(tk.call('focus'), '.foreign') + class DialogTest(AbstractDialogTest, unittest.TestCase): # Dialog's button box is modelled on tk::MessageBox. diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index dde4accecf61ba..11bb0ba9c89c4a 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -170,6 +170,9 @@ def _parse_version(version): releaselevel = {'a': 'alpha', 'b': 'beta'}[releaselevel] return _VersionInfoType(major, minor, micro, releaselevel, serial) +TCL_VERSION_INFO = _parse_version(_tkinter.TCL_PATCH_LEVEL) +TK_VERSION_INFO = _parse_version(_tkinter.TK_PATCH_LEVEL) + @enum._simple_enum(enum.StrEnum) class EventType: diff --git a/Lib/tkinter/simpledialog.py b/Lib/tkinter/simpledialog.py index 1b0cf51317ee08..caef785f936db3 100644 --- a/Lib/tkinter/simpledialog.py +++ b/Lib/tkinter/simpledialog.py @@ -722,12 +722,13 @@ def askstring(title, prompt, **kw): @contextlib.contextmanager def _temp_grab_focus(grab, focus=None, destroy=True): - old_focus = grab.focus_get() - old_grab = grab.grab_current() - if old_grab is not None and old_grab.winfo_exists(): - old_status = old_grab.grab_status() - else: - old_status = None + # Use Tcl window names rather than widgets, because the focus and the + # grab can be in a window which was not created by tkinter, such as + # a native message box. + tk = grab.tk + old_focus = tk.call('focus') + old_grab = tk.call('grab', 'current', grab._w) + old_status = tk.call('grab', 'status', old_grab) if old_grab else None # The "grab" command will fail if another application # already holds the grab. So catch it. try: @@ -741,9 +742,11 @@ def _temp_grab_focus(grab, focus=None, destroy=True): yield finally: - if old_focus is not None: + # The old windows and even the whole application can be destroyed + # by now, so catch errors. + if old_focus: try: - old_focus.focus_set() + tk.call('focus', old_focus) except TclError: pass try: @@ -755,15 +758,17 @@ def _temp_grab_focus(grab, focus=None, destroy=True): grab.destroy() except TclError: pass - if (old_grab is not None and old_grab.winfo_exists() - and old_grab.winfo_ismapped()): - # The "grab" command will fail if another application - # already holds the grab. So catch it. + if old_grab: + # Do not restore the grab if the window is no longer mapped: + # a local grab on an unmapped window would block the application. + # The "grab" command will fail if another application already + # holds the grab on a window with the same name. So catch it. try: - if old_status == 'global': - old_grab.grab_set_global() - else: - old_grab.grab_set() + if grab.getboolean(tk.call('winfo', 'ismapped', old_grab)): + if old_status == 'global': + tk.call('grab', 'set', '-global', old_grab) + else: + tk.call('grab', 'set', old_grab) except TclError: pass diff --git a/Misc/NEWS.d/next/Library/2026-09-14-07-49-51.gh-issue-157470.jgEHYK.rst b/Misc/NEWS.d/next/Library/2026-09-14-07-49-51.gh-issue-157470.jgEHYK.rst new file mode 100644 index 00000000000000..8e4caf84bf3583 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-07-49-51.gh-issue-157470.jgEHYK.rst @@ -0,0 +1,4 @@ +Add constants :const:`sqlite3.SQLITE_VERSION` and +:const:`sqlite3.SQLITE_VERSION_INFO` which provide information about +the version of the SQLite library that was used for building the module. +:const:`sqlite3.sqlite_version_info` is now a named tuple. diff --git a/Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst b/Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst new file mode 100644 index 00000000000000..8f8228e52b5597 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-09-02-19.gh-issue-157473.LlzPyM.rst @@ -0,0 +1,4 @@ +Add constant :const:`xml.parsers.expat.VERSION_INFO` which provides +information about the version of the Expat library that was used for +building the module. +:const:`xml.parsers.expat.version_info` is now a named tuple. diff --git a/Misc/NEWS.d/next/Library/2026-09-14-09-28-35.gh-issue-157475.dvuniU.rst b/Misc/NEWS.d/next/Library/2026-09-14-09-28-35.gh-issue-157475.dvuniU.rst new file mode 100644 index 00000000000000..d395a381022fca --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-09-28-35.gh-issue-157475.dvuniU.rst @@ -0,0 +1,4 @@ +Add constants :const:`decimal.LIBMPDEC_VERSION`, +:const:`decimal.libmpdec_version`, :const:`decimal.LIBMPDEC_VERSION_INFO`, +and :const:`decimal.libmpdec_version_info` in the C implementation, which +provide information about the version of the libmpdec library in use. diff --git a/Misc/NEWS.d/next/Library/2026-09-14-10-22-36.gh-issue-157478.F4cBKN.rst b/Misc/NEWS.d/next/Library/2026-09-14-10-22-36.gh-issue-157478.F4cBKN.rst new file mode 100644 index 00000000000000..462e8c4d53a7f8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-10-22-36.gh-issue-157478.F4cBKN.rst @@ -0,0 +1,4 @@ +Add constants :const:`dbm.gnu.GDBM_VERSION_INFO`, +:const:`dbm.gnu.gdbm_version_info` and :const:`dbm.gnu.gdbm_version`, which +provide information about the version of the GDBM library in use. +The private constant ``_gdbm._GDBM_VERSION`` has been removed. diff --git a/Misc/NEWS.d/next/Library/2026-09-14-10-39-05.gh-issue-157480.to5EnW.rst b/Misc/NEWS.d/next/Library/2026-09-14-10-39-05.gh-issue-157480.to5EnW.rst new file mode 100644 index 00000000000000..cd818acd4ad3a6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-10-39-05.gh-issue-157480.to5EnW.rst @@ -0,0 +1,8 @@ +Add constants in :mod:`dbm.ndbm` which provide information about the version +of the underlying library in use: :const:`~dbm.ndbm.GDBM_VERSION_INFO`, +:const:`~dbm.ndbm.gdbm_version_info` and :const:`~dbm.ndbm.gdbm_version` if +it is GDBM, or :const:`~dbm.ndbm.BDB_VERSION`, :const:`~dbm.ndbm.bdb_version`, +:const:`~dbm.ndbm.BDB_VERSION_INFO` and :const:`~dbm.ndbm.bdb_version_info` if +it is Berkeley DB. +:const:`dbm.ndbm.library` is now ``'ndbm'`` instead of ``'GNU gdbm'`` for a +classic NDBM library. diff --git a/Misc/NEWS.d/next/Library/2026-09-14-11-38-23.gh-issue-157485.yAK9LN.rst b/Misc/NEWS.d/next/Library/2026-09-14-11-38-23.gh-issue-157485.yAK9LN.rst new file mode 100644 index 00000000000000..49dcdeae48c2bf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-11-38-23.gh-issue-157485.yAK9LN.rst @@ -0,0 +1,3 @@ +Add constants :const:`readline.READLINE_VERSION_INFO`, +:const:`readline.readline_version_info` and :const:`readline.readline_version`, +which provide information about the version of the Readline library in use. diff --git a/Misc/NEWS.d/next/Library/2026-09-14-18-36-31.gh-issue-157513.EW4QOC.rst b/Misc/NEWS.d/next/Library/2026-09-14-18-36-31.gh-issue-157513.EW4QOC.rst new file mode 100644 index 00000000000000..c9d19314a7604d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-18-36-31.gh-issue-157513.EW4QOC.rst @@ -0,0 +1,3 @@ +:const:`ssl.OPENSSL_VERSION_INFO` is now a named tuple. +Add :const:`ssl.OPENSSL_API_VERSION_INFO` which provides information about +the version of the OpenSSL library that was used for building the module. diff --git a/Misc/NEWS.d/next/Library/2026-09-14-18-36-32.gh-issue-157514.02FZED.rst b/Misc/NEWS.d/next/Library/2026-09-14-18-36-32.gh-issue-157514.02FZED.rst new file mode 100644 index 00000000000000..80c2ad9a7d2aa9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-14-18-36-32.gh-issue-157514.02FZED.rst @@ -0,0 +1,4 @@ +Add constants :const:`tkinter.TCL_VERSION_INFO` and +:const:`tkinter.TK_VERSION_INFO`, named tuples with the versions of the Tcl +and Tk libraries that were used for building the module, and +``_tkinter.TCL_PATCH_LEVEL`` and ``_tkinter.TK_PATCH_LEVEL`` strings. diff --git a/Misc/NEWS.d/next/Security/2026-08-14-11-55-00.gh-issue-157579.Kq3Vt2.rst b/Misc/NEWS.d/next/Security/2026-08-14-11-55-00.gh-issue-157579.Kq3Vt2.rst new file mode 100644 index 00000000000000..46e87575dba983 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-08-14-11-55-00.gh-issue-157579.Kq3Vt2.rst @@ -0,0 +1,6 @@ +Fix a race condition in the cleanup of :class:`tempfile.TemporaryDirectory`. +When working around file system permission errors, files are now removed +relative to open directory file descriptors instead of resolving their path +again, so that replacing a directory of the tree with a symbolic link can no +longer make the cleanup delete files outside of the temporary directory. +This addresses :cve:`2026-12345`. diff --git a/Misc/NEWS.d/next/Windows/2026-06-09-11-40-48.gh-issue-151159.JKVfme.rst b/Misc/NEWS.d/next/Windows/2026-06-09-11-40-48.gh-issue-151159.JKVfme.rst deleted file mode 100644 index ad1be115db5ce8..00000000000000 --- a/Misc/NEWS.d/next/Windows/2026-06-09-11-40-48.gh-issue-151159.JKVfme.rst +++ /dev/null @@ -1 +0,0 @@ -Updated bundled version of OpenSSL to 3.5.7. diff --git a/Misc/NEWS.d/next/Windows/2026-08-28-10-51-35.gh-issue-156369.MPcRL5.rst b/Misc/NEWS.d/next/Windows/2026-08-28-10-51-35.gh-issue-156369.MPcRL5.rst deleted file mode 100644 index 33c02132cfdaae..00000000000000 --- a/Misc/NEWS.d/next/Windows/2026-08-28-10-51-35.gh-issue-156369.MPcRL5.rst +++ /dev/null @@ -1 +0,0 @@ -Updated Windows builds to use OpenSSL 3.5.8. diff --git a/Misc/NEWS.d/next/Windows/2026-09-29-09-56-14.gh-issue-158010.MPQX4w.rst b/Misc/NEWS.d/next/Windows/2026-09-29-09-56-14.gh-issue-158010.MPQX4w.rst new file mode 100644 index 00000000000000..827173387f8e8a --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-09-29-09-56-14.gh-issue-158010.MPQX4w.rst @@ -0,0 +1 @@ +Updated Windows builds to use OpenSSL 3.5.9. diff --git a/Misc/externals.spdx.json b/Misc/externals.spdx.json index 1b658b7d4b7929..0800347e34016d 100644 --- a/Misc/externals.spdx.json +++ b/Misc/externals.spdx.json @@ -70,21 +70,21 @@ "checksums": [ { "algorithm": "SHA256", - "checksumValue": "36af13403a6f34251c875946d1d524c228cbf7b82fd54dd13d9019fbc37da87e" + "checksumValue": "d6cf0d9a651f25fe506f6b3dc133a80b9b79a0bc565f570dcf8fdbd597551485" } ], - "downloadLocation": "https://github.com/python/cpython-source-deps/archive/refs/tags/openssl-3.5.8.tar.gz", + "downloadLocation": "https://github.com/python/cpython-source-deps/archive/refs/tags/openssl-3.5.9.tar.gz", "externalRefs": [ { "referenceCategory": "SECURITY", - "referenceLocator": "cpe:2.3:a:openssl:openssl:3.5.8:*:*:*:*:*:*:*", + "referenceLocator": "cpe:2.3:a:openssl:openssl:3.5.9:*:*:*:*:*:*:*", "referenceType": "cpe23Type" } ], "licenseConcluded": "NOASSERTION", "name": "openssl", "primaryPackagePurpose": "SOURCE", - "versionInfo": "3.5.8" + "versionInfo": "3.5.9" }, { "SPDXID": "SPDXRef-PACKAGE-sqlite", diff --git a/Modules/_dbmmodule.c b/Modules/_dbmmodule.c index 0a5192546982d4..39fa804d1dcef4 100644 --- a/Modules/_dbmmodule.c +++ b/Modules/_dbmmodule.c @@ -28,7 +28,12 @@ static const char which_dbm[] = "GNU gdbm"; #elif defined(USE_NDBM) #include + #ifdef _GDBM_H_ + /* ndbm.h is the GDBM compatibility interface. */ static const char which_dbm[] = "GNU gdbm"; + #else + static const char which_dbm[] = "ndbm"; + #endif #elif defined(USE_BERKDB) #ifndef DB_DBM_HSEARCH #define DB_DBM_HSEARCH 1 @@ -634,6 +639,136 @@ static PyMethodDef dbmmodule_methods[] = { { 0, 0 }, }; +#if defined(GDBM_VERSION_MAJOR) || defined(DB_VERSION_MAJOR) +static PyStructSequence_Field version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"patch", "Patch release number"}, + {0} +}; + +static PyObject * +make_version_info(PyTypeObject *type, int major, int minor, int patch) +{ + PyObject *version; + int pos = 0; + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromLong(major)) + SetItem(PyLong_FromLong(minor)) + SetItem(PyLong_FromLong(patch)) +#undef SetItem + + return version; +} +#endif + +#if defined(GDBM_VERSION_MAJOR) +PyDoc_STRVAR(gdbm_version_info__doc__, +"_dbm.gdbm_version_info\n\ +\n\ +GDBM version information as a named tuple."); + +static PyStructSequence_Desc gdbm_version_info_desc = { + "_dbm.gdbm_version_info", /* name */ + gdbm_version_info__doc__, /* doc */ + version_info_fields, /* fields */ + 3 +}; + +static int +add_version_constants(PyObject *module) +{ + if (PyModule_AddStringConstant(module, "gdbm_version", gdbm_version) < 0) { + return -1; + } + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&gdbm_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(module, "GDBM_VERSION_INFO", + make_version_info(version_type, GDBM_VERSION_MAJOR, + GDBM_VERSION_MINOR, GDBM_VERSION_PATCH)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(module, "gdbm_version_info", + make_version_info(version_type, gdbm_version_number[0], + gdbm_version_number[1], + gdbm_version_number[2])) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); + return 0; +} +#elif defined(DB_VERSION_MAJOR) +PyDoc_STRVAR(bdb_version_info__doc__, +"_dbm.bdb_version_info\n\ +\n\ +Berkeley DB version information as a named tuple."); + +static PyStructSequence_Desc bdb_version_info_desc = { + "_dbm.bdb_version_info", /* name */ + bdb_version_info__doc__, /* doc */ + version_info_fields, /* fields */ + 3 +}; + +static int +add_version_constants(PyObject *module) +{ + int major, minor, patch; + const char *version = db_version(&major, &minor, &patch); + if (PyModule_AddStringConstant(module, "BDB_VERSION", DB_VERSION_STRING) < 0) { + return -1; + } + if (PyModule_AddStringConstant(module, "bdb_version", version) < 0) { + return -1; + } + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&bdb_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(module, "BDB_VERSION_INFO", + make_version_info(version_type, DB_VERSION_MAJOR, + DB_VERSION_MINOR, DB_VERSION_PATCH)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(module, "bdb_version_info", + make_version_info(version_type, major, minor, patch)) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); + return 0; +} +#else +static int +add_version_constants(PyObject *module) +{ + return 0; +} +#endif + static int _dbm_exec(PyObject *module) { @@ -650,6 +785,9 @@ _dbm_exec(PyObject *module) if (PyModule_AddStringConstant(module, "library", which_dbm) < 0) { return -1; } + if (add_version_constants(module) < 0) { + return -1; + } if (PyModule_AddType(module, (PyTypeObject *)state->dbm_error) < 0) { return -1; } diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 20e4f5bea88464..1f7da94b2971e1 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -7756,6 +7756,104 @@ cfunc_noargs(PyTypeObject *t, const char *name) return NULL; } +PyDoc_STRVAR(libmpdec_version_info__doc__, +"decimal.libmpdec_version_info\n\ +\n\ +libmpdec version information as a named tuple."); + +static PyStructSequence_Field libmpdec_version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"micro", "Micro release number"}, + {0} +}; + +static PyStructSequence_Desc libmpdec_version_info_desc = { + "decimal.libmpdec_version_info", /* name */ + libmpdec_version_info__doc__, /* doc */ + libmpdec_version_info_fields, /* fields */ + 3 +}; + +static PyObject * +make_libmpdec_version_info(PyTypeObject *type, int major, int minor, int micro) +{ + PyObject *version; + int pos = 0; + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromLong(major)) + SetItem(PyLong_FromLong(minor)) + SetItem(PyLong_FromLong(micro)) +#undef SetItem + + return version; +} + +static PyObject * +parse_libmpdec_version_info(PyTypeObject *type, const char *version) +{ + int major, minor, micro; + if (sscanf(version, "%d.%d.%d", &major, &minor, µ) != 3) { + PyErr_Format(PyExc_RuntimeError, + "unexpected libmpdec version string %s", version); + return NULL; + } + return make_libmpdec_version_info(type, major, minor, micro); +} + +static int +add_version_constants(PyObject *m) +{ + const char *version = mpd_version(); + if (PyModule_AddStringConstant(m, "LIBMPDEC_VERSION", MPD_VERSION) < 0) { + return -1; + } + PyObject *obj = PyUnicode_FromString(version); + if (obj == NULL) { + return -1; + } + if (PyModule_AddObjectRef(m, "libmpdec_version", obj) < 0 || + PyModule_AddObjectRef(m, "__libmpdec_version__", obj) < 0) + { + Py_DECREF(obj); + return -1; + } + Py_DECREF(obj); + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&libmpdec_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(m, "LIBMPDEC_VERSION_INFO", + make_libmpdec_version_info(version_type, MPD_MAJOR_VERSION, + MPD_MINOR_VERSION, + MPD_MICRO_VERSION)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(m, "libmpdec_version_info", + parse_libmpdec_version_info(version_type, version)) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); + return 0; +} + static int minalloc_is_set = 0; static int @@ -8004,7 +8102,7 @@ _decimal_exec(PyObject *m) /* Add specification version number */ CHECK_INT(PyModule_AddStringConstant(m, "SPEC_VERSION", MPD_SPEC_VERSION)); - CHECK_INT(PyModule_AddStringConstant(m, "__libmpdec_version__", mpd_version())); + CHECK_INT(add_version_constants(m)); return 0; diff --git a/Modules/_gdbmmodule.c b/Modules/_gdbmmodule.c index 47c02b72de2a76..bdc2d96d8324b0 100644 --- a/Modules/_gdbmmodule.c +++ b/Modules/_gdbmmodule.c @@ -862,6 +862,85 @@ static PyMethodDef _gdbm_module_methods[] = { { 0, 0 }, }; +PyDoc_STRVAR(gdbm_version_info__doc__, +"_gdbm.gdbm_version_info\n\ +\n\ +GDBM version information as a named tuple."); + +static PyStructSequence_Field gdbm_version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"patch", "Patch release number"}, + {0} +}; + +static PyStructSequence_Desc gdbm_version_info_desc = { + "_gdbm.gdbm_version_info", /* name */ + gdbm_version_info__doc__, /* doc */ + gdbm_version_info_fields, /* fields */ + 3 +}; + +static PyObject * +make_gdbm_version_info(PyTypeObject *type, int major, int minor, int patch) +{ + PyObject *version; + int pos = 0; + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromLong(major)) + SetItem(PyLong_FromLong(minor)) + SetItem(PyLong_FromLong(patch)) +#undef SetItem + + return version; +} + +static int +add_version_constants(PyObject *module) +{ + if (PyModule_AddStringConstant(module, "gdbm_version", gdbm_version) < 0) { + return -1; + } +#if defined(GDBM_VERSION_MAJOR) && defined(GDBM_VERSION_MINOR) && \ + defined(GDBM_VERSION_PATCH) + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&gdbm_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(module, "GDBM_VERSION_INFO", + make_gdbm_version_info(version_type, GDBM_VERSION_MAJOR, + GDBM_VERSION_MINOR, + GDBM_VERSION_PATCH)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(module, "gdbm_version_info", + make_gdbm_version_info(version_type, gdbm_version_number[0], + gdbm_version_number[1], + gdbm_version_number[2])) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); +#endif + return 0; +} + static int _gdbm_exec(PyObject *module) { @@ -883,14 +962,9 @@ _gdbm_exec(PyObject *module) return -1; } -#if defined(GDBM_VERSION_MAJOR) && defined(GDBM_VERSION_MINOR) && \ - defined(GDBM_VERSION_PATCH) - PyObject *obj = Py_BuildValue("iii", GDBM_VERSION_MAJOR, - GDBM_VERSION_MINOR, GDBM_VERSION_PATCH); - if (PyModule_Add(module, "_GDBM_VERSION", obj) < 0) { + if (add_version_constants(module) < 0) { return -1; } -#endif return 0; } diff --git a/Modules/_sqlite/module.c b/Modules/_sqlite/module.c index 512d9744d57416..5eebacaa79d4e6 100644 --- a/Modules/_sqlite/module.c +++ b/Modules/_sqlite/module.c @@ -439,6 +439,87 @@ add_keyword_tuple(PyObject *module) #endif } +PyDoc_STRVAR(sqlite_version_info__doc__, +"_sqlite3.sqlite_version_info\n\ +\n\ +SQLite version information as a named tuple."); + +static PyStructSequence_Field sqlite_version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"patch", "Patch release number"}, + {0} +}; + +static PyStructSequence_Desc sqlite_version_info_desc = { + "_sqlite3.sqlite_version_info", /* name */ + sqlite_version_info__doc__, /* doc */ + sqlite_version_info_fields, /* fields */ + 3 +}; + +static PyObject * +make_sqlite_version_info(PyTypeObject *type, int number) +{ + PyObject *version; + int pos = 0; + int major = number / 1000000; + int minor = (number % 1000000) / 1000; + int patch = number % 1000; + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromLong(major)) + SetItem(PyLong_FromLong(minor)) + SetItem(PyLong_FromLong(patch)) +#undef SetItem + + return version; +} + +static int +add_version_constants(PyObject *module) +{ + if (PyModule_AddStringMacro(module, SQLITE_VERSION) < 0) { + return -1; + } + if (PyModule_AddStringConstant(module, "sqlite_version", + sqlite3_libversion()) < 0) + { + return -1; + } + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&sqlite_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(module, "SQLITE_VERSION_INFO", + make_sqlite_version_info(version_type, SQLITE_VERSION_NUMBER)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(module, "sqlite_version_info", + make_sqlite_version_info(version_type, + sqlite3_libversion_number())) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); + return 0; +} + static int add_integer_constants(PyObject *module) { #define ADD_INT(ival) \ @@ -741,7 +822,7 @@ module_exec(PyObject *module) goto error; } - if (PyModule_AddStringConstant(module, "sqlite_version", sqlite3_libversion())) { + if (add_version_constants(module) < 0) { goto error; } diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 4fd8bf0ac3b957..360aa3e2454f8b 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -7229,12 +7229,62 @@ parse_openssl_version(unsigned long libver, *major = libver & 0xFF; } +PyDoc_STRVAR(openssl_version_info__doc__, +"ssl.OPENSSL_VERSION_INFO\n\ +\n\ +OpenSSL version information as a named tuple."); + +static PyStructSequence_Field openssl_version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"fix", "Fix release number"}, + {"patch", "Patch release number"}, + {"status", "Release status"}, + {0} +}; + +static PyStructSequence_Desc openssl_version_info_desc = { + "ssl.OPENSSL_VERSION_INFO", /* name */ + openssl_version_info__doc__, /* doc */ + openssl_version_info_fields, /* fields */ + 5 +}; + +static PyObject * +make_openssl_version_info(PyTypeObject *type, unsigned long libver) +{ + PyObject *version; + int pos = 0; + unsigned int major, minor, fix, patch, status; + + parse_openssl_version(libver, &major, &minor, &fix, &patch, &status); + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromUnsignedLong(major)) + SetItem(PyLong_FromUnsignedLong(minor)) + SetItem(PyLong_FromUnsignedLong(fix)) + SetItem(PyLong_FromUnsignedLong(patch)) + SetItem(PyLong_FromUnsignedLong(status)) +#undef SetItem + + return version; +} + static int sslmodule_init_versioninfo(PyObject *m) { PyObject *r; unsigned long libver; - unsigned int major, minor, fix, patch, status; /* OpenSSL version */ /* SSLeay() gives us the version of the library linked against, @@ -7245,20 +7295,33 @@ sslmodule_init_versioninfo(PyObject *m) if (PyModule_Add(m, "OPENSSL_VERSION_NUMBER", r) < 0) return -1; - parse_openssl_version(libver, &major, &minor, &fix, &patch, &status); - r = Py_BuildValue("IIIII", major, minor, fix, patch, status); - if (PyModule_Add(m, "OPENSSL_VERSION_INFO", r) < 0) - return -1; - r = PyUnicode_FromString(OpenSSL_version(OPENSSL_VERSION)); if (PyModule_Add(m, "OPENSSL_VERSION", r) < 0) return -1; - libver = OPENSSL_VERSION_NUMBER; - parse_openssl_version(libver, &major, &minor, &fix, &patch, &status); - r = Py_BuildValue("IIIII", major, minor, fix, patch, status); - if (PyModule_Add(m, "_OPENSSL_API_VERSION", r) < 0) + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&openssl_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(m, "OPENSSL_VERSION_INFO", + make_openssl_version_info(version_type, libver)) < 0) + { + Py_DECREF(version_type); + return -1; + } + r = make_openssl_version_info(version_type, OPENSSL_VERSION_NUMBER); + Py_DECREF(version_type); + if (r == NULL) { return -1; + } + if (PyModule_AddObjectRef(m, "OPENSSL_API_VERSION_INFO", r) < 0 || + PyModule_AddObjectRef(m, "_OPENSSL_API_VERSION", r) < 0) + { + Py_DECREF(r); + return -1; + } + Py_DECREF(r); return 0; } diff --git a/Modules/_testcapi/bytes.c b/Modules/_testcapi/bytes.c index a5b904dc2f90bc..f5a5fb0481d263 100644 --- a/Modules/_testcapi/bytes.c +++ b/Modules/_testcapi/bytes.c @@ -209,6 +209,26 @@ writer_format_i(PyObject *self_raw, PyObject *args) } +static PyObject* +writer_format_s(PyObject *self_raw, PyObject *args) +{ + WriterObject *self = (WriterObject *)self_raw; + if (writer_check(self) < 0) { + return NULL; + } + + char *format, *str; + if (!PyArg_ParseTuple(args, "yy", &format, &str)) { + return NULL; + } + + if (PyBytesWriter_Format(self->writer, format, str) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + // PyBytesWriter_Resize static PyObject* writer_resize(PyObject *self_raw, PyObject *args) @@ -333,13 +353,14 @@ static PyMethodDef writer_methods[] = { {"write", _PyCFunction_CAST(writer_write), METH_VARARGS | METH_KEYWORDS}, {"write_bytes", _PyCFunction_CAST(writer_write_bytes), METH_VARARGS}, {"format_i", _PyCFunction_CAST(writer_format_i), METH_VARARGS}, + {"format_s", _PyCFunction_CAST(writer_format_s), METH_VARARGS}, {"resize", _PyCFunction_CAST(writer_resize), METH_VARARGS}, {"grow", _PyCFunction_CAST(writer_grow), METH_VARARGS}, {"get_data", _PyCFunction_CAST(writer_get_data), METH_VARARGS}, {"get_size", _PyCFunction_CAST(writer_get_size), METH_NOARGS}, {"finish", _PyCFunction_CAST(writer_finish), METH_NOARGS}, {"finish_with_size", _PyCFunction_CAST(writer_finish_with_size), METH_VARARGS}, - {"discard", _PyCFunction_CAST(writer_discard), METH_VARARGS}, + {"discard", _PyCFunction_CAST(writer_discard), METH_NOARGS}, {NULL, NULL} /* sentinel */ }; @@ -470,7 +491,7 @@ test_byteswriter_ptr(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) memset(str, 'x', 100); str += 100; - // make sure that the test switchs to a bytes object + // make sure that the test switches to a bytes object assert((100 + 200) > pybyteswriter_small_buffer_size()); char *old_str = str; str = PyBytesWriter_GrowAndUpdatePointer(writer, 200, str); @@ -489,10 +510,10 @@ test_byteswriter_ptr(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) } assert(PyBytes_GET_SIZE(result) == 300); str = PyBytes_AS_STRING(result); - for (Py_ssize_t i=0; i < 100; i++) { + for (Py_ssize_t i = 0; i < 100; i++) { assert(str[i] == 'x'); } - for (Py_ssize_t i=0; i < 200; i++) { + for (Py_ssize_t i = 0; i < 200; i++) { assert(str[100 + i] == 'y'); } Py_DECREF(result); @@ -615,7 +636,6 @@ _PyTestCapi_Init_Bytes(PyObject *m) // PyBytesWriter.obj is the second member, small_buffer is the first member long size = (long)pybyteswriter_small_buffer_size(); if (PyModule_AddIntConstant(m, "PyBytesWriter_small_buffer", size) < 0) { - Py_DECREF(writer_type); return -1; } diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index cff855d61cdbda..07798dfaa42a0c 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3880,6 +3880,14 @@ PyInit__tkinter(void) Py_DECREF(m); return NULL; } + if (PyModule_AddStringConstant(m, "TK_PATCH_LEVEL", TK_PATCH_LEVEL)) { + Py_DECREF(m); + return NULL; + } + if (PyModule_AddStringConstant(m, "TCL_PATCH_LEVEL", TCL_PATCH_LEVEL)) { + Py_DECREF(m); + return NULL; + } Tkapp_Type = PyType_FromSpec(&Tkapp_Type_spec); if (PyModule_AddObjectRef(m, "TkappType", Tkapp_Type)) { diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index e97f6fbc4542c8..4ab076224cdd91 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -2436,6 +2436,78 @@ pyexpat_capsule_destructor(PyObject *capsule) } +PyDoc_STRVAR(version_info__doc__, +"pyexpat.version_info\n\ +\n\ +Expat version information as a named tuple."); + +static PyStructSequence_Field version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {"micro", "Micro release number"}, + {0} +}; + +static PyStructSequence_Desc version_info_desc = { + "pyexpat.version_info", /* name */ + version_info__doc__, /* doc */ + version_info_fields, /* fields */ + 3 +}; + +static PyObject * +make_version_info(PyTypeObject *type, int major, int minor, int micro) +{ + PyObject *version; + int pos = 0; + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromLong(major)) + SetItem(PyLong_FromLong(minor)) + SetItem(PyLong_FromLong(micro)) +#undef SetItem + + return version; +} + +static int +add_version_info(PyObject *mod) +{ + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(mod, "VERSION_INFO", + make_version_info(version_type, XML_MAJOR_VERSION, + XML_MINOR_VERSION, XML_MICRO_VERSION)) < 0) + { + Py_DECREF(version_type); + return -1; + } + XML_Expat_Version info = XML_ExpatVersionInfo(); + if (PyModule_Add(mod, "version_info", + make_version_info(version_type, info.major, + info.minor, info.micro)) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); + return 0; +} + static int pyexpat_exec(PyObject *mod) { @@ -2479,15 +2551,8 @@ pyexpat_exec(PyObject *mod) XML_ExpatVersion()) < 0) { return -1; } - { - XML_Expat_Version info = XML_ExpatVersionInfo(); - PyObject *versionInfo = Py_BuildValue("(iii)", - info.major, - info.minor, - info.micro); - if (PyModule_Add(mod, "version_info", versionInfo) < 0) { - return -1; - } + if (add_version_info(mod) < 0) { + return -1; } /* XXX When Expat supports some way of figuring out how it was compiled, this should check and set native_encoding diff --git a/Modules/readline.c b/Modules/readline.c index e0ff7a9ffebf79..649604055ed4b3 100644 --- a/Modules/readline.c +++ b/Modules/readline.c @@ -1802,6 +1802,96 @@ call_readline(FILE *sys_stdin, FILE *sys_stdout, const char *prompt) } +PyDoc_STRVAR(readline_version_info__doc__, +"readline.readline_version_info\n\ +\n\ +Readline version information as a named tuple."); + +static PyStructSequence_Field readline_version_info_fields[] = { + {"major", "Major release number"}, + {"minor", "Minor release number"}, + {0} +}; + +static PyStructSequence_Desc readline_version_info_desc = { + "readline.readline_version_info", /* name */ + readline_version_info__doc__, /* doc */ + readline_version_info_fields, /* fields */ + 2 +}; + +static PyObject * +make_readline_version_info(PyTypeObject *type, int number) +{ + PyObject *version; + int pos = 0; + int major = (number >> 8) & 0xff; + int minor = number & 0xff; + + version = PyStructSequence_New(type); + if (version == NULL) { + return NULL; + } + +#define SetItem(VALUE) \ + PyStructSequence_SET_ITEM(version, pos++, VALUE); \ + if (PyErr_Occurred()) { \ + Py_DECREF(version); \ + return NULL; \ + } + + SetItem(PyLong_FromLong(major)) + SetItem(PyLong_FromLong(minor)) +#undef SetItem + + return version; +} + +static int +add_version_constants(PyObject *m) +{ + if (PyModule_AddIntConstant(m, "_READLINE_VERSION", + RL_READLINE_VERSION) < 0) { + return -1; + } + if (PyModule_AddIntConstant(m, "_READLINE_RUNTIME_VERSION", + rl_readline_version) < 0) { + return -1; + } + PyObject *obj = PyUnicode_FromString(rl_library_version); + if (obj == NULL) { + return -1; + } + if (PyModule_AddObjectRef(m, "readline_version", obj) < 0 || + PyModule_AddObjectRef(m, "_READLINE_LIBRARY_VERSION", obj) < 0) + { + Py_DECREF(obj); + return -1; + } + Py_DECREF(obj); + PyTypeObject *version_type; + version_type = PyStructSequence_NewType(&readline_version_info_desc); + if (version_type == NULL) { + return -1; + } + if (PyModule_Add(m, "READLINE_VERSION_INFO", + make_readline_version_info(version_type, + RL_READLINE_VERSION)) < 0) + { + Py_DECREF(version_type); + return -1; + } + if (PyModule_Add(m, "readline_version_info", + make_readline_version_info(version_type, + rl_readline_version)) < 0) + { + Py_DECREF(version_type); + return -1; + } + Py_DECREF(version_type); + return 0; +} + /* Initialize the module */ PyDoc_STRVAR(doc_module, @@ -1853,17 +1943,7 @@ PyInit_readline(void) PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); #endif - if (PyModule_AddIntConstant(m, "_READLINE_VERSION", - RL_READLINE_VERSION) < 0) { - goto error; - } - if (PyModule_AddIntConstant(m, "_READLINE_RUNTIME_VERSION", - rl_readline_version) < 0) { - goto error; - } - if (PyModule_AddStringConstant(m, "_READLINE_LIBRARY_VERSION", - rl_library_version) < 0) - { + if (add_version_constants(m) < 0) { goto error; } diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index fcda380dbeb762..7e677108983f0f 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -208,7 +208,7 @@ bytes_fromformat(PyBytesWriter *writer, Py_ssize_t writer_pos, size_t len = (len_expr); \ s = PyBytesWriter_GrowAndUpdatePointer(writer, len, s); \ if (s == NULL) { \ - goto error; \ + return NULL; \ } \ memcpy(s, (str), len); \ s += len; \ @@ -262,7 +262,7 @@ bytes_fromformat(PyBytesWriter *writer, Py_ssize_t writer_pos, PyErr_SetString(PyExc_OverflowError, "PyBytes_FromFormatV(): %c format " "expects an integer in range [0; 255]"); - goto error; + return NULL; } *s++ = (unsigned char)c; break; @@ -355,9 +355,6 @@ bytes_fromformat(PyBytesWriter *writer, Py_ssize_t writer_pos, #undef WRITE_BYTES_LEN return s; - - error: - return NULL; } @@ -3920,7 +3917,7 @@ PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size) } PyObject *result; - if (size == 0) { + if (size == 0 && !writer->use_bytearray) { result = bytes_get_empty(); } else if (writer->obj != NULL) { @@ -4030,6 +4027,7 @@ PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t new_size) else { // The buffer is already large enough. Never shrink the buffer. } + writer->size = new_size; #ifdef Py_DEBUG byteswriter_write_canary_byte(writer); @@ -4131,6 +4129,7 @@ int PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) { Py_ssize_t pos = writer->size; + Py_ssize_t old_pos = pos; if (PyBytesWriter_Grow(writer, strlen(format)) < 0) { return -1; } @@ -4140,6 +4139,15 @@ PyBytesWriter_Format(PyBytesWriter *writer, const char *format, ...) char *buf = bytes_fromformat(writer, pos, format, vargs); va_end(vargs); + if (buf == NULL) { + // On error, reset the writer to its previous state (undo any write) + writer->size = old_pos; +#ifdef Py_DEBUG + byteswriter_write_canary_byte(writer); +#endif + return -1; + } + Py_ssize_t size = buf - byteswriter_data(writer); return PyBytesWriter_Resize(writer, size); } diff --git a/PCbuild/get_externals.bat b/PCbuild/get_externals.bat index 812949dc8370db..b5f070ca2d858d 100644 --- a/PCbuild/get_externals.bat +++ b/PCbuild/get_externals.bat @@ -54,7 +54,7 @@ echo.Fetching external libraries... set libraries= set libraries=%libraries% bzip2-1.0.8 if NOT "%IncludeLibffiSrc%"=="false" set libraries=%libraries% libffi-3.4.4 -if NOT "%IncludeSSLSrc%"=="false" set libraries=%libraries% openssl-3.5.8 +if NOT "%IncludeSSLSrc%"=="false" set libraries=%libraries% openssl-3.5.9 set libraries=%libraries% mpdecimal-4.0.0 set libraries=%libraries% sqlite-3.53.4.0 if NOT "%IncludeTkinterSrc%"=="false" set libraries=%libraries% tcl-9.0.4.0 @@ -79,7 +79,7 @@ echo.Fetching external binaries... set binaries= if NOT "%IncludeLibffi%"=="false" set binaries=%binaries% libffi-3.4.4 -if NOT "%IncludeSSL%"=="false" set binaries=%binaries% openssl-bin-3.5.8 +if NOT "%IncludeSSL%"=="false" set binaries=%binaries% openssl-bin-3.5.9 if NOT "%IncludeTkinter%"=="false" set binaries=%binaries% tcltk-9.0.4.0 if NOT "%IncludeSSLSrc%"=="false" set binaries=%binaries% nasm-2.11.06 if NOT "%IncludeLLVM%"=="false" set binaries=%binaries% llvm-21.1.4.0 diff --git a/PCbuild/python.props b/PCbuild/python.props index 97f04456505cd5..4c49decd7490d1 100644 --- a/PCbuild/python.props +++ b/PCbuild/python.props @@ -105,8 +105,8 @@ $(libffiDir)$(ArchName)\ $(libffiOutDir)include $(ExternalsDir)\mpdecimal-4.0.0\ - $(ExternalsDir)openssl-3.5.8\ - $(ExternalsDir)openssl-bin-3.5.8\$(ArchName)\ + $(ExternalsDir)openssl-3.5.9\ + $(ExternalsDir)openssl-bin-3.5.9\$(ArchName)\ $(opensslOutDir)include $(ExternalsDir)\nasm-2.11.06\ $(ExternalsDir)\zlib-1.3.1\ diff --git a/Tools/ssl/multissltests.py b/Tools/ssl/multissltests.py index 037f1852180b62..8f01246150bd34 100755 --- a/Tools/ssl/multissltests.py +++ b/Tools/ssl/multissltests.py @@ -44,17 +44,17 @@ OPENSSL_OLD_VERSIONS = [ "1.1.1w", + "3.0.22", "3.1.8", "3.2.6", "3.3.7", ] OPENSSL_RECENT_VERSIONS = [ - "3.0.22", - "3.4.7", - "3.5.8", - "3.6.4", - "4.0.2", + "3.4.8", + "3.5.9", + "3.6.5", + "4.0.3", # See make_ssl_data.py for notes on adding a new version. ] @@ -65,7 +65,7 @@ ] AWSLC_RECENT_VERSIONS = [ - "5.5.0", + "5.10.0", ] # store files in ../multissl diff --git a/configure b/configure index 8b8bab4ca425db..7d1003a0e0e985 100755 --- a/configure +++ b/configure @@ -18446,6 +18446,34 @@ esac { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $have_ndbm ($dbm_ndbm)" >&5 printf "%s\n" "$have_ndbm ($dbm_ndbm)" >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether ndbm.h is provided by gdbm" >&5 +printf %s "checking whether ndbm.h is provided by gdbm... " >&6; } +if test ${ac_cv_ndbm_h_is_gdbm+y} +then : + printf %s "(cached) " >&6 +else case e in #( + e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + #include + #ifndef _GDBM_H_ + #error "not gdbm" + #endif + +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + ac_cv_ndbm_h_is_gdbm=yes +else case e in #( + e) ac_cv_ndbm_h_is_gdbm=no ;; +esac +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;; +esac +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_ndbm_h_is_gdbm" >&5 +printf "%s\n" "$ac_cv_ndbm_h_is_gdbm" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gdbm/ndbm.h" >&5 printf %s "checking for gdbm/ndbm.h... " >&6; } if test ${ac_cv_header_gdbm_slash_ndbm_h+y} @@ -18721,6 +18749,10 @@ for db in $with_dbmliborder; do if test "$have_ndbm" = yes; then DBM_CFLAGS="-DUSE_NDBM" DBM_LIBS="$dbm_ndbm" + if test "x$ac_cv_ndbm_h_is_gdbm" = xyes +then : + DBM_LIBS="$DBM_LIBS $GDBM_LIBS" +fi have_dbm=yes break fi @@ -18728,7 +18760,7 @@ for db in $with_dbmliborder; do gdbm) if test "$have_gdbm_compat" = yes; then DBM_CFLAGS="-DUSE_GDBM_COMPAT" - DBM_LIBS="-lgdbm_compat" + DBM_LIBS="-lgdbm_compat $GDBM_LIBS" have_dbm=yes break fi diff --git a/configure.ac b/configure.ac index 40191c05952290..e1d55a7a3cef90 100644 --- a/configure.ac +++ b/configure.ac @@ -4736,6 +4736,15 @@ AS_CASE([$ac_cv_search_dbm_open], ) AC_MSG_RESULT([$have_ndbm ($dbm_ndbm)]) +dnl gdbm's ndbm.h includes gdbm.h; libgdbm is then needed for the version info. +AC_CACHE_CHECK([whether ndbm.h is provided by gdbm], [ac_cv_ndbm_h_is_gdbm], + [AC_COMPILE_IFELSE([AC_LANG_SOURCE([[ + #include + #ifndef _GDBM_H_ + #error "not gdbm" + #endif + ]])], [ac_cv_ndbm_h_is_gdbm=yes], [ac_cv_ndbm_h_is_gdbm=no])]) + dnl "gdbm-ndbm.h" and "gdbm/ndbm.h" are both normalized to "gdbm_ndbm_h" AC_CACHE_CHECK([for gdbm/ndbm.h], [ac_cv_header_gdbm_slash_ndbm_h], [AC_PREPROC_IFELSE([AC_LANG_SOURCE([@%:@include ])], @@ -4818,6 +4827,7 @@ for db in $with_dbmliborder; do if test "$have_ndbm" = yes; then DBM_CFLAGS="-DUSE_NDBM" DBM_LIBS="$dbm_ndbm" + AS_VAR_IF([ac_cv_ndbm_h_is_gdbm], [yes], [DBM_LIBS="$DBM_LIBS $GDBM_LIBS"]) have_dbm=yes break fi @@ -4825,7 +4835,7 @@ for db in $with_dbmliborder; do gdbm) if test "$have_gdbm_compat" = yes; then DBM_CFLAGS="-DUSE_GDBM_COMPAT" - DBM_LIBS="-lgdbm_compat" + DBM_LIBS="-lgdbm_compat $GDBM_LIBS" have_dbm=yes break fi