Skip to content

Commit f7be4ae

Browse files
Joakim Uddholmoschwald
authored andcommitted
Adds support for with-statement (context manager) for the reader object.
Closes #21
1 parent c6578d4 commit f7be4ae

File tree

4 files changed

+74
-0
lines changed

4 files changed

+74
-0
lines changed

HISTORY.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
History
44
-------
55

6+
1.3.0
7+
+++++++++++++++++
8+
9+
* ``maxminddb.Reader`` and the C extension now support being used in a context
10+
manager. Pull request by Joakim Uddholm. GitHub #21 & #28.
611
* Provide a more useful error message when ``MODE_MMAP_EXT`` is requested but
712
the C extension is not available.
813

maxminddb/extension/maxminddb.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ static PyObject *MaxMindDB_error;
1212
typedef struct {
1313
PyObject_HEAD /* no semicolon */
1414
MMDB_s *mmdb;
15+
PyObject *closed;
1516
} Reader_obj;
1617

1718
typedef struct {
@@ -99,6 +100,7 @@ static int Reader_init(PyObject *self, PyObject *args, PyObject *kwds)
99100
}
100101

101102
mmdb_obj->mmdb = mmdb;
103+
mmdb_obj->closed = Py_False;
102104
return 0;
103105
}
104106

@@ -209,6 +211,28 @@ static PyObject *Reader_close(PyObject *self, PyObject *UNUSED(args))
209211
mmdb_obj->mmdb = NULL;
210212
}
211213

214+
mmdb_obj->closed = Py_True;
215+
216+
Py_RETURN_NONE;
217+
}
218+
219+
static PyObject *Reader__enter__(PyObject *self, PyObject *UNUSED(args))
220+
{
221+
Reader_obj *mmdb_obj = (Reader_obj *)self;
222+
223+
if(mmdb_obj->closed == Py_True) {
224+
PyErr_SetString(PyExc_ValueError,
225+
"Attempt to reopen a closed MaxMind DB.");
226+
return NULL;
227+
}
228+
229+
Py_INCREF(self);
230+
return (PyObject *)self;
231+
}
232+
233+
static PyObject *Reader__exit__(PyObject *self, PyObject *UNUSED(args))
234+
{
235+
Reader_close(self, NULL);
212236
Py_RETURN_NONE;
213237
}
214238

@@ -455,16 +479,24 @@ static PyMethodDef Reader_methods[] = {
455479
{ "metadata", Reader_metadata, METH_NOARGS,
456480
"Returns metadata object for database" },
457481
{ "close", Reader_close, METH_NOARGS, "Closes database"},
482+
{ "__exit__", Reader__exit__, METH_VARARGS, "Called when exiting a with-context. Calls close"},
483+
{ "__enter__", Reader__enter__, METH_NOARGS, "Called when entering a with-context."},
458484
{ NULL, NULL, 0, NULL }
459485
};
460486

487+
static PyMemberDef Reader_members[] = {
488+
{ "closed", T_OBJECT, offsetof(Reader_obj, closed), READONLY, NULL },
489+
{ NULL, 0, 0, 0, NULL }
490+
};
491+
461492
static PyTypeObject Reader_Type = {
462493
PyVarObject_HEAD_INIT(NULL, 0)
463494
.tp_basicsize = sizeof(Reader_obj),
464495
.tp_dealloc = Reader_dealloc,
465496
.tp_doc = "Reader object",
466497
.tp_flags = Py_TPFLAGS_DEFAULT,
467498
.tp_methods = Reader_methods,
499+
.tp_members = Reader_members,
468500
.tp_name = "Reader",
469501
.tp_init = Reader_init,
470502
};

maxminddb/reader.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def __init__(self, database, mode=MODE_AUTO):
8181

8282
self._decoder = Decoder(self._buffer, self._metadata.search_tree_size +
8383
self._DATA_SECTION_SEPARATOR_SIZE)
84+
self.closed = False
8485

8586
def metadata(self):
8687
"""Return the metadata associated with the MaxMind DB file"""
@@ -181,6 +182,15 @@ def close(self):
181182
# pylint: disable=unidiomatic-typecheck
182183
if type(self._buffer) not in (str, bytes):
183184
self._buffer.close()
185+
self.closed = True
186+
187+
def __exit__(self, *args):
188+
self.close()
189+
190+
def __enter__(self):
191+
if self.closed:
192+
raise ValueError('Attempt to reopen a closed MaxMind DB')
193+
return self
184194

185195

186196
class Metadata(object):

tests/reader_test.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,33 @@ def test_closed_get(self):
201201
'Attempt to read from a closed MaxMind DB.'
202202
'|closed', reader.get, ('1.1.1.1'))
203203

204+
def test_with_statement(self):
205+
filename = 'tests/data/test-data/MaxMind-DB-test-ipv4-24.mmdb'
206+
with open_database(filename, self.mode) as reader:
207+
self._check_ip_v4(reader, filename)
208+
self.assertEqual(reader.closed, True)
209+
210+
def test_with_statement_close(self):
211+
filename = 'tests/data/test-data/MaxMind-DB-test-ipv4-24.mmdb'
212+
reader = open_database(filename, self.mode)
213+
reader.close()
214+
215+
def use_with(reader):
216+
with reader:
217+
pass
218+
219+
self.assertRaisesRegex(ValueError, 'Attempt to reopen a closed MaxMind DB',
220+
use_with, reader)
221+
222+
223+
def test_closed(self):
224+
reader = open_database(
225+
'tests/data/test-data/MaxMind-DB-test-decoder.mmdb', self.mode)
226+
self.assertEqual(reader.closed, False)
227+
reader.close()
228+
self.assertEqual(reader.closed, True)
229+
230+
204231
# XXX - Figure out whether we want to have the same behavior on both the
205232
# extension and the pure Python reader. If we do, the pure Python
206233
# reader will need to throw an exception or the extension will need

0 commit comments

Comments
 (0)