diff --git a/hypothesis-python/.coveragerc b/hypothesis-python/.coveragerc index ef37f8d774..baa88b2536 100644 --- a/hypothesis-python/.coveragerc +++ b/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 diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..5bc62a7a9c --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,8 @@ +RELEASE_TYPE: minor + +This release modifies our :pypi:`pytest` plugin, to avoid importing Hypothesis +and therefore triggering :ref:`Hypothesis' entry points ` for +test suites where Hypothesis is installed but not actually used (:issue:`3140`). + +If you :ref:`manually load the plugin `, you'll need +to update the module name accordingly. diff --git a/hypothesis-python/docs/strategies.rst b/hypothesis-python/docs/strategies.rst index 781e5f713e..25b76a3455 100644 --- a/hypothesis-python/docs/strategies.rst +++ b/hypothesis-python/docs/strategies.rst @@ -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* @@ -188,6 +189,8 @@ And that's all it takes! package to be installed. +.. _disabling-pytest-plugin: + Interaction with :pypi:`pytest-cov` ----------------------------------- @@ -201,5 +204,5 @@ opting out of the pytest plugin entirely. Alternatively, you can ensure that Hy is loaded after coverage measurement is started by disabling the entrypoint, and loading our pytest plugin from your ``conftest.py`` instead:: - echo "pytest_plugins = ['hypothesis.extra.pytestplugin']\n" > tests/conftest.py + echo "pytest_plugins = ['_hypothesis_pytestplugin']\n" > tests/conftest.py pytest -p "no:hypothesispytest" ... diff --git a/hypothesis-python/setup.py b/hypothesis-python/setup.py index 32a5817bfd..6a73a57285 100644 --- a/hypothesis-python/setup.py +++ b/hypothesis-python/setup.py @@ -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(), diff --git a/hypothesis-python/src/hypothesis/extra/pytestplugin.py b/hypothesis-python/src/_hypothesis_pytestplugin.py similarity index 81% rename from hypothesis-python/src/hypothesis/extra/pytestplugin.py rename to hypothesis-python/src/_hypothesis_pytestplugin.py index 767e2a3651..58fb8cbd5f 100644 --- a/hypothesis-python/src/hypothesis/extra/pytestplugin.py +++ b/hypothesis-python/src/_hypothesis_pytestplugin.py @@ -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) @@ -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: @@ -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( @@ -95,37 +92,46 @@ 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: @@ -133,14 +139,27 @@ def pytest_configure(config): 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): @@ -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") diff --git a/hypothesis-python/tests/pytest/test_profiles.py b/hypothesis-python/tests/pytest/test_profiles.py index 4150b6a605..bfcf9f8423 100644 --- a/hypothesis-python/tests/pytest/test_profiles.py +++ b/hypothesis-python/tests/pytest/test_profiles.py @@ -15,7 +15,7 @@ import pytest -from hypothesis.extra.pytestplugin import LOAD_PROFILE_OPTION +from _hypothesis_pytestplugin import LOAD_PROFILE_OPTION from hypothesis.version import __version__ pytest_plugins = "pytester" diff --git a/hypothesis-python/tests/pytest/test_pytest_detection.py b/hypothesis-python/tests/pytest/test_pytest_detection.py index 9319d30c82..278adc8ba4 100644 --- a/hypothesis-python/tests/pytest/test_pytest_detection.py +++ b/hypothesis-python/tests/pytest/test_pytest_detection.py @@ -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) diff --git a/hypothesis-python/tests/pytest/test_statistics.py b/hypothesis-python/tests/pytest/test_statistics.py index 73dc3ab1a1..b74584f8fc 100644 --- a/hypothesis-python/tests/pytest/test_statistics.py +++ b/hypothesis-python/tests/pytest/test_statistics.py @@ -17,7 +17,7 @@ import pytest -from hypothesis.extra.pytestplugin import PRINT_STATISTICS_OPTION +from _hypothesis_pytestplugin import PRINT_STATISTICS_OPTION pytest_plugins = "pytester"