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 23, 2021
1 parent 2fa6d0a commit a087245
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 29 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
8 changes: 8 additions & 0 deletions 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 <entry-points>` for
test suites where Hypothesis is installed but not actually used (:issue:`3140`).

If you :ref:`manually load the plugin <disabling-pytest-plugin>`, you'll need
to update the module name accordingly.
9 changes: 6 additions & 3 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 Expand Up @@ -188,6 +189,8 @@ And that's all it takes!
package to be installed.


.. _disabling-pytest-plugin:

Interaction with :pypi:`pytest-cov`
-----------------------------------

Expand All @@ -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" ...
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,39 @@
# 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"]
_ALL_OPTIONS = [
LOAD_PROFILE_OPTION,
VERBOSITY_OPTION,
PRINT_STATISTICS_OPTION,
SEED_OPTION,
EXPLAIN_OPTION,
]


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 +57,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 +77,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 @@ -94,19 +98,31 @@ def pytest_addoption(parser):
default=False,
)

def _any_hypothesis_option(config):
return bool(any(config.getoption(opt) for opt in _ALL_OPTIONS))

def pytest_report_header(config):
if not (
config.option.verbose >= 1
or "hypothesis" in sys.modules
or _any_hypothesis_option(config)
):
return None

from hypothesis import Verbosity, settings

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()
settings_str = settings.default.show_changed()
if settings_str != "":
settings_str = f" -> {settings_str}"
return f"hypothesis profile {profile!r}{settings_str}"
return f"hypothesis profile {settings._current_profile!r}{settings_str}"

def pytest_configure(config):
core.running_under_pytest = True
if not _any_hypothesis_option(config):
return
from hypothesis import Phase, Verbosity, core, settings

profile = config.getoption(LOAD_PROFILE_OPTION)
if profile:
settings.load_profile(profile)
Expand Down Expand Up @@ -138,9 +154,21 @@ def pytest_configure(config):

@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 +301,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
2 changes: 1 addition & 1 deletion hypothesis-python/tests/pytest/test_profiles.py
Expand Up @@ -14,8 +14,8 @@
# END HEADER

import pytest
from _hypothesis_pytestplugin import LOAD_PROFILE_OPTION

from hypothesis.extra.pytestplugin import LOAD_PROFILE_OPTION
from hypothesis.version import __version__

pytest_plugins = "pytester"
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(testdir):
testdir.makepyfile(DOES_NOT_IMPORT_HYPOTHESIS)
testdir.runpytest_subprocess().assert_outcomes(passed=1)
3 changes: 1 addition & 2 deletions hypothesis-python/tests/pytest/test_statistics.py
Expand Up @@ -16,8 +16,7 @@
from distutils.version import LooseVersion

import pytest

from hypothesis.extra.pytestplugin import PRINT_STATISTICS_OPTION
from _hypothesis_pytestplugin import PRINT_STATISTICS_OPTION

pytest_plugins = "pytester"

Expand Down

0 comments on commit a087245

Please sign in to comment.