diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..0e092470da --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +This release refactors internals of :class:`hypothesis.stateful.Bundle` to have a more consistent representation internally. diff --git a/hypothesis-python/setup.py b/hypothesis-python/setup.py index 9e6d2ba918..6915669514 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.14", "crosshair-tool>=0.0.72"], + "crosshair": ["hypothesis-crosshair>=0.0.14", "crosshair-tool>=0.0.73"], # 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/stateful.py b/hypothesis-python/src/hypothesis/stateful.py index 067d9c77c6..711762d252 100644 --- a/hypothesis-python/src/hypothesis/stateful.py +++ b/hypothesis-python/src/hypothesis/stateful.py @@ -196,6 +196,10 @@ def output(s): for k, v in list(data.items()): if isinstance(v, VarReference): data[k] = machine.names_to_values[v.name] + elif isinstance(v, list) and all( + isinstance(item, VarReference) for item in v + ): + data[k] = [machine.names_to_values[item.name] for item in v] label = f"execute:rule:{rule.function.__name__}" start = perf_counter() @@ -300,6 +304,10 @@ def __init__(self) -> None: def _pretty_print(self, value): if isinstance(value, VarReference): return value.name + elif isinstance(value, list) and all( + isinstance(item, VarReference) for item in value + ): + return "[" + ", ".join([item.name for item in value]) + "]" self.__stream.seek(0) self.__stream.truncate(0) self.__printer.output_width = 0 @@ -457,11 +465,8 @@ def __attrs_post_init__(self): self.arguments_strategies = {} bundles = [] for k, v in sorted(self.arguments.items()): - assert not isinstance(v, BundleReferenceStrategy) if isinstance(v, Bundle): bundles.append(v) - consume = isinstance(v, BundleConsumer) - v = BundleReferenceStrategy(v.name, consume=consume) self.arguments_strategies[k] = v self.bundles = tuple(bundles) @@ -474,26 +479,6 @@ def __repr__(self) -> str: self_strategy = st.runner() -class BundleReferenceStrategy(SearchStrategy): - def __init__(self, name: str, *, consume: bool = False): - self.name = name - self.consume = consume - - def do_draw(self, data): - machine = data.draw(self_strategy) - bundle = machine.bundle(self.name) - if not bundle: - data.mark_invalid(f"Cannot draw from empty bundle {self.name!r}") - # Shrink towards the right rather than the left. This makes it easier - # to delete data generated earlier, as when the error is towards the - # end there can be a lot of hard to remove padding. - position = data.draw_integer(0, len(bundle) - 1, shrink_towards=len(bundle)) - if self.consume: - return bundle.pop(position) # pragma: no cover # coverage is flaky here - else: - return bundle[position] - - class Bundle(SearchStrategy[Ex]): """A collection of values for use in stateful testing. @@ -518,15 +503,29 @@ class MyStateMachine(RuleBasedStateMachine): def __init__(self, name: str, *, consume: bool = False) -> None: self.name = name - self.__reference_strategy = BundleReferenceStrategy(name, consume=consume) + self.consume = consume def do_draw(self, data): machine = data.draw(self_strategy) - reference = data.draw(self.__reference_strategy) - return machine.names_to_values[reference.name] + + bundle = machine.bundle(self.name) + if not bundle: + data.mark_invalid(f"Cannot draw from empty bundle {self.name!r}") + # Shrink towards the right rather than the left. This makes it easier + # to delete data generated earlier, as when the error is towards the + # end there can be a lot of hard to remove padding. + position = data.draw_integer(0, len(bundle) - 1, shrink_towards=len(bundle)) + if self.consume: + reference = bundle.pop( + position + ) # pragma: no cover # coverage is flaky here + else: + reference = bundle[position] + + return reference def __repr__(self): - consume = self.__reference_strategy.consume + consume = self.consume if consume is False: return f"Bundle(name={self.name!r})" return f"Bundle(name={self.name!r}, {consume=})" @@ -543,11 +542,6 @@ def available(self, data): return bool(machine.bundle(self.name)) -class BundleConsumer(Bundle[Ex]): - def __init__(self, bundle: Bundle[Ex]) -> None: - super().__init__(bundle.name, consume=True) - - def consumes(bundle: Bundle[Ex]) -> SearchStrategy[Ex]: """When introducing a rule in a RuleBasedStateMachine, this function can be used to mark bundles from which each value used in a step with the @@ -563,7 +557,10 @@ def consumes(bundle: Bundle[Ex]) -> SearchStrategy[Ex]: """ if not isinstance(bundle, Bundle): raise TypeError("Argument to be consumed must be a bundle.") - return BundleConsumer(bundle) + return type(bundle)( + name=bundle.name, + consume=True, + ) @attr.s() @@ -610,7 +607,7 @@ def _convert_targets(targets, target): ) raise InvalidArgument(msg % (t, type(t))) while isinstance(t, Bundle): - if isinstance(t, BundleConsumer): + if t.consume: note_deprecation( f"Using consumes({t.name}) doesn't makes sense in this context. " "This will be an error in a future version of Hypothesis.", diff --git a/requirements/coverage.txt b/requirements/coverage.txt index 119ad34985..39d171f05a 100644 --- a/requirements/coverage.txt +++ b/requirements/coverage.txt @@ -28,7 +28,7 @@ exceptiongroup==1.2.2 ; python_version < "3.11" # pytest execnet==2.1.1 # via pytest-xdist -fakeredis==2.25.0 +fakeredis==2.25.1 # via -r requirements/coverage.in iniconfig==2.0.0 # via pytest @@ -38,7 +38,7 @@ libcst==1.4.0 # via -r requirements/coverage.in mypy-extensions==1.0.0 # via black -numpy==2.1.1 +numpy==2.1.2 # via # -r requirements/coverage.in # pandas @@ -80,7 +80,7 @@ pytz==2024.2 # pandas pyyaml==6.0.2 # via libcst -redis==5.1.0 +redis==5.1.1 # via fakeredis six==1.16.0 # via python-dateutil @@ -88,7 +88,7 @@ sortedcontainers==2.4.0 # via # fakeredis # hypothesis (hypothesis-python/setup.py) -tomli==2.0.1 +tomli==2.0.2 # via # black # coverage diff --git a/requirements/fuzzing.txt b/requirements/fuzzing.txt index ce1e3d7010..bf5ca6f177 100644 --- a/requirements/fuzzing.txt +++ b/requirements/fuzzing.txt @@ -51,13 +51,13 @@ exceptiongroup==1.2.2 ; python_version < "3.11" # pytest execnet==2.1.1 # via pytest-xdist -fakeredis==2.25.0 +fakeredis==2.25.1 # via -r requirements/coverage.in flask==3.0.3 # via dash hypofuzz==24.9.1 # via -r requirements/fuzzing.in -hypothesis[cli]==6.112.1 +hypothesis[cli]==6.112.2 # via hypofuzz idna==3.10 # via requests @@ -87,7 +87,7 @@ mypy-extensions==1.0.0 # via black nest-asyncio==1.6.0 # via dash -numpy==2.1.1 +numpy==2.1.2 # via # -r requirements/coverage.in # pandas @@ -139,7 +139,7 @@ pytz==2024.2 # pandas pyyaml==6.0.2 # via libcst -redis==5.1.0 +redis==5.1.1 # via fakeredis requests==2.32.3 # via @@ -147,7 +147,7 @@ requests==2.32.3 # hypofuzz retrying==1.3.4 # via dash -rich==13.8.1 +rich==13.9.2 # via hypothesis six==1.16.0 # via @@ -160,7 +160,7 @@ sortedcontainers==2.4.0 # hypothesis (hypothesis-python/setup.py) tenacity==9.0.0 # via plotly -tomli==2.0.1 +tomli==2.0.2 # via # black # coverage @@ -171,6 +171,7 @@ typing-extensions==4.12.2 # black # dash # fakeredis + # rich tzdata==2024.2 # via pandas urllib3==2.2.3 diff --git a/requirements/test.txt b/requirements/test.txt index ea06393011..bcbce73f98 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -32,5 +32,5 @@ pytest-xdist==3.6.1 # via -r requirements/test.in sortedcontainers==2.4.0 # via hypothesis (hypothesis-python/setup.py) -tomli==2.0.1 +tomli==2.0.2 # via pytest diff --git a/requirements/tools.txt b/requirements/tools.txt index 53e60087d7..5d18b33a31 100644 --- a/requirements/tools.txt +++ b/requirements/tools.txt @@ -97,7 +97,7 @@ importlib-metadata==8.5.0 # twine iniconfig==2.0.0 # via pytest -ipython==8.27.0 +ipython==8.28.0 # via -r requirements/tools.in isort==5.13.2 # via shed @@ -151,7 +151,7 @@ nh3==0.2.18 # via readme-renderer nodeenv==1.9.1 # via pyright -numpy==2.1.1 +numpy==2.1.2 # via -r requirements/tools.in ordered-set==4.1.0 # via pelican @@ -203,11 +203,11 @@ pygments==2.18.0 # sphinx pyproject-api==1.8.0 # via tox -pyproject-hooks==1.1.0 +pyproject-hooks==1.2.0 # via # build # pip-tools -pyright==1.1.382.post1 +pyright==1.1.383 # via -r requirements/tools.in pytest==8.3.3 # via -r requirements/tools.in @@ -238,11 +238,11 @@ restructuredtext-lint==1.4.0 # via -r requirements/tools.in rfc3986==2.0.0 # via twine -rich==13.8.1 +rich==13.9.2 # via # pelican # twine -ruff==0.6.8 +ruff==0.6.9 # via -r requirements/tools.in secretstorage==3.3.3 # via keyring @@ -299,7 +299,7 @@ stack-data==0.6.3 # via ipython tokenize-rt==6.0.0 # via pyupgrade -tomli==2.0.1 +tomli==2.0.2 # via # autoflake # black @@ -310,7 +310,7 @@ tomli==2.0.1 # pytest # sphinx # tox -tox==4.20.0 +tox==4.21.2 # via -r requirements/tools.in traitlets==5.14.3 # via @@ -324,9 +324,9 @@ types-click==7.1.8 # via -r requirements/tools.in types-pyopenssl==24.1.0.20240722 # via types-redis -types-pytz==2024.2.0.20240913 +types-pytz==2024.2.0.20241003 # via -r requirements/tools.in -types-redis==4.6.0.20240903 +types-redis==4.6.0.20241004 # via -r requirements/tools.in types-setuptools==75.1.0.20240917 # via types-cffi @@ -339,6 +339,8 @@ typing-extensions==4.12.2 # ipython # mypy # pyright + # rich + # tox unidecode==1.3.8 # via pelican urllib3==2.2.3 diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py index c6ebe01554..d0ce64bdce 100644 --- a/tooling/src/hypothesistooling/__main__.py +++ b/tooling/src/hypothesistooling/__main__.py @@ -450,8 +450,8 @@ def run_tox(task, version, *args): "3.9": "3.9.20", "3.10": "3.10.15", "3.11": "3.11.10", - "3.12": "3.12.6", - "3.13": "3.13.0rc2", + "3.12": "3.12.7", + "3.13": "3.13.0rc3", "3.13t": "3.13t-dev", "3.14": "3.14-dev", "3.14t": "3.14t-dev",