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.
Files changed (123) hide show
  1. py_predicate-1.7.0/PKG-INFO +253 -0
  2. py_predicate-1.7.0/README.md +204 -0
  3. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/__init__.py +45 -7
  4. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/all_predicate.py +3 -5
  5. py_predicate-1.7.0/predicate/analysis.py +22 -0
  6. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/any_predicate.py +5 -1
  7. py_predicate-1.7.0/predicate/compile_predicate.py +451 -0
  8. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/dict_of_predicate.py +34 -17
  9. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/formatter/__init__.py +4 -0
  10. py_predicate-1.7.0/predicate/formatter/fn_source.py +57 -0
  11. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/formatter/format_dot.py +78 -1
  12. py_predicate-1.7.0/predicate/formatter/format_json.py +196 -0
  13. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/formatter/format_latex.py +10 -0
  14. py_predicate-1.7.0/predicate/formatter/format_yaml.py +9 -0
  15. py_predicate-1.7.0/predicate/formatter/from_json.py +178 -0
  16. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/generate_false.py +210 -69
  17. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/generate_true.py +102 -15
  18. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/helpers.py +46 -46
  19. py_predicate-1.7.0/predicate/has_key_predicate.py +27 -0
  20. py_predicate-1.7.0/predicate/has_path_predicate.py +60 -0
  21. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/helpers.py +9 -0
  22. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/implies.py +28 -3
  23. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/in_predicate.py +4 -0
  24. py_predicate-1.7.0/predicate/is_async_predicate.py +23 -0
  25. py_predicate-1.7.0/predicate/is_close_predicate.py +33 -0
  26. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_instance_predicate.py +12 -16
  27. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_none_predicate.py +2 -2
  28. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_not_none_predicate.py +7 -1
  29. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_subclass_predicate.py +3 -4
  30. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/juxt_predicate.py +9 -1
  31. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/list_of_predicate.py +10 -9
  32. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/mutual_recur_predicate.py +2 -1
  33. py_predicate-1.7.0/predicate/optimizer/intersects_optimizer.py +9 -0
  34. py_predicate-1.7.0/predicate/optimizer/list_of_optimizer.py +33 -0
  35. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/or_optimizer.py +7 -6
  36. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/predicate_optimizer.py +8 -0
  37. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/xor_optimizer.py +8 -8
  38. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/plus_predicate.py +1 -1
  39. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/predicate.py +5 -3
  40. py_predicate-1.7.0/predicate/raises_predicate.py +53 -0
  41. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/recur_predicate.py +2 -2
  42. py_predicate-1.7.0/predicate/reduce_predicate.py +38 -0
  43. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/set_of_predicate.py +3 -3
  44. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/set_predicates.py +31 -12
  45. py_predicate-1.7.0/predicate/spec/exercise.py +15 -0
  46. py_predicate-1.7.0/predicate/spec/exercise_class.py +77 -0
  47. py_predicate-1.7.0/predicate/spec/exercise_function.py +61 -0
  48. py_predicate-1.7.0/predicate/spec/exercise_helpers.py +135 -0
  49. py_predicate-1.7.0/predicate/spec/instrument.py +198 -0
  50. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/spec/spec.py +1 -0
  51. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/standard_predicates.py +2 -2
  52. py_predicate-1.7.0/predicate/struct_predicate.py +69 -0
  53. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/tuple_of_predicate.py +4 -3
  54. {py_predicate-1.5 → py_predicate-1.7.0}/pyproject.toml +17 -5
  55. py_predicate-1.5/PKG-INFO +0 -126
  56. py_predicate-1.5/README.md +0 -79
  57. py_predicate-1.5/predicate/formatter/format_json.py +0 -62
  58. py_predicate-1.5/predicate/has_key_predicate.py +0 -26
  59. py_predicate-1.5/predicate/has_path_predicate.py +0 -66
  60. py_predicate-1.5/predicate/reduce_predicate.py +0 -25
  61. py_predicate-1.5/predicate/spec/exercise.py +0 -256
  62. py_predicate-1.5/predicate/spec/instrument.py +0 -48
  63. {py_predicate-1.5 → py_predicate-1.7.0}/LICENSE +0 -0
  64. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/always_false_predicate.py +0 -0
  65. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/always_true_predicate.py +0 -0
  66. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/comp_predicate.py +0 -0
  67. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/constructor/__init__.py +0 -0
  68. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/constructor/construct.py +0 -0
  69. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/constructor/helpers.py +0 -0
  70. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/constructor/mutate.py +0 -0
  71. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/consumes.py +0 -0
  72. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/count_predicate.py +0 -0
  73. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/eq_predicate.py +0 -0
  74. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/exactly_predicate.py +0 -0
  75. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/exception_predicate.py +0 -0
  76. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/explain.py +0 -0
  77. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/fn_predicate.py +0 -0
  78. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/formatter/helpers.py +0 -0
  79. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/ge_predicate.py +0 -0
  80. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/__init__.py +0 -0
  81. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/generator/generate_predicate.py +0 -0
  82. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/gt_predicate.py +0 -0
  83. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/has_length_predicate.py +0 -0
  84. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/implies_predicate.py +0 -0
  85. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/in_predicate_predicate.py +0 -0
  86. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/ip_address_predicates.py +0 -0
  87. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_callable_predicate.py +0 -0
  88. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_falsy_predicate.py +0 -0
  89. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_lambda_predicate.py +0 -0
  90. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_predicate.py +0 -0
  91. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_predicate_of_p.py +0 -0
  92. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_same_predicate.py +0 -0
  93. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/is_truthy_predicate.py +0 -0
  94. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/lazy_predicate.py +0 -0
  95. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/le_predicate.py +0 -0
  96. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/lt_predicate.py +0 -0
  97. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/match_predicate.py +0 -0
  98. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/named_predicate.py +0 -0
  99. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/ne_predicate.py +0 -0
  100. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/negate.py +0 -0
  101. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/not_in_predicate.py +0 -0
  102. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/__init__.py +0 -0
  103. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/all_optimizer.py +0 -0
  104. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/and_optimizer.py +0 -0
  105. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/any_optimizer.py +0 -0
  106. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/helpers.py +0 -0
  107. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/in_optimizer.py +0 -0
  108. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/not_in_optimizer.py +0 -0
  109. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optimizer/not_optimizer.py +0 -0
  110. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/optional_predicate.py +0 -0
  111. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/parser.py +0 -0
  112. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/predicate_factory.py +0 -0
  113. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/property_predicate.py +0 -0
  114. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/range_predicate.py +0 -0
  115. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/regex_predicate.py +0 -0
  116. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/repeat_predicate.py +0 -0
  117. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/root_predicate.py +0 -0
  118. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/spec/__init__.py +0 -0
  119. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/star_predicate.py +0 -0
  120. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/str_predicates.py +0 -0
  121. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/tee_predicate.py +0 -0
  122. {py_predicate-1.5 → py_predicate-1.7.0}/predicate/this_predicate.py +0 -0
  123. {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
+ ![Documentation](https://github.com/mrijk/py-predicate/actions/workflows/pages.yaml/badge.svg)
50
+ ![Test](https://github.com/mrijk/py-predicate/actions/workflows/test.yaml/badge.svg)
51
+ [![codecov](https://codecov.io/gh/mrijk/py-predicate/graph/badge.svg?token=KMBDJNC3W9)](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
+ ![Documentation](https://github.com/mrijk/py-predicate/actions/workflows/pages.yaml/badge.svg)
2
+ ![Test](https://github.com/mrijk/py-predicate/actions/workflows/test.yaml/badge.svg)
3
+ [![codecov](https://codecov.io/gh/mrijk/py-predicate/graph/badge.svg?token=KMBDJNC3W9)](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__ = "0.0.1"
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 PredicateError, exception_p
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 instrument_function
86
- from predicate.spec.spec import Spec
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
- "PredicateError",
104
- "Spec",
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
- fail = first_false(iterable, self.predicate)
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
- return {"reason": f"No item matches predicate {self.predicate!r}"}
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]: