py-predicate 0.1__tar.gz → 0.3__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-0.1 → py_predicate-0.3}/PKG-INFO +46 -12
- py_predicate-0.3/README.md +56 -0
- py_predicate-0.3/predicate/__init__.py +148 -0
- py_predicate-0.3/predicate/comp_predicate.py +18 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/formatter/format_dot.py +78 -10
- {py_predicate-0.1 → py_predicate-0.3}/predicate/formatter/format_json.py +12 -1
- py_predicate-0.3/predicate/implies.py +53 -0
- py_predicate-0.3/predicate/lazy_predicate.py +34 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/negate.py +33 -4
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/and_optimizer.py +24 -40
- py_predicate-0.3/predicate/optimizer/in_optimizer.py +13 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/not_optimizer.py +4 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/or_optimizer.py +27 -31
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/predicate_optimizer.py +4 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/rules.py +2 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/xor_optimizer.py +18 -14
- py_predicate-0.3/predicate/parser.py +69 -0
- py_predicate-0.3/predicate/predicate.py +490 -0
- py_predicate-0.3/predicate/regex_predicate.py +18 -0
- py_predicate-0.3/predicate/root_predicate.py +40 -0
- py_predicate-0.3/predicate/standard_predicates.py +241 -0
- py_predicate-0.3/predicate/this_predicate.py +51 -0
- py_predicate-0.3/predicate/truth_table.py +63 -0
- {py_predicate-0.1 → py_predicate-0.3}/pyproject.toml +7 -5
- py_predicate-0.1/README.md +0 -26
- py_predicate-0.1/predicate/__init__.py +0 -63
- py_predicate-0.1/predicate/optimizer/parser.py +0 -74
- py_predicate-0.1/predicate/predicate.py +0 -263
- py_predicate-0.1/predicate/standard_predicates.py +0 -89
- {py_predicate-0.1 → py_predicate-0.3}/predicate/formatter/__init__.py +0 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/__init__.py +0 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/all_optimizer.py +0 -0
- {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/any_optimizer.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: py_predicate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3
|
|
4
4
|
Summary: Module to create composable predicates
|
|
5
5
|
Author-email: Maurits Rijk <maurits.rijk@gmail.com>
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Classifier: Intended Audience :: Information Technology
|
|
9
9
|
Classifier: Intended Audience :: Developers
|
|
@@ -15,14 +15,16 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
16
16
|
Classifier: Topic :: Software Development
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
|
-
Classifier: Development Status ::
|
|
18
|
+
Classifier: Development Status :: 4 - Beta
|
|
19
19
|
Classifier: Environment :: Web Environment
|
|
20
20
|
Classifier: License :: OSI Approved :: MIT License
|
|
21
21
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
24
24
|
Requires-Dist: graphviz
|
|
25
|
+
Requires-Dist: lark
|
|
25
26
|
Requires-Dist: more-itertools
|
|
27
|
+
Requires-Dist: typer
|
|
26
28
|
Requires-Dist: bumpversion ; extra == "dev"
|
|
27
29
|
Requires-Dist: jsonschema ; extra == "dev"
|
|
28
30
|
Requires-Dist: pre-commit ; extra == "dev"
|
|
@@ -32,6 +34,7 @@ Requires-Dist: mypy_extensions ; extra == "test"
|
|
|
32
34
|
Requires-Dist: pytest ; extra == "test"
|
|
33
35
|
Requires-Dist: pytest-cov ; extra == "test"
|
|
34
36
|
Requires-Dist: ruff ; extra == "test"
|
|
37
|
+
Project-URL: Documentation, https://mrijk.github.io/py-predicate/
|
|
35
38
|
Project-URL: Source, https://github.com/mrijk/py-predicate
|
|
36
39
|
Provides-Extra: dev
|
|
37
40
|
Provides-Extra: test
|
|
@@ -42,23 +45,54 @@ Provides-Extra: test
|
|
|
42
45
|
|
|
43
46
|
# Introduction
|
|
44
47
|
|
|
45
|
-
py-predicate is a Python library to create composable predicates
|
|
48
|
+
py-predicate is a typed Python library to create composable predicates.
|
|
46
49
|
|
|
47
|
-
#
|
|
50
|
+
# Getting started
|
|
51
|
+
|
|
52
|
+
To get started, install the library with [pip](https://pip.pypa.io/en/stable/)
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
pip install py-predicate
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The full documentation can be found [here](https://mrijk.github.io/py-predicate/). We give 2 small examples
|
|
59
|
+
to show what the library can do.
|
|
60
|
+
|
|
61
|
+
# Example 1
|
|
48
62
|
|
|
49
63
|
```python
|
|
50
|
-
|
|
64
|
+
filtered = [x for x in range(10) if x >= 2 and x <= 3]
|
|
51
65
|
```
|
|
52
66
|
|
|
53
|
-
Version with predicates:
|
|
67
|
+
## Version with predicates:
|
|
54
68
|
|
|
55
69
|
```python
|
|
56
|
-
|
|
57
|
-
le_3 = le_p(3)
|
|
70
|
+
from predicate import ge_p, le_p
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
ge_2 = ge_p(2)
|
|
73
|
+
le_3 = le_p(3)
|
|
74
|
+
|
|
75
|
+
between_2_and_3 = ge_2 & le_3
|
|
76
|
+
filtered = [x for x in range(10) if between_2_and_3(x)]
|
|
61
77
|
```
|
|
62
78
|
|
|
63
79
|
Of course this example looks way more complicated than the original version. The point here is that you can build
|
|
64
|
-
reusable predicates that can be used in multiple locations
|
|
80
|
+
reusable predicates that can be used in multiple locations.
|
|
81
|
+
|
|
82
|
+
# Example 2
|
|
83
|
+
|
|
84
|
+
A unique (?) py-predicate feature is that you can define self referencing predicates.
|
|
85
|
+
This makes it easy to apply predicates to arbitrarily nested structures, like JSON data.
|
|
86
|
+
|
|
87
|
+
In the next example we define a predicate, that tests if a given data structure is
|
|
88
|
+
either a string, or a list of data that can again either be a string or a list of
|
|
89
|
+
data. Ad infinitum.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from predicate import all_p, is_list_p, is_str_p, lazy_p
|
|
93
|
+
|
|
94
|
+
str_or_list_of_str = is_str_p | (is_list_p & all_p(lazy_p("str_or_list_of_str")))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Using plain Python, the above one-liner would have to be coded as a (recursive) function.
|
|
98
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
[](https://codecov.io/gh/mrijk/py-predicate)
|
|
4
|
+
|
|
5
|
+
# Introduction
|
|
6
|
+
|
|
7
|
+
py-predicate is a typed Python library to create composable predicates.
|
|
8
|
+
|
|
9
|
+
# Getting started
|
|
10
|
+
|
|
11
|
+
To get started, install the library with [pip](https://pip.pypa.io/en/stable/)
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
pip install py-predicate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The full documentation can be found [here](https://mrijk.github.io/py-predicate/). We give 2 small examples
|
|
18
|
+
to show what the library can do.
|
|
19
|
+
|
|
20
|
+
# Example 1
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
filtered = [x for x in range(10) if x >= 2 and x <= 3]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Version with predicates:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from predicate import ge_p, le_p
|
|
30
|
+
|
|
31
|
+
ge_2 = ge_p(2)
|
|
32
|
+
le_3 = le_p(3)
|
|
33
|
+
|
|
34
|
+
between_2_and_3 = ge_2 & le_3
|
|
35
|
+
filtered = [x for x in range(10) if between_2_and_3(x)]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Of course this example looks way more complicated than the original version. The point here is that you can build
|
|
39
|
+
reusable predicates that can be used in multiple locations.
|
|
40
|
+
|
|
41
|
+
# Example 2
|
|
42
|
+
|
|
43
|
+
A unique (?) py-predicate feature is that you can define self referencing predicates.
|
|
44
|
+
This makes it easy to apply predicates to arbitrarily nested structures, like JSON data.
|
|
45
|
+
|
|
46
|
+
In the next example we define a predicate, that tests if a given data structure is
|
|
47
|
+
either a string, or a list of data that can again either be a string or a list of
|
|
48
|
+
data. Ad infinitum.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from predicate import all_p, is_list_p, is_str_p, lazy_p
|
|
52
|
+
|
|
53
|
+
str_or_list_of_str = is_str_p | (is_list_p & all_p(lazy_p("str_or_list_of_str")))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Using plain Python, the above one-liner would have to be coded as a (recursive) function.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""The py-predicate module."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.0.1"
|
|
4
|
+
|
|
5
|
+
from predicate.formatter.format_dot import to_dot
|
|
6
|
+
from predicate.formatter.format_json import to_json
|
|
7
|
+
from predicate.optimizer.predicate_optimizer import can_optimize, optimize
|
|
8
|
+
from predicate.predicate import (
|
|
9
|
+
AllPredicate,
|
|
10
|
+
AlwaysFalsePredicate,
|
|
11
|
+
AlwaysTruePredicate,
|
|
12
|
+
AndPredicate,
|
|
13
|
+
AnyPredicate,
|
|
14
|
+
EqPredicate,
|
|
15
|
+
FnPredicate,
|
|
16
|
+
GePredicate,
|
|
17
|
+
GtPredicate,
|
|
18
|
+
InPredicate,
|
|
19
|
+
IsEmptyPredicate,
|
|
20
|
+
IsInstancePredicate,
|
|
21
|
+
IsNonePredicate,
|
|
22
|
+
IsNotNonePredicate,
|
|
23
|
+
LePredicate,
|
|
24
|
+
LtPredicate,
|
|
25
|
+
NePredicate,
|
|
26
|
+
NotInPredicate,
|
|
27
|
+
NotPredicate,
|
|
28
|
+
OrPredicate,
|
|
29
|
+
Predicate,
|
|
30
|
+
XorPredicate,
|
|
31
|
+
always_false_p,
|
|
32
|
+
always_true_p,
|
|
33
|
+
is_empty_p,
|
|
34
|
+
)
|
|
35
|
+
from predicate.standard_predicates import (
|
|
36
|
+
all_p,
|
|
37
|
+
any_p,
|
|
38
|
+
comp_p,
|
|
39
|
+
eq_false_p,
|
|
40
|
+
eq_p,
|
|
41
|
+
eq_true_p,
|
|
42
|
+
fn_p,
|
|
43
|
+
ge_p,
|
|
44
|
+
gt_p,
|
|
45
|
+
in_p,
|
|
46
|
+
is_bool_p,
|
|
47
|
+
is_callable_p,
|
|
48
|
+
is_complex_p,
|
|
49
|
+
is_datetime_p,
|
|
50
|
+
is_dict_p,
|
|
51
|
+
is_falsy_p,
|
|
52
|
+
is_float_p,
|
|
53
|
+
is_instance_p,
|
|
54
|
+
is_int_p,
|
|
55
|
+
is_iterable_of_p,
|
|
56
|
+
is_iterable_p,
|
|
57
|
+
is_list_of_p,
|
|
58
|
+
is_list_p,
|
|
59
|
+
is_none_p,
|
|
60
|
+
is_not_none_p,
|
|
61
|
+
is_predicate_p,
|
|
62
|
+
is_set_of_p,
|
|
63
|
+
is_set_p,
|
|
64
|
+
is_str_p,
|
|
65
|
+
is_tuple_of_p,
|
|
66
|
+
is_tuple_p,
|
|
67
|
+
is_uuid_p,
|
|
68
|
+
lazy_p,
|
|
69
|
+
le_p,
|
|
70
|
+
lt_p,
|
|
71
|
+
ne_p,
|
|
72
|
+
not_in_p,
|
|
73
|
+
regex_p,
|
|
74
|
+
root_p,
|
|
75
|
+
this_p,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"AllPredicate",
|
|
80
|
+
"AlwaysFalsePredicate",
|
|
81
|
+
"AlwaysTruePredicate",
|
|
82
|
+
"AndPredicate",
|
|
83
|
+
"AnyPredicate",
|
|
84
|
+
"EqPredicate",
|
|
85
|
+
"FnPredicate",
|
|
86
|
+
"GePredicate",
|
|
87
|
+
"GtPredicate",
|
|
88
|
+
"InPredicate",
|
|
89
|
+
"IsEmptyPredicate",
|
|
90
|
+
"IsInstancePredicate",
|
|
91
|
+
"IsNonePredicate",
|
|
92
|
+
"IsNotNonePredicate",
|
|
93
|
+
"LePredicate",
|
|
94
|
+
"LtPredicate",
|
|
95
|
+
"NePredicate",
|
|
96
|
+
"NotInPredicate",
|
|
97
|
+
"NotPredicate",
|
|
98
|
+
"OrPredicate",
|
|
99
|
+
"Predicate",
|
|
100
|
+
"XorPredicate",
|
|
101
|
+
"all_p",
|
|
102
|
+
"always_false_p",
|
|
103
|
+
"always_true_p",
|
|
104
|
+
"any_p",
|
|
105
|
+
"can_optimize",
|
|
106
|
+
"comp_p",
|
|
107
|
+
"eq_false_p",
|
|
108
|
+
"eq_p",
|
|
109
|
+
"eq_true_p",
|
|
110
|
+
"is_falsy_p",
|
|
111
|
+
"fn_p",
|
|
112
|
+
"ge_p",
|
|
113
|
+
"gt_p",
|
|
114
|
+
"in_p",
|
|
115
|
+
"is_bool_p",
|
|
116
|
+
"is_callable_p",
|
|
117
|
+
"is_complex_p",
|
|
118
|
+
"is_datetime_p",
|
|
119
|
+
"is_dict_p",
|
|
120
|
+
"is_empty_p",
|
|
121
|
+
"is_float_p",
|
|
122
|
+
"is_instance_p",
|
|
123
|
+
"is_int_p",
|
|
124
|
+
"is_iterable_p",
|
|
125
|
+
"is_iterable_of_p",
|
|
126
|
+
"is_list_p",
|
|
127
|
+
"is_list_of_p",
|
|
128
|
+
"is_none_p",
|
|
129
|
+
"is_not_none_p",
|
|
130
|
+
"is_predicate_p",
|
|
131
|
+
"is_set_p",
|
|
132
|
+
"is_set_of_p",
|
|
133
|
+
"is_str_p",
|
|
134
|
+
"is_tuple_p",
|
|
135
|
+
"is_tuple_of_p",
|
|
136
|
+
"is_uuid_p",
|
|
137
|
+
"lazy_p",
|
|
138
|
+
"le_p",
|
|
139
|
+
"lt_p",
|
|
140
|
+
"ne_p",
|
|
141
|
+
"not_in_p",
|
|
142
|
+
"optimize",
|
|
143
|
+
"regex_p",
|
|
144
|
+
"root_p",
|
|
145
|
+
"this_p",
|
|
146
|
+
"to_dot",
|
|
147
|
+
"to_json",
|
|
148
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from predicate.predicate import Predicate
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class CompPredicate[S, T](Predicate[T]):
|
|
9
|
+
"""A predicate class that transforms the input according to a function and then evaluates the predicate."""
|
|
10
|
+
|
|
11
|
+
fn: Callable[[S], T]
|
|
12
|
+
predicate: Predicate[T]
|
|
13
|
+
|
|
14
|
+
def __call__(self, x: S) -> bool:
|
|
15
|
+
return self.predicate(self.fn(x))
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return f"comp_p({repr(self.predicate)})"
|
|
@@ -1,31 +1,47 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from functools import partial
|
|
1
3
|
from itertools import count
|
|
2
4
|
|
|
3
5
|
import graphviz # type: ignore
|
|
6
|
+
from more_itertools import first
|
|
4
7
|
|
|
5
|
-
from predicate import
|
|
6
|
-
|
|
8
|
+
from predicate.comp_predicate import CompPredicate
|
|
9
|
+
from predicate.lazy_predicate import LazyPredicate, find_predicate_by_ref
|
|
10
|
+
from predicate.optimizer.predicate_optimizer import optimize
|
|
11
|
+
from predicate.predicate import (
|
|
7
12
|
AlwaysFalsePredicate,
|
|
8
13
|
AlwaysTruePredicate,
|
|
9
14
|
AndPredicate,
|
|
15
|
+
IsFalsyPredicate,
|
|
16
|
+
IsTruthyPredicate,
|
|
17
|
+
NamedPredicate,
|
|
18
|
+
NotPredicate,
|
|
19
|
+
OrPredicate,
|
|
20
|
+
Predicate,
|
|
21
|
+
XorPredicate,
|
|
22
|
+
)
|
|
23
|
+
from predicate.root_predicate import RootPredicate, find_root_predicate
|
|
24
|
+
from predicate.standard_predicates import (
|
|
25
|
+
AllPredicate,
|
|
10
26
|
AnyPredicate,
|
|
11
27
|
EqPredicate,
|
|
12
28
|
FnPredicate,
|
|
13
29
|
GePredicate,
|
|
14
30
|
GtPredicate,
|
|
15
31
|
InPredicate,
|
|
32
|
+
IsInstancePredicate,
|
|
33
|
+
IsNonePredicate,
|
|
16
34
|
LePredicate,
|
|
17
35
|
LtPredicate,
|
|
18
36
|
NePredicate,
|
|
19
37
|
NotInPredicate,
|
|
20
|
-
|
|
21
|
-
OrPredicate,
|
|
22
|
-
Predicate,
|
|
23
|
-
XorPredicate,
|
|
38
|
+
PredicateFactory,
|
|
24
39
|
)
|
|
25
|
-
from predicate.
|
|
40
|
+
from predicate.this_predicate import ThisPredicate, find_this_predicate
|
|
26
41
|
|
|
27
42
|
|
|
28
43
|
def to_dot(predicate: Predicate, predicate_string: str = "", show_optimized: bool = False):
|
|
44
|
+
"""Format predicate as a .dot file."""
|
|
29
45
|
graph_attr = {"label": predicate_string, "labelloc": "t"}
|
|
30
46
|
|
|
31
47
|
node_attr = {"shape": "rectangle", "style": "filled", "fillcolor": "#B7D7A8"}
|
|
@@ -45,13 +61,18 @@ def to_dot(predicate: Predicate, predicate_string: str = "", show_optimized: boo
|
|
|
45
61
|
|
|
46
62
|
|
|
47
63
|
def render(dot, predicate: Predicate, node_nr):
|
|
48
|
-
|
|
64
|
+
node_predicate_mapping: dict[str, Predicate] = {}
|
|
65
|
+
|
|
66
|
+
def _add_node(name: str, *, label: str, predicate: Predicate):
|
|
49
67
|
node = next(node_nr)
|
|
50
68
|
unique_name = f"{name}_{node}"
|
|
51
69
|
dot.node(unique_name, label=label)
|
|
70
|
+
node_predicate_mapping[unique_name] = predicate
|
|
52
71
|
return unique_name
|
|
53
72
|
|
|
54
73
|
def to_value(predicate: Predicate):
|
|
74
|
+
add_node = partial(_add_node, predicate=predicate)
|
|
75
|
+
|
|
55
76
|
match predicate:
|
|
56
77
|
case AllPredicate(all_predicate):
|
|
57
78
|
node = add_node("all", label="∀")
|
|
@@ -74,12 +95,19 @@ def render(dot, predicate: Predicate, node_nr):
|
|
|
74
95
|
child = to_value(any_predicate)
|
|
75
96
|
dot.edge(node, child)
|
|
76
97
|
return node
|
|
98
|
+
case CompPredicate(_fn, comp_predicate):
|
|
99
|
+
node = add_node("comp", label="f")
|
|
100
|
+
child = to_value(comp_predicate)
|
|
101
|
+
dot.edge(node, child)
|
|
102
|
+
return node
|
|
77
103
|
case EqPredicate(v):
|
|
78
104
|
return add_node("eq", label=f"x = {v}")
|
|
105
|
+
case IsFalsyPredicate():
|
|
106
|
+
return add_node("falsy", label="falsy")
|
|
107
|
+
case IsTruthyPredicate():
|
|
108
|
+
return add_node("truthy", label="truthy")
|
|
79
109
|
case FnPredicate(predicate_fn):
|
|
80
110
|
name = predicate_fn.__code__.co_name
|
|
81
|
-
# code = inspect.getsource(predicate_fn)
|
|
82
|
-
# m = re.match(r".*\(predicate_fn=(.*)\)", code)
|
|
83
111
|
return add_node("fn", label=f"fn: {name}")
|
|
84
112
|
case GePredicate(v):
|
|
85
113
|
return add_node("ge", label=f"x ≥ {v}")
|
|
@@ -88,10 +116,19 @@ def render(dot, predicate: Predicate, node_nr):
|
|
|
88
116
|
case InPredicate(v):
|
|
89
117
|
items = ", ".join(str(item) for item in v)
|
|
90
118
|
return add_node("in", label=f"x ∈ {{{items}}}")
|
|
119
|
+
case IsInstancePredicate(klass):
|
|
120
|
+
name = klass[0].__name__ # type: ignore
|
|
121
|
+
return add_node("instance", label=f"is_{name}_p")
|
|
122
|
+
case IsNonePredicate():
|
|
123
|
+
return add_node("none", label="x = None")
|
|
124
|
+
case LazyPredicate(ref):
|
|
125
|
+
return add_node("lazy", label=ref)
|
|
91
126
|
case LePredicate(v):
|
|
92
127
|
return add_node("le", label=f"x ≤ {v}")
|
|
93
128
|
case LtPredicate(v):
|
|
94
129
|
return add_node("lt", label=f"x < {v}")
|
|
130
|
+
case NamedPredicate(name):
|
|
131
|
+
return add_node("named", label=name)
|
|
95
132
|
case NotInPredicate(v):
|
|
96
133
|
items = ", ".join(str(item) for item in v)
|
|
97
134
|
return add_node("in", label=f"x ∉ {{{items}}}")
|
|
@@ -109,6 +146,12 @@ def render(dot, predicate: Predicate, node_nr):
|
|
|
109
146
|
dot.edge(node, left_node)
|
|
110
147
|
dot.edge(node, right_node)
|
|
111
148
|
return node
|
|
149
|
+
case PredicateFactory() as factory:
|
|
150
|
+
return to_value(factory.predicate)
|
|
151
|
+
case RootPredicate():
|
|
152
|
+
return add_node("root", label="root")
|
|
153
|
+
case ThisPredicate():
|
|
154
|
+
return add_node("this", label="this")
|
|
112
155
|
case XorPredicate(left, right):
|
|
113
156
|
node = add_node("xor", label="⊻")
|
|
114
157
|
left_node = to_value(left)
|
|
@@ -121,6 +164,31 @@ def render(dot, predicate: Predicate, node_nr):
|
|
|
121
164
|
|
|
122
165
|
to_value(predicate)
|
|
123
166
|
|
|
167
|
+
render_lazy_references(dot, node_predicate_mapping)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def render_lazy_references(dot, node_predicate_mapping):
|
|
171
|
+
def find_in_mapping(lookup: Predicate) -> str:
|
|
172
|
+
return first(node for node, predicate in node_predicate_mapping.items() if predicate == lookup)
|
|
173
|
+
|
|
174
|
+
def add_dashed_line(node: str, lookup: Predicate) -> None:
|
|
175
|
+
found = find_in_mapping(lookup)
|
|
176
|
+
dot.edge(node, found, style="dashed")
|
|
177
|
+
|
|
178
|
+
frame = inspect.currentframe()
|
|
179
|
+
|
|
180
|
+
for node, predicate in node_predicate_mapping.items():
|
|
181
|
+
match predicate:
|
|
182
|
+
case LazyPredicate():
|
|
183
|
+
if reference := find_predicate_by_ref(frame, predicate.ref):
|
|
184
|
+
add_dashed_line(node, reference)
|
|
185
|
+
case RootPredicate():
|
|
186
|
+
if root := find_root_predicate(frame, predicate):
|
|
187
|
+
add_dashed_line(node, root)
|
|
188
|
+
case ThisPredicate():
|
|
189
|
+
if this := find_this_predicate(frame, predicate):
|
|
190
|
+
add_dashed_line(node, this)
|
|
191
|
+
|
|
124
192
|
|
|
125
193
|
def render_original(dot, predicate: Predicate, node_nr):
|
|
126
194
|
with dot.subgraph(name="cluster_original") as original:
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
from predicate import (
|
|
3
|
+
from predicate.predicate import (
|
|
4
4
|
AllPredicate,
|
|
5
5
|
AlwaysFalsePredicate,
|
|
6
6
|
AlwaysTruePredicate,
|
|
7
7
|
AndPredicate,
|
|
8
8
|
AnyPredicate,
|
|
9
9
|
FnPredicate,
|
|
10
|
+
IsFalsyPredicate,
|
|
11
|
+
IsTruthyPredicate,
|
|
12
|
+
NamedPredicate,
|
|
10
13
|
NePredicate,
|
|
11
14
|
NotPredicate,
|
|
12
15
|
OrPredicate,
|
|
@@ -16,6 +19,8 @@ from predicate import (
|
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def to_json(predicate: Predicate) -> dict[str, Any]:
|
|
22
|
+
"""Format predicate as json."""
|
|
23
|
+
|
|
19
24
|
def to_value(predicate) -> tuple[str, Any]:
|
|
20
25
|
match predicate:
|
|
21
26
|
case AllPredicate(all_predicate):
|
|
@@ -31,6 +36,12 @@ def to_json(predicate: Predicate) -> dict[str, Any]:
|
|
|
31
36
|
case FnPredicate(predicate_fn):
|
|
32
37
|
name = predicate_fn.__code__.co_name
|
|
33
38
|
return "fn", {"name": name}
|
|
39
|
+
case IsFalsyPredicate():
|
|
40
|
+
return "is_falsy", None
|
|
41
|
+
case NamedPredicate(name):
|
|
42
|
+
return "variable", name
|
|
43
|
+
case IsTruthyPredicate():
|
|
44
|
+
return "is_truthy", None
|
|
34
45
|
case NePredicate(v):
|
|
35
46
|
return "ne", {"v": v}
|
|
36
47
|
case NotPredicate(not_predicate):
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from functools import singledispatch
|
|
2
|
+
|
|
3
|
+
from predicate.predicate import (
|
|
4
|
+
AlwaysFalsePredicate,
|
|
5
|
+
AlwaysTruePredicate,
|
|
6
|
+
EqPredicate,
|
|
7
|
+
GePredicate,
|
|
8
|
+
GtPredicate,
|
|
9
|
+
InPredicate,
|
|
10
|
+
Predicate,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@singledispatch
|
|
15
|
+
def implies(predicate: Predicate, other: Predicate) -> bool:
|
|
16
|
+
"""Return True if predicate implies another predicate, otherwise False."""
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@implies.register
|
|
21
|
+
def implies_false(_predicate: AlwaysFalsePredicate, _other: Predicate) -> bool:
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@implies.register
|
|
26
|
+
def implies_true(_predicate: AlwaysTruePredicate, other: Predicate) -> bool:
|
|
27
|
+
return other == AlwaysTruePredicate()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@implies.register
|
|
31
|
+
def _(predicate: GePredicate, other: Predicate) -> bool:
|
|
32
|
+
match other:
|
|
33
|
+
case GePredicate(v):
|
|
34
|
+
return predicate.v >= v
|
|
35
|
+
case GtPredicate(v):
|
|
36
|
+
return predicate.v > v
|
|
37
|
+
case _:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@implies.register
|
|
42
|
+
def _(predicate: EqPredicate, other: Predicate) -> bool:
|
|
43
|
+
match other:
|
|
44
|
+
case EqPredicate(v):
|
|
45
|
+
return predicate.v == v
|
|
46
|
+
case GePredicate(v):
|
|
47
|
+
return predicate.v >= v
|
|
48
|
+
case GtPredicate(v):
|
|
49
|
+
return predicate.v > v
|
|
50
|
+
case InPredicate(v):
|
|
51
|
+
return predicate.v in v
|
|
52
|
+
case _:
|
|
53
|
+
return False
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
|
|
5
|
+
from predicate.predicate import Predicate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class LazyPredicate[T](Predicate[T]):
|
|
10
|
+
"""A predicate class that lazily references another predicate by name."""
|
|
11
|
+
|
|
12
|
+
ref: str
|
|
13
|
+
|
|
14
|
+
@cached_property
|
|
15
|
+
def predicate(self) -> Predicate | None:
|
|
16
|
+
return find_predicate_by_ref(self.frame, self.ref)
|
|
17
|
+
|
|
18
|
+
def __call__(self, x: T) -> bool:
|
|
19
|
+
self.frame = inspect.currentframe()
|
|
20
|
+
if self.predicate:
|
|
21
|
+
return self.predicate(x)
|
|
22
|
+
raise ValueError(f"Could not find predicate with reference {self.ref}")
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return f'lazy_p("{self.ref}")'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_predicate_by_ref(frame, ref: str) -> Predicate | None:
|
|
29
|
+
for key, value in frame.f_locals.items():
|
|
30
|
+
if key == ref:
|
|
31
|
+
return value
|
|
32
|
+
if next_frame := frame.f_back:
|
|
33
|
+
return find_predicate_by_ref(next_frame, ref)
|
|
34
|
+
return None
|
|
@@ -7,6 +7,7 @@ from predicate import (
|
|
|
7
7
|
GePredicate,
|
|
8
8
|
GtPredicate,
|
|
9
9
|
InPredicate,
|
|
10
|
+
IsEmptyPredicate,
|
|
10
11
|
IsNonePredicate,
|
|
11
12
|
IsNotNonePredicate,
|
|
12
13
|
LePredicate,
|
|
@@ -15,11 +16,19 @@ from predicate import (
|
|
|
15
16
|
NotInPredicate,
|
|
16
17
|
NotPredicate,
|
|
17
18
|
Predicate,
|
|
19
|
+
always_false_p,
|
|
20
|
+
always_true_p,
|
|
21
|
+
is_empty_p,
|
|
22
|
+
is_none_p,
|
|
23
|
+
is_not_none_p,
|
|
18
24
|
)
|
|
25
|
+
from predicate.predicate import IsFalsyPredicate, IsNotEmptyPredicate, IsTruthyPredicate, is_not_empty_p
|
|
26
|
+
from predicate.standard_predicates import is_falsy_p, is_truthy_p
|
|
19
27
|
|
|
20
28
|
|
|
21
29
|
@singledispatch
|
|
22
30
|
def negate[T](predicate: Predicate[T]) -> Predicate[T]:
|
|
31
|
+
"""Return the negation of a predicate."""
|
|
23
32
|
return NotPredicate(predicate=predicate)
|
|
24
33
|
|
|
25
34
|
|
|
@@ -30,12 +39,22 @@ def negate_is_not(predicate: NotPredicate) -> Predicate:
|
|
|
30
39
|
|
|
31
40
|
@negate.register
|
|
32
41
|
def negate_is_false(_predicate: AlwaysFalsePredicate) -> Predicate:
|
|
33
|
-
return
|
|
42
|
+
return always_true_p
|
|
34
43
|
|
|
35
44
|
|
|
36
45
|
@negate.register
|
|
37
46
|
def negate_is_true(_predicate: AlwaysTruePredicate) -> Predicate:
|
|
38
|
-
return
|
|
47
|
+
return always_false_p
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@negate.register
|
|
51
|
+
def negate_is_falsy(_predicate: IsFalsyPredicate) -> Predicate:
|
|
52
|
+
return is_truthy_p
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@negate.register
|
|
56
|
+
def negate_is_truthy(_predicate: IsTruthyPredicate) -> Predicate:
|
|
57
|
+
return is_falsy_p
|
|
39
58
|
|
|
40
59
|
|
|
41
60
|
@negate.register
|
|
@@ -80,9 +99,19 @@ def negate_le(predicate: LePredicate) -> Predicate:
|
|
|
80
99
|
|
|
81
100
|
@negate.register
|
|
82
101
|
def negate_is_none(_predicate: IsNonePredicate) -> Predicate:
|
|
83
|
-
return
|
|
102
|
+
return is_not_none_p
|
|
84
103
|
|
|
85
104
|
|
|
86
105
|
@negate.register
|
|
87
106
|
def negate_is_not_none(_predicate: IsNotNonePredicate) -> Predicate:
|
|
88
|
-
return
|
|
107
|
+
return is_none_p
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@negate.register
|
|
111
|
+
def negate_is_empty(_predicate: IsEmptyPredicate) -> Predicate:
|
|
112
|
+
return is_not_empty_p
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@negate.register
|
|
116
|
+
def negate_is_not_empty(_predicate: IsNotEmptyPredicate) -> Predicate:
|
|
117
|
+
return is_empty_p
|