1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Copyright the Hypothesis Authors.
5# Individual contributors are listed in AUTHORS.rst and the git log.
6#
7# This Source Code Form is subject to the terms of the Mozilla Public License,
8# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9# obtain one at https://mozilla.org/MPL/2.0/.
10
11import os
12import sys
13import warnings
14from pathlib import Path
15from typing import Union
16
17import _hypothesis_globals
18
19from hypothesis.errors import HypothesisSideeffectWarning
20
21__hypothesis_home_directory_default = Path.cwd() / ".hypothesis"
22__hypothesis_home_directory = None
23
24
25def set_hypothesis_home_dir(directory: Union[str, Path, None]) -> None:
26 global __hypothesis_home_directory
27 __hypothesis_home_directory = None if directory is None else Path(directory)
28
29
30def storage_directory(*names: str, intent_to_write: bool = True) -> Path:
31 if intent_to_write:
32 check_sideeffect_during_initialization(
33 "accessing storage for {}", "/".join(names)
34 )
35
36 global __hypothesis_home_directory
37 if not __hypothesis_home_directory:
38 if where := os.getenv("HYPOTHESIS_STORAGE_DIRECTORY"):
39 __hypothesis_home_directory = Path(where)
40 if not __hypothesis_home_directory:
41 __hypothesis_home_directory = __hypothesis_home_directory_default
42 return __hypothesis_home_directory.joinpath(*names)
43
44
45_first_postinit_what = None
46
47
48def check_sideeffect_during_initialization(
49 what: str, *fmt_args: object, is_restart: bool = False
50) -> None:
51 """Called from locations that should not be executed during initialization, for example
52 touching disk or materializing lazy/deferred strategies from plugins. If initialization
53 is in progress, a warning is emitted.
54
55 Note that computing the repr can take nontrivial time or memory, so we avoid doing so
56 unless (and until) we're actually emitting the warning.
57 """
58 global _first_postinit_what
59 # This is not a particularly hot path, but neither is it doing productive work, so we want to
60 # minimize the cost by returning immediately. The drawback is that we require
61 # notice_initialization_restarted() to be called if in_initialization changes away from zero.
62 if _first_postinit_what is not None:
63 return
64 elif _hypothesis_globals.in_initialization > 0:
65 msg = what.format(*fmt_args)
66 if is_restart:
67 when = "between importing hypothesis and loading the hypothesis plugin"
68 elif "_hypothesis_pytestplugin" in sys.modules or os.getenv(
69 "HYPOTHESIS_EXTEND_INITIALIZATION"
70 ):
71 when = "during pytest plugin or conftest initialization"
72 else: # pragma: no cover
73 # This can be triggered by Hypothesis plugins, but is really annoying
74 # to test automatically - drop st.text().example() in hypothesis.run()
75 # to manually confirm that we get the warning.
76 when = "at import time"
77 # Note: -Werror is insufficient under pytest, as doesn't take effect until
78 # test session start.
79 text = (
80 f"Slow code in plugin: avoid {msg} {when}! Set PYTHONWARNINGS=error "
81 "to get a traceback and show which plugin is responsible."
82 )
83 if is_restart:
84 text += " Additionally, set HYPOTHESIS_EXTEND_INITIALIZATION=1 to pinpoint the exact location."
85 warnings.warn(
86 text,
87 HypothesisSideeffectWarning,
88 stacklevel=3,
89 )
90 else:
91 _first_postinit_what = (what, fmt_args)
92
93
94def notice_initialization_restarted(*, warn: bool = True) -> None:
95 """Reset _first_postinit_what, so that we don't think we're in post-init. Additionally, if it
96 was set that means that there has been a sideeffect that we haven't warned about, so do that
97 now (the warning text will be correct, and we also hint that the stacktrace can be improved).
98 """
99 global _first_postinit_what
100 if _first_postinit_what is not None:
101 what, *fmt_args = _first_postinit_what
102 _first_postinit_what = None
103 if warn:
104 check_sideeffect_during_initialization(
105 what,
106 *fmt_args,
107 is_restart=True,
108 )