diff --git a/CHANGELOG.md b/CHANGELOG.md index f22a3f5..bdbfe10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/api.rst b/docs/api.rst index 399ac90..090f971 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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 diff --git a/docs/index.rst b/docs/index.rst index 568c50f..3207f4c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,13 +10,3 @@ :maxdepth: 4 api - ---------------------------- - -Contents --------- - -.. toctree:: - :maxdepth: 2 - - api diff --git a/src/aiodbm/__init__.py b/src/aiodbm/__init__.py index 02206bd..5a5fcd9 100644 --- a/src/aiodbm/__init__.py +++ b/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"] diff --git a/src/aiodbm/core.py b/src/aiodbm/core.py index e8dbd97..6cb9be1 100644 --- a/src/aiodbm/core.py +++ b/src/aiodbm/core.py @@ -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__() @@ -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. @@ -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(): diff --git a/tests/test_core.py b/tests/test_core.py index 8434b03..8ff2c16 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,4 +1,3 @@ -import dbm import shutil import sys import tempfile @@ -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): @@ -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 @@ -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"})