diff options
| author | robot-piglet <[email protected]> | 2024-07-27 12:19:49 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2024-07-27 12:28:17 +0300 |
| commit | 25820897807c4bbe6fc46855ef8b7afc18f475df (patch) | |
| tree | 74cf3d50ef7246caf81bdc9b21449c75026187b5 /contrib/python/hypothesis/py3 | |
| parent | dd3740bc9af7fa735444c348d70607a1bd135661 (diff) | |
Intermediate changes
Diffstat (limited to 'contrib/python/hypothesis/py3')
14 files changed, 246 insertions, 169 deletions
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA index 8c732599ab2..c9d68c09490 100644 --- a/contrib/python/hypothesis/py3/.dist-info/METADATA +++ b/contrib/python/hypothesis/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: hypothesis -Version: 6.105.1 +Version: 6.107.0 Summary: A library for property-based testing Home-page: https://hypothesis.works Author: David R. MacIver and Zac Hatfield-Dodds diff --git a/contrib/python/hypothesis/py3/hypothesis/_settings.py b/contrib/python/hypothesis/py3/hypothesis/_settings.py index 061292e9bd0..d337be4f579 100644 --- a/contrib/python/hypothesis/py3/hypothesis/_settings.py +++ b/contrib/python/hypothesis/py3/hypothesis/_settings.py @@ -482,7 +482,7 @@ class HealthCheck(Enum, metaclass=HealthCheckMeta): def all(cls) -> List["HealthCheck"]: # Skipping of deprecated attributes is handled in HealthCheckMeta.__iter__ note_deprecation( - "`Healthcheck.all()` is deprecated; use `list(HealthCheck)` instead.", + "`HealthCheck.all()` is deprecated; use `list(HealthCheck)` instead.", since="2023-04-16", has_codemod=True, stacklevel=1, diff --git a/contrib/python/hypothesis/py3/hypothesis/control.py b/contrib/python/hypothesis/py3/hypothesis/control.py index ea085b0b551..c9bcacc391a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/control.py +++ b/contrib/python/hypothesis/py3/hypothesis/control.py @@ -138,13 +138,11 @@ class BuildContext: # The printer will discard duplicates which return different representations. self.known_object_printers = defaultdict(list) - def record_call(self, obj, func, args, kwargs, arg_slices=None): + def record_call(self, obj, func, args, kwargs): name = get_pretty_function_description(func) self.known_object_printers[IDKey(obj)].append( - lambda obj, p, cycle: ( - p.text("<...>") - if cycle - else p.repr_call(name, args, kwargs, arg_slices=arg_slices) + lambda obj, p, cycle: p.maybe_repr_known_object_as_call( + obj, cycle, name, args, kwargs ) ) diff --git a/contrib/python/hypothesis/py3/hypothesis/core.py b/contrib/python/hypothesis/py3/hypothesis/core.py index 69593c463b1..2e21b98f641 100644 --- a/contrib/python/hypothesis/py3/hypothesis/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/core.py @@ -60,7 +60,6 @@ from hypothesis.errors import ( FailedHealthCheck, Flaky, Found, - HypothesisDeprecationWarning, HypothesisException, HypothesisWarning, InvalidArgument, @@ -799,6 +798,15 @@ class StateForActualGivenExecution: current_pytest_item.value, "nodeid", None ) or get_pretty_function_description(self.wrapped_test) + def _should_trace(self): + _trace_obs = TESTCASE_CALLBACKS and OBSERVABILITY_COLLECT_COVERAGE + _trace_failure = ( + self.failed_normally + and not self.failed_due_to_deadline + and {Phase.shrink, Phase.explain}.issubset(self.settings.phases) + ) + return _trace_obs or _trace_failure + def execute_once( self, data, @@ -1007,35 +1015,7 @@ class StateForActualGivenExecution: """ trace: Trace = set() try: - # this is actually covered by our tests, but only on >= 3.12. - if ( - sys.version_info[:2] >= (3, 12) - and sys.monitoring.get_tool(MONITORING_TOOL_ID) is not None - ): # pragma: no cover - warnings.warn( - "avoiding tracing test function because tool id " - f"{MONITORING_TOOL_ID} is already taken by tool " - f"{sys.monitoring.get_tool(MONITORING_TOOL_ID)}.", - HypothesisWarning, - # I'm not sure computing a correct stacklevel is reasonable - # given the number of entry points here. - stacklevel=1, - ) - - _can_trace = ( - (sys.version_info[:2] < (3, 12) and sys.gettrace() is None) - or ( - sys.version_info[:2] >= (3, 12) - and sys.monitoring.get_tool(MONITORING_TOOL_ID) is None - ) - ) and not PYPY - _trace_obs = TESTCASE_CALLBACKS and OBSERVABILITY_COLLECT_COVERAGE - _trace_failure = ( - self.failed_normally - and not self.failed_due_to_deadline - and {Phase.shrink, Phase.explain}.issubset(self.settings.phases) - ) - if _can_trace and (_trace_obs or _trace_failure): # pragma: no cover + if self._should_trace() and Tracer.can_trace(): # pragma: no cover # This is in fact covered by our *non-coverage* tests, but due to the # settrace() contention *not* by our coverage tests. Ah well. with Tracer() as tracer: @@ -1063,7 +1043,6 @@ class StateForActualGivenExecution: # OK to re-raise it. raise except ( - HypothesisDeprecationWarning, FailedHealthCheck, *skip_exceptions_to_reraise(), ): @@ -1071,12 +1050,11 @@ class StateForActualGivenExecution: # engine, so we re-raise them. raise except failure_exceptions_to_catch() as e: - # If the error was raised by Hypothesis-internal code, re-raise it - # as a fatal error instead of treating it as a test failure. + # If an unhandled (i.e., non-Hypothesis) error was raised by + # Hypothesis-internal code, re-raise it as a fatal error instead + # of treating it as a test failure. filepath = traceback.extract_tb(e.__traceback__)[-1][0] - if is_hypothesis_file(filepath) and not isinstance( - e, (HypothesisException, StopTest, UnsatisfiedAssumption) - ): + if is_hypothesis_file(filepath) and not isinstance(e, HypothesisException): raise if data.frozen: @@ -1184,6 +1162,23 @@ class StateForActualGivenExecution: rep = get_pretty_function_description(self.test) raise Unsatisfiable(f"Unable to satisfy assumptions of {rep}") + # If we have not traced executions, warn about that now (but only when + # we'd expect to do so reliably, i.e. on CPython>=3.12) + if ( + sys.version_info[:2] >= (3, 12) + and not PYPY + and self._should_trace() + and not Tracer.can_trace() + ): # pragma: no cover + # actually covered by our tests, but only on >= 3.12 + warnings.warn( + "avoiding tracing test function because tool id " + f"{MONITORING_TOOL_ID} is already taken by tool " + f"{sys.monitoring.get_tool(MONITORING_TOOL_ID)}.", + HypothesisWarning, + stacklevel=3, + ) + if not self.falsifying_examples: return elif not (self.settings.report_multiple_bugs and pytest_shows_exceptiongroups): @@ -1222,10 +1217,20 @@ class StateForActualGivenExecution: info._expected_traceback, ), ) - except (UnsatisfiedAssumption, StopTest) as e: + except StopTest: + err = Flaky( + "Inconsistent results: An example which failed on the " + "first run now succeeds (or fails with another error)." + ) + # Link the expected exception from the first run. Not sure + # how to access the current exception, if it failed + # differently on this run. + err.__cause__ = err.__context__ = info._expected_exception + errors_to_report.append((fragments, err)) + except UnsatisfiedAssumption as e: # pragma: no cover # ironically flaky err = Flaky( "Unreliable assumption: An example which satisfied " - "assumptions on the first run now fails it.", + "assumptions on the first run now fails it." ) err.__cause__ = err.__context__ = e errors_to_report.append((fragments, err)) diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/codemods.py b/contrib/python/hypothesis/py3/hypothesis/extra/codemods.py index 3de0580ada3..85d1d44fd66 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/codemods.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/codemods.py @@ -68,7 +68,7 @@ def refactor(code: str) -> str: transforms: List[VisitorBasedCodemodCommand] = [ HypothesisFixPositionalKeywonlyArgs(context), HypothesisFixComplexMinMagnitude(context), - HypothesisFixHealthcheckAll(context), + HypothesisFixHealthCheckAll(context), HypothesisFixCharactersArguments(context), ] for transform in transforms: @@ -228,16 +228,16 @@ class HypothesisFixPositionalKeywonlyArgs(VisitorBasedCodemodCommand): return updated_node.with_changes(args=newargs) -class HypothesisFixHealthcheckAll(VisitorBasedCodemodCommand): - """Replace Healthcheck.all() with list(Healthcheck)""" +class HypothesisFixHealthCheckAll(VisitorBasedCodemodCommand): + """Replace HealthCheck.all() with list(HealthCheck)""" - DESCRIPTION = "Replace Healthcheck.all() with list(Healthcheck)" + DESCRIPTION = "Replace HealthCheck.all() with list(HealthCheck)" - @m.leave(m.Call(func=m.Attribute(m.Name("Healthcheck"), m.Name("all")), args=[])) + @m.leave(m.Call(func=m.Attribute(m.Name("HealthCheck"), m.Name("all")), args=[])) def replace_healthcheck(self, original_node, updated_node): return updated_node.with_changes( func=cst.Name("list"), - args=[cst.Arg(value=cst.Name("Healthcheck"))], + args=[cst.Arg(value=cst.Name("HealthCheck"))], ) diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py b/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py index 0c4a5d85234..9179e2d28fe 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/ghostwriter.py @@ -603,10 +603,14 @@ def _assert_eq(style: str, a: str, b: str) -> str: def _imports_for_object(obj): """Return the imports for `obj`, which may be empty for e.g. lambdas""" + if type(obj) is getattr(types, "UnionType", object()): + return {mod for mod, _ in set().union(*map(_imports_for_object, obj.__args__))} if isinstance(obj, (re.Pattern, re.Match)): return {"re"} if isinstance(obj, st.SearchStrategy): return _imports_for_strategy(obj) + if isinstance(obj, getattr(sys.modules.get("numpy"), "dtype", ())): + return {("numpy", "dtype")} try: if is_generic_type(obj): if isinstance(obj, TypeVar): @@ -772,7 +776,7 @@ def _get_module(obj): raise RuntimeError(f"Could not find module for ufunc {obj.__name__} ({obj!r}") -def _get_qualname(obj, *, include_module=False): +def _get_qualname(obj: Any, *, include_module: bool = False) -> str: # Replacing angle-brackets for objects defined in `.<locals>.` qname = getattr(obj, "__qualname__", obj.__name__) qname = qname.replace("<", "_").replace(">", "_").replace(" ", "") @@ -1133,6 +1137,60 @@ ROUNDTRIP_PAIRS = ( ) +def _get_testable_functions(thing: object) -> Dict[str, Callable]: + by_name = {} + if callable(thing): + funcs: List[Optional[Any]] = [thing] + elif isinstance(thing, types.ModuleType): + if hasattr(thing, "__all__"): + funcs = [getattr(thing, name, None) for name in thing.__all__] + elif hasattr(thing, "__package__"): + pkg = thing.__package__ + funcs = [ + v + for k, v in vars(thing).items() + if callable(v) + and not is_mock(v) + and ((not pkg) or getattr(v, "__module__", pkg).startswith(pkg)) + and not k.startswith("_") + ] + if pkg and any(getattr(f, "__module__", pkg) == pkg for f in funcs): + funcs = [f for f in funcs if getattr(f, "__module__", pkg) == pkg] + else: + raise InvalidArgument(f"Can't test non-module non-callable {thing!r}") + + for f in list(funcs): + if inspect.isclass(f): + funcs += [ + v.__get__(f) + for k, v in vars(f).items() + if hasattr(v, "__func__") and not is_mock(v) and not k.startswith("_") + ] + for f in funcs: + try: + if ( + (not is_mock(f)) + and callable(f) + and _get_params(f) + and not isinstance(f, enum.EnumMeta) + ): + if getattr(thing, "__name__", None): + if inspect.isclass(thing): + KNOWN_FUNCTION_LOCATIONS[f] = _get_module_helper(thing) + elif isinstance(thing, types.ModuleType): + KNOWN_FUNCTION_LOCATIONS[f] = thing.__name__ + try: + _get_params(f) + by_name[_get_qualname(f, include_module=True)] = f + except Exception: + # usually inspect.signature on C code such as socket.inet_aton, + # or Pandas 'CallableDynamicDoc' object has no attr. '__name__' + pass + except (TypeError, ValueError): + pass + return by_name + + def magic( *modules_or_functions: Union[Callable, types.ModuleType], except_: Except = (), @@ -1157,85 +1215,42 @@ def magic( _check_style(style) if not modules_or_functions: raise InvalidArgument("Must pass at least one function or module to test.") - functions = set() + + parts = [] + by_name = {} + imports = set() + for thing in modules_or_functions: - if callable(thing): - functions.add(thing) - # class need to be added for exploration - if inspect.isclass(thing): - funcs: List[Optional[Any]] = [thing] - else: - funcs = [] - elif isinstance(thing, types.ModuleType): - if hasattr(thing, "__all__"): - funcs = [getattr(thing, name, None) for name in thing.__all__] - elif hasattr(thing, "__package__"): - pkg = thing.__package__ - funcs = [ - v - for k, v in vars(thing).items() - if callable(v) - and not is_mock(v) - and ((not pkg) or getattr(v, "__module__", pkg).startswith(pkg)) - and not k.startswith("_") - ] - if pkg and any(getattr(f, "__module__", pkg) == pkg for f in funcs): - funcs = [f for f in funcs if getattr(f, "__module__", pkg) == pkg] - else: - raise InvalidArgument(f"Can't test non-module non-callable {thing!r}") - - for f in list(funcs): - if inspect.isclass(f): - funcs += [ - v.__get__(f) - for k, v in vars(f).items() - if hasattr(v, "__func__") - and not is_mock(v) - and not k.startswith("_") - ] - for f in funcs: - try: + by_name.update(found := _get_testable_functions(thing)) + if (not found) and isinstance(thing, types.ModuleType): + msg = f"# Found no testable functions in {thing.__name__} (from {thing.__file__!r})" + mods: list = [] + for k in sorted(sys.modules, key=len): if ( - (not is_mock(f)) - and callable(f) - and _get_params(f) - and not isinstance(f, enum.EnumMeta) + k.startswith(f"{thing.__name__}.") + and "._" not in k[len(thing.__name__) :] + and not k.startswith(tuple(f"{m}." for m in mods)) + and _get_testable_functions(sys.modules[k]) ): - functions.add(f) - if getattr(thing, "__name__", None): - if inspect.isclass(thing): - KNOWN_FUNCTION_LOCATIONS[f] = _get_module_helper(thing) - else: - KNOWN_FUNCTION_LOCATIONS[f] = thing.__name__ - except (TypeError, ValueError): - pass + mods.append(k) + if mods: + msg += ( + f"\n# Try writing tests for submodules, e.g. by using:\n" + f"# hypothesis write {' '.join(sorted(mods))}" + ) + parts.append(msg) - if annotate is None: - annotate = _are_annotations_used(*functions) + if not by_name: + return "\n\n".join(parts) - imports = set() - parts = [] + if annotate is None: + annotate = _are_annotations_used(*by_name.values()) def make_(how, *args, **kwargs): imp, body = how(*args, **kwargs, except_=except_, style=style) imports.update(imp) parts.append(body) - by_name = {} - for f in functions: - try: - _get_params(f) - by_name[_get_qualname(f, include_module=True)] = f - except Exception: - # usually inspect.signature on C code such as socket.inet_aton, sometimes - # e.g. Pandas 'CallableDynamicDoc' object has no attribute '__name__' - pass - if not by_name: - return ( - f"# Found no testable functions in\n" - f"# {functions!r} from {modules_or_functions}\n" - ) - # Look for pairs of functions that roundtrip, based on known naming patterns. for writename, readname in ROUNDTRIP_PAIRS: for name in sorted(by_name): diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py index 3754192d707..de0940c97d0 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py @@ -10,6 +10,7 @@ import importlib import math +import types from typing import ( TYPE_CHECKING, Any, @@ -523,6 +524,11 @@ def arrays( hundreds or more elements, having a fill value is essential if you want your tests to run in reasonable time. """ + # Our dtype argument might be a union, e.g. `np.float64 | np.complex64`; we handle + # that by turning it into a strategy up-front. + if type(dtype) in (getattr(types, "UnionType", object()), Union): + dtype = st.one_of(*(from_dtype(np.dtype(d)) for d in dtype.__args__)) # type: ignore + # We support passing strategies as arguments for convenience, or at least # for legacy reasons, but don't want to pay the perf cost of a composite # strategy (i.e. repeated argument handling and validation) when it's not @@ -1279,7 +1285,7 @@ def _unpack_generic(thing): def _unpack_dtype(dtype): dtype_args = getattr(dtype, "__args__", ()) - if dtype_args: + if dtype_args and type(dtype) not in (getattr(types, "UnionType", object()), Union): assert len(dtype_args) == 1 if isinstance(dtype_args[0], TypeVar): # numpy.dtype[+ScalarType] @@ -1291,22 +1297,22 @@ def _unpack_dtype(dtype): return dtype -def _dtype_and_shape_from_args(args): +def _dtype_from_args(args): if len(args) <= 1: # Zero args: ndarray, _SupportsArray # One arg: ndarray[type], _SupportsArray[type] - shape = Any dtype = _unpack_dtype(args[0]) if args else Any else: # Two args: ndarray[shape, type], NDArray[*] assert len(args) == 2 - shape = args[0] - assert shape is Any + assert args[0] is Any dtype = _unpack_dtype(args[1]) - return ( - scalar_dtypes() if dtype is Any else np.dtype(dtype), - array_shapes(max_dims=2) if shape is Any else shape, - ) + + if dtype is Any: + return scalar_dtypes() + elif type(dtype) in (getattr(types, "UnionType", object()), Union): + return dtype + return np.dtype(dtype) def _from_type(thing: Type[Ex]) -> Optional[st.SearchStrategy[Ex]]: @@ -1394,8 +1400,8 @@ def _from_type(thing: Type[Ex]) -> Optional[st.SearchStrategy[Ex]]: ) if real_thing in [np.ndarray, _SupportsArray]: - dtype, shape = _dtype_and_shape_from_args(args) - return arrays(dtype, shape) # type: ignore[return-value] + dtype = _dtype_from_args(args) + return arrays(dtype, array_shapes(max_dims=2)) # type: ignore[return-value] # We didn't find a type to resolve, continue return None diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py b/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py index e48802ee77c..47abae66ea9 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py @@ -22,7 +22,7 @@ class IntervalSet: x = cls((ord(c), ord(c)) for c in sorted(s)) return x.union(x) - def __init__(self, intervals): + def __init__(self, intervals=()): self.intervals = tuple(intervals) self.offsets = [0] for u, v in self.intervals: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py b/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py index d99e767c1d6..0cd760cf17e 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/scrutineer.py @@ -21,6 +21,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from hypothesis._settings import Phase, Verbosity +from hypothesis.internal.compat import PYPY from hypothesis.internal.escalation import is_hypothesis_file if TYPE_CHECKING: @@ -58,6 +59,16 @@ class Tracer: self.branches: Trace = set() self._previous_location = None + @staticmethod + def can_trace(): + return ( + (sys.version_info[:2] < (3, 12) and sys.gettrace() is None) + or ( + sys.version_info[:2] >= (3, 12) + and sys.monitoring.get_tool(MONITORING_TOOL_ID) is None + ) + ) and not PYPY + def trace(self, frame, event, arg): try: if event == "call": @@ -80,8 +91,9 @@ class Tracer: self._previous_location = current_location def __enter__(self): + assert self.can_trace() # caller checks in core.py + if sys.version_info[:2] < (3, 12): - assert sys.gettrace() is None # caller checks in core.py sys.settrace(self.trace) return self diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py index 570645509d6..572679d4033 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py @@ -21,7 +21,7 @@ import warnings from contextvars import ContextVar from decimal import Context, Decimal, localcontext from fractions import Fraction -from functools import lru_cache, reduce +from functools import reduce from inspect import Parameter, Signature, isabstract, isclass from types import FunctionType from typing import ( @@ -137,6 +137,7 @@ from hypothesis.strategies._internal.strings import ( BytesStrategy, OneCharStringStrategy, TextStrategy, + _check_is_single_character, ) from hypothesis.strategies._internal.utils import ( cacheable, @@ -789,19 +790,6 @@ characters.__signature__ = (__sig := get_signature(characters)).replace( # type ) -# Cache size is limited by sys.maxunicode, but passing None makes it slightly faster. -@lru_cache(maxsize=None) -def _check_is_single_character(c): - # In order to mitigate the performance cost of this check, we use a shared cache, - # even at the cost of showing the culprit strategy in the error message. - if not isinstance(c, str): - type_ = get_pretty_function_description(type(c)) - raise InvalidArgument(f"Got non-string {c!r} (type {type_})") - if len(c) != 1: - raise InvalidArgument(f"Got {c!r} (length {len(c)} != 1)") - return c - - @cacheable @defines_strategy(force_reusable_values=True) def text( @@ -924,19 +912,7 @@ def from_regex( check_type((str, SearchStrategy), alphabet, "alphabet") if not isinstance(pattern, str): raise InvalidArgument("alphabet= is not supported for bytestrings") - - if isinstance(alphabet, str): - alphabet = characters(categories=(), include_characters=alphabet) - char_strategy = unwrap_strategies(alphabet) - if isinstance(char_strategy, SampledFromStrategy): - alphabet = characters( - categories=(), - include_characters=alphabet.elements, # type: ignore - ) - elif not isinstance(char_strategy, OneCharStringStrategy): - raise InvalidArgument( - f"{alphabet=} must be a sampled_from() or characters() strategy" - ) + alphabet = OneCharStringStrategy.from_alphabet(alphabet) elif isinstance(pattern, str): alphabet = characters(codec="utf-8") diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py index 8df955e632a..b6e6dd8deb3 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py @@ -17,13 +17,31 @@ from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.internal import charmap from hypothesis.internal.filtering import max_len, min_len from hypothesis.internal.intervalsets import IntervalSet +from hypothesis.internal.reflection import get_pretty_function_description from hypothesis.strategies._internal.collections import ListStrategy from hypothesis.strategies._internal.lazy import unwrap_strategies from hypothesis.strategies._internal.numbers import IntegersStrategy -from hypothesis.strategies._internal.strategies import SearchStrategy +from hypothesis.strategies._internal.strategies import ( + OneOfStrategy, + SampledFromStrategy, + SearchStrategy, +) from hypothesis.vendor.pretty import pretty +# Cache size is limited by sys.maxunicode, but passing None makes it slightly faster. +@lru_cache(maxsize=None) +def _check_is_single_character(c): + # In order to mitigate the performance cost of this check, we use a shared cache, + # even at the cost of showing the culprit strategy in the error message. + if not isinstance(c, str): + type_ = get_pretty_function_description(type(c)) + raise InvalidArgument(f"Got non-string {c!r} (type {type_})") + if len(c) != 1: + raise InvalidArgument(f"Got {c!r} (length {len(c)} != 1)") + return c + + class OneCharStringStrategy(SearchStrategy): """A strategy which generates single character strings of text type.""" @@ -72,6 +90,31 @@ class OneCharStringStrategy(SearchStrategy): ) return cls(intervals, force_repr=f"characters({_arg_repr})") + @classmethod + def from_alphabet(cls, alphabet): + if isinstance(alphabet, str): + return cls.from_characters_args(categories=(), include_characters=alphabet) + + assert isinstance(alphabet, SearchStrategy) + char_strategy = unwrap_strategies(alphabet) + if isinstance(char_strategy, cls): + return char_strategy + elif isinstance(char_strategy, SampledFromStrategy): + for c in char_strategy.elements: + _check_is_single_character(c) + return cls.from_characters_args( + categories=(), + include_characters=char_strategy.elements, + ) + elif isinstance(char_strategy, OneOfStrategy): + intervals = IntervalSet() + for s in char_strategy.element_strategies: + intervals = intervals.union(cls.from_alphabet(s).intervals) + return cls(intervals, force_repr=repr(alphabet)) + raise InvalidArgument( + f"{alphabet=} must be a sampled_from() or characters() strategy" + ) + def __repr__(self): return self._force_repr or f"OneCharStringStrategy({self.intervals!r})" @@ -189,13 +232,10 @@ def _string_filter_rewrite(self, kind, condition): ): from hypothesis.strategies._internal.regex import regex_strategy - print(f"{condition=}") - print(f"{condition.__name__=}") - if condition.__name__ == "match": # Replace with an easier-to-handle equivalent condition - caret = "^" if kind is str else b"^" - pattern = re.compile(caret + pattern.pattern, flags=pattern.flags) + caret, close = ("^(?:", ")") if kind is str else (b"^(?:", b")") + pattern = re.compile(caret + pattern.pattern + close, flags=pattern.flags) condition = pattern.search if condition.__name__ in ("search", "findall", "fullmatch"): diff --git a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py index ceffe3a6aa2..6b4718ff79c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py +++ b/contrib/python/hypothesis/py3/hypothesis/vendor/pretty.py @@ -62,6 +62,7 @@ Inheritance diagram: :license: BSD License. """ +import ast import datetime import re import struct @@ -69,7 +70,7 @@ import sys import types import warnings from collections import defaultdict, deque -from contextlib import contextmanager +from contextlib import contextmanager, suppress from enum import Flag from io import StringIO from math import copysign, isnan @@ -373,6 +374,29 @@ class RepresentationPrinter: self.flush() return self.output.getvalue() + def maybe_repr_known_object_as_call(self, obj, cycle, name, args, kwargs): + # pprint this object as a call, _unless_ the call would be invalid syntax + # and the repr would be valid and there are not comments on arguments. + if cycle: + return self.text("<...>") + # Since we don't yet track comments for sub-argument parts, we omit the + # "if no comments" condition here for now. Add it when we revive + # https://github.com/HypothesisWorks/hypothesis/pull/3624/ + with suppress(Exception): + # Check whether the repr is valid syntax: + ast.parse(repr(obj)) + # Given that the repr is valid syntax, check the call: + p = RepresentationPrinter() + p.stack = self.stack.copy() + p.known_object_printers = self.known_object_printers + p.repr_call(name, args, kwargs) + # If the call is not valid syntax, use the repr + try: + ast.parse(p.getvalue()) + except Exception: + return _repr_pprint(obj, self, cycle) + return self.repr_call(name, args, kwargs) + def repr_call( self, func_name, @@ -423,6 +447,7 @@ class RepresentationPrinter: self.text(leading_comment) self.break_() else: + assert leading_comment is None # only passed by top-level report self.breakable(" " if i else "") if k: self.text(f"{k}=") diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py index 1883646c339..d81e1c4cef8 100644 --- a/contrib/python/hypothesis/py3/hypothesis/version.py +++ b/contrib/python/hypothesis/py3/hypothesis/version.py @@ -8,5 +8,5 @@ # 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/. -__version_info__ = (6, 105, 1) +__version_info__ = (6, 107, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make index 13e59544308..adf8c798663 100644 --- a/contrib/python/hypothesis/py3/ya.make +++ b/contrib/python/hypothesis/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(6.105.1) +VERSION(6.107.0) LICENSE(MPL-2.0) |
