diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..e64b965b46 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +|DirectoryBasedExampleDatabase| now removes empty directories after |ExampleDatabase.delete| is called. diff --git a/hypothesis-python/docs/conf.py b/hypothesis-python/docs/conf.py index 598bcad7c1..f794f26e1a 100644 --- a/hypothesis-python/docs/conf.py +++ b/hypothesis-python/docs/conf.py @@ -113,6 +113,25 @@ def setup(app): assert "xps" not in sys.modules sys.modules["xps"] = mod + def process_signature(app, what, name, obj, options, signature, return_annotation): + # manually override an ugly signature from .. autofunction. Alternative we + # could manually document with `.. function:: run_conformance_test(...)`, + # but that's less likely to stay up to date. + if ( + name + == "hypothesis.internal.conjecture.provider_conformance.run_conformance_test" + ): + # so we know if this ever becomes obsolete + assert "_realize_objects" in signature + signature = re.sub( + r"_realize_objects=.*", + "_realize_objects=st.from_type(object) | st.from_type(type).flatmap(st.from_type))", + signature, + ) + return signature, return_annotation + + app.connect("autodoc-process-signature", process_signature) + language = "en" exclude_patterns = ["_build", "prolog.rst"] diff --git a/hypothesis-python/src/hypothesis/database.py b/hypothesis-python/src/hypothesis/database.py index 9429339997..b394a4ce77 100644 --- a/hypothesis-python/src/hypothesis/database.py +++ b/hypothesis-python/src/hypothesis/database.py @@ -511,8 +511,20 @@ def move(self, src: bytes, dest: bytes, value: bytes) -> None: def delete(self, key: bytes, value: bytes) -> None: try: self._value_path(key, value).unlink() + except OSError: + return + + # try deleting the key dir, which will only succeed if the dir is empty + # (i.e. ``value`` was the last value in this key). + try: + self._key_path(key).rmdir() except OSError: pass + else: + # if the deletion succeeded, also delete this key entry from metakeys. + # (if this key happens to be the metakey itself, this deletion will + # fail; that's ok and faster than checking for this rare case.) + self.delete(self._metakeys_name, key) def _start_listening(self) -> None: try: diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/provider_conformance.py b/hypothesis-python/src/hypothesis/internal/conjecture/provider_conformance.py index 49668f6ba7..f4fdd57c8f 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/provider_conformance.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/provider_conformance.py @@ -338,8 +338,9 @@ def run_conformance_test( *, context_manager_exceptions: Collection[type[BaseException]] = (), settings: Optional[Settings] = None, - _realize_objects: SearchStrategy[Any] = st.from_type(object) - | st.from_type(type).flatmap(st.from_type), + _realize_objects: SearchStrategy[Any] = ( + st.from_type(object) | st.from_type(type).flatmap(st.from_type) + ), ) -> None: """ Test that the given ``Provider`` class conforms to the |PrimitiveProvider| diff --git a/hypothesis-python/tests/cover/test_database_backend.py b/hypothesis-python/tests/cover/test_database_backend.py index 9a7fb62fbe..7a635f3b5e 100644 --- a/hypothesis-python/tests/cover/test_database_backend.py +++ b/hypothesis-python/tests/cover/test_database_backend.py @@ -738,13 +738,13 @@ def test_metakeys(tmp_path): db.save(b"k1", b"v2") assert set(db.fetch(db._metakeys_name)) == {b"k1"} - # deleting all the values from a key doesn't (currently?) clean up that key + # deleting all the values from a key removes that metakey db.delete(b"k1", b"v1") db.delete(b"k1", b"v2") - assert set(db.fetch(db._metakeys_name)) == {b"k1"} + assert set(db.fetch(db._metakeys_name)) == set() db.save(b"k2", b"v1") - assert set(db.fetch(db._metakeys_name)) == {b"k1", b"k2"} + assert set(db.fetch(db._metakeys_name)) == {b"k2"} class TracksListens(ExampleDatabase): @@ -849,3 +849,19 @@ def test_database_equal(db1, db2): ) def test_database_not_equal(db1, db2): assert db1 != db2 + + +def test_directory_db_removes_empty_dirs(tmp_path): + db = DirectoryBasedExampleDatabase(tmp_path) + db.save(b"k1", b"v1") + db.save(b"k1", b"v2") + assert db._key_path(b"k1").exists() + assert set(db.fetch(db._metakeys_name)) == {b"k1"} + + db.delete(b"k1", b"v1") + assert db._key_path(b"k1").exists() + assert set(db.fetch(db._metakeys_name)) == {b"k1"} + + db.delete(b"k1", b"v2") + assert not db._key_path(b"k1").exists() + assert set(db.fetch(db._metakeys_name)) == set()