Skip to content

Commit

Permalink
Merge pull request #85 from maxmind/greg/path-lib
Browse files Browse the repository at this point in the history
Support os.PathLike object in the C extension
  • Loading branch information
horgh authored Sep 17, 2021
2 parents c37eed5 + c1e1241 commit c631155
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 9 deletions.
12 changes: 12 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
History
-------

2.1.0
++++++++++++++++++

* The C extension now correctly supports objects that implement the
``os.PathLike`` interface.
* When opening a database fails due to an access issue, the correct
``OSError`` subclass will now be thrown.
* The ``Metadata`` class object is now available from the C extension
module as ``maxminddb.extension.Metadata`` rather than
``maxminddb.extension.extension``.
* Type stubs have been added for ``maxminddb.extension``.

2.0.3 (2020-10-16)
++++++++++++++++++

Expand Down
28 changes: 21 additions & 7 deletions extension/maxminddb.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,27 @@ static int ip_converter(PyObject *obj, struct sockaddr_storage *ip_address);
#endif

static int Reader_init(PyObject *self, PyObject *args, PyObject *kwds) {
char *filename;
PyObject *filepath = NULL;
int mode = 0;

static char *kwlist[] = {"database", "mode", NULL};
if (!PyArg_ParseTupleAndKeywords(
args, kwds, "s|i", kwlist, &filename, &mode)) {
if (!PyArg_ParseTupleAndKeywords(args,
kwds,
"O&|i",
kwlist,
PyUnicode_FSConverter,
&filepath,
&mode)) {
return -1;
}

char *filename = PyBytes_AS_STRING(filepath);
if (filename == NULL) {
return -1;
}

if (mode != 0 && mode != 1) {
Py_XDECREF(filepath);
PyErr_Format(
PyExc_ValueError,
"Unsupported open mode (%i). Only "
Expand All @@ -67,26 +78,29 @@ static int Reader_init(PyObject *self, PyObject *args, PyObject *kwds) {
}

if (0 != access(filename, R_OK)) {
PyErr_Format(PyExc_FileNotFoundError,
"No such file or directory: '%s'",
filename);

PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, filepath);
Py_XDECREF(filepath);
return -1;
}

MMDB_s *mmdb = (MMDB_s *)malloc(sizeof(MMDB_s));
if (NULL == mmdb) {
Py_XDECREF(filepath);
PyErr_NoMemory();
return -1;
}

Reader_obj *mmdb_obj = (Reader_obj *)self;
if (!mmdb_obj) {
Py_XDECREF(filepath);
free(mmdb);
PyErr_NoMemory();
return -1;
}

uint16_t status = MMDB_open(filename, MMDB_MODE_MMAP, mmdb);
Py_XDECREF(filepath);

if (MMDB_SUCCESS != status) {
free(mmdb);
Expand Down Expand Up @@ -723,7 +737,7 @@ PyMODINIT_FUNC PyInit_extension(void) {
if (PyType_Ready(&Metadata_Type)) {
return NULL;
}
PyModule_AddObject(m, "extension", (PyObject *)&Metadata_Type);
PyModule_AddObject(m, "Metadata", (PyObject *)&Metadata_Type);

PyObject *error_mod = PyImport_ImportModule("maxminddb.errors");
if (error_mod == NULL) {
Expand Down
44 changes: 44 additions & 0 deletions maxminddb/extension.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from ipaddress import IPv4Address, IPv6Address
from os import PathLike
from typing import Any, AnyStr, IO, Mapping, Optional, Sequence, Text, Tuple, Union

from maxminddb import MODE_AUTO
from maxminddb.errors import InvalidDatabaseError as InvalidDatabaseError
from maxminddb.types import Record

class Reader:
closed: bool = ...
def __init__(
self, database: Union[AnyStr, int, PathLike, IO], mode: int = MODE_AUTO
) -> None: ...
def close(self) -> None: ...
def get(
self, ip_address: Union[str, IPv6Address, IPv4Address]
) -> Optional[Record]: ...
def get_with_prefix_len(
self, ip_address: Union[str, IPv6Address, IPv4Address]
) -> Tuple[Optional[Record], int]: ...
def metadata(self) -> "Metadata": ...
def __enter__(self) -> "Reader": ...
def __exit__(self, *args) -> None: ...

class Metadata:
@property
def node_count(self) -> int: ...
@property
def record_size(self) -> int: ...
@property
def ip_version(self) -> int: ...
@property
def database_type(self) -> Text: ...
@property
def languages(self) -> Sequence[Text]: ...
@property
def binary_format_major_version(self) -> int: ...
@property
def binary_format_minor_version(self) -> int: ...
@property
def build_epoch(self) -> int: ...
@property
def description(self) -> Mapping[Text, Text]: ...
def __init__(self, **kwargs: Any) -> None: ...
2 changes: 1 addition & 1 deletion maxminddb/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,5 +331,5 @@ def search_tree_size(self) -> int:
return self.node_count * self.node_byte_size

def __repr__(self):
args = ", ".join("%s=%r" % x for x in self.__dict__.items())
args = ", ".join(f"{k}={v}" for k, v in self.__dict__.items())
return f"{self.__module__}.{self.__class__.__name__}({args})"
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def run_setup(with_cext):
long_description=README,
url="http://www.maxmind.com/",
packages=find_packages("."),
package_data={"": ["LICENSE"], "maxminddb": ["py.typed"]},
package_data={"": ["LICENSE"], "maxminddb": ["extension.pyi", "py.typed"]},
package_dir={"maxminddb": "maxminddb"},
python_requires=">=3.6",
include_package_data=True,
Expand Down
7 changes: 7 additions & 0 deletions tests/reader_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import ipaddress
import os
import pathlib
import threading
import unittest
import unittest.mock as mock
Expand Down Expand Up @@ -242,6 +243,12 @@ def test_ipv6_address_in_ipv4_database(self):
reader.get(self.ipf("2001::"))
reader.close()

def test_opening_path(self):
with open_database(
pathlib.Path("tests/data/test-data/MaxMind-DB-test-decoder.mmdb"), self.mode
) as reader:
self.assertEqual(reader.metadata().database_type, "MaxMind DB Decoder Test")

def test_no_extension_exception(self):
real_extension = maxminddb.extension
maxminddb.extension = None
Expand Down

0 comments on commit c631155

Please sign in to comment.