Skip to content

Commit

Permalink
Merge pull request #2882 from Zac-HD/rule-xor-invariant
Browse files Browse the repository at this point in the history
Allow `@rule()` xor `@invariant()` decorators on rule-based state machine methods.
  • Loading branch information
Zac-HD authored Mar 7, 2021
2 parents 2823ad6 + e4dbf99 commit ad25f58
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 0 deletions.
7 changes: 7 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
RELEASE_TYPE: minor

This release makes it an explicit error to apply :func:`~hypothesis.stateful.invariant`
to a :func:`~hypothesis.stateful.rule` or :func:`~hypothesis.stateful.initialize` rule
in :doc:`stateful testing <stateful>`. Such a combination had unclear semantics,
especially in combination with :func:`~hypothesis.stateful.precondition`, and was never
meant to be allowed (:issue:`2681`).
15 changes: 15 additions & 0 deletions hypothesis-python/src/hypothesis/stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,11 @@ def rule(*, targets=(), target=None, **kwargs):
check_strategy(v, name=k)

def accept(f):
if getattr(f, INVARIANT_MARKER, None):
raise InvalidDefinition(
"A function cannot be used for both a rule and an invariant.",
Settings.default,
)
existing_rule = getattr(f, RULE_MARKER, None)
existing_initialize_rule = getattr(f, INITIALIZE_RULE_MARKER, None)
if existing_rule is not None or existing_initialize_rule is not None:
Expand Down Expand Up @@ -576,6 +581,11 @@ def initialize(*, targets=(), target=None, **kwargs):
check_strategy(v, name=k)

def accept(f):
if getattr(f, INVARIANT_MARKER, None):
raise InvalidDefinition(
"A function cannot be used for both a rule and an invariant.",
Settings.default,
)
existing_rule = getattr(f, RULE_MARKER, None)
existing_initialize_rule = getattr(f, INITIALIZE_RULE_MARKER, None)
if existing_rule is not None or existing_initialize_rule is not None:
Expand Down Expand Up @@ -689,6 +699,11 @@ def is_nonzero(self):
"""

def accept(f):
if getattr(f, RULE_MARKER, None) or getattr(f, INITIALIZE_RULE_MARKER, None):
raise InvalidDefinition(
"A function cannot be used for both a rule and an invariant.",
Settings.default,
)
existing_invariant = getattr(f, INVARIANT_MARKER, None)
if existing_invariant is not None:
raise InvalidDefinition(
Expand Down
26 changes: 26 additions & 0 deletions hypothesis-python/tests/cover/test_stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,32 @@ def do_stuff(self):
run_state_machine_as_test(Invariant)


@pytest.mark.parametrize(
"decorators",
[
(invariant(), rule()),
(rule(), invariant()),
(invariant(), initialize()),
(initialize(), invariant()),
(invariant(), precondition(lambda self: True), rule()),
(rule(), precondition(lambda self: True), invariant()),
(precondition(lambda self: True), invariant(), rule()),
(precondition(lambda self: True), rule(), invariant()),
],
ids=lambda x: "-".join(f.__qualname__.split(".")[0] for f in x),
)
def test_invariant_and_rule_are_incompatible(decorators):
"""It's an error to apply @invariant and @rule to the same method."""

def method(self):
pass

for d in decorators[:-1]:
method = d(method)
with pytest.raises(InvalidDefinition):
decorators[-1](method)


def test_invalid_rule_argument():
"""Rule kwargs that are not a Strategy are expected to raise an InvalidArgument error."""
with pytest.raises(InvalidArgument):
Expand Down

0 comments on commit ad25f58

Please sign in to comment.