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 codecs
12import copy
13import dataclasses
14import inspect
15import itertools
16import platform
17import sys
18import sysconfig
19import typing
20from functools import partial
21from typing import (
22 TYPE_CHECKING,
23 Any,
24 ForwardRef,
25 Optional,
26 get_args,
27)
28
29if TYPE_CHECKING:
30 from typing_extensions import override as override
31
32 from hypothesis.internal.conjecture.engine import ConjectureRunner
33else:
34 try:
35 from typing import (
36 override as override,
37 )
38 except ImportError:
39 try:
40 from typing_extensions import (
41 override as override,
42 )
43 except ImportError:
44 override = lambda f: f
45
46PYPY = platform.python_implementation() == "PyPy"
47GRAALPY = platform.python_implementation() == "GraalVM"
48WINDOWS = platform.system() == "Windows"
49# First defined in CPython 3.13, defaults to False
50FREE_THREADED_CPYTHON = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
51
52
53def add_note(exc, note):
54 try:
55 exc.add_note(note)
56 except AttributeError:
57 pass # give up, might be e.g. a frozen dataclass
58
59
60def escape_unicode_characters(s: str) -> str:
61 return codecs.encode(s, "unicode_escape").decode("ascii")
62
63
64def int_from_bytes(data: bytes | bytearray) -> int:
65 return int.from_bytes(data, "big")
66
67
68def int_to_bytes(i: int, size: int) -> bytes:
69 return i.to_bytes(size, "big")
70
71
72def int_to_byte(i: int) -> bytes:
73 return bytes([i])
74
75
76def is_typed_named_tuple(cls: type) -> bool:
77 """Return True if cls is probably a subtype of `typing.NamedTuple`.
78
79 Unfortunately types created with `class T(NamedTuple):` actually
80 subclass `tuple` directly rather than NamedTuple. This is annoying,
81 and means we just have to hope that nobody defines a different tuple
82 subclass with similar attributes.
83 """
84 return (
85 issubclass(cls, tuple)
86 and hasattr(cls, "_fields")
87 and (hasattr(cls, "_field_types") or hasattr(cls, "__annotations__"))
88 )
89
90
91def _hint_and_args(x):
92 return (x, *get_args(x))
93
94
95def get_type_hints(thing: object) -> dict[str, Any]:
96 """Like the typing version, but tries harder and never errors.
97
98 Tries harder: if the thing to inspect is a class but typing.get_type_hints
99 raises an error or returns no hints, then this function will try calling it
100 on the __init__ method. This second step often helps with user-defined
101 classes on older versions of Python. The third step we take is trying
102 to fetch types from the __signature__ property.
103 They override any other ones we found earlier.
104
105 Never errors: instead of raising TypeError for uninspectable objects, or
106 NameError for unresolvable forward references, just return an empty dict.
107 """
108 if isinstance(thing, partial):
109 from hypothesis.internal.reflection import get_signature
110
111 bound = set(get_signature(thing.func).parameters).difference(
112 get_signature(thing).parameters
113 )
114 return {k: v for k, v in get_type_hints(thing.func).items() if k not in bound}
115
116 try:
117 hints = typing.get_type_hints(thing, include_extras=True)
118 except (AttributeError, TypeError, NameError):
119 hints = {}
120
121 if inspect.isclass(thing):
122 try:
123 hints.update(typing.get_type_hints(thing.__init__, include_extras=True))
124 except (TypeError, NameError, AttributeError):
125 pass
126
127 try:
128 if hasattr(thing, "__signature__"):
129 # It is possible for the signature and annotations attributes to
130 # differ on an object due to renamed arguments.
131 from hypothesis.internal.reflection import get_signature
132 from hypothesis.strategies._internal.types import is_a_type
133
134 vkinds = (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
135 for p in get_signature(thing).parameters.values():
136 if (
137 p.kind not in vkinds
138 and is_a_type(p.annotation)
139 and p.annotation is not p.empty
140 ):
141 p_hint = p.annotation
142
143 # Defer to `get_type_hints` if signature annotation is, or
144 # contains, a forward reference that is otherwise resolved.
145 if any(
146 isinstance(sig_hint, ForwardRef)
147 and not isinstance(hint, ForwardRef)
148 for sig_hint, hint in zip(
149 _hint_and_args(p.annotation),
150 _hint_and_args(hints.get(p.name, Any)),
151 strict=False,
152 )
153 ):
154 p_hint = hints[p.name]
155 if p.default is None:
156 hints[p.name] = p_hint | None
157 else:
158 hints[p.name] = p_hint
159 except (AttributeError, TypeError, NameError): # pragma: no cover
160 pass
161
162 return hints
163
164
165# Under Python 2, math.floor and math.ceil returned floats, which cannot
166# represent large integers - eg `float(2**53) == float(2**53 + 1)`.
167# We therefore implement them entirely in (long) integer operations.
168# We still use the same trick on Python 3, because Numpy values and other
169# custom __floor__ or __ceil__ methods may convert via floats.
170# See issue #1667, Numpy issue 9068.
171def floor(x):
172 y = int(x)
173 if y != x and x < 0:
174 return y - 1
175 return y
176
177
178def ceil(x):
179 y = int(x)
180 if y != x and x > 0:
181 return y + 1
182 return y
183
184
185def extract_bits(x: int, /, width: int | None = None) -> list[int]:
186 assert x >= 0
187 result = []
188 while x:
189 result.append(x & 1)
190 x >>= 1
191 if width is not None:
192 result = (result + [0] * width)[:width]
193 result.reverse()
194 return result
195
196
197def bad_django_TestCase(runner: Optional["ConjectureRunner"]) -> bool:
198 if runner is None or "django.test" not in sys.modules:
199 return False
200 else: # pragma: no cover
201 if not isinstance(runner, sys.modules["django.test"].TransactionTestCase):
202 return False
203
204 from hypothesis.extra.django._impl import HypothesisTestCase
205
206 return not isinstance(runner, HypothesisTestCase)
207
208
209# see issue #3812
210if sys.version_info[:2] < (3, 12):
211
212 def _asdict_inner(obj, dict_factory):
213 if dataclasses._is_dataclass_instance(obj):
214 return dict_factory(
215 (f.name, _asdict_inner(getattr(obj, f.name), dict_factory))
216 for f in dataclasses.fields(obj)
217 )
218 elif isinstance(obj, tuple) and hasattr(obj, "_fields"):
219 return type(obj)(*[_asdict_inner(v, dict_factory) for v in obj])
220 elif isinstance(obj, (list, tuple)):
221 return type(obj)(_asdict_inner(v, dict_factory) for v in obj)
222 elif isinstance(obj, dict):
223 if hasattr(type(obj), "default_factory"):
224 result = type(obj)(obj.default_factory)
225 for k, v in obj.items():
226 result[_asdict_inner(k, dict_factory)] = _asdict_inner(
227 v, dict_factory
228 )
229 return result
230 return type(obj)(
231 (_asdict_inner(k, dict_factory), _asdict_inner(v, dict_factory))
232 for k, v in obj.items()
233 )
234 else:
235 return copy.deepcopy(obj)
236
237 def dataclass_asdict(obj, *, dict_factory=dict):
238 """
239 A vendored variant of dataclasses.asdict. Includes the bugfix for
240 defaultdicts (cpython/32056) for all versions. See also issues/3812.
241
242 This should be removed whenever we drop support for 3.11. We can use the
243 standard dataclasses.asdict after that point.
244 """
245 if not dataclasses._is_dataclass_instance(obj):
246 raise TypeError("asdict() should be called on dataclass instances")
247 return _asdict_inner(obj, dict_factory)
248
249else:
250 dataclass_asdict = dataclasses.asdict
251
252
253if sys.version_info[:2] < (3, 13):
254 # batched was added in 3.12, strict flag in 3.13
255 # copied from 3.13 docs reference implementation
256
257 def batched(iterable, n, *, strict=False):
258 if n < 1:
259 raise ValueError("n must be at least one")
260 iterator = iter(iterable)
261 while batch := tuple(itertools.islice(iterator, n)):
262 if strict and len(batch) != n:
263 raise ValueError("batched(): incomplete batch")
264 yield batch
265
266else:
267 batched = itertools.batched