Skip to content

Commit 450fb66

Browse files
authored
Merge pull request #33 from nkinkade/file-descriptor
Adds ability to pass a file descriptor when opening a new Reader() object
2 parents 893403f + 05c95bd commit 450fb66

File tree

5 files changed

+57
-16
lines changed

5 files changed

+57
-16
lines changed

README.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,22 @@ provide `free GeoLite2 databases
4040
<http://dev.maxmind.com/geoip/geoip2/geolite2>`_. These files must be
4141
decompressed with ``gunzip``.
4242

43-
After you have obtained a database and importing the module, call
44-
``open_database`` with a path to the database as the first argument.
45-
Optionally, you may pass a mode as the second argument. The modes are
46-
exported from ``maxminddb``. Valid modes are:
43+
After you have obtained a database and imported the module, call
44+
``open_database`` with a path, or file descriptor (in the case of MODE_FD),
45+
to the database as the first argument. Optionally, you may pass a mode as the
46+
second argument. The modes are exported from ``maxminddb``. Valid modes are:
4747

4848
* MODE_MMAP_EXT - use the C extension with memory map.
4949
* MODE_MMAP - read from memory map. Pure Python.
5050
* MODE_FILE - read database as standard file. Pure Python.
5151
* MODE_MEMORY - load database into memory. Pure Python.
52+
* MODE_FD - load database into memory from a file descriptor. Pure Python.
5253
* MODE_AUTO - try MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that order. Default.
5354

55+
**NOTE**: When using ``MODE_FD``, it is the *caller's* responsibility to be
56+
sure that the file descriptor gets closed properly, even though this module
57+
*may* close it after the ``Reader`` object is created.
58+
5459
The ``open_database`` function returns a ``Reader`` object. To look up an IP
5560
address, use the ``get`` method on this object. The method will return the
5661
corresponding values for the IP address from the database (e.g., a dictionary

maxminddb/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@
99
maxminddb.extension = None
1010

1111
from maxminddb.const import (MODE_AUTO, MODE_MMAP, MODE_MMAP_EXT, MODE_FILE,
12-
MODE_MEMORY)
12+
MODE_MEMORY, MODE_FD)
1313
from maxminddb.decoder import InvalidDatabaseError
1414

1515

1616
def open_database(database, mode=MODE_AUTO):
1717
"""Open a Maxmind DB database
1818
1919
Arguments:
20-
database -- A path to a valid MaxMind DB file such as a GeoIP2
21-
database file.
20+
database -- A path to a valid MaxMind DB file such as a GeoIP2 database
21+
file, or a file descriptor in the case of MODE_FD.
2222
mode -- mode to open the database with. Valid mode are:
2323
* MODE_MMAP_EXT - use the C extension with memory map.
2424
* MODE_MMAP - read from memory map. Pure Python.
2525
* MODE_FILE - read database as standard file. Pure Python.
2626
* MODE_MEMORY - load database into memory. Pure Python.
27+
* MODE_FD - the param passed via database is a file descriptor, not
28+
a path. This mode implies MODE_MEMORY.
2729
* MODE_AUTO - tries MODE_MMAP_EXT, MODE_MMAP, MODE_FILE in that
2830
order. Default mode.
2931
"""
@@ -35,7 +37,7 @@ def open_database(database, mode=MODE_AUTO):
3537
"MODE_MMAP_EXT requires the maxminddb.extension module to be available"
3638
)
3739
return maxminddb.extension.Reader(database)
38-
elif mode in (MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY):
40+
elif mode in (MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY, MODE_FD):
3941
return maxminddb.reader.Reader(database, mode)
4042
raise ValueError('Unsupported open mode: {0}'.format(mode))
4143

maxminddb/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
MODE_MMAP = 2
66
MODE_FILE = 4
77
MODE_MEMORY = 8
8+
MODE_FD = 16

maxminddb/reader.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from maxminddb.compat import (byte_from_int, compat_ip_address, string_type,
1919
string_type_name)
20-
from maxminddb.const import MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY
20+
from maxminddb.const import MODE_AUTO, MODE_MMAP, MODE_FILE, MODE_MEMORY, MODE_FD
2121
from maxminddb.decoder import Decoder
2222
from maxminddb.errors import InvalidDatabaseError
2323
from maxminddb.file import FileBuffer
@@ -38,30 +38,39 @@ def __init__(self, database, mode=MODE_AUTO):
3838
"""Reader for the MaxMind DB file format
3939
4040
Arguments:
41-
database -- A path to a valid MaxMind DB file such as a GeoIP2
42-
database file.
41+
database -- A path to a valid MaxMind DB file such as a GeoIP2 database
42+
file, or a file descriptor in the case of MODE_FD.
4343
mode -- mode to open the database with. Valid mode are:
4444
* MODE_MMAP - read from memory map.
4545
* MODE_FILE - read database as standard file.
4646
* MODE_MEMORY - load database into memory.
4747
* MODE_AUTO - tries MODE_MMAP and then MODE_FILE. Default.
48+
* MODE_FD - the param passed via database is a file descriptor, not
49+
a path. This mode implies MODE_MEMORY.
4850
"""
4951
if (mode == MODE_AUTO and mmap) or mode == MODE_MMAP:
5052
with open(database, 'rb') as db_file:
5153
self._buffer = mmap.mmap(
5254
db_file.fileno(), 0, access=mmap.ACCESS_READ)
5355
self._buffer_size = self._buffer.size()
56+
filename = database
5457
elif mode in (MODE_AUTO, MODE_FILE):
5558
self._buffer = FileBuffer(database)
5659
self._buffer_size = self._buffer.size()
60+
filename = database
5761
elif mode == MODE_MEMORY:
5862
with open(database, 'rb') as db_file:
5963
self._buffer = db_file.read()
6064
self._buffer_size = len(self._buffer)
65+
filename = database
66+
elif mode == MODE_FD:
67+
self._buffer = database.read()
68+
self._buffer_size = len(self._buffer)
69+
filename = database.name
6170
else:
6271
raise ValueError(
63-
'Unsupported open mode ({0}). Only MODE_AUTO, '
64-
' MODE_FILE, and MODE_MEMORY are support by the pure Python '
72+
'Unsupported open mode ({0}). Only MODE_AUTO, MODE_FILE, '
73+
'MODE_MEMORY and MODE_FD are supported by the pure Python '
6574
'Reader'.format(mode))
6675

6776
metadata_start = self._buffer.rfind(
@@ -72,7 +81,7 @@ def __init__(self, database, mode=MODE_AUTO):
7281
self.close()
7382
raise InvalidDatabaseError('Error opening database file ({0}). '
7483
'Is this a valid MaxMind DB file?'
75-
''.format(database))
84+
''.format(filename))
7685

7786
metadata_start += len(self._METADATA_START_MARKER)
7887
metadata_decoder = Decoder(self._buffer, metadata_start)

tests/reader_test.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import unicode_literals
55

66
import logging
7+
import mock
78
import os
89
import sys
910
import threading
@@ -22,7 +23,7 @@
2223
from maxminddb import open_database, InvalidDatabaseError
2324
from maxminddb.compat import FileNotFoundError
2425
from maxminddb.const import (MODE_AUTO, MODE_MMAP_EXT, MODE_MMAP, MODE_FILE,
25-
MODE_MEMORY)
26+
MODE_MEMORY, MODE_FD)
2627

2728
if sys.version_info[:2] == (2, 6):
2829
import unittest2 as unittest
@@ -34,6 +35,18 @@
3435
unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches
3536

3637

38+
def get_reader_from_file_descriptor(filepath, mode):
39+
"""Patches open_database() for class TestFDReader()."""
40+
if mode == MODE_FD:
41+
with open(filepath, 'rb') as mmdb_fh:
42+
return maxminddb.open_database(mmdb_fh, mode)
43+
else:
44+
# There are a few cases where mode is statically defined in
45+
# BaseTestReader(). In those cases just call an unpatched
46+
# open_database() with a string path.
47+
return maxminddb.open_database(filepath, mode)
48+
49+
3750
class BaseTestReader(object):
3851
def test_reader(self):
3952
for record_size in [24, 28, 32]:
@@ -205,7 +218,7 @@ def test_double_close(self):
205218
'Double close does not throw an exception')
206219

207220
def test_closed_get(self):
208-
if self.mode == MODE_MEMORY:
221+
if self.mode in [MODE_MEMORY, MODE_FD]:
209222
return
210223
reader = open_database(
211224
'tests/data/test-data/MaxMind-DB-test-decoder.mmdb', self.mode)
@@ -425,6 +438,17 @@ class TestMemoryReader(BaseTestReader, unittest.TestCase):
425438
readerClass = [maxminddb.reader.Reader]
426439

427440

441+
class TestFDReader(BaseTestReader, unittest.TestCase):
442+
def setUp(self):
443+
self.open_database_patcher = mock.patch('reader_test.open_database')
444+
self.addCleanup(self.open_database_patcher.stop)
445+
self.open_database = self.open_database_patcher.start()
446+
self.open_database.side_effect = get_reader_from_file_descriptor
447+
448+
mode = MODE_FD
449+
readerClass = [maxminddb.reader.Reader]
450+
451+
428452
class TestOldReader(unittest.TestCase):
429453
def test_old_reader(self):
430454
reader = maxminddb.Reader(

0 commit comments

Comments
 (0)