Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dd5e4d9

Browse files
erlend-aaslandrhettingerserhiy-storchakafelixxm
authored
gh-100414: Add SQLite backend to dbm (#114481)
Co-authored-by: Raymond Hettinger <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: Mariusz Felisiak <[email protected]>
1 parent 57e4c81 commit dd5e4d9

File tree

7 files changed

+544
-6
lines changed

7 files changed

+544
-6
lines changed

Doc/library/dbm.rst

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
88

99
--------------
1010

11-
:mod:`dbm` is a generic interface to variants of the DBM database ---
12-
:mod:`dbm.gnu` or :mod:`dbm.ndbm`. If none of these modules is installed, the
11+
:mod:`dbm` is a generic interface to variants of the DBM database:
12+
13+
* :mod:`dbm.sqlite3`
14+
* :mod:`dbm.gnu`
15+
* :mod:`dbm.ndbm`
16+
17+
If none of these modules are installed, the
1318
slow-but-simple implementation in module :mod:`dbm.dumb` will be used. There
1419
is a `third party interface <https://www.jcea.es/programacion/pybsddb.htm>`_ to
1520
the Oracle Berkeley DB.
@@ -25,8 +30,8 @@ the Oracle Berkeley DB.
2530
.. function:: whichdb(filename)
2631

2732
This function attempts to guess which of the several simple database modules
28-
available --- :mod:`dbm.gnu`, :mod:`dbm.ndbm` or :mod:`dbm.dumb` --- should
29-
be used to open a given file.
33+
available --- :mod:`dbm.sqlite3`, :mod:`dbm.gnu`, :mod:`dbm.ndbm`,
34+
or :mod:`dbm.dumb` --- should be used to open a given file.
3035

3136
Return one of the following values:
3237

@@ -144,6 +149,46 @@ then prints out the contents of the database::
144149

145150
The individual submodules are described in the following sections.
146151

152+
:mod:`dbm.sqlite3` --- SQLite backend for dbm
153+
---------------------------------------------
154+
155+
.. module:: dbm.sqlite3
156+
:platform: All
157+
:synopsis: SQLite backend for dbm
158+
159+
.. versionadded:: 3.13
160+
161+
**Source code:** :source:`Lib/dbm/sqlite3.py`
162+
163+
--------------
164+
165+
This module uses the standard library :mod:`sqlite3` module to provide an
166+
SQLite backend for the :mod:`dbm` module.
167+
The files created by :mod:`dbm.sqlite3` can thus be opened by :mod:`sqlite3`,
168+
or any other SQLite browser, including the SQLite CLI.
169+
170+
.. function:: open(filename, /, flag="r", mode=0o666)
171+
172+
Open an SQLite database.
173+
The returned object behaves like a :term:`mapping`,
174+
implements a :meth:`!close` method,
175+
and supports a "closing" context manager via the :keyword:`with` keyword.
176+
177+
:param filename:
178+
The path to the database to be opened.
179+
:type filename: :term:`path-like object`
180+
181+
:param str flag:
182+
183+
* ``'r'`` (default): |flag_r|
184+
* ``'w'``: |flag_w|
185+
* ``'c'``: |flag_c|
186+
* ``'n'``: |flag_n|
187+
188+
:param mode:
189+
The Unix file access mode of the file (default: octal ``0o666``),
190+
used only when the database has to be created.
191+
147192

148193
:mod:`dbm.gnu` --- GNU database manager
149194
---------------------------------------

Doc/whatsnew/3.13.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,16 @@ dis
231231
the ``show_offsets`` parameter.
232232
(Contributed by Irit Katriel in :gh:`112137`.)
233233

234+
dbm
235+
---
236+
237+
* Add :meth:`dbm.gnu.gdbm.clear` and :meth:`dbm.ndbm.ndbm.clear` methods that remove all items
238+
from the database.
239+
(Contributed by Donghee Na in :gh:`107122`.)
240+
241+
* Add new :mod:`dbm.sqlite3` backend.
242+
(Contributed by Raymond Hettinger and Erlend E. Aasland in :gh:`100414`.)
243+
234244
doctest
235245
-------
236246

Lib/dbm/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import dbm
66
d = dbm.open(file, 'w', 0o666)
77
8-
The returned object is a dbm.gnu, dbm.ndbm or dbm.dumb object, dependent on the
8+
The returned object is a dbm.sqlite3, dbm.gnu, dbm.ndbm or dbm.dumb database object, dependent on the
99
type of database being opened (determined by the whichdb function) in the case
1010
of an existing dbm. If the dbm does not exist and the create or new flag ('c'
1111
or 'n') was specified, the dbm type will be determined by the availability of
@@ -38,7 +38,7 @@
3838
class error(Exception):
3939
pass
4040

41-
_names = ['dbm.gnu', 'dbm.ndbm', 'dbm.dumb']
41+
_names = ['dbm.gnu', 'dbm.ndbm', 'dbm.sqlite3', 'dbm.dumb']
4242
_defaultmod = None
4343
_modules = {}
4444

@@ -164,6 +164,10 @@ def whichdb(filename):
164164
if len(s) != 4:
165165
return ""
166166

167+
# Check for SQLite3 header string.
168+
if s16 == b"SQLite format 3\0":
169+
return "dbm.sqlite3"
170+
167171
# Convert to 4-byte int in native byte order -- return "" if impossible
168172
try:
169173
(magic,) = struct.unpack("=l", s)

Lib/dbm/sqlite3.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import os
2+
import sqlite3
3+
import sys
4+
from pathlib import Path
5+
from contextlib import suppress, closing
6+
from collections.abc import MutableMapping
7+
8+
BUILD_TABLE = """
9+
CREATE TABLE IF NOT EXISTS Dict (
10+
key BLOB UNIQUE NOT NULL,
11+
value BLOB NOT NULL
12+
)
13+
"""
14+
GET_SIZE = "SELECT COUNT (key) FROM Dict"
15+
LOOKUP_KEY = "SELECT value FROM Dict WHERE key = CAST(? AS BLOB)"
16+
STORE_KV = "REPLACE INTO Dict (key, value) VALUES (CAST(? AS BLOB), CAST(? AS BLOB))"
17+
DELETE_KEY = "DELETE FROM Dict WHERE key = CAST(? AS BLOB)"
18+
ITER_KEYS = "SELECT key FROM Dict"
19+
20+
21+
class error(OSError):
22+
pass
23+
24+
25+
_ERR_CLOSED = "DBM object has already been closed"
26+
_ERR_REINIT = "DBM object does not support reinitialization"
27+
28+
29+
def _normalize_uri(path):
30+
path = Path(path)
31+
uri = path.absolute().as_uri()
32+
while "//" in uri:
33+
uri = uri.replace("//", "/")
34+
return uri
35+
36+
37+
class _Database(MutableMapping):
38+
39+
def __init__(self, path, /, *, flag, mode):
40+
if hasattr(self, "_cx"):
41+
raise error(_ERR_REINIT)
42+
43+
path = os.fsdecode(path)
44+
match flag:
45+
case "r":
46+
flag = "ro"
47+
case "w":
48+
flag = "rw"
49+
case "c":
50+
flag = "rwc"
51+
Path(path).touch(mode=mode, exist_ok=True)
52+
case "n":
53+
flag = "rwc"
54+
Path(path).unlink(missing_ok=True)
55+
Path(path).touch(mode=mode)
56+
case _:
57+
raise ValueError("Flag must be one of 'r', 'w', 'c', or 'n', "
58+
f"not {flag!r}")
59+
60+
# We use the URI format when opening the database.
61+
uri = _normalize_uri(path)
62+
uri = f"{uri}?mode={flag}"
63+
64+
try:
65+
self._cx = sqlite3.connect(uri, autocommit=True, uri=True)
66+
except sqlite3.Error as exc:
67+
raise error(str(exc))
68+
69+
# This is an optimization only; it's ok if it fails.
70+
with suppress(sqlite3.OperationalError):
71+
self._cx.execute("PRAGMA journal_mode = wal")
72+
73+
if flag == "rwc":
74+
self._execute(BUILD_TABLE)
75+
76+
def _execute(self, *args, **kwargs):
77+
if not self._cx:
78+
raise error(_ERR_CLOSED)
79+
try:
80+
return closing(self._cx.execute(*args, **kwargs))
81+
except sqlite3.Error as exc:
82+
raise error(str(exc))
83+
84+
def __len__(self):
85+
with self._execute(GET_SIZE) as cu:
86+
row = cu.fetchone()
87+
return row[0]
88+
89+
def __getitem__(self, key):
90+
with self._execute(LOOKUP_KEY, (key,)) as cu:
91+
row = cu.fetchone()
92+
if not row:
93+
raise KeyError(key)
94+
return row[0]
95+
96+
def __setitem__(self, key, value):
97+
self._execute(STORE_KV, (key, value))
98+
99+
def __delitem__(self, key):
100+
with self._execute(DELETE_KEY, (key,)) as cu:
101+
if not cu.rowcount:
102+
raise KeyError(key)
103+
104+
def __iter__(self):
105+
try:
106+
with self._execute(ITER_KEYS) as cu:
107+
for row in cu:
108+
yield row[0]
109+
except sqlite3.Error as exc:
110+
raise error(str(exc))
111+
112+
def close(self):
113+
if self._cx:
114+
self._cx.close()
115+
self._cx = None
116+
117+
def keys(self):
118+
return list(super().keys())
119+
120+
def __enter__(self):
121+
return self
122+
123+
def __exit__(self, *args):
124+
self.close()
125+
126+
127+
def open(filename, /, flag="r", mode=0o666):
128+
"""Open a dbm.sqlite3 database and return the dbm object.
129+
130+
The 'filename' parameter is the name of the database file.
131+
132+
The optional 'flag' parameter can be one of ...:
133+
'r' (default): open an existing database for read only access
134+
'w': open an existing database for read/write access
135+
'c': create a database if it does not exist; open for read/write access
136+
'n': always create a new, empty database; open for read/write access
137+
138+
The optional 'mode' parameter is the Unix file access mode of the database;
139+
only used when creating a new database. Default: 0o666.
140+
"""
141+
return _Database(filename, flag=flag, mode=mode)

Lib/test/test_dbm.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
from test.support import import_helper
77
from test.support import os_helper
88

9+
10+
try:
11+
from dbm import sqlite3 as dbm_sqlite3
12+
except ImportError:
13+
dbm_sqlite3 = None
14+
15+
916
try:
1017
from dbm import ndbm
1118
except ImportError:
@@ -213,6 +220,27 @@ def test_whichdb_ndbm(self):
213220
for path in fnames:
214221
self.assertIsNone(self.dbm.whichdb(path))
215222

223+
@unittest.skipUnless(dbm_sqlite3, reason='Test requires dbm.sqlite3')
224+
def test_whichdb_sqlite3(self):
225+
# Databases created by dbm.sqlite3 are detected correctly.
226+
with dbm_sqlite3.open(_fname, "c") as db:
227+
db["key"] = "value"
228+
self.assertEqual(self.dbm.whichdb(_fname), "dbm.sqlite3")
229+
230+
@unittest.skipUnless(dbm_sqlite3, reason='Test requires dbm.sqlite3')
231+
def test_whichdb_sqlite3_existing_db(self):
232+
# Existing sqlite3 databases are detected correctly.
233+
sqlite3 = import_helper.import_module("sqlite3")
234+
try:
235+
# Create an empty database.
236+
with sqlite3.connect(_fname) as cx:
237+
cx.execute("CREATE TABLE dummy(database)")
238+
cx.commit()
239+
finally:
240+
cx.close()
241+
self.assertEqual(self.dbm.whichdb(_fname), "dbm.sqlite3")
242+
243+
216244
def setUp(self):
217245
self.addCleanup(cleaunup_test_dir)
218246
setup_test_dir()

0 commit comments

Comments
 (0)