Skip to content

Commit

Permalink
Defer plugin imports
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Nov 22, 2021
1 parent 1f1c11b commit 7428c52
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 44 deletions.
2 changes: 1 addition & 1 deletion hypothesis-python/.coveragerc
@@ -1,7 +1,7 @@
[run]
branch = True
omit =
**/extra/pytestplugin.py
**/_hypothesis_pytestplugin.py
**/extra/array_api.py
**/extra/cli.py
**/extra/django/*.py
Expand Down
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
@@ -0,0 +1,5 @@
RELEASE_TYPE: minor

This release modifies our :pypi:`pytest` plugin, to avoid importing Hypothesis
and therefore triggering :ref:`Hypothesis' entry points <entry-points>` for
test suites where Hypothesis is installed but not actually used (:issue:`3140`).
5 changes: 3 additions & 2 deletions hypothesis-python/docs/strategies.rst
Expand Up @@ -128,13 +128,14 @@ test is using Hypothesis:
.. _entry-points:

--------------------------------------------------
Registering strategies via setuptools entry points
Hypothesis integration via setuptools entry points
--------------------------------------------------

If you would like to ship Hypothesis strategies for a custom type - either as
part of the upstream library, or as a third-party extension, there's a catch:
:func:`~hypothesis.strategies.from_type` only works after the corresponding
call to :func:`~hypothesis.strategies.register_type_strategy`. This means that
call to :func:`~hypothesis.strategies.register_type_strategy`, and you'll have
the same problem with :func:`~hypothesis.register_random`. This means that
either

- you have to try importing Hypothesis to register the strategy when *your*
Expand Down
3 changes: 2 additions & 1 deletion hypothesis-python/setup.py
Expand Up @@ -129,8 +129,9 @@ def local_file(name):
"Topic :: Software Development :: Testing",
"Typing :: Typed",
],
py_modules=["_hypothesis_pytestplugin"],
entry_points={
"pytest11": ["hypothesispytest = hypothesis.extra.pytestplugin"],
"pytest11": ["hypothesispytest = _hypothesis_pytestplugin"],
"console_scripts": ["hypothesis = hypothesis.extra.cli:main"],
},
long_description=open(README).read(),
Expand Down
Expand Up @@ -14,33 +14,32 @@
# END HEADER

import base64
import sys
from inspect import signature

import pytest

from hypothesis import HealthCheck, Phase, Verbosity, core, settings
from hypothesis.errors import InvalidArgument
from hypothesis.internal.detection import is_hypothesis_test
from hypothesis.internal.escalation import current_pytest_item
from hypothesis.internal.healthcheck import fail_health_check
from hypothesis.reporting import default as default_reporter, with_reporter
from hypothesis.statistics import collector, describe_statistics

LOAD_PROFILE_OPTION = "--hypothesis-profile"
VERBOSITY_OPTION = "--hypothesis-verbosity"
PRINT_STATISTICS_OPTION = "--hypothesis-show-statistics"
SEED_OPTION = "--hypothesis-seed"
EXPLAIN_OPTION = "--hypothesis-explain"

_VERBOSITY_NAMES = ["quiet", "normal", "verbose", "debug"]


class StoringReporter:
def __init__(self, config):
assert "hypothesis" in sys.modules
from hypothesis.reporting import default

self.report = default
self.config = config
self.results = []

def __call__(self, msg):
if self.config.getoption("capture", "fd") == "no":
default_reporter(msg)
self.report(msg)
if not isinstance(msg, str):
msg = repr(msg)
self.results.append(msg)
Expand All @@ -51,15 +50,13 @@ def __call__(self, msg):
if tuple(map(int, pytest.__version__.split(".")[:2])) < (4, 6): # pragma: no cover
import warnings

from hypothesis.errors import HypothesisWarning

PYTEST_TOO_OLD_MESSAGE = """
You are using pytest version %s. Hypothesis tests work with any test
runner, but our pytest plugin requires pytest 4.6 or newer.
Note that the pytest developers no longer support your version either!
Disabling the Hypothesis pytest plugin...
"""
warnings.warn(PYTEST_TOO_OLD_MESSAGE % (pytest.__version__,), HypothesisWarning)
warnings.warn(PYTEST_TOO_OLD_MESSAGE % (pytest.__version__,))

else:

Expand All @@ -73,7 +70,7 @@ def pytest_addoption(parser):
group.addoption(
VERBOSITY_OPTION,
action="store",
choices=[opt.name for opt in Verbosity],
choices=_VERBOSITY_NAMES,
help="Override profile with verbosity setting specified",
)
group.addoption(
Expand All @@ -95,52 +92,74 @@ def pytest_addoption(parser):
)

def pytest_report_header(config):
if config.option.verbose < 1 and settings.default.verbosity < Verbosity.verbose:
return None
profile = config.getoption(LOAD_PROFILE_OPTION)
if not profile:
profile = settings._current_profile
settings_str = settings.get_profile(profile).show_changed()
if settings_str != "":
settings_str = f" -> {settings_str}"
return f"hypothesis profile {profile!r}{settings_str}"
if profile or (config.option.verbose >= 1 and "hypothesis" in sys.modules):
from hypothesis import settings

if not profile:
profile = settings._current_profile
settings_str = settings.get_profile(profile).show_changed()
if settings_str != "":
settings_str = f" -> {settings_str}"
return f"hypothesis profile: {profile!r}{settings_str}"

def pytest_configure(config):
core.running_under_pytest = True
config.addinivalue_line("markers", "hypothesis: Tests which use hypothesis.")

profile = config.getoption(LOAD_PROFILE_OPTION)
if profile:
from hypothesis import Phase, Verbosity, core, settings

settings.load_profile(profile)

verbosity_name = config.getoption(VERBOSITY_OPTION)
if verbosity_name and verbosity_name != settings.default.verbosity.name:
verbosity_value = Verbosity[verbosity_name]
name = f"{settings._current_profile}-with-{verbosity_name}-verbosity"
# register_profile creates a new profile, exactly like the current one,
# with the extra values given (in this case 'verbosity')
settings.register_profile(name, verbosity=verbosity_value)
settings.load_profile(name)
if (
config.getoption(EXPLAIN_OPTION)
and Phase.explain not in settings.default.phases
):
name = f"{settings._current_profile}-with-explain-phase"
phases = settings.default.phases + (Phase.explain,)
settings.register_profile(name, phases=phases)
settings.load_profile(name)
if verbosity_name:
from hypothesis import Verbosity, settings

if verbosity_name != settings.default.verbosity.name:
verbosity_value = Verbosity[verbosity_name]
name = f"{settings._current_profile}-with-{verbosity_name}-verbosity"
# register_profile creates a new profile, exactly like the current one,
# with the extra values given (in this case 'verbosity')
settings.register_profile(name, verbosity=verbosity_value)
settings.load_profile(name)

if config.getoption(EXPLAIN_OPTION):
from hypothesis import Phase, settings

if Phase.explain not in settings.default.phases:
name = f"{settings._current_profile}-with-explain-phase"
phases = settings.default.phases + (Phase.explain,)
settings.register_profile(name, phases=phases)
settings.load_profile(name)

seed = config.getoption(SEED_OPTION)
if seed is not None:
try:
seed = int(seed)
except ValueError:
pass
from hypothesis import core

core.global_force_seed = seed
config.addinivalue_line("markers", "hypothesis: Tests which use hypothesis.")

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
if not hasattr(item, "obj"):
if not (hasattr(item, "obj") and "hypothesis" in sys.modules):
yield
elif not is_hypothesis_test(item.obj):
return

from hypothesis import HealthCheck, core, settings
from hypothesis.errors import InvalidArgument
from hypothesis.internal.detection import is_hypothesis_test
from hypothesis.internal.escalation import current_pytest_item
from hypothesis.internal.healthcheck import fail_health_check
from hypothesis.reporting import with_reporter
from hypothesis.statistics import collector, describe_statistics

core.running_under_pytest = True

if not is_hypothesis_test(item.obj):
# If @given was not applied, check whether other hypothesis
# decorators were applied, and raise an error if they were.
if getattr(item.obj, "is_hypothesis_strategy_function", False):
Expand Down Expand Up @@ -273,6 +292,11 @@ def report(properties):
report(global_properties)

def pytest_collection_modifyitems(items):
if "hypothesis" not in sys.modules:
return

from hypothesis.internal.detection import is_hypothesis_test

for item in items:
if isinstance(item, pytest.Function) and is_hypothesis_test(item.obj):
item.add_marker("hypothesis")
Expand Down
13 changes: 13 additions & 0 deletions hypothesis-python/tests/pytest/test_pytest_detection.py
Expand Up @@ -33,3 +33,16 @@ def test_is_not_running_under_pytest(tmpdir):
pyfile = tmpdir.join("test.py")
pyfile.write(FILE_TO_RUN)
subprocess.check_call([sys.executable, str(pyfile)])


DOES_NOT_IMPORT_HYPOTHESIS = """
import sys
def test_pytest_plugin_does_not_import_hypothesis():
assert "hypothesis" not in sys.modules
"""


def test_plugin_does_not_import_pytest(pytester):
pytester.makepyfile(DOES_NOT_IMPORT_HYPOTHESIS)
pytester.runpytest().assert_outcomes(passed=1)

0 comments on commit 7428c52

Please sign in to comment.