From faee42c0a15873b79ca02252590b41ba042dd0da Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Wed, 19 Jun 2024 11:18:42 +0200 Subject: [PATCH 01/29] Harmonize cache implementations with documentation. There was a potential bug here, in that the entry score was not updated on access for concrete cache implementations if they followed the documentated behaviour of on_access. The only in-tree implementation worked fine, as it did not. Also makes the documentation more specific in a couple of places. --- .../src/hypothesis/internal/cache.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index eae61a2578..2d11aa093e 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -45,7 +45,7 @@ class GenericCache: self.new_entry(key, value) 2. whenever an existing key is read or written, self.on_access(key, value, score) is called. This returns a new score for the key. - 3. When a key is evicted, self.on_evict(key, value, score) is called. + 3. After a key is evicted, self.on_evict(key, value, score) is called. The cache will be in a valid state in all of these cases. @@ -96,11 +96,17 @@ def __len__(self): def __contains__(self, key): return key in self.keys_to_indices + def __entry_was_accessed(self, entry, i): + new_score = self.on_access(entry.key, entry.value, entry.score) + if new_score != entry.score: + entry.score = new_score + if entry.pins == 0: + self.__balance(i) + def __getitem__(self, key): i = self.keys_to_indices[key] result = self.data[i] - self.on_access(result.key, result.value, result.score) - self.__balance(i) + self.__entry_was_accessed(result, i) return result.value def __setitem__(self, key, value): @@ -125,13 +131,12 @@ def __setitem__(self, key, value): i = len(self.data) self.data.append(entry) self.keys_to_indices[key] = i + self.__balance(i) else: entry = self.data[i] assert entry.key == key entry.value = value - entry.score = self.on_access(entry.key, entry.value, entry.score) - - self.__balance(i) + self.__entry_was_accessed(entry, i) if evicted is not None: if self.data[0] is not entry: @@ -172,7 +177,7 @@ def is_pinned(self, key): return self.data[i].pins > 0 def clear(self): - """Remove all keys, clearing their pinned status.""" + """Remove all keys, regardless of their pinned status.""" del self.data[:] self.keys_to_indices.clear() self.__pinned_entry_count = 0 @@ -258,10 +263,10 @@ class LRUReusedCache(GenericCache): """The only concrete implementation of GenericCache we use outside of tests currently. - Adopts a modified least-frequently used eviction policy: It evicts the key + Adopts a modified least-recently used eviction policy: It evicts the key that has been used least recently, but it will always preferentially evict - keys that have only ever been accessed once. Among keys that have been - accessed more than once, it ignores the number of accesses. + keys that have never been accessed after insertion. Among keys that have been + accessed, it ignores the number of accesses. This retains most of the benefits of an LRU cache, but adds an element of scan-resistance to the process: If we end up scanning through a large @@ -280,12 +285,10 @@ def tick(self): return self.__tick def new_entry(self, key, value): - return [1, self.tick()] + return (1, self.tick()) def on_access(self, key, value, score): - score[0] = 2 - score[1] = self.tick() - return score + return (2, self.tick()) def pin(self, key): try: From 309ab67891ba6cd7cb73eeca11065d21736a3abe Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Wed, 19 Jun 2024 11:24:32 +0200 Subject: [PATCH 02/29] Drop unnecessary bookkeeping for cache __pinned_entry_count --- .../src/hypothesis/internal/cache.py | 23 ++++--------------- .../tests/cover/test_cache_implementation.py | 3 +++ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index 2d11aa093e..7e34b17538 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -81,14 +81,6 @@ def data(self): self._threadlocal.data = [] return self._threadlocal.data - @property - def __pinned_entry_count(self): - return getattr(self._threadlocal, "_pinned_entry_count", 0) - - @__pinned_entry_count.setter - def __pinned_entry_count(self, value): - self._threadlocal._pinned_entry_count = value - def __len__(self): assert len(self.keys_to_indices) == len(self.data) return len(self.data) @@ -116,14 +108,13 @@ def __setitem__(self, key, value): try: i = self.keys_to_indices[key] except KeyError: - if self.max_size == self.__pinned_entry_count: - raise ValueError( - "Cannot increase size of cache where all keys have been pinned." - ) from None entry = Entry(key, value, self.new_entry(key, value)) if len(self.data) >= self.max_size: evicted = self.data[0] - assert evicted.pins == 0 + if evicted.pins > 0: + raise ValueError( + "Cannot increase size of cache where all keys have been pinned." + ) from None del self.keys_to_indices[evicted.key] i = 0 self.data[0] = entry @@ -155,8 +146,6 @@ def pin(self, key): entry = self.data[i] entry.pins += 1 if entry.pins == 1: - self.__pinned_entry_count += 1 - assert self.__pinned_entry_count <= self.max_size self.__balance(i) def unpin(self, key): @@ -168,7 +157,6 @@ def unpin(self, key): raise ValueError(f"Key {key!r} has not been pinned") entry.pins -= 1 if entry.pins == 0: - self.__pinned_entry_count -= 1 self.__balance(i) def is_pinned(self, key): @@ -180,7 +168,6 @@ def clear(self): """Remove all keys, regardless of their pinned status.""" del self.data[:] self.keys_to_indices.clear() - self.__pinned_entry_count = 0 def __repr__(self): return "{" + ", ".join(f"{e.key!r}: {e.value!r}" for e in self.data) + "}" @@ -225,7 +212,7 @@ def __swap(self, i, j): self.keys_to_indices[self.data[j].key] = j def __balance(self, i): - """When we have made a modification to the heap such that means that + """When we have made a modification to the heap such that the heap property has been violated locally around i but previously held for all other indexes (and no other values have been modified), this fixes the heap so that the heap property holds everywhere.""" diff --git a/hypothesis-python/tests/cover/test_cache_implementation.py b/hypothesis-python/tests/cover/test_cache_implementation.py index 5ffd171bfa..b8f204ecda 100644 --- a/hypothesis-python/tests/cover/test_cache_implementation.py +++ b/hypothesis-python/tests/cover/test_cache_implementation.py @@ -237,6 +237,9 @@ def test_will_error_instead_of_evicting_pin(): with pytest.raises(ValueError): cache[2] = 2 + assert 1 in cache + assert 2 not in cache + def test_will_error_for_bad_unpin(): cache = LRUReusedCache(max_size=1) From 2dcb6ac0cd95dc0e161e66e7df1a5a5b00d33c8d Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 20 Jun 2024 11:29:58 +0200 Subject: [PATCH 03/29] Fix unsafe cache pin semantics --- .../src/hypothesis/internal/cache.py | 26 ++++++++----------- .../hypothesis/internal/conjecture/engine.py | 4 +-- .../tests/cover/test_cache_implementation.py | 10 ++++--- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index 7e34b17538..474c1d1353 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -30,6 +30,8 @@ def sort_key(self): # worry about their relative order. return (1,) +Unset = object() + class GenericCache: """Generic supertype for cache implementations. @@ -137,11 +139,18 @@ def __setitem__(self, key, value): def __iter__(self): return iter(self.keys_to_indices) - def pin(self, key): + def pin(self, key, value=Unset): """Mark ``key`` as pinned. That is, it may not be evicted until ``unpin(key)`` has been called. The same key may be pinned multiple times and will not be unpinned until the same number of calls to - unpin have been made.""" + unpin have been made. + + If value is set, an atomic set-and-pin operation will be performed. + Otherwise, KeyError is raised if the key has already been evicted. + """ + if value is not Unset: + self[key] = value + i = self.keys_to_indices[key] entry = self.data[i] entry.pins += 1 @@ -276,16 +285,3 @@ def new_entry(self, key, value): def on_access(self, key, value, score): return (2, self.tick()) - - def pin(self, key): - try: - super().pin(key) - except KeyError: - # The whole point of an LRU cache is that it might drop things for you - assert key not in self.keys_to_indices - - def unpin(self, key): - try: - super().unpin(key) - except KeyError: - assert key not in self.keys_to_indices diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py index f1d29191ba..8faca627dc 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py @@ -529,7 +529,7 @@ def test_function(self, data: ConjectureData) -> None: if changed: self.save_buffer(data.buffer) self.interesting_examples[key] = data.as_result() # type: ignore - self.__data_cache.pin(data.buffer) + self.__data_cache.pin(data.buffer, data.as_result()) self.shrunk_examples.discard(key) if self.shrinks >= MAX_SHRINKS: @@ -876,7 +876,7 @@ def generate_new_examples(self) -> None: zero_data = self.cached_test_function(bytes(BUFFER_SIZE)) if zero_data.status > Status.OVERRUN: assert isinstance(zero_data, ConjectureResult) - self.__data_cache.pin(zero_data.buffer) + self.__data_cache.pin(zero_data.buffer, zero_data.as_result()) # Pin forever if zero_data.status == Status.OVERRUN or ( zero_data.status == Status.VALID diff --git a/hypothesis-python/tests/cover/test_cache_implementation.py b/hypothesis-python/tests/cover/test_cache_implementation.py index b8f204ecda..e65e50d979 100644 --- a/hypothesis-python/tests/cover/test_cache_implementation.py +++ b/hypothesis-python/tests/cover/test_cache_implementation.py @@ -334,7 +334,9 @@ def test_pin_and_unpin_are_noops_if_dropped(): cache[i] = False assert 30 not in cache - cache.pin(30) - assert 30 not in cache - cache.unpin(30) - assert 30 not in cache + with pytest.raises(KeyError): + cache.pin(30) + + cache.pin(30, False) + assert 30 in cache + assert cache.is_pinned(30) From 38926a87c2c7572f2eabb126e04060f3ce1617e3 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Mon, 24 Jun 2024 12:43:36 +0200 Subject: [PATCH 04/29] RELEASE + format fix --- hypothesis-python/RELEASE.rst | 3 +++ hypothesis-python/src/hypothesis/internal/cache.py | 1 + 2 files changed, 4 insertions(+) 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..43d9f7137b --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +Clean up internal cache implementation. diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index 474c1d1353..156805506f 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -30,6 +30,7 @@ def sort_key(self): # worry about their relative order. return (1,) + Unset = object() From 66ea64e9014989b38510af46079e917d5b087c81 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Mon, 24 Jun 2024 13:16:35 +0200 Subject: [PATCH 05/29] Formatting --- .../src/hypothesis/internal/conjecture/engine.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py index 8faca627dc..bd85309837 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py @@ -876,7 +876,9 @@ def generate_new_examples(self) -> None: zero_data = self.cached_test_function(bytes(BUFFER_SIZE)) if zero_data.status > Status.OVERRUN: assert isinstance(zero_data, ConjectureResult) - self.__data_cache.pin(zero_data.buffer, zero_data.as_result()) # Pin forever + self.__data_cache.pin( + zero_data.buffer, zero_data.as_result() + ) # Pin forever if zero_data.status == Status.OVERRUN or ( zero_data.status == Status.VALID From 8af01ae6897f372f1ee69ae0a939e85567e3d600 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Mon, 24 Jun 2024 21:56:46 +0200 Subject: [PATCH 06/29] Use convention.not_set --- hypothesis-python/src/hypothesis/internal/cache.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index 156805506f..a9d36b19aa 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -12,6 +12,8 @@ import attr +from hypothesis.utils.conventions import not_set + @attr.s(slots=True) class Entry: @@ -31,9 +33,6 @@ def sort_key(self): return (1,) -Unset = object() - - class GenericCache: """Generic supertype for cache implementations. @@ -140,16 +139,17 @@ def __setitem__(self, key, value): def __iter__(self): return iter(self.keys_to_indices) - def pin(self, key, value=Unset): + def pin(self, key, value=not_set): """Mark ``key`` as pinned. That is, it may not be evicted until ``unpin(key)`` has been called. The same key may be pinned multiple times and will not be unpinned until the same number of calls to unpin have been made. If value is set, an atomic set-and-pin operation will be performed. - Otherwise, KeyError is raised if the key has already been evicted. + Otherwise, KeyError is raised if the key is not present (due to + early eviction for example). """ - if value is not Unset: + if value is not not_set: self[key] = value i = self.keys_to_indices[key] From b5ab6ee3b9959dbcedbe505a9b6e0e342638b328 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Mon, 24 Jun 2024 22:49:27 +0200 Subject: [PATCH 07/29] Disallow size-0 cache --- hypothesis-python/src/hypothesis/internal/cache.py | 5 +++-- hypothesis-python/tests/cover/test_cache_implementation.py | 7 ------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index a9d36b19aa..1286a3b5c9 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -58,6 +58,9 @@ class GenericCache: __slots__ = ("max_size", "_threadlocal") def __init__(self, max_size): + if max_size <= 0: + raise ValueError("Cache size must be nonzero.") + self.max_size = max_size # Implementation: We store a binary heap of Entry objects in self.data, @@ -104,8 +107,6 @@ def __getitem__(self, key): return result.value def __setitem__(self, key, value): - if self.max_size == 0: - return evicted = None try: i = self.keys_to_indices[key] diff --git a/hypothesis-python/tests/cover/test_cache_implementation.py b/hypothesis-python/tests/cover/test_cache_implementation.py index e65e50d979..9b0ba137b2 100644 --- a/hypothesis-python/tests/cover/test_cache_implementation.py +++ b/hypothesis-python/tests/cover/test_cache_implementation.py @@ -186,13 +186,6 @@ def test_can_clear_a_cache(): assert len(x) == 0 -def test_max_size_cache_ignores(): - x = ValueScored(0) - x[0] = 1 - with pytest.raises(KeyError): - x[0] - - def test_pinning_prevents_eviction(): cache = LRUReusedCache(max_size=10) cache[20] = 1 From 9b8edc3ed09ac05b00390b4f0c3ec807d5efb179 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Tue, 25 Jun 2024 08:36:32 +0200 Subject: [PATCH 08/29] Fix coverage, use InvalidArgument for bad max_size --- .../src/hypothesis/internal/cache.py | 25 +++++++++++-------- .../tests/cover/test_cache_implementation.py | 6 +++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index 1286a3b5c9..3c0a746640 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -12,6 +12,7 @@ import attr +from hypothesis.errors import InvalidArgument from hypothesis.utils.conventions import not_set @@ -39,7 +40,8 @@ class GenericCache: Defines a dict-like mapping with a maximum size, where as well as mapping to a value, each key also maps to a score. When a write would cause the dict to exceed its maximum size, it first evicts the existing key with - the smallest score, then adds the new key to the map. + the smallest score, then adds the new key to the map. If due to pinning + no key can be evicted, ValueError is raised. A key has the following lifecycle: @@ -59,7 +61,7 @@ class GenericCache: def __init__(self, max_size): if max_size <= 0: - raise ValueError("Cache size must be nonzero.") + raise InvalidArgument("Cache size must be nonzero.") self.max_size = max_size @@ -93,17 +95,10 @@ def __len__(self): def __contains__(self, key): return key in self.keys_to_indices - def __entry_was_accessed(self, entry, i): - new_score = self.on_access(entry.key, entry.value, entry.score) - if new_score != entry.score: - entry.score = new_score - if entry.pins == 0: - self.__balance(i) - def __getitem__(self, key): i = self.keys_to_indices[key] result = self.data[i] - self.__entry_was_accessed(result, i) + self.__entry_was_accessed(i) return result.value def __setitem__(self, key, value): @@ -130,7 +125,7 @@ def __setitem__(self, key, value): entry = self.data[i] assert entry.key == key entry.value = value - self.__entry_was_accessed(entry, i) + self.__entry_was_accessed(i) if evicted is not None: if self.data[0] is not entry: @@ -215,6 +210,14 @@ def check_valid(self): if j < len(self.data): assert e.score <= self.data[j].score, self.data + def __entry_was_accessed(self, i): + entry = self.data[i] + new_score = self.on_access(entry.key, entry.value, entry.score) + if new_score != entry.score: + entry.score = new_score + if entry.pins == 0: + self.__balance(i) + def __swap(self, i, j): assert i < j assert self.data[j].sort_key < self.data[i].sort_key diff --git a/hypothesis-python/tests/cover/test_cache_implementation.py b/hypothesis-python/tests/cover/test_cache_implementation.py index 9b0ba137b2..8154cc4293 100644 --- a/hypothesis-python/tests/cover/test_cache_implementation.py +++ b/hypothesis-python/tests/cover/test_cache_implementation.py @@ -23,6 +23,7 @@ settings, strategies as st, ) +from hypothesis.errors import InvalidArgument from hypothesis.internal.cache import GenericCache, LRUReusedCache from tests.common.utils import skipif_emscripten @@ -186,6 +187,11 @@ def test_can_clear_a_cache(): assert len(x) == 0 +def test_max_size_must_be_positive(): + with pytest.raises(InvalidArgument): + ValueScored(max_size=0) + + def test_pinning_prevents_eviction(): cache = LRUReusedCache(max_size=10) cache[20] = 1 From 03d97a756951685735d64fd92187f82a93ec8152 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 30 Jun 2024 02:00:07 -0400 Subject: [PATCH 09/29] add clarifying comment --- hypothesis-python/src/hypothesis/internal/cache.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index 3c0a746640..def214996e 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -215,6 +215,8 @@ def __entry_was_accessed(self, i): new_score = self.on_access(entry.key, entry.value, entry.score) if new_score != entry.score: entry.score = new_score + # changing the score of a pinned entry cannot unbalance the heap, as + # we place all pinned entries after unpinned ones, regardless of score. if entry.pins == 0: self.__balance(i) From e9538084074e3d6dbf989b5410fbc3c2f3d14ead Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Sun, 30 Jun 2024 11:48:10 +0200 Subject: [PATCH 10/29] Make it impossible to pin without value --- .../src/hypothesis/internal/cache.py | 18 +++----- .../tests/cover/test_cache_implementation.py | 45 +++++-------------- .../nocover/test_cache_implementation.py | 2 +- 3 files changed, 19 insertions(+), 46 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index def214996e..69fee690be 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -13,7 +13,6 @@ import attr from hypothesis.errors import InvalidArgument -from hypothesis.utils.conventions import not_set @attr.s(slots=True) @@ -135,18 +134,13 @@ def __setitem__(self, key, value): def __iter__(self): return iter(self.keys_to_indices) - def pin(self, key, value=not_set): - """Mark ``key`` as pinned. That is, it may not be evicted until - ``unpin(key)`` has been called. The same key may be pinned multiple - times and will not be unpinned until the same number of calls to - unpin have been made. - - If value is set, an atomic set-and-pin operation will be performed. - Otherwise, KeyError is raised if the key is not present (due to - early eviction for example). + def pin(self, key, value): + """Mark ``key`` as pinned (with the given value). That is, it may not + be evicted until ``unpin(key)`` has been called. The same key may be + pinned multiple times, possibly changing its value, and will not be + unpinned until the same number of calls to unpin have been made. """ - if value is not not_set: - self[key] = value + self[key] = value i = self.keys_to_indices[key] entry = self.data[i] diff --git a/hypothesis-python/tests/cover/test_cache_implementation.py b/hypothesis-python/tests/cover/test_cache_implementation.py index 8154cc4293..f907457129 100644 --- a/hypothesis-python/tests/cover/test_cache_implementation.py +++ b/hypothesis-python/tests/cover/test_cache_implementation.py @@ -194,8 +194,7 @@ def test_max_size_must_be_positive(): def test_pinning_prevents_eviction(): cache = LRUReusedCache(max_size=10) - cache[20] = 1 - cache.pin(20) + cache.pin(20, 1) for i in range(20): cache[i] = 0 assert cache[20] == 1 @@ -203,8 +202,7 @@ def test_pinning_prevents_eviction(): def test_unpinning_allows_eviction(): cache = LRUReusedCache(max_size=10) - cache[20] = True - cache.pin(20) + cache.pin(20, True) for i in range(20): cache[i] = False @@ -218,21 +216,22 @@ def test_unpinning_allows_eviction(): def test_unpins_must_match_pins(): cache = LRUReusedCache(max_size=2) - cache[1] = 1 - cache.pin(1) + cache.pin(1, 1) assert cache.is_pinned(1) - cache.pin(1) + assert cache[1] == 1 + cache.pin(1, 2) assert cache.is_pinned(1) + assert cache[1] == 2 cache.unpin(1) assert cache.is_pinned(1) + assert cache[1] == 2 cache.unpin(1) assert not cache.is_pinned(1) def test_will_error_instead_of_evicting_pin(): cache = LRUReusedCache(max_size=1) - cache[1] = 1 - cache.pin(1) + cache.pin(1, 1) with pytest.raises(ValueError): cache[2] = 2 @@ -277,19 +276,17 @@ def new_entry(self, key, value): assert len(cache) == 1 -def test_double_pinning_does_not_increase_pin_count(): +def test_double_pinning_does_not_add_entry(): cache = LRUReusedCache(2) - cache[0] = 0 - cache.pin(0) - cache.pin(0) + cache.pin(0, 0) + cache.pin(0, 1) cache[1] = 1 assert len(cache) == 2 def test_can_add_new_keys_after_unpinning(): cache = LRUReusedCache(1) - cache[0] = 0 - cache.pin(0) + cache.pin(0, 0) cache.unpin(0) cache[1] = 1 assert len(cache) == 1 @@ -321,21 +318,3 @@ def target(): worker.join() assert not errors - - -def test_pin_and_unpin_are_noops_if_dropped(): - # See https://github.com/HypothesisWorks/hypothesis/issues/3169 - cache = LRUReusedCache(max_size=10) - cache[30] = True - assert 30 in cache - - for i in range(20): - cache[i] = False - - assert 30 not in cache - with pytest.raises(KeyError): - cache.pin(30) - - cache.pin(30, False) - assert 30 in cache - assert cache.is_pinned(30) diff --git a/hypothesis-python/tests/nocover/test_cache_implementation.py b/hypothesis-python/tests/nocover/test_cache_implementation.py index 7175d63dee..184ce5c49d 100644 --- a/hypothesis-python/tests/nocover/test_cache_implementation.py +++ b/hypothesis-python/tests/nocover/test_cache_implementation.py @@ -79,7 +79,7 @@ def check_values(self): @rule(key=keys) def pin_key(self, key): if key in self.cache: - self.cache.pin(key) + self.cache.pin(key, self.__values[key]) if self.__pins[key] == 0: self.__total_pins += 1 self.__pins[key] += 1 From 0e0856b2a57566e1d272a9d5631de0206757a24d Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Wed, 26 Jun 2024 18:17:57 +0100 Subject: [PATCH 11/29] Update Django testenvs --- hypothesis-python/tox.ini | 16 ++++++---------- tooling/src/hypothesistooling/__main__.py | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/hypothesis-python/tox.ini b/hypothesis-python/tox.ini index 6042594930..46cf4633cd 100644 --- a/hypothesis-python/tox.ini +++ b/hypothesis-python/tox.ini @@ -156,22 +156,18 @@ commands = niche: bash scripts/other-tests.sh custom: python -bb -X dev -m pytest {posargs} -[testenv:django32] -commands = - pip install .[pytz] - pip install django~=3.2.15 - python -bb -X dev -m tests.django.manage test tests.django {posargs} - -[testenv:django41] +[testenv:django42] +setenv= + PYTHONWARNDEFAULTENCODING=1 commands = - pip install django~=4.1.0 + pip install django~=4.2.0 python -bb -X dev -m tests.django.manage test tests.django {posargs} -[testenv:django42] +[testenv:django50] setenv= PYTHONWARNDEFAULTENCODING=1 commands = - pip install django~=4.2.0 + pip install django~=5.0.0 python -bb -X dev -m tests.django.manage test tests.django {posargs} [testenv:py{37,38,39}-nose] diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py index f57bbb0804..6933f50f6c 100644 --- a/tooling/src/hypothesistooling/__main__.py +++ b/tooling/src/hypothesistooling/__main__.py @@ -497,7 +497,7 @@ def standard_tox_task(name, py=ci_version): standard_tox_task("py39-pytest54", py="3.9") standard_tox_task("pytest62") -for n in [32, 41, 42]: +for n in [42, 50]: standard_tox_task(f"django{n}") for n in [13, 14, 15, 20, 21, 22]: From 81a57fd479c75d72a65ca0aa226985b4c364dfba Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Wed, 26 Jun 2024 22:07:22 +0100 Subject: [PATCH 12/29] Add Django 5.0 support --- hypothesis-python/src/hypothesis/extra/django/_fields.py | 4 +++- hypothesis-python/tests/django/manage.py | 6 ++++++ hypothesis-python/tests/django/toys/settings.py | 9 ++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/django/_fields.py b/hypothesis-python/src/hypothesis/extra/django/_fields.py index 181c8869f9..29f6dcf00a 100644 --- a/hypothesis-python/src/hypothesis/extra/django/_fields.py +++ b/hypothesis-python/src/hypothesis/extra/django/_fields.py @@ -57,7 +57,9 @@ def inner(field): def timezones(): # From Django 4.0, the default is to use zoneinfo instead of pytz. assert getattr(django.conf.settings, "USE_TZ", False) - if getattr(django.conf.settings, "USE_DEPRECATED_PYTZ", True): + if django.VERSION < (5, 0, 0) and getattr( + django.conf.settings, "USE_DEPRECATED_PYTZ", True + ): from hypothesis.extra.pytz import timezones else: from hypothesis.strategies import timezones diff --git a/hypothesis-python/tests/django/manage.py b/hypothesis-python/tests/django/manage.py index be364ea9a3..6d1de7eebd 100755 --- a/hypothesis-python/tests/django/manage.py +++ b/hypothesis-python/tests/django/manage.py @@ -38,6 +38,12 @@ except ImportError: RemovedInDjango50Warning = () + try: + from django.utils.deprecation import RemovedInDjango60Warning + except ImportError: + RemovedInDjango60Warning = () + with warnings.catch_warnings(): warnings.simplefilter("ignore", category=RemovedInDjango50Warning) + warnings.simplefilter("ignore", category=RemovedInDjango60Warning) execute_from_command_line(sys.argv) diff --git a/hypothesis-python/tests/django/toys/settings.py b/hypothesis-python/tests/django/toys/settings.py index 6f753e9eca..6f8ae97fb5 100644 --- a/hypothesis-python/tests/django/toys/settings.py +++ b/hypothesis-python/tests/django/toys/settings.py @@ -19,6 +19,8 @@ import os +import django + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) @@ -85,7 +87,8 @@ USE_I18N = True -USE_L10N = True +if django.VERSION < (5, 0, 0): + USE_L10N = True USE_TZ = os.environ.get("HYPOTHESIS_DJANGO_USETZ", "TRUE") == "TRUE" @@ -121,3 +124,7 @@ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] + +# Transitional setting until 6.0. See +# https://docs.djangoproject.com/en/5.0/ref/forms/fields/#django.forms.URLField.assume_scheme +FORMS_URLFIELD_ASSUME_HTTPS = True From c8c6aac774a8403c1f68f3ef2dec9e9c40d7a225 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Thu, 27 Jun 2024 22:20:24 +0100 Subject: [PATCH 13/29] Add handling and tests for Django 5.0's GeneratedField --- .../src/hypothesis/extra/django/_impl.py | 4 ++++ .../tests/django/toystore/models.py | 22 +++++++++++++++++++ .../django/toystore/test_given_models.py | 17 ++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/hypothesis-python/src/hypothesis/extra/django/_impl.py b/hypothesis-python/src/hypothesis/extra/django/_impl.py index 5a7ab8f0e3..2721c453ee 100644 --- a/hypothesis-python/src/hypothesis/extra/django/_impl.py +++ b/hypothesis-python/src/hypothesis/extra/django/_impl.py @@ -13,6 +13,7 @@ from functools import partial from typing import TYPE_CHECKING, Optional, Type, TypeVar, Union +import django from django import forms as df, test as dt from django.contrib.staticfiles import testing as dst from django.core.exceptions import ValidationError @@ -105,6 +106,9 @@ def from_model( name not in field_strategies and not field.auto_created and not isinstance(field, dm.AutoField) + and not ( + django.VERSION >= (5, 0, 0) and isinstance(field, dm.GeneratedField) + ) and field.default is dm.fields.NOT_PROVIDED ): field_strategies[name] = from_field(field) diff --git a/hypothesis-python/tests/django/toystore/models.py b/hypothesis-python/tests/django/toystore/models.py index 80810fc8c1..b945f87243 100644 --- a/hypothesis-python/tests/django/toystore/models.py +++ b/hypothesis-python/tests/django/toystore/models.py @@ -8,7 +8,9 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. +import django from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator from django.db import models @@ -149,3 +151,23 @@ class CompanyExtension(models.Model): class UserSpecifiedAutoId(models.Model): my_id = models.AutoField(primary_key=True) + + +if django.VERSION >= (5, 0, 0): + import math + + class Pizza(models.Model): + AREA = math.pi * models.F("radius") ** 2 + + radius = models.IntegerField(validators=[MinValueValidator(1)]) + slices = models.PositiveIntegerField(validators=[MinValueValidator(2)]) + total_area = models.GeneratedField( + expression=AREA, + output_field=models.FloatField(), + db_persist=True, + ) + slice_area = models.GeneratedField( + expression=AREA / models.F("slices"), + output_field=models.FloatField(), + db_persist=False, + ) diff --git a/hypothesis-python/tests/django/toystore/test_given_models.py b/hypothesis-python/tests/django/toystore/test_given_models.py index ff291cd63e..8e393ca25c 100644 --- a/hypothesis-python/tests/django/toystore/test_given_models.py +++ b/hypothesis-python/tests/django/toystore/test_given_models.py @@ -11,6 +11,7 @@ import datetime as dt from uuid import UUID +import django from django.conf import settings as django_settings from django.contrib.auth.models import User @@ -210,3 +211,19 @@ class TestUserSpecifiedAutoId(TestCase): def test_user_specified_auto_id(self, user_specified_auto_id): self.assertIsInstance(user_specified_auto_id, UserSpecifiedAutoId) self.assertIsNotNone(user_specified_auto_id.pk) + + +if django.VERSION >= (5, 0, 0): + from tests.django.toystore.models import Pizza + + class TestModelWithGeneratedField(TestCase): + @given(from_model(Pizza)) + def test_create_pizza(self, pizza): + """ + Strategies are not inferred for GeneratedField. + """ + + pizza.full_clean() + # Check the expected types of the generated fields. + self.assertIsInstance(pizza.slice_area, float) + self.assertIsInstance(pizza.total_area, float) From 74012a557b986a2aaa6a3d9d100508c51fe50a0f Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Mon, 1 Jul 2024 08:33:50 +0100 Subject: [PATCH 14/29] Add release notes --- hypothesis-python/RELEASE.rst | 5 +++++ 1 file changed, 5 insertions(+) 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..dcf027ccf6 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,5 @@ +RELEASE_TYPE: minor + +This release adds support for Django 5.0, and drops support for Django < 4.2. + +Thanks to Joshua Munn for this contribution. From 2b300fe41c4a329145d1891ab43ac49236322898 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Tue, 2 Jul 2024 09:39:43 +0200 Subject: [PATCH 15/29] Add failing test --- hypothesis-python/tests/nocover/test_cache_implementation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/tests/nocover/test_cache_implementation.py b/hypothesis-python/tests/nocover/test_cache_implementation.py index 184ce5c49d..13912a3fca 100644 --- a/hypothesis-python/tests/nocover/test_cache_implementation.py +++ b/hypothesis-python/tests/nocover/test_cache_implementation.py @@ -73,8 +73,9 @@ def set_key(self, key): @invariant() def check_values(self): - for k in getattr(self, "cache", ()): - assert self.__values[k] == self.cache[k] + self.cache.check_valid() + for key in self.cache: + assert self.__values[key] == self.cache[key] @rule(key=keys) def pin_key(self, key): From d2cd1c5b72e7f5d82a4351eabe83afab39253e76 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Mon, 1 Jul 2024 11:26:48 +0200 Subject: [PATCH 16/29] Always compare sort keys, not scores --- hypothesis-python/src/hypothesis/internal/cache.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index 69fee690be..fae53e9b75 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -128,7 +128,7 @@ def __setitem__(self, key, value): if evicted is not None: if self.data[0] is not entry: - assert evicted.score <= self.data[0].score + assert evicted.sort_key <= self.data[0].sort_key self.on_evict(evicted.key, evicted.value, evicted.score) def __iter__(self): @@ -202,7 +202,7 @@ def check_valid(self): assert self.keys_to_indices[e.key] == i for j in [i * 2 + 1, i * 2 + 2]: if j < len(self.data): - assert e.score <= self.data[j].score, self.data + assert e.sort_key <= self.data[j].sort_key, self.data def __entry_was_accessed(self, i): entry = self.data[i] @@ -238,7 +238,8 @@ def __balance(self, i): while True: children = [j for j in (2 * i + 1, 2 * i + 2) if j < len(self.data)] if len(children) == 2: - children.sort(key=lambda j: self.data[j].score) + # try smallest child first + children.sort(key=lambda j: self.data[j].sort_key) for j in children: if self.__out_of_order(i, j): self.__swap(i, j) From c8943f6fbee90c45a2d66d5b69e2b641940fdfeb Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Mon, 1 Jul 2024 12:49:20 +0200 Subject: [PATCH 17/29] Remove misleading comment and no-cover --- hypothesis-python/src/hypothesis/internal/cache.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index fae53e9b75..d990f8ae67 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -232,9 +232,7 @@ def __balance(self, i): self.__swap(parent, i) i = parent else: - # This branch is never taken on versions of Python where dicts - # preserve their insertion order (pypy or cpython >= 3.7) - break # pragma: no cover + break while True: children = [j for j in (2 * i + 1, 2 * i + 2) if j < len(self.data)] if len(children) == 2: From b6d1571f2ab8f5743e15f027658f746db628f7e4 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Tue, 2 Jul 2024 09:32:46 +0200 Subject: [PATCH 18/29] Simplify balancing --- .../src/hypothesis/internal/cache.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index d990f8ae67..a8749b452f 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -198,6 +198,7 @@ def check_valid(self): Asserts that all of the cache's invariants hold. When everything is working correctly this should be an expensive no-op. """ + assert len(self.keys_to_indices) == len(self.data) for i, e in enumerate(self.data): assert self.keys_to_indices[e.key] == i for j in [i * 2 + 1, i * 2 + 2]: @@ -226,23 +227,19 @@ def __balance(self, i): the heap property has been violated locally around i but previously held for all other indexes (and no other values have been modified), this fixes the heap so that the heap property holds everywhere.""" - while i > 0: - parent = (i - 1) // 2 + # bubble up (if score is too low for current position) + while (parent := (i - 1) // 2) >= 0: if self.__out_of_order(parent, i): self.__swap(parent, i) i = parent else: break - while True: - children = [j for j in (2 * i + 1, 2 * i + 2) if j < len(self.data)] - if len(children) == 2: - # try smallest child first - children.sort(key=lambda j: self.data[j].sort_key) - for j in children: - if self.__out_of_order(i, j): - self.__swap(i, j) - i = j - break + # or bubble down (if score is too high for current position) + while children := [j for j in (2 * i + 1, 2 * i + 2) if j < len(self.data)]: + smallest_child = min(children, key=lambda j: self.data[j].sort_key) + if self.__out_of_order(i, smallest_child): + self.__swap(i, smallest_child) + i = smallest_child else: break From e1c13b7da114d064a260b9f94553afbd702b4da8 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Tue, 2 Jul 2024 20:49:27 +0100 Subject: [PATCH 19/29] Shorten GeneratedField check Co-authored-by: Zac Hatfield-Dodds --- hypothesis-python/src/hypothesis/extra/django/_impl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/django/_impl.py b/hypothesis-python/src/hypothesis/extra/django/_impl.py index 2721c453ee..82b478fc7f 100644 --- a/hypothesis-python/src/hypothesis/extra/django/_impl.py +++ b/hypothesis-python/src/hypothesis/extra/django/_impl.py @@ -106,9 +106,7 @@ def from_model( name not in field_strategies and not field.auto_created and not isinstance(field, dm.AutoField) - and not ( - django.VERSION >= (5, 0, 0) and isinstance(field, dm.GeneratedField) - ) + and not isinstance(field, getattr(dm, "GeneratedField", ())) and field.default is dm.fields.NOT_PROVIDED ): field_strategies[name] = from_field(field) From 2fcf81799d399e77c09d88beee80e1f3bd9baf47 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Tue, 2 Jul 2024 20:49:41 +0100 Subject: [PATCH 20/29] Improve release notes Co-authored-by: Zac Hatfield-Dodds --- hypothesis-python/RELEASE.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst index dcf027ccf6..15582e3079 100644 --- a/hypothesis-python/RELEASE.rst +++ b/hypothesis-python/RELEASE.rst @@ -1,5 +1,5 @@ RELEASE_TYPE: minor -This release adds support for Django 5.0, and drops support for Django < 4.2. +This release improves support for Django 5.0, and drops support for end-of-life Django versions (< 4.2). Thanks to Joshua Munn for this contribution. From fbc4b160bda839edc29c85d72d6380d48de2ba05 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Tue, 2 Jul 2024 20:50:32 +0100 Subject: [PATCH 21/29] Update Django testenvs in github action --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d4b17d2ab..1668099972 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,9 +67,8 @@ jobs: - check-py39-pytest46 - check-py39-pytest54 - check-pytest62 + - check-django50 - check-django42 - - check-django41 - - check-django32 - check-pandas22 - check-pandas21 - check-pandas20 From b31cea0423057bf5c4a05024145e1380c7be0891 Mon Sep 17 00:00:00 2001 From: Joshua Munn Date: Tue, 2 Jul 2024 20:55:06 +0100 Subject: [PATCH 22/29] fixup! Shorten GeneratedField check --- hypothesis-python/src/hypothesis/extra/django/_impl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/extra/django/_impl.py b/hypothesis-python/src/hypothesis/extra/django/_impl.py index 82b478fc7f..d4bcefb0c1 100644 --- a/hypothesis-python/src/hypothesis/extra/django/_impl.py +++ b/hypothesis-python/src/hypothesis/extra/django/_impl.py @@ -13,7 +13,6 @@ from functools import partial from typing import TYPE_CHECKING, Optional, Type, TypeVar, Union -import django from django import forms as df, test as dt from django.contrib.staticfiles import testing as dst from django.core.exceptions import ValidationError From 5ab25ae0bd616b8482f0c5d43d42e789650fd9df Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 4 Jul 2024 15:34:18 +0200 Subject: [PATCH 23/29] Update hypothesis-python/src/hypothesis/internal/cache.py Co-authored-by: Zac Hatfield-Dodds --- hypothesis-python/src/hypothesis/internal/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index a8749b452f..a497227077 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -60,7 +60,7 @@ class GenericCache: def __init__(self, max_size): if max_size <= 0: - raise InvalidArgument("Cache size must be nonzero.") + raise InvalidArgument("Cache size must be at least one.") self.max_size = max_size From 43c047d3a8d5d5a6f4efeeef7a31d5ece08c6409 Mon Sep 17 00:00:00 2001 From: Joachim B Haga Date: Thu, 4 Jul 2024 15:38:07 +0200 Subject: [PATCH 24/29] Clarify unpin value semantics in docstring --- hypothesis-python/src/hypothesis/internal/cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/cache.py b/hypothesis-python/src/hypothesis/internal/cache.py index a497227077..49c1956867 100644 --- a/hypothesis-python/src/hypothesis/internal/cache.py +++ b/hypothesis-python/src/hypothesis/internal/cache.py @@ -149,8 +149,8 @@ def pin(self, key, value): self.__balance(i) def unpin(self, key): - """Undo one previous call to ``pin(key)``. Once all calls are - undone this key may be evicted as normal.""" + """Undo one previous call to ``pin(key)``. The value stays the same. + Once all calls are undone this key may be evicted as normal.""" i = self.keys_to_indices[key] entry = self.data[i] if entry.pins == 0: From 70897dc381251342019906f53005cf9cffed4ee3 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Thu, 4 Jul 2024 11:25:26 -0700 Subject: [PATCH 25/29] Update dependencies + fix lint issues --- .github/workflows/main.yml | 9 +++-- hypothesis-python/RELEASE.rst | 3 ++ hypothesis-python/setup.py | 2 +- hypothesis-python/src/hypothesis/core.py | 10 ++++-- hypothesis-python/src/hypothesis/errors.py | 4 +-- .../hypothesis/internal/conjecture/data.py | 6 ++-- .../src/hypothesis/internal/escalation.py | 28 +--------------- .../tests/cover/test_escalation.py | 27 --------------- .../test_explore_arbitrary_languages.py | 9 ----- requirements/coverage.txt | 4 +-- requirements/fuzzing.txt | 14 ++++---- requirements/tools.txt | 24 +++++++------- tooling/src/hypothesistooling/__main__.py | 33 ++++++++++++++----- 13 files changed, 67 insertions(+), 106 deletions(-) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d4b17d2ab..c4cdd039e2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,9 +49,12 @@ jobs: - check-py313-cover - check-py313-nocover - check-py313-niche - # - check-py314-cover - # - check-py314-nocover - # - check-py314-niche + # - check-py313t-cover + # - check-py313t-nocover + # - check-py313t-niche + - check-py314-cover + - check-py314-nocover + - check-py314-niche - check-quality ## Skip all the (inactive/old) Rust and Ruby tests pending fixes # - lint-ruby diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..e688a1472d --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +This patch updates our autoformatting tools, improving our code style without any API changes. \ No newline at end of file diff --git a/hypothesis-python/setup.py b/hypothesis-python/setup.py index f5c4bf3aec..fd8fc44eae 100644 --- a/hypothesis-python/setup.py +++ b/hypothesis-python/setup.py @@ -60,7 +60,7 @@ def local_file(name): "pytest": ["pytest>=4.6"], "dpcontracts": ["dpcontracts>=0.4"], "redis": ["redis>=3.0.0"], - "crosshair": ["hypothesis-crosshair>=0.0.4", "crosshair-tool>=0.0.55"], + "crosshair": ["hypothesis-crosshair>=0.0.6", "crosshair-tool>=0.0.58"], # zoneinfo is an odd one: every dependency is conditional, because they're # only necessary on old versions of Python or Windows systems or emscripten. "zoneinfo": [ diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py index c2767421cb..51710a2e1f 100644 --- a/hypothesis-python/src/hypothesis/core.py +++ b/hypothesis-python/src/hypothesis/core.py @@ -18,6 +18,7 @@ import math import sys import time +import traceback import types import unittest import warnings @@ -60,6 +61,7 @@ Flaky, Found, HypothesisDeprecationWarning, + HypothesisException, HypothesisWarning, InvalidArgument, NoSuchExample, @@ -86,9 +88,9 @@ from hypothesis.internal.escalation import ( InterestingOrigin, current_pytest_item, - escalate_hypothesis_internal_error, format_exception, get_trimmed_traceback, + is_hypothesis_file, ) from hypothesis.internal.healthcheck import fail_health_check from hypothesis.internal.observability import ( @@ -1071,7 +1073,11 @@ def _execute_once_for_engine(self, data: ConjectureData) -> None: except failure_exceptions_to_catch() as e: # If the error was raised by Hypothesis-internal code, re-raise it # as a fatal error instead of treating it as a test failure. - escalate_hypothesis_internal_error() + filepath = traceback.extract_tb(e.__traceback__)[-1][0] + if is_hypothesis_file(filepath) and not isinstance( + e, (HypothesisException, StopTest, UnsatisfiedAssumption) + ): + raise if data.frozen: # This can happen if an error occurred in a finally diff --git a/hypothesis-python/src/hypothesis/errors.py b/hypothesis-python/src/hypothesis/errors.py index 0d376a7493..0b2c297084 100644 --- a/hypothesis-python/src/hypothesis/errors.py +++ b/hypothesis-python/src/hypothesis/errors.py @@ -175,11 +175,9 @@ class DidNotReproduce(HypothesisException): pass -class Found(Exception): +class Found(HypothesisException): """Signal that the example matches condition. Internal use only.""" - hypothesis_internal_never_escalate = True - class RewindRecursive(Exception): """Signal that the type inference should be rewound due to recursive types. Internal use only.""" diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 7d1e010a6e..960a61cc98 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -1072,10 +1072,8 @@ def ir_value_permitted(value, ir_type, kwargs): if max_value is not None and value > max_value: return False - if (max_value is None or min_value is None) and ( - value - shrink_towards - ).bit_length() >= 128: - return False + if max_value is None or min_value is None: + return (value - shrink_towards).bit_length() < 128 return True elif ir_type == "float": diff --git a/hypothesis-python/src/hypothesis/internal/escalation.py b/hypothesis-python/src/hypothesis/internal/escalation.py index b85d9fcdc9..9c242ba0c2 100644 --- a/hypothesis-python/src/hypothesis/internal/escalation.py +++ b/hypothesis-python/src/hypothesis/internal/escalation.py @@ -18,13 +18,7 @@ from typing import Dict, NamedTuple, Optional, Type import hypothesis -from hypothesis.errors import ( - DeadlineExceeded, - HypothesisException, - StopTest, - UnsatisfiedAssumption, - _Trimmable, -) +from hypothesis.errors import _Trimmable from hypothesis.internal.compat import BaseExceptionGroup from hypothesis.utils.dynamicvariables import DynamicVariable @@ -54,31 +48,11 @@ def accept(filepath): return accept -PREVENT_ESCALATION = os.getenv("HYPOTHESIS_DO_NOT_ESCALATE") == "true" - FILE_CACHE: Dict[bytes, bool] = {} is_hypothesis_file = belongs_to(hypothesis) -HYPOTHESIS_CONTROL_EXCEPTIONS = (DeadlineExceeded, StopTest, UnsatisfiedAssumption) - - -def escalate_hypothesis_internal_error(): - if PREVENT_ESCALATION: - return - - _, e, tb = sys.exc_info() - - if getattr(e, "hypothesis_internal_never_escalate", False): - return - - filepath = None if tb is None else traceback.extract_tb(tb)[-1][0] - if is_hypothesis_file(filepath) and not isinstance( - e, (HypothesisException, *HYPOTHESIS_CONTROL_EXCEPTIONS) - ): - raise - def get_trimmed_traceback(exception=None): """Return the current traceback, minus any frames added by Hypothesis.""" diff --git a/hypothesis-python/tests/cover/test_escalation.py b/hypothesis-python/tests/cover/test_escalation.py index 2a176403f3..32744cc08e 100644 --- a/hypothesis-python/tests/cover/test_escalation.py +++ b/hypothesis-python/tests/cover/test_escalation.py @@ -18,33 +18,6 @@ from hypothesis.internal.compat import BaseExceptionGroup -def test_does_not_escalate_errors_in_non_hypothesis_file(): - try: - raise AssertionError - except AssertionError: - esc.escalate_hypothesis_internal_error() - - -def test_does_escalate_errors_in_hypothesis_file(monkeypatch): - monkeypatch.setattr(esc, "is_hypothesis_file", lambda x: True) - - with pytest.raises(AssertionError): - try: - raise AssertionError - except AssertionError: - esc.escalate_hypothesis_internal_error() - - -def test_does_not_escalate_errors_in_hypothesis_file_if_disabled(monkeypatch): - monkeypatch.setattr(esc, "is_hypothesis_file", lambda x: True) - monkeypatch.setattr(esc, "PREVENT_ESCALATION", True) - - try: - raise AssertionError - except AssertionError: - esc.escalate_hypothesis_internal_error() - - def test_is_hypothesis_file_not_confused_by_prefix(monkeypatch): # Errors in third-party extensions such as `hypothesis-trio` or # `hypothesis-jsonschema` used to be incorrectly considered to be diff --git a/hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py b/hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py index fb3338c6c2..4c83481d64 100644 --- a/hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py +++ b/hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py @@ -23,19 +23,10 @@ settings, strategies as st, ) -from hypothesis.internal import escalation as esc from hypothesis.internal.conjecture.data import Status from hypothesis.internal.conjecture.engine import ConjectureRunner -def setup_module(module): - esc.PREVENT_ESCALATION = True - - -def teardown_module(module): - esc.PREVENT_ESCALATION = False - - @attr.s() class Write: value = attr.ib() diff --git a/requirements/coverage.txt b/requirements/coverage.txt index ee217445b3..675a0ece5f 100644 --- a/requirements/coverage.txt +++ b/requirements/coverage.txt @@ -26,7 +26,7 @@ exceptiongroup==1.2.1 ; python_version < "3.11" # pytest execnet==2.1.1 # via pytest-xdist -fakeredis==2.23.2 +fakeredis==2.23.3 # via -r requirements/coverage.in iniconfig==2.0.0 # via pytest @@ -78,7 +78,7 @@ pytz==2024.1 # pandas pyyaml==6.0.1 # via libcst -redis==5.0.6 +redis==5.0.7 # via fakeredis six==1.16.0 # via python-dateutil diff --git a/requirements/fuzzing.txt b/requirements/fuzzing.txt index 93dca017fe..3f9778f325 100644 --- a/requirements/fuzzing.txt +++ b/requirements/fuzzing.txt @@ -19,7 +19,7 @@ black==24.4.2 # hypothesis blinker==1.8.2 # via flask -certifi==2024.6.2 +certifi==2024.7.4 # via requests charset-normalizer==3.3.2 # via requests @@ -50,17 +50,17 @@ exceptiongroup==1.2.1 ; python_version < "3.11" # pytest execnet==2.1.1 # via pytest-xdist -fakeredis==2.23.2 +fakeredis==2.23.3 # via -r requirements/coverage.in flask==3.0.3 # via dash hypofuzz==24.2.3 # via -r requirements/fuzzing.in -hypothesis[cli]==6.103.2 +hypothesis[cli]==6.104.2 # via hypofuzz idna==3.7 # via requests -importlib-metadata==7.2.1 +importlib-metadata==8.0.0 # via dash iniconfig==2.0.0 # via pytest @@ -138,7 +138,7 @@ pytz==2024.1 # pandas pyyaml==6.0.1 # via libcst -redis==5.0.6 +redis==5.0.7 # via fakeredis requests==2.32.3 # via @@ -157,7 +157,7 @@ sortedcontainers==2.4.0 # fakeredis # hypothesis # hypothesis (hypothesis-python/setup.py) -tenacity==8.4.1 +tenacity==8.4.2 # via plotly tomli==2.0.1 # via @@ -182,5 +182,5 @@ zipp==3.19.2 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==70.1.0 +setuptools==70.2.0 # via dash diff --git a/requirements/tools.txt b/requirements/tools.txt index 1bbe528026..6605067836 100644 --- a/requirements/tools.txt +++ b/requirements/tools.txt @@ -30,7 +30,7 @@ build==1.2.1 # via pip-tools cachetools==5.3.3 # via tox -certifi==2024.6.2 +certifi==2024.7.4 # via requests cffi==1.16.0 # via cryptography @@ -91,13 +91,13 @@ idna==3.7 # requests imagesize==1.4.1 # via sphinx -importlib-metadata==7.2.1 +importlib-metadata==8.0.0 # via # keyring # twine iniconfig==2.0.0 # via pytest -ipython==8.25.0 +ipython==8.26.0 # via -r requirements/tools.in isort==5.13.2 # via shed @@ -141,7 +141,7 @@ more-itertools==10.3.0 # via # jaraco-classes # jaraco-functools -mypy==1.10.0 +mypy==1.10.1 # via -r requirements/tools.in mypy-extensions==1.0.0 # via @@ -173,7 +173,7 @@ pexpect==4.9.0 # via ipython pip-tools==7.4.1 # via -r requirements/tools.in -pkginfo==1.11.1 +pkginfo==1.10.0 # via twine platformdirs==4.2.2 # via @@ -207,7 +207,7 @@ pyproject-hooks==1.1.0 # via # build # pip-tools -pyright==1.1.368 +pyright==1.1.370 # via -r requirements/tools.in pytest==8.2.2 # via -r requirements/tools.in @@ -242,7 +242,7 @@ rich==13.7.1 # via # pelican # twine -ruff==0.4.10 +ruff==0.5.0 # via -r requirements/tools.in secretstorage==3.3.3 # via keyring @@ -310,13 +310,13 @@ tomli==2.0.1 # pytest # sphinx # tox -tox==4.15.1 +tox==4.16.0 # via -r requirements/tools.in traitlets==5.14.3 # via # ipython # matplotlib-inline -twine==5.1.0 +twine==5.1.1 # via -r requirements/tools.in types-cffi==1.16.0.20240331 # via types-pyopenssl @@ -330,7 +330,7 @@ types-pytz==2024.1.0.20240417 # via -r requirements/tools.in types-redis==4.6.0.20240425 # via -r requirements/tools.in -types-setuptools==70.0.0.20240524 +types-setuptools==70.2.0.20240704 # via types-cffi typing-extensions==4.12.2 # via @@ -358,7 +358,7 @@ zipp==3.19.2 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -pip==24.1 +pip==24.1.1 # via pip-tools -setuptools==70.1.0 +setuptools==70.2.0 # via pip-tools diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py index f57bbb0804..95402bfadb 100644 --- a/tooling/src/hypothesistooling/__main__.py +++ b/tooling/src/hypothesistooling/__main__.py @@ -269,22 +269,35 @@ def compile_requirements(*, upgrade=False): def update_python_versions(): install.ensure_python(PYTHONS[ci_version]) - cmd = "~/.cache/hypothesis-build-runtimes/pyenv/bin/pyenv install --list" - result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE).stdout.decode() + where = os.path.expanduser("~/.cache/hypothesis-build-runtimes/pyenv/") + subprocess.run( + "git fetch && git reset --hard origin/master", + cwd=where, + shell=True, + capture_output=True, + ) + cmd = "bin/pyenv install --list" + result = subprocess.run( + cmd, shell=True, stdout=subprocess.PIPE, cwd=where + ).stdout.decode() # pyenv reports available versions in chronological order, so we keep the newest # *unless* our current ends with a digit (is stable) and the candidate does not. - stable = re.compile(r".*3\.\d+.\d+$") + # (plus some special cases for the `t` suffix for free-threading builds) + stable = re.compile(r".*3\.\d+.\d+t?$") min_minor_version = re.search( r'python_requires=">= ?3.(\d+)"', Path("hypothesis-python/setup.py").read_text(encoding="utf-8"), ).group(1) best = {} for line in map(str.strip, result.splitlines()): - if m := re.match(r"(?:pypy)?3\.(?:[789]|\d\d)", line): + if m := re.match(r"(?:pypy)?3\.(?:[789]|\d\dt?)", line): key = m.group() - if stable.match(line) or not stable.match(best.get(key, line)): - if int(key.split(".")[-1]) >= int(min_minor_version): - best[key] = line + if ( + (stable.match(line) or not stable.match(best.get(key, line))) + and int(key.split(".")[-1].rstrip("t")) >= int(min_minor_version) + and key.endswith("t") == line.endswith(("t", "t-dev")) + ): + best[key] = line if best == PYTHONS: return @@ -435,9 +448,11 @@ def run_tox(task, version, *args): "3.9": "3.9.19", "3.10": "3.10.14", "3.11": "3.11.9", - "3.12": "3.12.3", - "3.13": "3.13.0b2", + "3.12": "3.12.4", + "3.13": "3.13.0b3", + "3.13t": "3.13t-dev", "3.14": "3.14-dev", + "3.14t": "3.14t-dev", "pypy3.8": "pypy3.8-7.3.11", "pypy3.9": "pypy3.9-7.3.16", "pypy3.10": "pypy3.10-7.3.16", From f34ac1cfb3b972a070a22afcba0f40f2ed455f5a Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Thu, 4 Jul 2024 11:25:26 -0700 Subject: [PATCH 26/29] Update for Python 3.14 --- .github/workflows/main.yml | 3 +++ .../hypothesis/strategies/_internal/types.py | 17 ++++++++++------- hypothesis-python/tests/cover/test_lookup.py | 11 +++++++++-- .../tests/cover/test_sampled_from.py | 2 ++ .../tests/cover/test_type_lookup.py | 6 ++++-- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4cdd039e2..544424e61b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,6 +55,9 @@ jobs: - check-py314-cover - check-py314-nocover - check-py314-niche + # - check-py314t-cover + # - check-py314t-nocover + # - check-py314t-niche - check-quality ## Skip all the (inactive/old) Rust and Ruby tests pending fixes # - lint-ruby diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/types.py b/hypothesis-python/src/hypothesis/strategies/_internal/types.py index 11e6aa381b..8753bfb784 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/types.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/types.py @@ -514,8 +514,9 @@ def from_typing_type(thing): for T in [*union_elems, elem_type] ): mapping.pop(bytes, None) - mapping.pop(collections.abc.ByteString, None) - mapping.pop(typing.ByteString, None) + if sys.version_info[:2] <= (3, 13): + mapping.pop(collections.abc.ByteString, None) + mapping.pop(typing.ByteString, None) elif ( (not mapping) and isinstance(thing, typing.ForwardRef) @@ -699,14 +700,16 @@ def _networks(bits): # which includes this... but we don't actually ever want to build one. _global_type_lookup[os._Environ] = st.just(os.environ) +if sys.version_info[:2] <= (3, 13): + # Note: while ByteString notionally also represents the bytearray and + # memoryview types, it is a subclass of Hashable and those types are not. + # We therefore only generate the bytes type. type-ignored due to deprecation. + _global_type_lookup[typing.ByteString] = st.binary() # type: ignore + _global_type_lookup[collections.abc.ByteString] = st.binary() # type: ignore + _global_type_lookup.update( { - # Note: while ByteString notionally also represents the bytearray and - # memoryview types, it is a subclass of Hashable and those types are not. - # We therefore only generate the bytes type. type-ignored due to deprecation. - typing.ByteString: st.binary(), # type: ignore - collections.abc.ByteString: st.binary(), # type: ignore # TODO: SupportsAbs and SupportsRound should be covariant, ie have functions. typing.SupportsAbs: st.one_of( st.booleans(), diff --git a/hypothesis-python/tests/cover/test_lookup.py b/hypothesis-python/tests/cover/test_lookup.py index f0907c9275..b9231f54b1 100644 --- a/hypothesis-python/tests/cover/test_lookup.py +++ b/hypothesis-python/tests/cover/test_lookup.py @@ -59,7 +59,10 @@ # We ignore TypeVar, because it is not a Generic type: if isinstance(t, types.typing_root_type) and t != typing.TypeVar - and (sys.version_info[:2] <= (3, 11) or t != typing.ByteString) + and ( + sys.version_info[:2] <= (3, 11) + or t != getattr(typing, "ByteString", object()) + ) ), key=str, ) @@ -112,7 +115,10 @@ def test_typing_Type_Union(ex): @pytest.mark.parametrize( "typ", [ - collections.abc.ByteString, + pytest.param( + getattr(collections.abc, "ByteString", ...), + marks=pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="removed"), + ), typing.Match, typing.Pattern, re.Match, @@ -833,6 +839,7 @@ def test_bytestring_not_treated_as_generic_sequence(val): assert isinstance(x, set) +@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314") @pytest.mark.parametrize( "type_", [int, Real, object, typing.Union[int, str], typing.Union[Real, str]] ) diff --git a/hypothesis-python/tests/cover/test_sampled_from.py b/hypothesis-python/tests/cover/test_sampled_from.py index 05df230896..255ca850f2 100644 --- a/hypothesis-python/tests/cover/test_sampled_from.py +++ b/hypothesis-python/tests/cover/test_sampled_from.py @@ -10,6 +10,7 @@ import collections import enum +import sys import pytest @@ -196,6 +197,7 @@ class AnnotationsInsteadOfElements(enum.Enum): a: "int" +@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314") def test_suggests_elements_instead_of_annotations(): with pytest.raises(InvalidArgument, match="Cannot sample.*annotations.*dataclass"): check_can_generate_examples(st.sampled_from(AnnotationsInsteadOfElements)) diff --git a/hypothesis-python/tests/cover/test_type_lookup.py b/hypothesis-python/tests/cover/test_type_lookup.py index bf4a9e5c46..9d88cf42b8 100644 --- a/hypothesis-python/tests/cover/test_type_lookup.py +++ b/hypothesis-python/tests/cover/test_type_lookup.py @@ -10,6 +10,7 @@ import abc import enum +import sys from inspect import Parameter as P, Signature from typing import Callable, Dict, Generic, List, Sequence, TypeVar, Union @@ -73,6 +74,7 @@ continue +@pytest.mark.skipif(sys.version_info[:2] >= (3, 14), reason="FIXME-py314") def test_generic_sequence_of_integers_may_be_lists_or_bytes(): strat = st.from_type(Sequence[int]) find_any(strat, lambda x: isinstance(x, bytes)) @@ -292,12 +294,12 @@ def test_generic_origin_empty(): def test_issue_2951_regression(): lines_strat = st.builds(Lines, lines=st.lists(st.text())) + prev_seq_int_repr = repr(st.from_type(Sequence[int])) with temp_registered(Lines, lines_strat): assert st.from_type(Lines) == lines_strat # Now let's test that the strategy for ``Sequence[int]`` did not # change just because we registered a strategy for ``Lines``: - expected = "one_of(binary(), lists(integers()))" - assert repr(st.from_type(Sequence[int])) == expected + assert repr(st.from_type(Sequence[int])) == prev_seq_int_repr def test_issue_2951_regression_two_params(): From f1f33d827cba4010415a0d56519aafe0c7a6c91c Mon Sep 17 00:00:00 2001 From: CI on behalf of the Hypothesis team Date: Thu, 4 Jul 2024 19:42:13 +0000 Subject: [PATCH 27/29] Bump hypothesis-python version to 6.104.3 and update changelog [skip ci] --- hypothesis-python/RELEASE.rst | 3 --- hypothesis-python/docs/changes.rst | 8 ++++++++ hypothesis-python/src/hypothesis/version.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) delete mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst deleted file mode 100644 index e688a1472d..0000000000 --- a/hypothesis-python/RELEASE.rst +++ /dev/null @@ -1,3 +0,0 @@ -RELEASE_TYPE: patch - -This patch updates our autoformatting tools, improving our code style without any API changes. \ No newline at end of file diff --git a/hypothesis-python/docs/changes.rst b/hypothesis-python/docs/changes.rst index 3a55db4614..eaa3265a82 100644 --- a/hypothesis-python/docs/changes.rst +++ b/hypothesis-python/docs/changes.rst @@ -18,6 +18,14 @@ Hypothesis 6.x .. include:: ../RELEASE.rst +.. _v6.104.3: + +-------------------- +6.104.3 - 2024-07-04 +-------------------- + +This patch updates our autoformatting tools, improving our code style without any API changes. + .. _v6.104.2: -------------------- diff --git a/hypothesis-python/src/hypothesis/version.py b/hypothesis-python/src/hypothesis/version.py index 5259509771..e01ebc81cd 100644 --- a/hypothesis-python/src/hypothesis/version.py +++ b/hypothesis-python/src/hypothesis/version.py @@ -8,5 +8,5 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -__version_info__ = (6, 104, 2) +__version_info__ = (6, 104, 3) __version__ = ".".join(map(str, __version_info__)) From c96e3bf410276b82d5e5ced37fa619ad7fb8824f Mon Sep 17 00:00:00 2001 From: CI on behalf of the Hypothesis team Date: Thu, 4 Jul 2024 21:39:34 +0000 Subject: [PATCH 28/29] Bump hypothesis-python version to 6.104.4 and update changelog [skip ci] --- hypothesis-python/RELEASE.rst | 3 --- hypothesis-python/docs/changes.rst | 8 ++++++++ hypothesis-python/src/hypothesis/version.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) delete mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst deleted file mode 100644 index 43d9f7137b..0000000000 --- a/hypothesis-python/RELEASE.rst +++ /dev/null @@ -1,3 +0,0 @@ -RELEASE_TYPE: patch - -Clean up internal cache implementation. diff --git a/hypothesis-python/docs/changes.rst b/hypothesis-python/docs/changes.rst index eaa3265a82..ba00e9057b 100644 --- a/hypothesis-python/docs/changes.rst +++ b/hypothesis-python/docs/changes.rst @@ -18,6 +18,14 @@ Hypothesis 6.x .. include:: ../RELEASE.rst +.. _v6.104.4: + +-------------------- +6.104.4 - 2024-07-04 +-------------------- + +Clean up internal cache implementation. + .. _v6.104.3: -------------------- diff --git a/hypothesis-python/src/hypothesis/version.py b/hypothesis-python/src/hypothesis/version.py index e01ebc81cd..e4b9306764 100644 --- a/hypothesis-python/src/hypothesis/version.py +++ b/hypothesis-python/src/hypothesis/version.py @@ -8,5 +8,5 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -__version_info__ = (6, 104, 3) +__version_info__ = (6, 104, 4) __version__ = ".".join(map(str, __version_info__)) From 0ce1c8f75048e28c3ad2dc8dcb4483f8e91b37c1 Mon Sep 17 00:00:00 2001 From: CI on behalf of the Hypothesis team Date: Thu, 4 Jul 2024 22:03:01 +0000 Subject: [PATCH 29/29] Bump hypothesis-python version to 6.105.0 and update changelog [skip ci] --- hypothesis-python/RELEASE.rst | 5 ----- hypothesis-python/docs/changes.rst | 10 ++++++++++ hypothesis-python/src/hypothesis/version.py | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst deleted file mode 100644 index 15582e3079..0000000000 --- a/hypothesis-python/RELEASE.rst +++ /dev/null @@ -1,5 +0,0 @@ -RELEASE_TYPE: minor - -This release improves support for Django 5.0, and drops support for end-of-life Django versions (< 4.2). - -Thanks to Joshua Munn for this contribution. diff --git a/hypothesis-python/docs/changes.rst b/hypothesis-python/docs/changes.rst index ba00e9057b..8e5f292a7b 100644 --- a/hypothesis-python/docs/changes.rst +++ b/hypothesis-python/docs/changes.rst @@ -18,6 +18,16 @@ Hypothesis 6.x .. include:: ../RELEASE.rst +.. _v6.105.0: + +-------------------- +6.105.0 - 2024-07-04 +-------------------- + +This release improves support for Django 5.0, and drops support for end-of-life Django versions (< 4.2). + +Thanks to Joshua Munn for this contribution. + .. _v6.104.4: -------------------- diff --git a/hypothesis-python/src/hypothesis/version.py b/hypothesis-python/src/hypothesis/version.py index e4b9306764..d94d798ba2 100644 --- a/hypothesis-python/src/hypothesis/version.py +++ b/hypothesis-python/src/hypothesis/version.py @@ -8,5 +8,5 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -__version_info__ = (6, 104, 4) +__version_info__ = (6, 105, 0) __version__ = ".".join(map(str, __version_info__))