diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 44f688ad0d6422b..a6a30928ce83d21 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 53983637f520c8a..a6739f956060b2e 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -580,6 +580,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. diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index b59e2acb9376f1e..7c25cc58b2658b7 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -505,7 +505,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: diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index c9d402e11a8826f..bd061e27bb4867b 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -8,6 +8,7 @@ import time import unittest import weakref +import _tkinter import tkinter from tkinter import TclError, ttk import enum @@ -22,6 +23,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): @@ -828,30 +872,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/tkinter/__init__.py b/Lib/tkinter/__init__.py index dde4accecf61ba4..11bb0ba9c89c4ac 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/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 000000000000000..80c2ad9a7d2aa97 --- /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/Modules/_tkinter.c b/Modules/_tkinter.c index f5fb1841f0d5940..7223e6446a9e1e1 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3848,6 +3848,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)) {