From 543a6b7416eb5f8be1ad4af40cfafc3e485b8cb8 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 14 Sep 2026 13:58:25 +0300 Subject: [PATCH] gh-157480: Add structured version info for dbm.ndbm Add constants which provide information about the version of the underlying library in use: GDBM_VERSION_INFO, gdbm_version_info and gdbm_version if it is GDBM, or BDB_VERSION, bdb_version, BDB_VERSION_INFO and bdb_version_info if it is Berkeley DB. dbm.ndbm.library is now 'ndbm' instead of 'GNU gdbm' for a classic NDBM library. Co-authored-by: Claude Opus 5 (1M context) --- Doc/library/dbm.rst | 64 +++++++- Doc/whatsnew/3.16.rst | 14 ++ Lib/test/pythoninfo.py | 12 ++ Lib/test/test_dbm_ndbm.py | 48 ++++++ ...-09-14-10-39-05.gh-issue-157480.to5EnW.rst | 8 + Modules/_dbmmodule.c | 138 ++++++++++++++++++ configure | 34 ++++- configure.ac | 12 +- 8 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-14-10-39-05.gh-issue-157480.to5EnW.rst diff --git a/Doc/library/dbm.rst b/Doc/library/dbm.rst index 646981e8692cc53..94439fbf0d0c79d 100644 --- a/Doc/library/dbm.rst +++ b/Doc/library/dbm.rst @@ -382,7 +382,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/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 53983637f520c8a..4d474752af46776 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -340,6 +340,20 @@ concurrent.futures (Contributed by xzmeng and Serhiy Storchaka in :gh:`108518`.) +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 ------- diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index b59e2acb9376f1e..f4494fb52bcd8eb 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -935,6 +935,17 @@ def collect_cc(info_add): info_add('CC.version', text) +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 @@ -1356,6 +1367,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_dbm_ndbm.py b/Lib/test/test_dbm_ndbm.py index e0f31c9a9a337d2..d86badfdd2726ea 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/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 000000000000000..cd818acd4ad3a6a --- /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/Modules/_dbmmodule.c b/Modules/_dbmmodule.c index a9f4f27d9eb742e..ffce6779cfcbf57 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 @@ -628,6 +633,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) { @@ -644,6 +779,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/configure b/configure index 1115be256afb495..b0bb18b59d80314 100755 --- a/configure +++ b/configure @@ -18454,6 +18454,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} @@ -18729,6 +18757,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 @@ -18736,7 +18768,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 902a822e43a42f0..42c7846d89ea87b 100644 --- a/configure.ac +++ b/configure.ac @@ -4733,6 +4733,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 ])], @@ -4815,6 +4824,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 @@ -4822,7 +4832,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