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
15
16import _hypothesis_globals
17
18from hypothesis.errors import HypothesisSideeffectWarning
19
20__hypothesis_home_directory_default = Path.cwd() / ".hypothesis"
21__hypothesis_home_directory = None
22
23
24def set_hypothesis_home_dir(directory: str | Path | None) -> None:
25 global __hypothesis_home_directory
26 __hypothesis_home_directory = None if directory is None else Path(directory)
27
28
29_GITIGNORE_STRING = """\
30# This .gitignore file was automatically created by Hypothesis. Hypothesis gitignores
31# .hypothesis by default, because we generally recommend that .hypothesis not be checked
32# into version control.
33#
34# If you *would* like to check .hypothesis into version control, you should delete this
35# file. Hypothesis will not re-create this .gitignore unless .hypothesis is deleted (and
36# if it does, that's a bug - please report it!)
37
38*
39"""
40
41
42class StorageDirectory:
43 def __init__(self, path: Path, *, home_directory: Path) -> None:
44 self.path = path
45 self.home_directory = home_directory
46
47 def create_if_missing(self) -> None:
48 # create the appropriate directory and files, if necessary.
49
50 existed_before = self.home_directory.exists()
51 self.path.mkdir(parents=True, exist_ok=True)
52 if not existed_before:
53 p = self.home_directory / ".gitignore"
54 p.write_text(_GITIGNORE_STRING, encoding="utf-8")
55
56
57def storage_directory(*names: str, intent_to_write: bool = True) -> StorageDirectory:
58 if intent_to_write:
59 check_sideeffect_during_initialization(
60 "accessing storage for {}", "/".join(names)
61 )
62
63 global __hypothesis_home_directory
64 if not __hypothesis_home_directory:
65 if where := os.getenv("HYPOTHESIS_STORAGE_DIRECTORY"):
66 __hypothesis_home_directory = Path(where)
67 if not __hypothesis_home_directory:
68 __hypothesis_home_directory = __hypothesis_home_directory_default
69 return StorageDirectory(
70 __hypothesis_home_directory.joinpath(*names),
71 home_directory=__hypothesis_home_directory,
72 )
73
74
75_first_postinit_what = None
76
77
78def check_sideeffect_during_initialization(
79 what: str, *fmt_args: object, is_restart: bool = False
80) -> None:
81 """Called from locations that should not be executed during initialization, for example
82 touching disk or materializing lazy/deferred strategies from plugins. If initialization
83 is in progress, a warning is emitted.
84
85 Note that computing the repr can take nontrivial time or memory, so we avoid doing so
86 unless (and until) we're actually emitting the warning.
87 """
88 global _first_postinit_what
89 # This is not a particularly hot path, but neither is it doing productive work, so we want to
90 # minimize the cost by returning immediately. The drawback is that we require
91 # notice_initialization_restarted() to be called if in_initialization changes away from zero.
92 if _first_postinit_what is not None:
93 return
94 elif _hypothesis_globals.in_initialization > 0:
95 msg = what.format(*fmt_args)
96 if is_restart:
97 when = "between importing hypothesis and loading the hypothesis plugin"
98 elif "_hypothesis_pytestplugin" in sys.modules or os.getenv(
99 "HYPOTHESIS_EXTEND_INITIALIZATION"
100 ):
101 when = "during pytest plugin or conftest initialization"
102 else: # pragma: no cover
103 # This can be triggered by Hypothesis plugins, but is really annoying
104 # to test automatically - drop st.text().example() in hypothesis.run()
105 # to manually confirm that we get the warning.
106 when = "at import time"
107 # Note: -Werror is insufficient under pytest, as doesn't take effect until
108 # test session start.
109 text = (
110 f"Slow code in plugin: avoid {msg} {when}! Set PYTHONWARNINGS=error "
111 "to get a traceback and show which plugin is responsible."
112 )
113 if is_restart:
114 text += " Additionally, set HYPOTHESIS_EXTEND_INITIALIZATION=1 to pinpoint the exact location."
115 warnings.warn(
116 text,
117 HypothesisSideeffectWarning,
118 stacklevel=3,
119 )
120 else:
121 _first_postinit_what = (what, fmt_args)
122
123
124def notice_initialization_restarted(*, warn: bool = True) -> None:
125 """Reset _first_postinit_what, so that we don't think we're in post-init. Additionally, if it
126 was set that means that there has been a sideeffect that we haven't warned about, so do that
127 now (the warning text will be correct, and we also hint that the stacktrace can be improved).
128 """
129 global _first_postinit_what
130 if _first_postinit_what is not None:
131 what, *fmt_args = _first_postinit_what
132 _first_postinit_what = None
133 if warn:
134 check_sideeffect_during_initialization(
135 what,
136 *fmt_args,
137 is_restart=True,
138 )