Skip to content

Commit

Permalink
Merge pull request #2058 from Zac-HD/settings-reprs
Browse files Browse the repository at this point in the history
Clarify settings reprs and refactor decoration logic
  • Loading branch information
Zac-HD committed Aug 4, 2019
2 parents a89af2d + c66a4d6 commit 5b598b0
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 46 deletions.
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RELEASE_TYPE: patch

This patch tidies up the repr of several ``settings``-related objects,
at runtime and in the documentation, and deprecates the undocumented
edge case that ``phases=None`` was treated like ``phases=tuple(Phase)``.
1 change: 1 addition & 0 deletions hypothesis-python/docs/healthchecks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Using a value of ``HealthCheck.all()`` will disable all health checks.
.. autoclass:: HealthCheck
:undoc-members:
:inherited-members:
:exclude-members: all


.. _deprecation-policy:
Expand Down
75 changes: 31 additions & 44 deletions hypothesis-python/src/hypothesis/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def __doc__(self):
return "\n\n".join(
[
description,
"default value: %s" % (default,),
"default value: ``%s``" % (default,),
(deprecation_message or "").strip(),
]
).strip()
Expand Down Expand Up @@ -149,6 +149,7 @@ class settings(settingsMeta("settings", (object,), {})): # type: ignore
_WHITELISTED_REAL_PROPERTIES = ["_construction_complete", "storage"]
__definitions_are_locked = False
_profiles = {} # type: dict
__module__ = "hypothesis"

def __getattr__(self, name):
if name in all_settings:
Expand Down Expand Up @@ -196,23 +197,12 @@ def __init__(self, parent=None, **kwargs):
def __call__(self, test):
"""Make the settings object (self) an attribute of the test.
The settings are later discovered by looking them up on the test
itself.
Also, we want to issue a deprecation warning for settings used alone
(without @given) so, note the deprecation in the new test, but also
attach the version without the warning as an attribute, so that @given
can unwrap it (since if @given is used, that means we don't want the
deprecation warning).
When it's time to turn the warning into an error, we'll raise an
exception instead of calling note_deprecation (and can delete
"test(*args, **kwargs)").
The settings are later discovered by looking them up on the test itself.
"""
if not callable(test):
raise InvalidArgument(
"settings objects can be called as a decorator with @given, "
"but test=%r" % (test,)
"but decorated test=%r is not callable." % (test,)
)
if inspect.isclass(test):
from hypothesis.stateful import GenericStateMachine
Expand All @@ -234,6 +224,8 @@ def __call__(self, test):
"functions, or on subclasses of GenericStateMachine."
)
if hasattr(test, "_hypothesis_internal_settings_applied"):
# Can't use _hypothesis_internal_use_settings as an indicator that
# @settings was applied, because @given also assigns that attribute.
raise InvalidArgument(
"%s has already been decorated with a settings object."
"\n Previous: %r\n This: %r"
Expand All @@ -245,29 +237,26 @@ def __call__(self, test):
)

test._hypothesis_internal_use_settings = self

# For double-@settings check:
test._hypothesis_internal_settings_applied = True
if getattr(test, "is_hypothesis_test", False):
return test

@proxies(test)
def new_test(*args, **kwargs):
"""@given has not been applied to `test`, so we replace it with this
wrapper so that using *only* @settings is an error.
We then attach the actual test as an attribute of this function, so
that we can unwrap it if @given is applied after the settings decorator.
"""
raise InvalidArgument(
"Using `@settings` on a test without `@given` is completely pointless."
)

# @given will get the test from this attribution (rather than use the
# version with the deprecation warning)
new_test._hypothesis_internal_test_function_without_warning = test

# This means @given has been applied, so we don't need to worry about
# warning for @settings alone.
has_given_applied = getattr(test, "is_hypothesis_test", False)
test_to_use = test if has_given_applied else new_test
test_to_use._hypothesis_internal_use_settings = self
# Can't use _hypothesis_internal_use_settings as an indicator that
# @settings was applied, because @given also assigns that attribute.
test._hypothesis_internal_settings_applied = True
return test_to_use
new_test._hypothesis_internal_use_settings = self
new_test._hypothesis_internal_settings_applied = True
return new_test

@classmethod
def _define_setting(
Expand Down Expand Up @@ -515,8 +504,8 @@ def _validate_database(db):
show_default=False,
description="""
An instance of hypothesis.database.ExampleDatabase that will be
used to save examples to and load previous examples from. May be None
in which case no storage will be used, `:memory:` for an in-memory
used to save examples to and load previous examples from. May be ``None``
in which case no storage will be used, ``":memory:"`` for an in-memory
database, or any path for a directory-based example database.
""",
validator=_validate_database,
Expand All @@ -530,6 +519,9 @@ class Phase(IntEnum):
generate = 2
shrink = 3

def __repr__(self):
return "Phase.%s" % (self.name,)


@unique
class HealthCheck(Enum):
Expand Down Expand Up @@ -574,14 +566,7 @@ def all(cls):

not_a_test_method = 8
"""Checks if :func:`@given <hypothesis.given>` has been applied to a
method of :class:`python:unittest.TestCase`."""


@unique
class Statistics(IntEnum):
never = 0
interesting = 1
always = 2
method defined by :class:`python:unittest.TestCase` (i.e. not a test)."""


@unique
Expand All @@ -605,28 +590,29 @@ def __repr__(self):

def _validate_phases(phases):
if phases is None:
return tuple(Phase)
phases = tuple(Phase)
note_deprecation("Use phases=%r, not None." % (phases,), since="RELEASEDAY")
phases = tuple(phases)
for a in phases:
if not isinstance(a, Phase):
raise InvalidArgument("%r is not a valid phase" % (a,))
return phases
return tuple(p for p in list(Phase) if p in phases)


settings._define_setting(
"phases",
default=tuple(Phase),
description=(
"Control which phases should be run. "
+ "See :ref:`the full documentation for more details <phases>`"
"See :ref:`the full documentation for more details <phases>`"
),
validator=_validate_phases,
)

settings._define_setting(
name="stateful_step_count",
default=50,
validator=lambda x: _ensure_positive_int(x, "stateful_step_count", "RELEASEDAY"),
validator=lambda x: _ensure_positive_int(x, "stateful_step_count", "2019-03-06"),
description="""
Number of steps to run a stateful program for before giving up on it breaking.
""",
Expand Down Expand Up @@ -665,7 +651,7 @@ def validate_health_check_suppressions(suppressions):
settings._define_setting(
"suppress_health_check",
default=(),
description="""A list of health checks to disable.""",
description="""A list of :class:`~hypothesis.HealthCheck` items to disable.""",
validator=validate_health_check_suppressions,
)

Expand All @@ -674,7 +660,8 @@ class duration(datetime.timedelta):
"""A timedelta specifically measured in milliseconds."""

def __repr__(self):
return "timedelta(milliseconds=%r)" % (self.total_seconds() * 1000,)
ms = self.total_seconds() * 1000
return "timedelta(milliseconds=%r)" % (int(ms) if ms == int(ms) else ms,)


def _validate_deadline(x):
Expand Down
6 changes: 4 additions & 2 deletions hypothesis-python/src/hypothesis/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,10 @@ def __init__(self, engine):
)

self.events = [
"%.2f%%, %s" % (c / engine.call_count * 100, e)
for e, c in sorted(engine.event_call_counts.items(), key=lambda x: -x[1])
"%6.2f%%, %s" % (c / engine.call_count * 100, e)
for e, c in sorted(
engine.event_call_counts.items(), key=lambda x: (-x[1], x[0])
)
]

total_runtime = math.fsum(engine.all_runtimes)
Expand Down
17 changes: 17 additions & 0 deletions hypothesis-python/tests/cover/test_phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from hypothesis import Phase, example, given, settings
from hypothesis.database import ExampleDatabase, InMemoryExampleDatabase
from hypothesis.errors import InvalidArgument
from tests.common.utils import checks_deprecated_behaviour


@example(11)
Expand All @@ -45,7 +46,23 @@ def test_this_would_fail_if_you_ran_it(b):
assert False


@pytest.mark.parametrize(
"arg,expected",
[
(tuple(Phase)[::-1], tuple(Phase)),
([Phase.explicit, Phase.explicit], (Phase.explicit,)),
],
)
def test_sorts_and_dedupes_phases(arg, expected):
assert settings(phases=arg).phases == expected


def test_phases_default_to_all():
assert settings().phases == tuple(Phase)


@checks_deprecated_behaviour
def test_phases_none_equals_all():
assert settings(phases=None).phases == tuple(Phase)


Expand Down

0 comments on commit 5b598b0

Please sign in to comment.