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