https://github.com/python/cpython/commit/89bfac6d437a86fd82743c326b5269f75c8c97c1
commit: 89bfac6d437a86fd82743c326b5269f75c8c97c1
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-29T13:20:28Z
summary:

gh-157480: Add structured version info for dbm.ndbm (GH-157484)

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) <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-14-10-39-05.gh-issue-157480.to5EnW.rst
M Doc/library/dbm.rst
M Doc/whatsnew/3.16.rst
M Lib/test/pythoninfo.py
M Lib/test/test_dbm_ndbm.py
M Modules/_dbmmodule.c
M configure
M configure.ac

diff --git a/Doc/library/dbm.rst b/Doc/library/dbm.rst
index 423d3d517c57d2..3e34395e3120c3 100644
--- a/Doc/library/dbm.rst
+++ b/Doc/library/dbm.rst
@@ -414,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/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 9dd73254b080c4..bb30c856b9880a 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -356,6 +356,20 @@ dbm.gnu
   (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
 -------
 
diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py
index e66d82ce5244c7..b6658d1bab3136 100644
--- a/Lib/test/pythoninfo.py
+++ b/Lib/test/pythoninfo.py
@@ -945,6 +945,17 @@ 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:
         import _gdbm
@@ -1368,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_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/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/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 <ndbm.h>
+  #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/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 <ndbm.h>
+                 #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 <ndbm.h>
+                 #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 
<gdbm/ndbm.h>])],
@@ -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

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

Reply via email to