Skip to content

Commit 8ad9b1b

Browse files
committed
Add backend parameter to dbm.open() and shelve.open() for explicit DBM backend selection
1 parent 34d7351 commit 8ad9b1b

File tree

6 files changed

+245
-10
lines changed

6 files changed

+245
-10
lines changed

Doc/library/dbm.rst

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ the Oracle Berkeley DB.
6666
The Unix file access mode of the file (default: octal ``0o666``),
6767
used only when the database has to be created.
6868

69-
.. function:: open(file, flag='r', mode=0o666)
69+
.. function:: open(file, flag='r', mode=0o666, *, backend=None)
7070

7171
Open a database and return the corresponding database object.
7272

@@ -87,9 +87,35 @@ the Oracle Berkeley DB.
8787
:param int mode:
8888
|mode_param_doc|
8989

90+
:param backend:
91+
The DBM backend implementation to use. If not specified, automatic
92+
backend selection is used as described above. When specified, the
93+
exact backend is used regardless of file existence or automatic detection.
94+
95+
Valid backends:
96+
97+
* ``'dbm.sqlite3'`` -- SQLite-based backend (if available)
98+
* ``'dbm.gnu'`` -- GNU DBM backend (if available)
99+
* ``'dbm.ndbm'`` -- NDBM backend (if available)
100+
* ``'dbm.dumb'`` -- Pure Python backend (always available)
101+
102+
Specifying a backend improves compatibility when working with custom
103+
serializers and ensures consistent behavior across different systems.
104+
105+
:raises TypeError: if not a string
106+
:raises ValueError: if not a supported backend name
107+
:raises ImportError: if the specified backend is not available
108+
:type backend: str or None
109+
90110
.. versionchanged:: 3.11
91111
*file* accepts a :term:`path-like object`.
92112

113+
.. versionchanged:: next
114+
Added *backend* parameter for explicit DBM backend selection.
115+
When specified, this overrides the automatic backend selection logic
116+
and uses the exact backend requested, improving compatibility and
117+
predictability across different systems.
118+
93119
The object returned by :func:`~dbm.open` supports the same basic functionality as a
94120
:class:`dict`; keys and their corresponding values can be stored, retrieved, and
95121
deleted, and the :keyword:`in` operator and the :meth:`!keys` method are

Doc/library/shelve.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ This includes most class instances, recursive data types, and objects containing
1717
lots of shared sub-objects. The keys are ordinary strings.
1818

1919

20-
.. function:: open(filename, flag='c', protocol=None, writeback=False, *, \
21-
serializer=None, deserializer=None)
20+
.. function:: open(filename, flag='c', protocol=None, writeback=False, \
21+
backend=None, *, serializer=None, deserializer=None)
2222
2323
Open a persistent dictionary. The filename specified is the base filename for
2424
the underlying database. As a side-effect, an extension may be added to the
@@ -42,6 +42,13 @@ lots of shared sub-objects. The keys are ordinary strings.
4242
determine which accessed entries are mutable, nor which ones were actually
4343
mutated).
4444

45+
The optional *backend* parameter allows explicit selection of the underlying
46+
DBM backend implementation. By default, :mod:`shelve` uses :func:`dbm.open`
47+
with automatic backend selection. When *backend* is specified, that exact
48+
backend will be used, which can improve compatibility when working with custom
49+
serializers and ensure consistent behavior across different systems.
50+
See :func:`dbm.open` for valid backend values.
51+
4552
By default, :mod:`shelve` uses :func:`pickle.dumps` and :func:`pickle.loads`
4653
for serializing and deserializing. This can be changed by supplying
4754
*serializer* and *deserializer*, respectively.
@@ -68,6 +75,10 @@ lots of shared sub-objects. The keys are ordinary strings.
6875
Accepts custom *serializer* and *deserializer* functions in place of
6976
:func:`pickle.dumps` and :func:`pickle.loads`.
7077

78+
.. versionchanged:: next
79+
Added *backend* parameter to specify which DBM backend implementation
80+
to use, improving compatibility and consistency across different systems.
81+
7182
.. note::
7283

7384
Do not rely on the shelf being closed automatically; always call

Lib/dbm/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class error(Exception):
5050
ndbm = None
5151

5252

53-
def open(file, flag='r', mode=0o666):
53+
def open(file, flag='r', mode=0o666, backend=None):
5454
"""Open or create database at path given by *file*.
5555
5656
Optional argument *flag* can be 'r' (default) for read-only access, 'w'
@@ -60,7 +60,21 @@ def open(file, flag='r', mode=0o666):
6060
6161
Note: 'r' and 'w' fail if the database doesn't exist; 'c' creates it
6262
only if it doesn't exist; and 'n' always creates a new database.
63+
64+
Optional argument *backend* specifies which DBM backend to use.
65+
Must be one of: 'dbm.sqlite3', 'dbm.gnu', 'dbm.ndbm', 'dbm.dumb'
6366
"""
67+
if backend is not None:
68+
if not isinstance(backend, str):
69+
raise TypeError(f"backend must be a string")
70+
if backend not in _names:
71+
raise ValueError(f"Unknown backend '{backend}'")
72+
try:
73+
mod = __import__(backend, fromlist=['open'])
74+
return mod.open(file, flag, mode)
75+
except ImportError as e:
76+
raise ImportError(f"Backend '{backend}' is not available: {e}")
77+
6478
global _defaultmod
6579
if _defaultmod is None:
6680
for name in _names:

Lib/shelve.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,12 @@ class DbfilenameShelf(Shelf):
236236
See the module's __doc__ string for an overview of the interface.
237237
"""
238238

239-
def __init__(self, filename, flag='c', protocol=None, writeback=False, *,
240-
serializer=None, deserializer=None):
239+
def __init__(self, filename, flag='c', protocol=None, writeback=False,
240+
backend=None, *, serializer=None, deserializer=None):
241241
import dbm
242-
Shelf.__init__(self, dbm.open(filename, flag), protocol, writeback,
243-
serializer=serializer, deserializer=deserializer)
242+
Shelf.__init__(self, dbm.open(filename, flag, 0o666, backend),
243+
protocol, writeback, serializer=serializer,
244+
deserializer=deserializer)
244245

245246
def clear(self):
246247
"""Remove all items from the shelf."""
@@ -249,7 +250,7 @@ def clear(self):
249250
self.cache.clear()
250251
self.dict.clear()
251252

252-
def open(filename, flag='c', protocol=None, writeback=False, *,
253+
def open(filename, flag='c', protocol=None, writeback=False, backend=None, *,
253254
serializer=None, deserializer=None):
254255
"""Open a persistent dictionary for reading and writing.
255256
@@ -263,5 +264,5 @@ def open(filename, flag='c', protocol=None, writeback=False, *,
263264
See the module's __doc__ string for an overview of the interface.
264265
"""
265266

266-
return DbfilenameShelf(filename, flag, protocol, writeback,
267+
return DbfilenameShelf(filename, flag, protocol, writeback, backend,
267268
serializer=serializer, deserializer=deserializer)

Lib/test/test_dbm.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,130 @@ def test_whichdb_sqlite3_existing_db(self):
302302
cx.close()
303303
self.assertEqual(self.dbm.whichdb(_fname), "dbm.sqlite3")
304304

305+
def setUp(self):
306+
self.addCleanup(cleaunup_test_dir)
307+
setup_test_dir()
308+
self.dbm = import_helper.import_fresh_module('dbm')
309+
305310

311+
class DBMBackendTestCase(unittest.TestCase):
306312
def setUp(self):
307313
self.addCleanup(cleaunup_test_dir)
308314
setup_test_dir()
309315
self.dbm = import_helper.import_fresh_module('dbm')
310316

317+
@staticmethod
318+
def get_available_backends():
319+
available_backends = []
320+
for backend_name in ['dbm.dumb', 'dbm.gnu', 'dbm.ndbm', 'dbm.sqlite3']:
321+
try:
322+
__import__(backend_name, fromlist=['open'])
323+
available_backends.append(backend_name)
324+
except ImportError:
325+
pass
326+
return available_backends
327+
328+
def test_backend_parameter_validation(self):
329+
invalid_types = [123, [], {}, object(), dbm]
330+
for invalid_backend in invalid_types:
331+
with self.subTest(invalid_backend=invalid_backend):
332+
with self.assertRaises(TypeError) as cm:
333+
dbm.open(_fname, backend=invalid_backend)
334+
self.assertIn("backend must be a string", str(cm.exception))
335+
336+
invalid_names = ['invalid', 'postgres', 'gnu', 'dumb', 'sqlite3']
337+
for invalid_name in invalid_names:
338+
with self.subTest(invalid_name=invalid_name):
339+
with self.assertRaises(ValueError) as cm:
340+
dbm.open(_fname, backend=invalid_name)
341+
self.assertIn("Unknown backend", str(cm.exception))
342+
343+
def test_backend_parameter_functionality(self):
344+
valid_backends = self.get_available_backends()
345+
if len(valid_backends) < 1:
346+
self.skipTest("Need at least 1 backends for this test")
347+
348+
for backend in valid_backends:
349+
with self.subTest(backend=backend):
350+
unique_fn = f"{_fname}_{backend}"
351+
with dbm.open(unique_fn, 'c', backend=backend) as db:
352+
db[b'test_key'] = b'test_value'
353+
db[b'backend'] = backend.encode('ascii')
354+
355+
self.assertEqual(dbm.whichdb(unique_fn), backend)
356+
357+
with dbm.open(unique_fn, 'r', backend=backend) as db:
358+
self.assertEqual(db[b'test_key'], b'test_value')
359+
self.assertEqual(db[b'backend'], backend.encode('ascii'))
360+
361+
def test_backend_none_preserves_auto_selection(self):
362+
with dbm.open(_fname, 'c') as db1:
363+
db1[b'test'] = b'auto_selection'
364+
backend1 = dbm.whichdb(_fname)
365+
366+
with dbm.open(_fname, 'c', backend=None) as db2:
367+
db2[b'test'] = b'explicit_none'
368+
backend2 = dbm.whichdb(_fname)
369+
370+
self.assertEqual(backend1, backend2)
371+
372+
def test_backend_with_different_flags(self):
373+
valid_backends = self.get_available_backends()
374+
if len(valid_backends) < 1:
375+
self.skipTest("Need at least 1 backends for this test")
376+
377+
for backend in valid_backends:
378+
with self.subTest(backend=backend):
379+
unique_fn = f"{_fname}_{backend}"
380+
with dbm.open(unique_fn, 'c', backend=backend) as db:
381+
db[b'flag_test'] = b'create'
382+
self.assertEqual(dbm.whichdb(unique_fn), backend)
383+
384+
# Test 'w' flag (write existing)
385+
with dbm.open(unique_fn, 'w', backend=backend) as db:
386+
self.assertEqual(db[b'flag_test'], b'create')
387+
db[b'flag_test'] = b'write'
388+
389+
# Test 'r' flag (read-only)
390+
with dbm.open(unique_fn, 'r', backend=backend) as db:
391+
self.assertEqual(db[b'flag_test'], b'write')
392+
with self.assertRaises(dbm.error):
393+
db[b'readonly_test'] = b'should_fail'
394+
395+
# Test 'n' flag (new database)
396+
new_file = f"{unique_fn}_new"
397+
with dbm.open(new_file, 'n', backend=backend) as db:
398+
db[b'new_test'] = b'new_data'
399+
400+
self.assertEqual(dbm.whichdb(new_file), backend)
401+
402+
def test_backend_parameter_positioning(self):
403+
valid_backends = self.get_available_backends()
404+
if len(valid_backends) < 1:
405+
self.skipTest("Need at least 1 backends for this test")
406+
407+
for backend in valid_backends:
408+
with self.subTest(backend=backend):
409+
unique_fn = f"{_fname}_{backend}"
410+
411+
# Test as keyword argument
412+
file_1 = f"{unique_fn}_keyword"
413+
with dbm.open(file_1, 'c', backend=backend) as db:
414+
db[b'test'] = b'keyword'
415+
416+
# Test as positional argument
417+
file_2 = f"{unique_fn}_positional"
418+
with dbm.open(file_2, 'c', 0o666, backend) as db:
419+
db[b'test'] = b'positional'
420+
421+
self.assertEqual(dbm.whichdb(file_1), backend)
422+
self.assertEqual(dbm.whichdb(file_2), backend)
423+
424+
def test_backend_import_error_handling(self):
425+
fake_backend = 'dbm.nonexistent'
426+
with self.assertRaises(ValueError) as cm:
427+
dbm.open(_fname, backend=fake_backend)
428+
self.assertIn('Unknown backend', str(cm.exception))
311429

312430
for mod in dbm_iterator():
313431
assert mod.__name__.startswith('dbm.')

Lib/test/test_shelve.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,71 @@ def deserializer(data):
400400
self.assertRaises(shelve.ShelveError, shelve.BsdDbShelf, {}, **kwargs)
401401

402402

403+
class TestShelveBackend(unittest.TestCase):
404+
dirname = os_helper.TESTFN
405+
fn = os.path.join(os_helper.TESTFN, "shelftemp.db")
406+
407+
@staticmethod
408+
def get_available_backends():
409+
available_backends = []
410+
for backend_name in ['dbm.dumb', 'dbm.gnu', 'dbm.ndbm', 'dbm.sqlite3']:
411+
try:
412+
__import__(backend_name, fromlist=['open'])
413+
available_backends.append(backend_name)
414+
except ImportError:
415+
pass
416+
return available_backends
417+
418+
def test_backend_parameter_validation(self):
419+
valid_backends = self.get_available_backends()
420+
if len(valid_backends) < 1:
421+
self.skipTest("Need at least 1 backends for this test")
422+
os.mkdir(self.dirname)
423+
self.addCleanup(os_helper.rmtree, self.dirname)
424+
425+
for backend in valid_backends:
426+
unique_fn = f"{self.fn}_{backend}"
427+
with self.subTest(backend=backend):
428+
with shelve.open(unique_fn, backend=backend) as shelf:
429+
shelf['test'] = 'data'
430+
self.assertEqual(shelf['test'], 'data')
431+
432+
invalid_types = [123, [], {}, object(), dbm]
433+
for invalid_backend in invalid_types:
434+
with self.subTest(invalid_backend=invalid_backend):
435+
with self.assertRaises(TypeError):
436+
shelve.open(self.fn, backend=invalid_backend)
437+
438+
invalid_names = ['invalid', 'postgres', 'gnu', 'dumb', 'sqlite3']
439+
for invalid_name in invalid_names:
440+
with self.subTest(invalid_name=invalid_name):
441+
with self.assertRaises(ValueError):
442+
shelve.open(self.fn, backend=invalid_name)
443+
444+
def test_backend_unavailable_error(self):
445+
fake_backend = 'dbm.nonexistent'
446+
with self.assertRaises(ValueError) as cm:
447+
shelve.open(self.fn, backend=fake_backend)
448+
self.assertIn('nonexistent', str(cm.exception))
449+
450+
def test_backend_compatibility_with_custom_serializers(self):
451+
valid_backends = self.get_available_backends()
452+
if len(valid_backends) < 1:
453+
self.skipTest("Need at least 1 backends for this test")
454+
os.mkdir(self.dirname)
455+
self.addCleanup(os_helper.rmtree, self.dirname)
456+
457+
for backend in valid_backends:
458+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
459+
with self.subTest(backend=backend, protocol=proto):
460+
unique_fn = f"{self.fn}_{backend}_proto_{proto}"
461+
with shelve.open(unique_fn, backend=backend,
462+
protocol=proto) as s:
463+
s["foo"] = "bar"
464+
self.assertEqual(s["foo"], "bar")
465+
actual_backend = dbm.whichdb(unique_fn)
466+
self.assertEqual(actual_backend, backend)
467+
403468
class TestShelveBase:
404469
type2test = shelve.Shelf
405470

0 commit comments

Comments
 (0)