From 7b837d54edc4e0c71396c5612724c8654f9ab0d7 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Thu, 27 Aug 2020 22:05:30 +1000 Subject: [PATCH] Annotate database code --- hypothesis-python/RELEASE.rst | 4 + hypothesis-python/docs/database.rst | 17 ++++ hypothesis-python/src/hypothesis/database.py | 93 +++++++++++-------- .../tests/cover/test_database_backend.py | 5 - .../tests/nocover/test_database_agreement.py | 2 - .../tests/nocover/test_strategy_state.py | 1 - 6 files changed, 74 insertions(+), 48 deletions(-) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..180c32bf17 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,4 @@ +RELEASE_TYPE: patch + +This patch adds type annotations to the :doc:`hypothesis.database ` +module. There is no runtime change, but your typechecker might notice. diff --git a/hypothesis-python/docs/database.rst b/hypothesis-python/docs/database.rst index 1c7e6049e7..73ec300fe4 100644 --- a/hypothesis-python/docs/database.rst +++ b/hypothesis-python/docs/database.rst @@ -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 diff --git a/hypothesis-python/src/hypothesis/database.py b/hypothesis-python/src/hypothesis/database.py index fa08fcacd6..83ec7c022e 100644 --- a/hypothesis-python/src/hypothesis/database.py +++ b/hypothesis-python/src/hypothesis/database.py @@ -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): """ @@ -59,43 +67,48 @@ 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) @@ -103,50 +116,50 @@ def move(self, src, dest, value): 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] @@ -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 @@ -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 @@ -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 @@ -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: diff --git a/hypothesis-python/tests/cover/test_database_backend.py b/hypothesis-python/tests/cover/test_database_backend.py index 89294d7fd8..bd0dfaf782 100644 --- a/hypothesis-python/tests/cover/test_database_backend.py +++ b/hypothesis-python/tests/cover/test_database_backend.py @@ -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): diff --git a/hypothesis-python/tests/nocover/test_database_agreement.py b/hypothesis-python/tests/nocover/test_database_agreement.py index 12607a602c..a7701e578f 100644 --- a/hypothesis-python/tests/nocover/test_database_agreement.py +++ b/hypothesis-python/tests/nocover/test_database_agreement.py @@ -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) diff --git a/hypothesis-python/tests/nocover/test_strategy_state.py b/hypothesis-python/tests/nocover/test_strategy_state.py index db16e11a26..8f1ce97de5 100644 --- a/hypothesis-python/tests/nocover/test_strategy_state.py +++ b/hypothesis-python/tests/nocover/test_strategy_state.py @@ -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()