-
Notifications
You must be signed in to change notification settings - Fork 575
/
filtering.py
108 lines (86 loc) · 4.12 KB
/
filtering.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Most of this work is copyright (C) 2013-2021 David R. MacIver
# (david@drmaciver.com), but it contains contributions by others. See
# CONTRIBUTING.rst for a full list of people who may hold copyright, and
# consult the git log if you need to determine who owns an individual
# contribution.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# 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/.
#
# END HEADER
"""Tools for understanding predicates, to satisfy them by construction.
For example::
integers().filter(lamda x: x >= 0) -> integers(min_value=0)
This is intractable in general, but reasonably easy for simple cases involving
numeric bounds, strings with length or regex constraints, and collection lengths -
and those are precisely the most common cases. When they arise in e.g. Pandas
dataframes, it's also pretty painful to do the constructive version by hand in
a library; so we prefer to share all the implementation effort here.
See https://github.com/HypothesisWorks/hypothesis/issues/2701 for details.
"""
import operator
from decimal import Decimal
from fractions import Fraction
from functools import partial
from typing import Any, Callable, Mapping, Optional, Tuple, TypeVar
from hypothesis.internal.compat import ceil, floor
Ex = TypeVar("Ex")
Predicate = Callable[[Ex], bool]
ConstructivePredicate = Tuple[Mapping[str, Any], Optional[Predicate]]
"""Return kwargs to the appropriate strategy, and the predicate if needed.
For example::
integers().filter(lambda x: x >= 0)
-> {"min_value": 0"}, None
integers().filter(lambda x: x >= 0 and x % 7)
-> {"min_value": 0"}, lambda x: x % 7
At least in principle - for now we usually return the predicate unchanged
if needed.
We have a separate get-predicate frontend for each "group" of strategies; e.g.
for each numeric type, for strings, for bytes, for collection sizes, etc.
"""
def get_numeric_predicate_bounds(predicate: Predicate) -> ConstructivePredicate:
"""Shared logic for understanding numeric bounds.
We then specialise this in the other functions below, to ensure that e.g.
all the values are representable in the types that we're planning to generate
so that the strategy validation doesn't complain.
"""
if (
type(predicate) is partial
and len(predicate.args) == 1
and not predicate.keywords
):
arg = predicate.args[0]
if (isinstance(arg, Decimal) and Decimal.is_snan(arg)) or not isinstance(
arg, (int, float, Fraction, Decimal)
):
return {}, predicate
options = {
# We're talking about op(arg, x) - the reverse of our usual intuition!
operator.lt: {"min_value": arg, "exclude_min": True}, # lambda x: arg < x
operator.le: {"min_value": arg}, # lambda x: arg <= x
operator.eq: {"min_value": arg, "max_value": arg}, # lambda x: arg == x
operator.ge: {"max_value": arg}, # lambda x: arg >= x
operator.gt: {"max_value": arg, "exclude_max": True}, # lambda x: arg > x
}
if predicate.func in options:
return options[predicate.func], None
# TODO: handle lambdas by AST analysis
return {}, predicate
def get_integer_predicate_bounds(predicate: Predicate) -> ConstructivePredicate:
kwargs, predicate = get_numeric_predicate_bounds(predicate)
if "min_value" in kwargs:
if kwargs["min_value"] != int(kwargs["min_value"]):
kwargs["min_value"] = ceil(kwargs["min_value"])
elif kwargs.get("exclude_min", False):
kwargs["min_value"] = int(kwargs["min_value"]) + 1
if "max_value" in kwargs:
if kwargs["max_value"] != int(kwargs["max_value"]):
kwargs["max_value"] = floor(kwargs["max_value"])
elif kwargs.get("exclude_max", False):
kwargs["max_value"] = int(kwargs["max_value"]) - 1
kwargs = {k: v for k, v in kwargs.items() if k in {"min_value", "max_value"}}
return kwargs, predicate