Browse Source

Intermediate changes

robot-piglet 1 year ago
parent
commit
24224c8cb7

+ 1 - 1
contrib/python/hypothesis/py3/.dist-info/METADATA

@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: hypothesis
-Version: 6.93.2
+Version: 6.94.0
 Summary: A library for property-based testing
 Home-page: https://hypothesis.works
 Author: David R. MacIver and Zac Hatfield-Dodds

+ 1 - 0
contrib/python/hypothesis/py3/.dist-info/top_level.txt

@@ -1,3 +1,4 @@
 _hypothesis_ftz_detector
+_hypothesis_globals
 _hypothesis_pytestplugin
 hypothesis

+ 28 - 0
contrib/python/hypothesis/py3/_hypothesis_globals.py

@@ -0,0 +1,28 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Copyright the Hypothesis Authors.
+# Individual contributors are listed in AUTHORS.rst and the git log.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+
+"""
+Module for globals shared between plugin(s) and the main hypothesis module, without
+depending on either. This file should have no imports outside of stdlib.
+"""
+
+import os
+
+in_initialization = 1
+"""If nonzero, indicates that hypothesis is still initializing (importing or loading
+the test environment). `import hypothesis` will cause this number to be decremented,
+and the pytest plugin increments at load time, then decrements it just before the test
+session starts. However, this leads to a hole in coverage if another pytest plugin
+imports hypothesis before our plugin is loaded. HYPOTHESIS_EXTEND_INITIALIZATION may
+be set to pre-increment the value on behalf of _hypothesis_pytestplugin, plugging the
+hole."""
+
+if os.environ.get("HYPOTHESIS_EXTEND_INITIALIZATION"):
+    in_initialization += 1

+ 25 - 0
contrib/python/hypothesis/py3/_hypothesis_pytestplugin.py

@@ -21,9 +21,12 @@ See https://github.com/HypothesisWorks/hypothesis/issues/3140 for details.
 
 import base64
 import json
+import os
 import sys
+import warnings
 from inspect import signature
 
+import _hypothesis_globals
 import pytest
 
 try:
@@ -94,6 +97,19 @@ if tuple(map(int, pytest.__version__.split(".")[:2])) < (4, 6):  # pragma: no co
     warnings.warn(PYTEST_TOO_OLD_MESSAGE % (pytest.__version__,), stacklevel=1)
 
 else:
+    # Restart side-effect detection as early as possible, to maximize coverage. We
+    # need balanced increment/decrement in configure/sessionstart to support nested
+    # pytest (e.g. runpytest_inprocess), so this early increment in effect replaces
+    # the first one in pytest_configure.
+    _configured = False
+    if not os.environ.get("HYPOTHESIS_EXTEND_INITIALIZATION"):
+        _hypothesis_globals.in_initialization += 1
+        if "hypothesis" in sys.modules:
+            # Some other plugin has imported hypothesis, so we'll check if there
+            # have been undetected side-effects and warn if so.
+            from hypothesis.configuration import notice_initialization_restarted
+
+            notice_initialization_restarted()
 
     def pytest_addoption(parser):
         group = parser.getgroup("hypothesis", "Hypothesis")
@@ -147,6 +163,12 @@ else:
         return f"hypothesis profile {settings._current_profile!r}{settings_str}"
 
     def pytest_configure(config):
+        global _configured
+        # skip first increment because we pre-incremented at import time
+        if _configured:
+            _hypothesis_globals.in_initialization += 1
+        _configured = True
+
         config.addinivalue_line("markers", "hypothesis: Tests which use hypothesis.")
         if not _any_hypothesis_option(config):
             return
@@ -407,6 +429,9 @@ else:
             if isinstance(item, pytest.Function) and is_hypothesis_test(item.obj):
                 item.add_marker("hypothesis")
 
+    def pytest_sessionstart(session):
+        _hypothesis_globals.in_initialization -= 1
+
     # Monkeypatch some internals to prevent applying @pytest.fixture() to a
     # function which has already been decorated with @hypothesis.given().
     # (the reverse case is already an explicit error in Hypothesis)

+ 5 - 0
contrib/python/hypothesis/py3/hypothesis/__init__.py

@@ -15,6 +15,8 @@ It verifies your code against a wide range of input and minimizes any
 failing examples it finds.
 """
 
+import _hypothesis_globals
+
 from hypothesis._settings import HealthCheck, Phase, Verbosity, settings
 from hypothesis.control import (
     assume,
@@ -54,3 +56,6 @@ __all__ = [
 
 run()
 del run
+
+_hypothesis_globals.in_initialization -= 1
+del _hypothesis_globals

+ 61 - 1
contrib/python/hypothesis/py3/hypothesis/configuration.py

@@ -9,8 +9,13 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import os
+import warnings
 from pathlib import Path
 
+import _hypothesis_globals
+
+from hypothesis.errors import HypothesisSideeffectWarning
+
 __hypothesis_home_directory_default = Path.cwd() / ".hypothesis"
 
 __hypothesis_home_directory = None
@@ -21,7 +26,12 @@ def set_hypothesis_home_dir(directory):
     __hypothesis_home_directory = None if directory is None else Path(directory)
 
 
-def storage_directory(*names):
+def storage_directory(*names, intent_to_write=True):
+    if intent_to_write:
+        check_sideeffect_during_initialization(
+            "accessing storage for {}", "/".join(names)
+        )
+
     global __hypothesis_home_directory
     if not __hypothesis_home_directory:
         if where := os.getenv("HYPOTHESIS_STORAGE_DIRECTORY"):
@@ -29,3 +39,53 @@ def storage_directory(*names):
     if not __hypothesis_home_directory:
         __hypothesis_home_directory = __hypothesis_home_directory_default
     return __hypothesis_home_directory.joinpath(*names)
+
+
+_first_postinit_what = None
+
+
+def check_sideeffect_during_initialization(
+    what: str, *fmt_args: object, extra: str = ""
+) -> None:
+    """Called from locations that should not be executed during initialization, for example
+    touching disk or materializing lazy/deferred strategies from plugins. If initialization
+    is in progress, a warning is emitted.
+
+    Note that computing the repr can take nontrivial time or memory, so we avoid doing so
+    unless (and until) we're actually emitting the warning.
+    """
+    global _first_postinit_what
+    # This is not a particularly hot path, but neither is it doing productive work, so we want to
+    # minimize the cost by returning immediately. The drawback is that we require
+    # notice_initialization_restarted() to be called if in_initialization changes away from zero.
+    if _first_postinit_what is not None:
+        return
+    elif _hypothesis_globals.in_initialization:
+        # Note: -Werror is insufficient under pytest, as doesn't take effect until
+        # test session start.
+        msg = what.format(*fmt_args)
+        warnings.warn(
+            f"Slow code in plugin: avoid {msg} at import time!  Set PYTHONWARNINGS=error "
+            "to get a traceback and show which plugin is responsible." + extra,
+            HypothesisSideeffectWarning,
+            stacklevel=3,
+        )
+    else:
+        _first_postinit_what = (what, fmt_args)
+
+
+def notice_initialization_restarted(*, warn: bool = True) -> None:
+    """Reset _first_postinit_what, so that we don't think we're in post-init. Additionally, if it
+    was set that means that there has been a sideeffect that we haven't warned about, so do that
+    now (the warning text will be correct, and we also hint that the stacktrace can be improved).
+    """
+    global _first_postinit_what
+    if _first_postinit_what is not None:
+        what, *fmt_args = _first_postinit_what
+        _first_postinit_what = None
+        if warn:
+            check_sideeffect_during_initialization(
+                what,
+                *fmt_args,
+                extra=" Additionally, set HYPOTHESIS_EXTEND_INITIALIZATION=1 to pinpoint the exact location.",
+            )

+ 3 - 1
contrib/python/hypothesis/py3/hypothesis/database.py

@@ -59,7 +59,7 @@ def _db_for_path(path=None):
                 "https://hypothesis.readthedocs.io/en/latest/settings.html#settings-profiles"
             )
 
-        path = storage_directory("examples")
+        path = storage_directory("examples", intent_to_write=False)
         if not _usable_dir(path):  # pragma: no cover
             warnings.warn(
                 "The database setting is not configured, and the default "
@@ -495,6 +495,8 @@ class GitHubArtifactDatabase(ExampleDatabase):
         self._initialized = True
 
     def _initialize_db(self) -> None:
+        # Trigger warning that we suppressed earlier by intent_to_write=False
+        storage_directory(self.path.name)
         # Create the cache directory if it doesn't exist
         self.path.mkdir(exist_ok=True, parents=True)
 

+ 7 - 0
contrib/python/hypothesis/py3/hypothesis/errors.py

@@ -117,6 +117,13 @@ class HypothesisDeprecationWarning(HypothesisWarning, FutureWarning):
     """
 
 
+class HypothesisSideeffectWarning(HypothesisWarning):
+    """A warning issued by Hypothesis when it sees actions that are
+    discouraged at import or initialization time because they are
+    slow or have user-visible side effects.
+    """
+
+
 class Frozen(HypothesisException):
     """Raised when a mutation method has been called on a ConjectureData object
     after freeze() has been called."""

+ 16 - 4
contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py

@@ -55,6 +55,7 @@ import attr
 from hypothesis._settings import note_deprecation
 from hypothesis.control import cleanup, current_build_context, note
 from hypothesis.errors import (
+    HypothesisSideeffectWarning,
     HypothesisWarning,
     InvalidArgument,
     ResolutionFailed,
@@ -2196,14 +2197,25 @@ def register_type_strategy(
             f"{custom_type=} is not allowed to be registered, "
             f"because there is no such thing as a runtime instance of {custom_type!r}"
         )
-    elif not (isinstance(strategy, SearchStrategy) or callable(strategy)):
+    if not (isinstance(strategy, SearchStrategy) or callable(strategy)):
         raise InvalidArgument(
             f"{strategy=} must be a SearchStrategy, or a function that takes "
             "a generic type and returns a specific SearchStrategy"
         )
-    elif isinstance(strategy, SearchStrategy) and strategy.is_empty:
-        raise InvalidArgument(f"{strategy=} must not be empty")
-    elif types.has_type_arguments(custom_type):
+    if isinstance(strategy, SearchStrategy):
+        with warnings.catch_warnings():
+            warnings.simplefilter("error", HypothesisSideeffectWarning)
+
+            # Calling is_empty forces materialization of lazy strategies. If this is done at import
+            # time, lazy strategies will warn about it; here, we force that warning to raise to
+            # avoid the materialization. Ideally, we'd just check if the strategy is lazy, but the
+            # lazy strategy may be wrapped underneath another strategy so that's complicated.
+            try:
+                if strategy.is_empty:
+                    raise InvalidArgument(f"{strategy=} must not be empty")
+            except HypothesisSideeffectWarning:  # pragma: no cover
+                pass
+    if types.has_type_arguments(custom_type):
         raise InvalidArgument(
             f"Cannot register generic type {custom_type!r}, because it has type "
             "arguments which would not be handled.  Instead, register a function "

+ 3 - 0
contrib/python/hypothesis/py3/hypothesis/strategies/_internal/deferred.py

@@ -10,6 +10,7 @@
 
 import inspect
 
+from hypothesis.configuration import check_sideeffect_during_initialization
 from hypothesis.errors import InvalidArgument
 from hypothesis.internal.reflection import get_pretty_function_description
 from hypothesis.strategies._internal.strategies import SearchStrategy, check_strategy
@@ -27,6 +28,8 @@ class DeferredStrategy(SearchStrategy):
     @property
     def wrapped_strategy(self):
         if self.__wrapped_strategy is None:
+            check_sideeffect_during_initialization("deferred evaluation of {!r}", self)
+
             if not inspect.isfunction(self.__definition):
                 raise InvalidArgument(
                     f"Expected definition to be a function but got {self.__definition!r} "

Some files were not shown because too many files changed in this diff