|
@@ -11,14 +11,17 @@
|
|
|
import copy
|
|
|
import re
|
|
|
import warnings
|
|
|
-from functools import lru_cache
|
|
|
+from functools import lru_cache, partial
|
|
|
|
|
|
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.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.vendor.pretty import pretty
|
|
|
|
|
|
|
|
|
class OneCharStringStrategy(SearchStrategy):
|
|
@@ -76,6 +79,33 @@ class OneCharStringStrategy(SearchStrategy):
|
|
|
return data.draw_string(self.intervals, min_size=1, max_size=1)
|
|
|
|
|
|
|
|
|
+_nonempty_names = (
|
|
|
+ "capitalize",
|
|
|
+ "expandtabs",
|
|
|
+ "join",
|
|
|
+ "lower",
|
|
|
+ "rsplit",
|
|
|
+ "split",
|
|
|
+ "splitlines",
|
|
|
+ "swapcase",
|
|
|
+ "title",
|
|
|
+ "upper",
|
|
|
+)
|
|
|
+_nonempty_and_content_names = (
|
|
|
+ "islower",
|
|
|
+ "isupper",
|
|
|
+ "isalnum",
|
|
|
+ "isalpha",
|
|
|
+ "isascii",
|
|
|
+ "isdigit",
|
|
|
+ "isspace",
|
|
|
+ "istitle",
|
|
|
+ "lstrip",
|
|
|
+ "rstrip",
|
|
|
+ "strip",
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
class TextStrategy(ListStrategy):
|
|
|
def do_draw(self, data):
|
|
|
# if our element strategy is OneCharStringStrategy, we can skip the
|
|
@@ -104,44 +134,17 @@ class TextStrategy(ListStrategy):
|
|
|
_nonempty_filters = (
|
|
|
*ListStrategy._nonempty_filters,
|
|
|
str,
|
|
|
- str.capitalize,
|
|
|
str.casefold,
|
|
|
str.encode,
|
|
|
- str.expandtabs,
|
|
|
- str.join,
|
|
|
- str.lower,
|
|
|
- str.rsplit,
|
|
|
- str.split,
|
|
|
- str.splitlines,
|
|
|
- str.swapcase,
|
|
|
- str.title,
|
|
|
- str.upper,
|
|
|
+ *(getattr(str, n) for n in _nonempty_names),
|
|
|
)
|
|
|
_nonempty_and_content_filters = (
|
|
|
- str.isidentifier,
|
|
|
- str.islower,
|
|
|
- str.isupper,
|
|
|
- str.isalnum,
|
|
|
- str.isalpha,
|
|
|
- str.isascii,
|
|
|
str.isdecimal,
|
|
|
- str.isdigit,
|
|
|
str.isnumeric,
|
|
|
- str.isspace,
|
|
|
- str.istitle,
|
|
|
- str.lstrip,
|
|
|
- str.rstrip,
|
|
|
- str.strip,
|
|
|
+ *(getattr(str, n) for n in _nonempty_and_content_names),
|
|
|
)
|
|
|
|
|
|
def filter(self, condition):
|
|
|
- if condition in (str.lower, str.title, str.upper):
|
|
|
- warnings.warn(
|
|
|
- f"You applied str.{condition.__name__} as a filter, but this allows "
|
|
|
- f"all nonempty strings! Did you mean str.is{condition.__name__}?",
|
|
|
- HypothesisWarning,
|
|
|
- stacklevel=2,
|
|
|
- )
|
|
|
elems = unwrap_strategies(self.element_strategy)
|
|
|
if (
|
|
|
condition is str.isidentifier
|
|
@@ -163,17 +166,76 @@ class TextStrategy(ListStrategy):
|
|
|
),
|
|
|
# Filter to ensure that NFKC normalization keeps working in future
|
|
|
).filter(str.isidentifier)
|
|
|
+ if (new := _string_filter_rewrite(self, str, condition)) is not None:
|
|
|
+ return new
|
|
|
+ return super().filter(condition)
|
|
|
|
|
|
- # We use ListStrategy filter logic for the conditions that *only* imply
|
|
|
- # the string is nonempty. Here, we increment the min_size but still apply
|
|
|
- # the filter for conditions that imply nonempty *and specific contents*.
|
|
|
- if condition in self._nonempty_and_content_filters:
|
|
|
- assert self.max_size >= 1, "Always-empty is special cased in st.text()"
|
|
|
- self = copy.copy(self)
|
|
|
- self.min_size = max(1, self.min_size)
|
|
|
- return ListStrategy.filter(self, condition)
|
|
|
|
|
|
- return super().filter(condition)
|
|
|
+def _string_filter_rewrite(self, kind, condition):
|
|
|
+ if condition in (kind.lower, kind.title, kind.upper):
|
|
|
+ k = kind.__name__
|
|
|
+ warnings.warn(
|
|
|
+ f"You applied {k}.{condition.__name__} as a filter, but this allows "
|
|
|
+ f"all nonempty strings! Did you mean {k}.is{condition.__name__}?",
|
|
|
+ HypothesisWarning,
|
|
|
+ stacklevel=2,
|
|
|
+ )
|
|
|
+
|
|
|
+ elems = unwrap_strategies(self.element_strategy)
|
|
|
+ if (
|
|
|
+ (kind is bytes or isinstance(elems, OneCharStringStrategy))
|
|
|
+ and isinstance(pattern := getattr(condition, "__self__", None), re.Pattern)
|
|
|
+ and isinstance(pattern.pattern, kind)
|
|
|
+ ):
|
|
|
+ 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)
|
|
|
+ condition = pattern.search
|
|
|
+
|
|
|
+ if condition.__name__ in ("search", "findall", "fullmatch"):
|
|
|
+ s = regex_strategy(
|
|
|
+ pattern,
|
|
|
+ fullmatch=condition.__name__ == "fullmatch",
|
|
|
+ alphabet=self.element_strategy if kind is str else None,
|
|
|
+ )
|
|
|
+ if self.min_size > 0:
|
|
|
+ s = s.filter(partial(min_len, self.min_size))
|
|
|
+ if self.max_size < 1e999:
|
|
|
+ s = s.filter(partial(max_len, self.max_size))
|
|
|
+ return s
|
|
|
+ elif condition.__name__ in ("finditer", "scanner"):
|
|
|
+ # PyPy implements `finditer` as an alias to their `scanner` method
|
|
|
+ warnings.warn(
|
|
|
+ f"You applied {pretty(condition)} as a filter, but this allows "
|
|
|
+ f"any string at all! Did you mean .findall ?",
|
|
|
+ HypothesisWarning,
|
|
|
+ stacklevel=3,
|
|
|
+ )
|
|
|
+ return self
|
|
|
+ elif condition.__name__ == "split":
|
|
|
+ warnings.warn(
|
|
|
+ f"You applied {pretty(condition)} as a filter, but this allows "
|
|
|
+ f"any nonempty string! Did you mean .search ?",
|
|
|
+ HypothesisWarning,
|
|
|
+ stacklevel=3,
|
|
|
+ )
|
|
|
+ return self.filter(bool)
|
|
|
+
|
|
|
+ # We use ListStrategy filter logic for the conditions that *only* imply
|
|
|
+ # the string is nonempty. Here, we increment the min_size but still apply
|
|
|
+ # the filter for conditions that imply nonempty *and specific contents*.
|
|
|
+ if condition in self._nonempty_and_content_filters and self.max_size >= 1:
|
|
|
+ self = copy.copy(self)
|
|
|
+ self.min_size = max(1, self.min_size)
|
|
|
+ return ListStrategy.filter(self, condition)
|
|
|
+
|
|
|
+ return None
|
|
|
|
|
|
|
|
|
# Excerpted from https://www.unicode.org/Public/15.0.0/ucd/PropList.txt
|
|
@@ -229,9 +291,26 @@ def _identifier_characters():
|
|
|
return id_start, id_continue
|
|
|
|
|
|
|
|
|
-class FixedSizeBytes(SearchStrategy):
|
|
|
- def __init__(self, size):
|
|
|
- self.size = size
|
|
|
+class BytesStrategy(ListStrategy):
|
|
|
+ def __init__(self, min_size, max_size):
|
|
|
+ super().__init__(IntegersStrategy(0, 255), min_size=min_size, max_size=max_size)
|
|
|
|
|
|
def do_draw(self, data):
|
|
|
- return bytes(data.draw_bytes(self.size))
|
|
|
+ # TODO: refactor the underlying provider to support variable-length bytes
|
|
|
+ if self.min_size == self.max_size:
|
|
|
+ return bytes(data.draw_bytes(self.min_size))
|
|
|
+ return bytes(super().do_draw(data))
|
|
|
+
|
|
|
+ _nonempty_filters = (
|
|
|
+ *ListStrategy._nonempty_filters,
|
|
|
+ bytes,
|
|
|
+ *(getattr(bytes, n) for n in _nonempty_names),
|
|
|
+ )
|
|
|
+ _nonempty_and_content_filters = (
|
|
|
+ *(getattr(bytes, n) for n in _nonempty_and_content_names),
|
|
|
+ )
|
|
|
+
|
|
|
+ def filter(self, condition):
|
|
|
+ if (new := _string_filter_rewrite(self, bytes, condition)) is not None:
|
|
|
+ return new
|
|
|
+ return super().filter(condition)
|