Skip to content

Commit

Permalink
Annotate database code
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Aug 27, 2020
1 parent f31c4d7 commit 7b837d5
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 48 deletions.
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This patch adds type annotations to the :doc:`hypothesis.database <database>`
module. There is no runtime change, but your typechecker might notice.
17 changes: 17 additions & 0 deletions hypothesis-python/docs/database.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,20 @@ Like everything under ``.hypothesis/``, the examples directory will be
transparently created on demand. Unlike the other subdirectories,
``examples/`` is designed to handle merges, deletes, etc if you just add the
directory into git, mercurial, or any similar version control system.


---------------------------------
Defining your own ExampleDatabase
---------------------------------

You can define your :class:`~hypothesis.database.ExampleDatabase`, for example
to use a shared datastore, with just a few methods:

.. autoclass:: hypothesis.database.ExampleDatabase
:members:

Two concrete :class:`~hypothesis.database.ExampleDatabase` classes ship with
Hypothesis:

.. autoclass:: hypothesis.database.DirectoryBasedExampleDatabase
.. autoclass:: hypothesis.database.InMemoryExampleDatabase
93 changes: 53 additions & 40 deletions hypothesis-python/src/hypothesis/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,23 @@
#
# END HEADER

import abc
import binascii
import os
import warnings
from hashlib import sha384
from typing import Iterable

from hypothesis.configuration import mkdir_p, storage_directory
from hypothesis.errors import HypothesisException, HypothesisWarning
from hypothesis.utils.conventions import not_set

__all__ = [
"DirectoryBasedExampleDatabase",
"ExampleDatabase",
"InMemoryExampleDatabase",
]


def _usable_dir(path):
"""
Expand Down Expand Up @@ -59,94 +67,99 @@ def _db_for_path(path=None):
return DirectoryBasedExampleDatabase(str(path))


class EDMeta(type):
class _EDMeta(abc.ABCMeta):
def __call__(self, *args, **kwargs):
if self is ExampleDatabase:
return _db_for_path(*args, **kwargs)
return super().__call__(*args, **kwargs)


class ExampleDatabase(metaclass=EDMeta):
"""Interface class for storage systems.
A key -> multiple distinct values mapping.
class ExampleDatabase(metaclass=_EDMeta):
"""An abstract base class for storing examples in Hypothesis' internal format.
Keys and values are binary data.
An ExampleDatabase maps each ``bytes`` key to many distinct ``bytes``
values, like a ``Mapping[bytes, AbstractSet[bytes]]``.
"""

def save(self, key, value):
@abc.abstractmethod
def save(self, key: bytes, value: bytes) -> None:
"""Save ``value`` under ``key``.
If this value is already present for this key, silently do
nothing
If this value is already present for this key, silently do nothing.
"""
raise NotImplementedError("%s.save" % (type(self).__name__))

def delete(self, key, value):
@abc.abstractmethod
def fetch(self, key: bytes) -> Iterable[bytes]:
"""Return an iterable over all values matching this key."""
raise NotImplementedError("%s.fetch" % (type(self).__name__))

@abc.abstractmethod
def delete(self, key: bytes, value: bytes) -> None:
"""Remove this value from this key.
If this value is not present, silently do nothing.
"""
raise NotImplementedError("%s.delete" % (type(self).__name__))

def move(self, src, dest, value):
"""Move value from key src to key dest. Equivalent to delete(src,
value) followed by save(src, value) but may have a more efficient
implementation.
def move(self, src: bytes, dest: bytes, value: bytes) -> None:
"""Move ``value`` from key ``src`` to key ``dest``. Equivalent to
``delete(src, value)`` followed by ``save(src, value)``, but may
have a more efficient implementation.
Note that value will be inserted at dest regardless of whether
it is currently present at src.
Note that ``value`` will be inserted at ``dest`` regardless of whether
it is currently present at ``src``.
"""
if src == dest:
self.save(src, value)
return
self.delete(src, value)
self.save(dest, value)

def fetch(self, key):
"""Return all values matching this key."""
raise NotImplementedError("%s.fetch" % (type(self).__name__))

def close(self):
"""Clear up any resources associated with this database."""
raise NotImplementedError("%s.close" % (type(self).__name__))
class InMemoryExampleDatabase(ExampleDatabase):
"""A non-persistent example database, implemented in terms of a dict of sets.
This can be useful if you call a test function several times in a single
session, or for testing other database implementations, but because it
does not persist between runs we do not recommend it for general use.
"""

class InMemoryExampleDatabase(ExampleDatabase):
def __init__(self):
self.data = {}

def __repr__(self):
def __repr__(self) -> str:
return "InMemoryExampleDatabase(%r)" % (self.data,)

def fetch(self, key):
def fetch(self, key: bytes) -> Iterable[bytes]:
yield from self.data.get(key, ())

def save(self, key, value):
def save(self, key: bytes, value: bytes) -> None:
self.data.setdefault(key, set()).add(bytes(value))

def delete(self, key, value):
def delete(self, key: bytes, value: bytes) -> None:
self.data.get(key, set()).discard(bytes(value))

def close(self):
pass


def _hash(key):
return sha384(key).hexdigest()[:16]


class DirectoryBasedExampleDatabase(ExampleDatabase):
def __init__(self, path):
"""Use a directory to store Hypothesis examples as files.
This is the default database for Hypothesis; see above for details.
.. i.e. see the documentation in database.rst
"""

def __init__(self, path: str) -> None:
self.path = path
self.keypaths = {}
self.keypaths = {} # type: dict

def __repr__(self):
def __repr__(self) -> str:
return "DirectoryBasedExampleDatabase(%r)" % (self.path,)

def close(self):
pass

def _key_path(self, key):
try:
return self.keypaths[key]
Expand All @@ -159,7 +172,7 @@ def _key_path(self, key):
def _value_path(self, key, value):
return os.path.join(self._key_path(key), _hash(value))

def fetch(self, key):
def fetch(self, key: bytes) -> Iterable[bytes]:
kp = self._key_path(key)
if not os.path.exists(kp):
return
Expand All @@ -170,7 +183,7 @@ def fetch(self, key):
except OSError:
pass

def save(self, key, value):
def save(self, key: bytes, value: bytes) -> None:
# Note: we attempt to create the dir in question now. We
# already checked for permissions, but there can still be other issues,
# e.g. the disk is full
Expand All @@ -187,7 +200,7 @@ def save(self, key, value):
os.unlink(tmpname)
assert not os.path.exists(tmpname)

def move(self, src, dest, value):
def move(self, src: bytes, dest: bytes, value: bytes) -> None:
if src == dest:
self.save(src, value)
return
Expand All @@ -197,7 +210,7 @@ def move(self, src, dest, value):
self.delete(src, value)
self.save(dest, value)

def delete(self, key, value):
def delete(self, key: bytes, value: bytes) -> None:
try:
os.unlink(self._value_path(key, value))
except OSError:
Expand Down
5 changes: 0 additions & 5 deletions hypothesis-python/tests/cover/test_database_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,12 @@ def test_saving_a_key_twice_fetches_it_once(exampledatabase):
assert list(exampledatabase.fetch(b"foo")) == [b"bar"]


def test_can_close_a_database_without_touching_it(exampledatabase):
exampledatabase.close()


def test_can_close_a_database_after_saving(exampledatabase):
exampledatabase.save(b"foo", b"bar")


def test_class_name_is_in_repr(exampledatabase):
assert type(exampledatabase).__name__ in repr(exampledatabase)
exampledatabase.close()


def test_an_absent_value_is_present_after_it_moves(exampledatabase):
Expand Down
2 changes: 0 additions & 2 deletions hypothesis-python/tests/nocover/test_database_agreement.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ def values_agree(self, k):
last_db = db

def teardown(self):
for d in self.dbs:
d.close()
shutil.rmtree(self.tempd)


Expand Down
1 change: 0 additions & 1 deletion hypothesis-python/tests/nocover/test_strategy_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ def teardown(self):
@rule()
def clear_database(self):
if self.database is not None:
self.database.close()
self.database = None

@rule()
Expand Down

0 comments on commit 7b837d5

Please sign in to comment.