py-predicate 1.5__tar.gz → 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_predicate-1.7.0/PKG-INFO +253 -0
- py_predicate-1.7.0/README.md +204 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/__init__.py +45 -7
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/all_predicate.py +3 -5
- py_predicate-1.7.0/predicate/analysis.py +22 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/any_predicate.py +5 -1
- py_predicate-1.7.0/predicate/compile_predicate.py +451 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/dict_of_predicate.py +34 -17
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/formatter/__init__.py +4 -0
- py_predicate-1.7.0/predicate/formatter/fn_source.py +57 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/formatter/format_dot.py +78 -1
- py_predicate-1.7.0/predicate/formatter/format_json.py +196 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/formatter/format_latex.py +10 -0
- py_predicate-1.7.0/predicate/formatter/format_yaml.py +9 -0
- py_predicate-1.7.0/predicate/formatter/from_json.py +178 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/generate_false.py +210 -69
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/generate_true.py +102 -15
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/helpers.py +46 -46
- py_predicate-1.7.0/predicate/has_key_predicate.py +27 -0
- py_predicate-1.7.0/predicate/has_path_predicate.py +60 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/helpers.py +9 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/implies.py +28 -3
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/in_predicate.py +4 -0
- py_predicate-1.7.0/predicate/is_async_predicate.py +23 -0
- py_predicate-1.7.0/predicate/is_close_predicate.py +33 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_instance_predicate.py +12 -16
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_none_predicate.py +2 -2
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_not_none_predicate.py +7 -1
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_subclass_predicate.py +3 -4
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/juxt_predicate.py +9 -1
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/list_of_predicate.py +10 -9
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/mutual_recur_predicate.py +2 -1
- py_predicate-1.7.0/predicate/optimizer/intersects_optimizer.py +9 -0
- py_predicate-1.7.0/predicate/optimizer/list_of_optimizer.py +33 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/or_optimizer.py +7 -6
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/predicate_optimizer.py +8 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/xor_optimizer.py +8 -8
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/plus_predicate.py +1 -1
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/predicate.py +5 -3
- py_predicate-1.7.0/predicate/raises_predicate.py +53 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/recur_predicate.py +2 -2
- py_predicate-1.7.0/predicate/reduce_predicate.py +38 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/set_of_predicate.py +3 -3
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/set_predicates.py +31 -12
- py_predicate-1.7.0/predicate/spec/exercise.py +15 -0
- py_predicate-1.7.0/predicate/spec/exercise_class.py +77 -0
- py_predicate-1.7.0/predicate/spec/exercise_function.py +61 -0
- py_predicate-1.7.0/predicate/spec/exercise_helpers.py +135 -0
- py_predicate-1.7.0/predicate/spec/instrument.py +198 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/spec/spec.py +1 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/standard_predicates.py +2 -2
- py_predicate-1.7.0/predicate/struct_predicate.py +69 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/tuple_of_predicate.py +4 -3
- {py_predicate-1.5 → py_predicate-1.7.0}/pyproject.toml +17 -5
- py_predicate-1.5/PKG-INFO +0 -126
- py_predicate-1.5/README.md +0 -79
- py_predicate-1.5/predicate/formatter/format_json.py +0 -62
- py_predicate-1.5/predicate/has_key_predicate.py +0 -26
- py_predicate-1.5/predicate/has_path_predicate.py +0 -66
- py_predicate-1.5/predicate/reduce_predicate.py +0 -25
- py_predicate-1.5/predicate/spec/exercise.py +0 -256
- py_predicate-1.5/predicate/spec/instrument.py +0 -48
- {py_predicate-1.5 → py_predicate-1.7.0}/LICENSE +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/always_false_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/always_true_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/comp_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/constructor/__init__.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/constructor/construct.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/constructor/helpers.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/constructor/mutate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/consumes.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/count_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/eq_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/exactly_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/exception_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/explain.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/fn_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/formatter/helpers.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/ge_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/__init__.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/generate_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/gt_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/has_length_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/implies_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/in_predicate_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/ip_address_predicates.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_callable_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_falsy_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_lambda_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_predicate_of_p.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_same_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_truthy_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/lazy_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/le_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/lt_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/match_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/named_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/ne_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/negate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/not_in_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/__init__.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/all_optimizer.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/and_optimizer.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/any_optimizer.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/helpers.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/in_optimizer.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/not_in_optimizer.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/not_optimizer.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optional_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/parser.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/predicate_factory.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/property_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/range_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/regex_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/repeat_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/root_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/spec/__init__.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/star_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/str_predicates.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/tee_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/this_predicate.py +0 -0
- {py_predicate-1.5 → py_predicate-1.7.0}/predicate/truth_table.py +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py_predicate
|
|
3
|
+
Version: 1.7.0
|
|
4
|
+
Summary: Module to create composable predicates
|
|
5
|
+
Author-email: Maurits Rijk <maurits.rijk@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Intended Audience :: Information Technology
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Topic :: Internet
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
19
|
+
Classifier: Environment :: Web Environment
|
|
20
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: exrex
|
|
27
|
+
Requires-Dist: graphviz
|
|
28
|
+
Requires-Dist: lark
|
|
29
|
+
Requires-Dist: more-itertools
|
|
30
|
+
Requires-Dist: pyyaml
|
|
31
|
+
Requires-Dist: typer
|
|
32
|
+
Requires-Dist: bumpversion ; extra == "dev"
|
|
33
|
+
Requires-Dist: jsonschema ; extra == "dev"
|
|
34
|
+
Requires-Dist: mutmut ; extra == "dev"
|
|
35
|
+
Requires-Dist: pre-commit ; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy ; extra == "test"
|
|
37
|
+
Requires-Dist: mypy_extensions ; extra == "test"
|
|
38
|
+
Requires-Dist: pytest ; extra == "test"
|
|
39
|
+
Requires-Dist: pytest-asyncio ; extra == "test"
|
|
40
|
+
Requires-Dist: pytest-benchmark ; extra == "test"
|
|
41
|
+
Requires-Dist: pytest-cov ; extra == "test"
|
|
42
|
+
Requires-Dist: ruff ; extra == "test"
|
|
43
|
+
Requires-Dist: types-PyYAML ; extra == "test"
|
|
44
|
+
Project-URL: Documentation, https://mrijk.github.io/py-predicate/
|
|
45
|
+
Project-URL: Source, https://github.com/mrijk/py-predicate
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Provides-Extra: test
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+

|
|
51
|
+
[](https://codecov.io/gh/mrijk/py-predicate)
|
|
52
|
+
|
|
53
|
+
# py-predicate
|
|
54
|
+
|
|
55
|
+
A typed Python library for composable predicates.
|
|
56
|
+
|
|
57
|
+
Predicates are first-class objects that can be combined with boolean operators, optimized algebraically, compiled to native callables for performance, serialized to multiple formats, and used to instrument functions with runtime contracts.
|
|
58
|
+
|
|
59
|
+
Full documentation: [mrijk.github.io/py-predicate](https://mrijk.github.io/py-predicate/)
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install py-predicate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from predicate import ge_p, le_p
|
|
71
|
+
|
|
72
|
+
ge_2 = ge_p(2)
|
|
73
|
+
le_5 = le_p(5)
|
|
74
|
+
|
|
75
|
+
between = ge_2 & le_5
|
|
76
|
+
|
|
77
|
+
between(3) # True
|
|
78
|
+
between(7) # False
|
|
79
|
+
|
|
80
|
+
filtered = [x for x in range(10) if between(x)] # [2, 3, 4, 5]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Predicates compose with standard Python operators:
|
|
84
|
+
|
|
85
|
+
| Operator | Meaning |
|
|
86
|
+
|----------|---------|
|
|
87
|
+
| `p & q` | both must hold (`AndPredicate`) |
|
|
88
|
+
| `p \| q` | either must hold (`OrPredicate`) |
|
|
89
|
+
| `p ^ q` | exactly one must hold (`XorPredicate`) |
|
|
90
|
+
| `~p` | negation (`NotPredicate`) |
|
|
91
|
+
|
|
92
|
+
## Built-in predicates
|
|
93
|
+
|
|
94
|
+
**Comparisons:** `eq_p`, `ne_p`, `gt_p`, `ge_p`, `lt_p`, `le_p`, `is_close_p`
|
|
95
|
+
|
|
96
|
+
**Ranges:** `ge_le_p`, `ge_lt_p`, `gt_le_p`, `gt_lt_p` (compiled to chained comparisons)
|
|
97
|
+
|
|
98
|
+
**Type checks:** `is_int_p`, `is_str_p`, `is_float_p`, `is_bool_p`, `is_list_p`, `is_dict_p`, `is_none_p`, `is_not_none_p`, `is_instance_p`, and many more
|
|
99
|
+
|
|
100
|
+
**Collections:** `all_p`, `any_p`, `in_p`, `not_in_p`, `has_length_p`, `is_empty_p`, `is_not_empty_p`, `has_key_p`, `has_path_p`
|
|
101
|
+
|
|
102
|
+
**Typed collections:** `is_list_of_p`, `is_set_of_p`, `is_dict_of_p`, `is_tuple_of_p`
|
|
103
|
+
|
|
104
|
+
**Strings:** `regex_p`, `starts_with_p`, `ends_with_p`, `is_alpha_p`, `is_digit_p`, `is_upper_p`, `is_lower_p`, and more
|
|
105
|
+
|
|
106
|
+
**Logic:** `always_p`, `never_p`, `implies_p`, `fn_p`
|
|
107
|
+
|
|
108
|
+
**Numeric:** `is_even_p`, `is_odd_p`, `is_nan_p`, `is_inf_p`, `is_finite_p`, `pos_p`, `neg_p`, `zero_p`
|
|
109
|
+
|
|
110
|
+
## Failure explanation
|
|
111
|
+
|
|
112
|
+
Use `explain` to get a structured description of why a predicate failed:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from predicate import explain, ge_p, le_p
|
|
116
|
+
|
|
117
|
+
between = ge_p(2) & le_p(5)
|
|
118
|
+
explain(between, 8)
|
|
119
|
+
# {'reason': 'Right predicate failed', 'right': {'reason': 'Expected 8 <= 5'}}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Optimizer
|
|
123
|
+
|
|
124
|
+
Predicate trees can be algebraically simplified:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from predicate import ge_p, le_p, optimize, can_optimize
|
|
128
|
+
|
|
129
|
+
p = ge_p(2) & le_p(5)
|
|
130
|
+
can_optimize(p) # True (may simplify to a range predicate, etc.)
|
|
131
|
+
optimized = optimize(p)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The optimizer handles identities like `p & always_true == p`, `p | always_true == always_true`, double-negation elimination, De Morgan's laws, and more.
|
|
135
|
+
|
|
136
|
+
## Compile to native callables
|
|
137
|
+
|
|
138
|
+
`compile_predicate` translates a predicate tree into a native Python lambda via AST generation, removing interpreter overhead on hot paths:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from predicate import compile_predicate, ge_p, le_p
|
|
142
|
+
|
|
143
|
+
between = ge_p(2) & le_p(5)
|
|
144
|
+
fast = compile_predicate(between) # returns a CompiledPredicate
|
|
145
|
+
|
|
146
|
+
fast(3) # True — evaluated by a native lambda: lambda x: x >= 2 and x <= 5
|
|
147
|
+
fast(8) # False
|
|
148
|
+
|
|
149
|
+
# Introspection is preserved
|
|
150
|
+
repr(fast) # same as repr(between)
|
|
151
|
+
fast.explain_failure # delegates to the original predicate
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Use `try_compile_predicate` to fall back to the original when compilation is not supported (e.g. `fn_p`, `regex_p`):
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from predicate import try_compile_predicate
|
|
158
|
+
|
|
159
|
+
safe = try_compile_predicate(some_predicate) # always returns a callable
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Value generation
|
|
163
|
+
|
|
164
|
+
Predicates can drive property-based testing by generating values that satisfy or violate them:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from predicate import generate_true, generate_false, ge_p, le_p
|
|
168
|
+
from more_itertools import take
|
|
169
|
+
|
|
170
|
+
between = ge_p(2) & le_p(5)
|
|
171
|
+
|
|
172
|
+
take(5, generate_true(between)) # e.g. [2, 3, 4, 5, 2]
|
|
173
|
+
take(5, generate_false(between)) # e.g. [0, 1, 6, 7, -1]
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Recursive predicates
|
|
177
|
+
|
|
178
|
+
Use `root_p` (or `this_p`) to define self-referencing predicates over arbitrarily nested structures without writing recursive functions:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from predicate import all_p, is_list_p, is_str_p, root_p
|
|
182
|
+
|
|
183
|
+
# Matches a string, or a list of strings/lists (recursively)
|
|
184
|
+
str_or_nested = is_str_p | (is_list_p & all_p(root_p))
|
|
185
|
+
|
|
186
|
+
str_or_nested("hello") # True
|
|
187
|
+
str_or_nested(["a", ["b", "c"]]) # True
|
|
188
|
+
str_or_nested(["a", 1]) # False
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
For mutually recursive predicates, use `mutual_recur_p`.
|
|
192
|
+
|
|
193
|
+
## Runtime instrumentation (Spec)
|
|
194
|
+
|
|
195
|
+
Instrument functions with predicate-based contracts that are checked at call time:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from predicate import ge_p, instrument, is_int_p, is_str_p, le_p
|
|
199
|
+
|
|
200
|
+
@instrument({"args": {"x": is_int_p & ge_p(0) & le_p(100)}, "ret": is_str_p})
|
|
201
|
+
def grade(x: int) -> str:
|
|
202
|
+
return "pass" if x >= 50 else "fail"
|
|
203
|
+
|
|
204
|
+
grade(75) # "pass"
|
|
205
|
+
grade(-1) # raises ValueError: Parameter predicate for function grade failed
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
You can also instrument all functions in a module or class:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from predicate import instrument_module, instrument_class
|
|
212
|
+
|
|
213
|
+
instrument_module(my_module)
|
|
214
|
+
instrument_class(MyClass)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Specs can include cross-argument constraints via `fn` or `fn_p` keys, and expected exception types via `raises`.
|
|
218
|
+
|
|
219
|
+
## Analysis
|
|
220
|
+
|
|
221
|
+
Check logical properties of predicates:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from predicate import are_equivalent, is_satisfiable, is_tautology, ge_p, le_p, always_p
|
|
225
|
+
|
|
226
|
+
is_tautology(always_p) # True
|
|
227
|
+
is_satisfiable(ge_p(5) & le_p(3)) # False (unsatisfiable)
|
|
228
|
+
are_equivalent(~~ge_p(2), ge_p(2)) # True
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Serialization
|
|
232
|
+
|
|
233
|
+
Export predicates to multiple formats:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from predicate import ge_p, le_p, to_json, to_yaml, to_dot, to_latex
|
|
237
|
+
|
|
238
|
+
p = ge_p(2) & le_p(5)
|
|
239
|
+
|
|
240
|
+
to_json(p) # JSON string
|
|
241
|
+
to_yaml(p) # YAML string
|
|
242
|
+
to_dot(p) # Graphviz DOT for visualization
|
|
243
|
+
to_latex(p) # LaTeX expression
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Deserialize from JSON:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from predicate.formatter.from_json import from_json
|
|
250
|
+
|
|
251
|
+
p = from_json('{"and": [{"ge": 2}, {"le": 5}]}')
|
|
252
|
+
```
|
|
253
|
+
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
[](https://codecov.io/gh/mrijk/py-predicate)
|
|
4
|
+
|
|
5
|
+
# py-predicate
|
|
6
|
+
|
|
7
|
+
A typed Python library for composable predicates.
|
|
8
|
+
|
|
9
|
+
Predicates are first-class objects that can be combined with boolean operators, optimized algebraically, compiled to native callables for performance, serialized to multiple formats, and used to instrument functions with runtime contracts.
|
|
10
|
+
|
|
11
|
+
Full documentation: [mrijk.github.io/py-predicate](https://mrijk.github.io/py-predicate/)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install py-predicate
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from predicate import ge_p, le_p
|
|
23
|
+
|
|
24
|
+
ge_2 = ge_p(2)
|
|
25
|
+
le_5 = le_p(5)
|
|
26
|
+
|
|
27
|
+
between = ge_2 & le_5
|
|
28
|
+
|
|
29
|
+
between(3) # True
|
|
30
|
+
between(7) # False
|
|
31
|
+
|
|
32
|
+
filtered = [x for x in range(10) if between(x)] # [2, 3, 4, 5]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Predicates compose with standard Python operators:
|
|
36
|
+
|
|
37
|
+
| Operator | Meaning |
|
|
38
|
+
|----------|---------|
|
|
39
|
+
| `p & q` | both must hold (`AndPredicate`) |
|
|
40
|
+
| `p \| q` | either must hold (`OrPredicate`) |
|
|
41
|
+
| `p ^ q` | exactly one must hold (`XorPredicate`) |
|
|
42
|
+
| `~p` | negation (`NotPredicate`) |
|
|
43
|
+
|
|
44
|
+
## Built-in predicates
|
|
45
|
+
|
|
46
|
+
**Comparisons:** `eq_p`, `ne_p`, `gt_p`, `ge_p`, `lt_p`, `le_p`, `is_close_p`
|
|
47
|
+
|
|
48
|
+
**Ranges:** `ge_le_p`, `ge_lt_p`, `gt_le_p`, `gt_lt_p` (compiled to chained comparisons)
|
|
49
|
+
|
|
50
|
+
**Type checks:** `is_int_p`, `is_str_p`, `is_float_p`, `is_bool_p`, `is_list_p`, `is_dict_p`, `is_none_p`, `is_not_none_p`, `is_instance_p`, and many more
|
|
51
|
+
|
|
52
|
+
**Collections:** `all_p`, `any_p`, `in_p`, `not_in_p`, `has_length_p`, `is_empty_p`, `is_not_empty_p`, `has_key_p`, `has_path_p`
|
|
53
|
+
|
|
54
|
+
**Typed collections:** `is_list_of_p`, `is_set_of_p`, `is_dict_of_p`, `is_tuple_of_p`
|
|
55
|
+
|
|
56
|
+
**Strings:** `regex_p`, `starts_with_p`, `ends_with_p`, `is_alpha_p`, `is_digit_p`, `is_upper_p`, `is_lower_p`, and more
|
|
57
|
+
|
|
58
|
+
**Logic:** `always_p`, `never_p`, `implies_p`, `fn_p`
|
|
59
|
+
|
|
60
|
+
**Numeric:** `is_even_p`, `is_odd_p`, `is_nan_p`, `is_inf_p`, `is_finite_p`, `pos_p`, `neg_p`, `zero_p`
|
|
61
|
+
|
|
62
|
+
## Failure explanation
|
|
63
|
+
|
|
64
|
+
Use `explain` to get a structured description of why a predicate failed:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from predicate import explain, ge_p, le_p
|
|
68
|
+
|
|
69
|
+
between = ge_p(2) & le_p(5)
|
|
70
|
+
explain(between, 8)
|
|
71
|
+
# {'reason': 'Right predicate failed', 'right': {'reason': 'Expected 8 <= 5'}}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Optimizer
|
|
75
|
+
|
|
76
|
+
Predicate trees can be algebraically simplified:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from predicate import ge_p, le_p, optimize, can_optimize
|
|
80
|
+
|
|
81
|
+
p = ge_p(2) & le_p(5)
|
|
82
|
+
can_optimize(p) # True (may simplify to a range predicate, etc.)
|
|
83
|
+
optimized = optimize(p)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The optimizer handles identities like `p & always_true == p`, `p | always_true == always_true`, double-negation elimination, De Morgan's laws, and more.
|
|
87
|
+
|
|
88
|
+
## Compile to native callables
|
|
89
|
+
|
|
90
|
+
`compile_predicate` translates a predicate tree into a native Python lambda via AST generation, removing interpreter overhead on hot paths:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from predicate import compile_predicate, ge_p, le_p
|
|
94
|
+
|
|
95
|
+
between = ge_p(2) & le_p(5)
|
|
96
|
+
fast = compile_predicate(between) # returns a CompiledPredicate
|
|
97
|
+
|
|
98
|
+
fast(3) # True — evaluated by a native lambda: lambda x: x >= 2 and x <= 5
|
|
99
|
+
fast(8) # False
|
|
100
|
+
|
|
101
|
+
# Introspection is preserved
|
|
102
|
+
repr(fast) # same as repr(between)
|
|
103
|
+
fast.explain_failure # delegates to the original predicate
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use `try_compile_predicate` to fall back to the original when compilation is not supported (e.g. `fn_p`, `regex_p`):
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from predicate import try_compile_predicate
|
|
110
|
+
|
|
111
|
+
safe = try_compile_predicate(some_predicate) # always returns a callable
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Value generation
|
|
115
|
+
|
|
116
|
+
Predicates can drive property-based testing by generating values that satisfy or violate them:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from predicate import generate_true, generate_false, ge_p, le_p
|
|
120
|
+
from more_itertools import take
|
|
121
|
+
|
|
122
|
+
between = ge_p(2) & le_p(5)
|
|
123
|
+
|
|
124
|
+
take(5, generate_true(between)) # e.g. [2, 3, 4, 5, 2]
|
|
125
|
+
take(5, generate_false(between)) # e.g. [0, 1, 6, 7, -1]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Recursive predicates
|
|
129
|
+
|
|
130
|
+
Use `root_p` (or `this_p`) to define self-referencing predicates over arbitrarily nested structures without writing recursive functions:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from predicate import all_p, is_list_p, is_str_p, root_p
|
|
134
|
+
|
|
135
|
+
# Matches a string, or a list of strings/lists (recursively)
|
|
136
|
+
str_or_nested = is_str_p | (is_list_p & all_p(root_p))
|
|
137
|
+
|
|
138
|
+
str_or_nested("hello") # True
|
|
139
|
+
str_or_nested(["a", ["b", "c"]]) # True
|
|
140
|
+
str_or_nested(["a", 1]) # False
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
For mutually recursive predicates, use `mutual_recur_p`.
|
|
144
|
+
|
|
145
|
+
## Runtime instrumentation (Spec)
|
|
146
|
+
|
|
147
|
+
Instrument functions with predicate-based contracts that are checked at call time:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from predicate import ge_p, instrument, is_int_p, is_str_p, le_p
|
|
151
|
+
|
|
152
|
+
@instrument({"args": {"x": is_int_p & ge_p(0) & le_p(100)}, "ret": is_str_p})
|
|
153
|
+
def grade(x: int) -> str:
|
|
154
|
+
return "pass" if x >= 50 else "fail"
|
|
155
|
+
|
|
156
|
+
grade(75) # "pass"
|
|
157
|
+
grade(-1) # raises ValueError: Parameter predicate for function grade failed
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
You can also instrument all functions in a module or class:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from predicate import instrument_module, instrument_class
|
|
164
|
+
|
|
165
|
+
instrument_module(my_module)
|
|
166
|
+
instrument_class(MyClass)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Specs can include cross-argument constraints via `fn` or `fn_p` keys, and expected exception types via `raises`.
|
|
170
|
+
|
|
171
|
+
## Analysis
|
|
172
|
+
|
|
173
|
+
Check logical properties of predicates:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from predicate import are_equivalent, is_satisfiable, is_tautology, ge_p, le_p, always_p
|
|
177
|
+
|
|
178
|
+
is_tautology(always_p) # True
|
|
179
|
+
is_satisfiable(ge_p(5) & le_p(3)) # False (unsatisfiable)
|
|
180
|
+
are_equivalent(~~ge_p(2), ge_p(2)) # True
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Serialization
|
|
184
|
+
|
|
185
|
+
Export predicates to multiple formats:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from predicate import ge_p, le_p, to_json, to_yaml, to_dot, to_latex
|
|
189
|
+
|
|
190
|
+
p = ge_p(2) & le_p(5)
|
|
191
|
+
|
|
192
|
+
to_json(p) # JSON string
|
|
193
|
+
to_yaml(p) # YAML string
|
|
194
|
+
to_dot(p) # Graphviz DOT for visualization
|
|
195
|
+
to_latex(p) # LaTeX expression
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Deserialize from JSON:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from predicate.formatter.from_json import from_json
|
|
202
|
+
|
|
203
|
+
p = from_json('{"and": [{"ge": 2}, {"le": 5}]}')
|
|
204
|
+
```
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
"""The py-predicate module."""
|
|
2
2
|
|
|
3
|
-
__version__ = "
|
|
3
|
+
__version__ = "1.6.0"
|
|
4
4
|
|
|
5
5
|
from predicate.all_predicate import all_p
|
|
6
6
|
from predicate.always_false_predicate import always_false_p, never_p
|
|
7
7
|
from predicate.always_true_predicate import always_p, always_true_p
|
|
8
|
+
from predicate.analysis import are_equivalent, is_satisfiable, is_tautology
|
|
8
9
|
from predicate.any_predicate import any_p
|
|
9
10
|
from predicate.comp_predicate import comp_p
|
|
10
11
|
from predicate.count_predicate import count_p, exactly_one_p, exactly_zero_p
|
|
11
12
|
from predicate.dict_of_predicate import is_dict_of_p
|
|
12
13
|
from predicate.eq_predicate import eq_false_p, eq_p, eq_true_p, zero_p
|
|
13
14
|
from predicate.exactly_predicate import exactly_n
|
|
14
|
-
from predicate.exception_predicate import
|
|
15
|
+
from predicate.exception_predicate import exception_p
|
|
16
|
+
from predicate.compile_predicate import compile_predicate, try_compile_predicate
|
|
15
17
|
from predicate.explain import explain
|
|
16
18
|
from predicate.fn_predicate import fn_p, is_even_p, is_finite_p, is_inf_p, is_nan_p, is_odd_p
|
|
17
|
-
from predicate.formatter import to_dot, to_json, to_latex
|
|
19
|
+
from predicate.formatter import to_dot, to_json, to_latex, to_yaml
|
|
18
20
|
from predicate.ge_predicate import ge_p
|
|
19
21
|
from predicate.generator.generate_false import generate_false
|
|
20
22
|
from predicate.generator.generate_true import generate_true
|
|
@@ -24,6 +26,8 @@ from predicate.has_length_predicate import has_length_p, is_empty_p, is_not_empt
|
|
|
24
26
|
from predicate.has_path_predicate import has_path_p
|
|
25
27
|
from predicate.implies_predicate import implies_p
|
|
26
28
|
from predicate.in_predicate import in_p
|
|
29
|
+
from predicate.is_async_predicate import is_async_p
|
|
30
|
+
from predicate.is_close_predicate import is_close_p
|
|
27
31
|
from predicate.is_falsy_predicate import is_falsy_p
|
|
28
32
|
from predicate.is_instance_predicate import (
|
|
29
33
|
is_bool_p,
|
|
@@ -69,6 +73,7 @@ from predicate.optimizer.predicate_optimizer import can_optimize, optimize
|
|
|
69
73
|
from predicate.optional_predicate import optional
|
|
70
74
|
from predicate.plus_predicate import plus
|
|
71
75
|
from predicate.predicate import and_p, or_p, xor_p
|
|
76
|
+
from predicate.raises_predicate import raises_exception_p, raises_p
|
|
72
77
|
from predicate.range_predicate import ge_le_p, ge_lt_p, gt_le_p, gt_lt_p
|
|
73
78
|
from predicate.recur_predicate import recur_p
|
|
74
79
|
from predicate.reduce_predicate import reduce_p
|
|
@@ -76,14 +81,20 @@ from predicate.regex_predicate import regex_p
|
|
|
76
81
|
from predicate.repeat_predicate import repeat
|
|
77
82
|
from predicate.set_of_predicate import is_set_of_p
|
|
78
83
|
from predicate.set_predicates import (
|
|
84
|
+
intersects_p,
|
|
79
85
|
is_real_subset_p,
|
|
80
86
|
is_real_superset_p,
|
|
81
87
|
is_subset_p,
|
|
82
88
|
is_superset_p,
|
|
83
89
|
)
|
|
84
90
|
from predicate.spec.exercise import exercise
|
|
85
|
-
from predicate.spec.instrument import
|
|
86
|
-
|
|
91
|
+
from predicate.spec.instrument import (
|
|
92
|
+
instrument,
|
|
93
|
+
instrument_class,
|
|
94
|
+
instrument_function,
|
|
95
|
+
instrument_module,
|
|
96
|
+
is_instrumented,
|
|
97
|
+
)
|
|
87
98
|
from predicate.standard_predicates import (
|
|
88
99
|
is_dict_p,
|
|
89
100
|
is_float_p,
|
|
@@ -96,12 +107,14 @@ from predicate.standard_predicates import (
|
|
|
96
107
|
this_p,
|
|
97
108
|
)
|
|
98
109
|
from predicate.star_predicate import star
|
|
110
|
+
from predicate.struct_predicate import is_struct_p
|
|
99
111
|
from predicate.tee_predicate import tee_p
|
|
100
112
|
from predicate.tuple_of_predicate import is_tuple_of_p
|
|
101
113
|
|
|
102
114
|
__all__ = [
|
|
103
|
-
"
|
|
104
|
-
"
|
|
115
|
+
"are_equivalent",
|
|
116
|
+
"is_satisfiable",
|
|
117
|
+
"is_tautology",
|
|
105
118
|
"all_p",
|
|
106
119
|
"always_false_p",
|
|
107
120
|
"always_p",
|
|
@@ -109,10 +122,12 @@ __all__ = [
|
|
|
109
122
|
"and_p",
|
|
110
123
|
"any_p",
|
|
111
124
|
"can_optimize",
|
|
125
|
+
"compile_predicate",
|
|
112
126
|
"comp_p",
|
|
113
127
|
"count_p",
|
|
114
128
|
"eq_false_p",
|
|
115
129
|
"eq_p",
|
|
130
|
+
"ends_with_p",
|
|
116
131
|
"eq_true_p",
|
|
117
132
|
"exactly_n",
|
|
118
133
|
"exactly_one_p",
|
|
@@ -134,18 +149,26 @@ __all__ = [
|
|
|
134
149
|
"has_path_p",
|
|
135
150
|
"implies_p",
|
|
136
151
|
"in_p",
|
|
152
|
+
"intersects_p",
|
|
153
|
+
"instrument",
|
|
154
|
+
"instrument_class",
|
|
137
155
|
"instrument_function",
|
|
156
|
+
"instrument_module",
|
|
157
|
+
"is_instrumented",
|
|
138
158
|
"is_alnum_p",
|
|
139
159
|
"is_alpha_p",
|
|
140
160
|
"is_ascii_p",
|
|
161
|
+
"is_async_p",
|
|
141
162
|
"is_bool_p",
|
|
142
163
|
"is_bytes_p",
|
|
143
164
|
"is_callable_p",
|
|
165
|
+
"is_close_p",
|
|
144
166
|
"is_complex_p",
|
|
145
167
|
"is_container_p",
|
|
146
168
|
"is_date_p",
|
|
147
169
|
"is_datetime_p",
|
|
148
170
|
"is_decimal_p",
|
|
171
|
+
"is_digit_p",
|
|
149
172
|
"is_dict_of_p",
|
|
150
173
|
"is_dict_p",
|
|
151
174
|
"is_frozenset_p",
|
|
@@ -174,20 +197,24 @@ __all__ = [
|
|
|
174
197
|
"is_none_p",
|
|
175
198
|
"is_not_empty_p",
|
|
176
199
|
"is_not_none_p",
|
|
200
|
+
"is_numeric_p",
|
|
177
201
|
"is_number_p",
|
|
178
202
|
"is_odd_p",
|
|
179
203
|
"is_p",
|
|
180
204
|
"is_path_p",
|
|
181
205
|
"is_predicate_of_p",
|
|
182
206
|
"is_predicate_p",
|
|
207
|
+
"is_printable_p",
|
|
183
208
|
"is_range_p",
|
|
184
209
|
"is_real_subset_p",
|
|
185
210
|
"is_real_superset_p",
|
|
186
211
|
"is_sequence_p",
|
|
187
212
|
"is_set_of_p",
|
|
188
213
|
"is_set_p",
|
|
214
|
+
"is_space_p",
|
|
189
215
|
"is_str_enum_p",
|
|
190
216
|
"is_str_p",
|
|
217
|
+
"is_struct_p",
|
|
191
218
|
"is_subclass_p",
|
|
192
219
|
"is_subset_p",
|
|
193
220
|
"is_superset_p",
|
|
@@ -219,26 +246,37 @@ __all__ = [
|
|
|
219
246
|
"pos_p",
|
|
220
247
|
"recur_p",
|
|
221
248
|
"reduce_p",
|
|
249
|
+
"raises_exception_p",
|
|
250
|
+
"raises_p",
|
|
222
251
|
"regex_p",
|
|
223
252
|
"repeat",
|
|
224
253
|
"root_p",
|
|
225
254
|
"star",
|
|
255
|
+
"starts_with_p",
|
|
226
256
|
"tee_p",
|
|
227
257
|
"this_p",
|
|
258
|
+
"try_compile_predicate",
|
|
228
259
|
"to_dot",
|
|
229
260
|
"to_json",
|
|
230
261
|
"to_latex",
|
|
262
|
+
"to_yaml",
|
|
231
263
|
"xor_p",
|
|
232
264
|
"zero_p",
|
|
233
265
|
]
|
|
234
266
|
|
|
235
267
|
from predicate.str_predicates import (
|
|
268
|
+
ends_with_p,
|
|
236
269
|
is_alnum_p,
|
|
237
270
|
is_alpha_p,
|
|
238
271
|
is_ascii_p,
|
|
239
272
|
is_decimal_p,
|
|
273
|
+
is_digit_p,
|
|
240
274
|
is_identifier_p,
|
|
241
275
|
is_lower_p,
|
|
276
|
+
is_numeric_p,
|
|
277
|
+
is_printable_p,
|
|
278
|
+
is_space_p,
|
|
242
279
|
is_title_p,
|
|
243
280
|
is_upper_p,
|
|
281
|
+
starts_with_p,
|
|
244
282
|
)
|
|
@@ -2,9 +2,8 @@ from dataclasses import dataclass
|
|
|
2
2
|
from itertools import takewhile
|
|
3
3
|
from typing import Any, Iterable, Iterator, override
|
|
4
4
|
|
|
5
|
-
from more_itertools import ilen
|
|
5
|
+
from more_itertools import first, ilen
|
|
6
6
|
|
|
7
|
-
from predicate.helpers import first_false
|
|
8
7
|
from predicate.predicate import Predicate, resolve_predicate
|
|
9
8
|
|
|
10
9
|
|
|
@@ -34,9 +33,8 @@ class AllPredicate[T](Predicate[T]):
|
|
|
34
33
|
|
|
35
34
|
@override
|
|
36
35
|
def explain_failure(self, iterable: Iterable[T]) -> dict:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return {"reason": f"Item '{fail}' didn't match predicate {self.predicate!r}"}
|
|
36
|
+
index, item = first((i, x) for i, x in enumerate(iterable) if not self.predicate(x))
|
|
37
|
+
return {"index": index, "value": item} | self.predicate.explain_failure(item)
|
|
40
38
|
|
|
41
39
|
@override
|
|
42
40
|
def consumes(self, iterable: Iterable[Any]) -> Iterator[int]:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from predicate.predicate import Predicate
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_tautology(predicate: Predicate) -> bool:
|
|
5
|
+
"""Return True if the predicate is True for all possible inputs."""
|
|
6
|
+
from predicate.always_true_predicate import AlwaysTruePredicate
|
|
7
|
+
from predicate.optimizer.predicate_optimizer import optimize
|
|
8
|
+
|
|
9
|
+
return isinstance(optimize(predicate), AlwaysTruePredicate)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_satisfiable(predicate: Predicate) -> bool:
|
|
13
|
+
"""Return True if the predicate can be True for at least one input."""
|
|
14
|
+
from predicate.always_false_predicate import AlwaysFalsePredicate
|
|
15
|
+
from predicate.optimizer.predicate_optimizer import optimize
|
|
16
|
+
|
|
17
|
+
return not isinstance(optimize(predicate), AlwaysFalsePredicate)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def are_equivalent(p: Predicate, q: Predicate) -> bool:
|
|
21
|
+
"""Return True if both predicates accept exactly the same values."""
|
|
22
|
+
return is_tautology(~(p ^ q))
|
|
@@ -33,7 +33,11 @@ class AnyPredicate[T](Predicate[T]):
|
|
|
33
33
|
|
|
34
34
|
@override
|
|
35
35
|
def explain_failure(self, iterable: Iterable[T]) -> dict:
|
|
36
|
-
|
|
36
|
+
failures = [
|
|
37
|
+
{"index": index, "value": item} | self.predicate.explain_failure(item)
|
|
38
|
+
for index, item in enumerate(iterable)
|
|
39
|
+
]
|
|
40
|
+
return {"failures": failures}
|
|
37
41
|
|
|
38
42
|
@override
|
|
39
43
|
def consumes(self, iterable: Iterable[Any]) -> Iterator[int]:
|