Skip to content

Commit 707137b

Browse files
bpo-40563: Support pathlike objects on dbm/shelve (GH-21849)
Co-authored-by: Hakan Çelik <[email protected]>
1 parent 62fa613 commit 707137b

File tree

14 files changed

+124
-68
lines changed

14 files changed

+124
-68
lines changed

Doc/library/dbm.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ the Oracle Berkeley DB.
3333
file's format can't be guessed; or a string containing the required module
3434
name, such as ``'dbm.ndbm'`` or ``'dbm.gnu'``.
3535

36+
.. versionchanged:: 3.11
37+
Accepts :term:`path-like object` for filename.
3638

3739
.. function:: open(file, flag='r', mode=0o666)
3840

@@ -77,6 +79,9 @@ available, as well as :meth:`get` and :meth:`setdefault`.
7779
Deleting a key from a read-only database raises database module specific error
7880
instead of :exc:`KeyError`.
7981

82+
.. versionchanged:: 3.11
83+
Accepts :term:`path-like object` for file.
84+
8085
Key and values are always stored as bytes. This means that when
8186
strings are used they are implicitly converted to the default encoding before
8287
being stored.
@@ -202,6 +207,9 @@ supported.
202207
In addition to the dictionary-like methods, ``gdbm`` objects have the
203208
following methods:
204209

210+
.. versionchanged:: 3.11
211+
Accepts :term:`path-like object` for filename.
212+
205213
.. method:: gdbm.firstkey()
206214

207215
It's possible to loop over every key in the database using this method and the
@@ -298,6 +306,9 @@ to locate the appropriate header file to simplify building this module.
298306
In addition to the dictionary-like methods, ``ndbm`` objects
299307
provide the following method:
300308

309+
.. versionchanged:: 3.11
310+
Accepts :term:`path-like object` for filename.
311+
301312
.. method:: ndbm.close()
302313

303314
Close the ``ndbm`` database.
@@ -379,6 +390,9 @@ The module defines the following:
379390
flags ``'r'`` and ``'w'`` no longer creates a database if it does not
380391
exist.
381392

393+
.. versionchanged:: 3.11
394+
Accepts :term:`path-like object` for filename.
395+
382396
In addition to the methods provided by the
383397
:class:`collections.abc.MutableMapping` class, :class:`dumbdbm` objects
384398
provide the following methods:

Doc/library/shelve.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ lots of shared sub-objects. The keys are ordinary strings.
4545
:data:`pickle.DEFAULT_PROTOCOL` is now used as the default pickle
4646
protocol.
4747

48+
.. versionchanged:: 3.11
49+
Accepts :term:`path-like object` for filename.
50+
4851
.. note::
4952

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

Lib/dbm/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,18 @@ def whichdb(filename):
109109
"""
110110

111111
# Check for ndbm first -- this has a .pag and a .dir file
112+
filename = os.fsencode(filename)
112113
try:
113-
f = io.open(filename + ".pag", "rb")
114+
f = io.open(filename + b".pag", "rb")
114115
f.close()
115-
f = io.open(filename + ".dir", "rb")
116+
f = io.open(filename + b".dir", "rb")
116117
f.close()
117118
return "dbm.ndbm"
118119
except OSError:
119120
# some dbm emulations based on Berkeley DB generate a .db file
120121
# some do not, but they should be caught by the bsd checks
121122
try:
122-
f = io.open(filename + ".db", "rb")
123+
f = io.open(filename + b".db", "rb")
123124
f.close()
124125
# guarantee we can actually open the file using dbm
125126
# kind of overkill, but since we are dealing with emulations
@@ -134,12 +135,12 @@ def whichdb(filename):
134135
# Check for dumbdbm next -- this has a .dir and a .dat file
135136
try:
136137
# First check for presence of files
137-
os.stat(filename + ".dat")
138-
size = os.stat(filename + ".dir").st_size
138+
os.stat(filename + b".dat")
139+
size = os.stat(filename + b".dir").st_size
139140
# dumbdbm files with no keys are empty
140141
if size == 0:
141142
return "dbm.dumb"
142-
f = io.open(filename + ".dir", "rb")
143+
f = io.open(filename + b".dir", "rb")
143144
try:
144145
if f.read(1) in (b"'", b'"'):
145146
return "dbm.dumb"

Lib/dbm/dumb.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class _Database(collections.abc.MutableMapping):
4646
_io = _io # for _commit()
4747

4848
def __init__(self, filebasename, mode, flag='c'):
49+
filebasename = self._os.fsencode(filebasename)
4950
self._mode = mode
5051
self._readonly = (flag == 'r')
5152

@@ -54,14 +55,14 @@ def __init__(self, filebasename, mode, flag='c'):
5455
# where key is the string key, pos is the offset into the dat
5556
# file of the associated value's first byte, and siz is the number
5657
# of bytes in the associated value.
57-
self._dirfile = filebasename + '.dir'
58+
self._dirfile = filebasename + b'.dir'
5859

5960
# The data file is a binary file pointed into by the directory
6061
# file, and holds the values associated with keys. Each value
6162
# begins at a _BLOCKSIZE-aligned byte offset, and is a raw
6263
# binary 8-bit string value.
63-
self._datfile = filebasename + '.dat'
64-
self._bakfile = filebasename + '.bak'
64+
self._datfile = filebasename + b'.dat'
65+
self._bakfile = filebasename + b'.bak'
6566

6667
# The index is an in-memory dict, mirroring the directory file.
6768
self._index = None # maps keys to (pos, siz) pairs

Lib/test/test_dbm.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import unittest
44
import glob
5+
import os
56
from test.support import import_helper
67
from test.support import os_helper
78

@@ -129,6 +130,15 @@ def test_anydbm_access(self):
129130
assert(f[key] == b"Python:")
130131
f.close()
131132

133+
def test_open_with_bytes(self):
134+
dbm.open(os.fsencode(_fname), "c").close()
135+
136+
def test_open_with_pathlib_path(self):
137+
dbm.open(os_helper.FakePath(_fname), "c").close()
138+
139+
def test_open_with_pathlib_path_bytes(self):
140+
dbm.open(os_helper.FakePath(os.fsencode(_fname)), "c").close()
141+
132142
def read_helper(self, f):
133143
keys = self.keys_helper(f)
134144
for key in self._dict:
@@ -144,34 +154,41 @@ def setUp(self):
144154

145155
class WhichDBTestCase(unittest.TestCase):
146156
def test_whichdb(self):
147-
for module in dbm_iterator():
148-
# Check whether whichdb correctly guesses module name
149-
# for databases opened with "module" module.
150-
# Try with empty files first
151-
name = module.__name__
152-
if name == 'dbm.dumb':
153-
continue # whichdb can't support dbm.dumb
154-
delete_files()
155-
f = module.open(_fname, 'c')
156-
f.close()
157-
self.assertEqual(name, self.dbm.whichdb(_fname))
158-
# Now add a key
159-
f = module.open(_fname, 'w')
160-
f[b"1"] = b"1"
161-
# and test that we can find it
162-
self.assertIn(b"1", f)
163-
# and read it
164-
self.assertEqual(f[b"1"], b"1")
165-
f.close()
166-
self.assertEqual(name, self.dbm.whichdb(_fname))
157+
_bytes_fname = os.fsencode(_fname)
158+
for path in [_fname, os_helper.FakePath(_fname),
159+
_bytes_fname, os_helper.FakePath(_bytes_fname)]:
160+
for module in dbm_iterator():
161+
# Check whether whichdb correctly guesses module name
162+
# for databases opened with "module" module.
163+
# Try with empty files first
164+
name = module.__name__
165+
if name == 'dbm.dumb':
166+
continue # whichdb can't support dbm.dumb
167+
delete_files()
168+
f = module.open(path, 'c')
169+
f.close()
170+
self.assertEqual(name, self.dbm.whichdb(path))
171+
# Now add a key
172+
f = module.open(path, 'w')
173+
f[b"1"] = b"1"
174+
# and test that we can find it
175+
self.assertIn(b"1", f)
176+
# and read it
177+
self.assertEqual(f[b"1"], b"1")
178+
f.close()
179+
self.assertEqual(name, self.dbm.whichdb(path))
167180

168181
@unittest.skipUnless(ndbm, reason='Test requires ndbm')
169182
def test_whichdb_ndbm(self):
170183
# Issue 17198: check that ndbm which is referenced in whichdb is defined
171184
db_file = '{}_ndbm.db'.format(_fname)
172185
with open(db_file, 'w'):
173186
self.addCleanup(os_helper.unlink, db_file)
187+
db_file_bytes = os.fsencode(db_file)
174188
self.assertIsNone(self.dbm.whichdb(db_file[:-3]))
189+
self.assertIsNone(self.dbm.whichdb(os_helper.FakePath(db_file[:-3])))
190+
self.assertIsNone(self.dbm.whichdb(db_file_bytes[:-3]))
191+
self.assertIsNone(self.dbm.whichdb(os_helper.FakePath(db_file_bytes[:-3])))
175192

176193
def tearDown(self):
177194
delete_files()

Lib/test/test_dbm_dumb.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,15 @@ def test_nonascii_filename(self):
294294
self.assertTrue(b'key' in db)
295295
self.assertEqual(db[b'key'], b'value')
296296

297+
def test_open_with_pathlib_path(self):
298+
dumbdbm.open(os_helper.FakePath(_fname), "c").close()
299+
300+
def test_open_with_bytes_path(self):
301+
dumbdbm.open(os.fsencode(_fname), "c").close()
302+
303+
def test_open_with_pathlib_bytes_path(self):
304+
dumbdbm.open(os_helper.FakePath(os.fsencode(_fname)), "c").close()
305+
297306
def tearDown(self):
298307
_delete_files()
299308

Lib/test/test_dbm_gnu.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
gdbm = import_helper.import_module("dbm.gnu") #skip if not supported
44
import unittest
55
import os
6-
from test.support.os_helper import TESTFN, TESTFN_NONASCII, unlink
6+
from test.support.os_helper import TESTFN, TESTFN_NONASCII, unlink, FakePath
77

88

99
filename = TESTFN
@@ -169,6 +169,15 @@ def test_nonexisting_file(self):
169169
self.assertIn(nonexisting_file, str(cm.exception))
170170
self.assertEqual(cm.exception.filename, nonexisting_file)
171171

172+
def test_open_with_pathlib_path(self):
173+
gdbm.open(FakePath(filename), "c").close()
174+
175+
def test_open_with_bytes_path(self):
176+
gdbm.open(os.fsencode(filename), "c").close()
177+
178+
def test_open_with_pathlib_bytes_path(self):
179+
gdbm.open(FakePath(os.fsencode(filename)), "c").close()
180+
172181

173182
if __name__ == '__main__':
174183
unittest.main()

Lib/test/test_dbm_ndbm.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ def test_nonexisting_file(self):
124124
self.assertIn(nonexisting_file, str(cm.exception))
125125
self.assertEqual(cm.exception.filename, nonexisting_file)
126126

127+
def test_open_with_pathlib_path(self):
128+
dbm.ndbm.open(os_helper.FakePath(self.filename), "c").close()
129+
130+
def test_open_with_bytes_path(self):
131+
dbm.ndbm.open(os.fsencode(self.filename), "c").close()
132+
133+
def test_open_with_pathlib_bytes_path(self):
134+
dbm.ndbm.open(os_helper.FakePath(os.fsencode(self.filename)), "c").close()
135+
127136

128137
if __name__ == '__main__':
129138
unittest.main()

Lib/test/test_shelve.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import shelve
33
import glob
44
import pickle
5+
import os
56

67
from test import support
78
from test.support import os_helper
@@ -65,29 +66,32 @@ def test_close(self):
6566
else:
6667
self.fail('Closed shelf should not find a key')
6768

68-
def test_ascii_file_shelf(self):
69-
s = shelve.open(self.fn, protocol=0)
69+
def test_open_template(self, filename=None, protocol=None):
70+
s = shelve.open(filename=filename if filename is not None else self.fn,
71+
protocol=protocol)
7072
try:
7173
s['key1'] = (1,2,3,4)
7274
self.assertEqual(s['key1'], (1,2,3,4))
7375
finally:
7476
s.close()
7577

78+
def test_ascii_file_shelf(self):
79+
self.test_open_template(protocol=0)
80+
7681
def test_binary_file_shelf(self):
77-
s = shelve.open(self.fn, protocol=1)
78-
try:
79-
s['key1'] = (1,2,3,4)
80-
self.assertEqual(s['key1'], (1,2,3,4))
81-
finally:
82-
s.close()
82+
self.test_open_template(protocol=1)
8383

8484
def test_proto2_file_shelf(self):
85-
s = shelve.open(self.fn, protocol=2)
86-
try:
87-
s['key1'] = (1,2,3,4)
88-
self.assertEqual(s['key1'], (1,2,3,4))
89-
finally:
90-
s.close()
85+
self.test_open_template(protocol=2)
86+
87+
def test_pathlib_path_file_shelf(self):
88+
self.test_open_template(filename=os_helper.FakePath(self.fn))
89+
90+
def test_bytes_path_file_shelf(self):
91+
self.test_open_template(filename=os.fsencode(self.fn))
92+
93+
def test_pathlib_bytes_path_file_shelf(self):
94+
self.test_open_template(filename=os_helper.FakePath(os.fsencode(self.fn)))
9195

9296
def test_in_memory_shelf(self):
9397
d1 = byteskeydict()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support pathlike objects on dbm/shelve. Patch by Hakan Çelik and Henry-Joseph Audéoud.

Modules/_dbmmodule.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ static PyType_Spec dbmtype_spec = {
433433
434434
_dbm.open as dbmopen
435435
436-
filename: unicode
436+
filename: object
437437
The filename to open.
438438
439439
flags: str="r"
@@ -452,7 +452,7 @@ Return a database object.
452452
static PyObject *
453453
dbmopen_impl(PyObject *module, PyObject *filename, const char *flags,
454454
int mode)
455-
/*[clinic end generated code: output=9527750f5df90764 input=376a9d903a50df59]*/
455+
/*[clinic end generated code: output=9527750f5df90764 input=d8cf50a9f81218c8]*/
456456
{
457457
int iflags;
458458
_dbm_state *state = get_dbm_state(module);
@@ -479,10 +479,11 @@ dbmopen_impl(PyObject *module, PyObject *filename, const char *flags,
479479
return NULL;
480480
}
481481

482-
PyObject *filenamebytes = PyUnicode_EncodeFSDefault(filename);
483-
if (filenamebytes == NULL) {
482+
PyObject *filenamebytes;
483+
if (!PyUnicode_FSConverter(filename, &filenamebytes)) {
484484
return NULL;
485485
}
486+
486487
const char *name = PyBytes_AS_STRING(filenamebytes);
487488
if (strlen(name) != (size_t)PyBytes_GET_SIZE(filenamebytes)) {
488489
Py_DECREF(filenamebytes);

Modules/_gdbmmodule.c

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ static PyType_Spec gdbmtype_spec = {
590590
/*[clinic input]
591591
_gdbm.open as dbmopen
592592
593-
filename: unicode
593+
filename: object
594594
flags: str="r"
595595
mode: int(py_default="0o666") = 0o666
596596
/
@@ -622,7 +622,7 @@ when the database has to be created. It defaults to octal 0o666.
622622
static PyObject *
623623
dbmopen_impl(PyObject *module, PyObject *filename, const char *flags,
624624
int mode)
625-
/*[clinic end generated code: output=9527750f5df90764 input=812b7d74399ceb0e]*/
625+
/*[clinic end generated code: output=9527750f5df90764 input=bca6ec81dc49292c]*/
626626
{
627627
int iflags;
628628
_gdbm_state *state = get_gdbm_state(module);
@@ -672,10 +672,11 @@ dbmopen_impl(PyObject *module, PyObject *filename, const char *flags,
672672
}
673673
}
674674

675-
PyObject *filenamebytes = PyUnicode_EncodeFSDefault(filename);
676-
if (filenamebytes == NULL) {
675+
PyObject *filenamebytes;
676+
if (!PyUnicode_FSConverter(filename, &filenamebytes)) {
677677
return NULL;
678678
}
679+
679680
const char *name = PyBytes_AS_STRING(filenamebytes);
680681
if (strlen(name) != (size_t)PyBytes_GET_SIZE(filenamebytes)) {
681682
Py_DECREF(filenamebytes);

0 commit comments

Comments
 (0)