Skip to content

Commit

Permalink
Merge pull request #3 from ErikKalkoken/improvements
Browse files Browse the repository at this point in the history
Add keys_iterator() and improve docs
  • Loading branch information
ErikKalkoken committed Jun 6, 2023
2 parents a11b987 + b25fd7a commit 54ab600
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 25 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Expand Up @@ -13,6 +13,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

## [0.4.0] - 2023-06-06

### Added

- `keys_iterator()` return an async iterator to loop over all keys.

### Changes

- Improved docs

## [0.3.0] - 2023-06-05

### Changed
Expand Down
23 changes: 22 additions & 1 deletion docs/api.rst
Expand Up @@ -4,4 +4,25 @@
API Reference
===============

.. automodule:: aiodbm
.. autodata:: error

A tuple containing the exceptions that can be raised by each
of the supported modules,
with a unique exception also named dbm.error as the first item —
the latter is used when dbm.error is raised.

Example usage:

.. code-block:: Python
try:
async with open("example.dbm", "c") as db:
...
except aiodbm.error as ex:
print(f"Error when trying to open the database: {ex}")
.. autofunction:: aiodbm.open

.. autofunction:: aiodbm.whichdb

.. autoclass:: aiodbm.Database
10 changes: 0 additions & 10 deletions docs/index.rst
Expand Up @@ -10,13 +10,3 @@
:maxdepth: 4

api

---------------------------

Contents
--------

.. toctree::
:maxdepth: 2

api
6 changes: 3 additions & 3 deletions src/aiodbm/__init__.py
@@ -1,7 +1,7 @@
"""An AsyncIO bridge for DBM."""

__version__ = "0.3.0"
__version__ = "0.4.0"

from .core import Database, open, whichdb
from .core import Database, error, open, whichdb

__all__ = ["open", "Database", "whichdb"]
__all__ = ["open", "Database", "whichdb", "error"]
54 changes: 46 additions & 8 deletions src/aiodbm/core.py
Expand Up @@ -3,17 +3,25 @@
import asyncio
import dbm
import logging
from dbm import error
from functools import partial
from pathlib import Path
from typing import Any, Callable, Generator, List, Optional, Union
from typing import Any, AsyncGenerator, Callable, Generator, List, Optional, Union

from aiodbm.threads import ThreadRunner

logger = logging.getLogger("aiodbm")


__all__ = ["Database", "error", "open", "whichdb"]


class Database:
"""A proxy for a DBM database."""
"""A DBM database.
Not that some methods are available on GDBM only.
You can check if your database is GDBM with :func:`is_gdbm`.
"""

def __init__(self, connector: Callable) -> None:
super().__init__()
Expand Down Expand Up @@ -143,6 +151,28 @@ async def firstkey(self) -> bytes:

return await self._execute(self._db_strict.firstkey)

async def keys_iterator(self) -> AsyncGenerator[bytes, None]:
"""Return all keys as async generator. GDBM only.
In contrast to :func:`keys` this method will not load the full list of keys into
memory, but instead fetch keys one after the other.
Note that the order of keys is implementation specific
and can not be relied on.
Usage example:
.. code-block:: Python
async for key in db.keys_iterator():
print(key)
"""
key = await self.firstkey()
while key is not None:
yield key
key = await self.nextkey(key)

async def nextkey(self, key: Union[str, bytes]) -> Optional[bytes]:
"""Return the next key, when looping over all keys.
Or return None, when the end of the loop has been reached.
Expand All @@ -166,20 +196,28 @@ async def sync(self) -> None:


def open(file: Union[str, Path], *args, **kwargs) -> Database:
"""Create and return a proxy to the DBM database.
"""Create and return a proxy to a DBM database.
Example:
Args:
file: filename for the DBM database
Returns:
DBM database proxy object
Usage A:
.. code-block:: Python
async with open("example.dbm", "c") as db:
...
Args:
file: filename for the DBM database
Usage B:
Returns:
DBM database proxy object
.. code-block:: Python
db = async open("example.dbm", "c"):
...
await db.close()
"""

def connector():
Expand Down
28 changes: 25 additions & 3 deletions tests/test_core.py
@@ -1,4 +1,3 @@
import dbm
import shutil
import sys
import tempfile
Expand Down Expand Up @@ -175,7 +174,7 @@ async def test_can_open_without_context_manager(self):
await db.close()

async def test_open_raises_exception_when_opening_fails(self):
with self.assertRaises(dbm.error):
with self.assertRaises(aiodbm.error):
await aiodbm.open(self.data_path, "r")

async def test_can_call_close_multiple_times(self):
Expand All @@ -191,9 +190,19 @@ async def test_should_raise_error_when_trying_to_connect_again(self):
with self.assertRaises(RuntimeError):
await db._connect()

async def test_should_raise_error_when_trying_to_access_closed_database(
self,
):
async with aiodbm.open(self.data_path, "c") as db:
# given
await db.close()
# when/then
with self.assertRaises(ValueError):
await db.get("alpha")


@unittest.skipIf(python_version in ["38", "39"], reason="Unsupported Python")
class TestGdbmFunctions(DbmAsyncioTestCase):
class TestGdbmFeatures(DbmAsyncioTestCase):
async def test_firstkey_should_return_first_key(self):
async with aiodbm.open(self.data_path, "c") as db:
# given
Expand Down Expand Up @@ -238,3 +247,16 @@ async def test_can_detect_gdbm(self):
# when/then
async with aiodbm.open(self.data_path, "c") as db:
self.assertTrue(db.is_gdbm)

async def test_can_iter_over_all_keys(self):
async with aiodbm.open(self.data_path, "c") as db:
# given
await db.set("alpha", "green")
await db.set("bravo", "green")
await db.set("charlie", "green")
# when
keys = set()
async for key in db.keys_iterator():
keys.add(key)
# then
self.assertSetEqual(keys, {b"alpha", b"bravo", b"charlie"})

0 comments on commit 54ab600

Please sign in to comment.