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.
Files changed (33) hide show
  1. {py_predicate-0.1 → py_predicate-0.3}/PKG-INFO +46 -12
  2. py_predicate-0.3/README.md +56 -0
  3. py_predicate-0.3/predicate/__init__.py +148 -0
  4. py_predicate-0.3/predicate/comp_predicate.py +18 -0
  5. {py_predicate-0.1 → py_predicate-0.3}/predicate/formatter/format_dot.py +78 -10
  6. {py_predicate-0.1 → py_predicate-0.3}/predicate/formatter/format_json.py +12 -1
  7. py_predicate-0.3/predicate/implies.py +53 -0
  8. py_predicate-0.3/predicate/lazy_predicate.py +34 -0
  9. {py_predicate-0.1 → py_predicate-0.3}/predicate/negate.py +33 -4
  10. {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/and_optimizer.py +24 -40
  11. py_predicate-0.3/predicate/optimizer/in_optimizer.py +13 -0
  12. {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/not_optimizer.py +4 -0
  13. {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/or_optimizer.py +27 -31
  14. {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/predicate_optimizer.py +4 -0
  15. {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/rules.py +2 -0
  16. {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/xor_optimizer.py +18 -14
  17. py_predicate-0.3/predicate/parser.py +69 -0
  18. py_predicate-0.3/predicate/predicate.py +490 -0
  19. py_predicate-0.3/predicate/regex_predicate.py +18 -0
  20. py_predicate-0.3/predicate/root_predicate.py +40 -0
  21. py_predicate-0.3/predicate/standard_predicates.py +241 -0
  22. py_predicate-0.3/predicate/this_predicate.py +51 -0
  23. py_predicate-0.3/predicate/truth_table.py +63 -0
  24. {py_predicate-0.1 → py_predicate-0.3}/pyproject.toml +7 -5
  25. py_predicate-0.1/README.md +0 -26
  26. py_predicate-0.1/predicate/__init__.py +0 -63
  27. py_predicate-0.1/predicate/optimizer/parser.py +0 -74
  28. py_predicate-0.1/predicate/predicate.py +0 -263
  29. py_predicate-0.1/predicate/standard_predicates.py +0 -89
  30. {py_predicate-0.1 → py_predicate-0.3}/predicate/formatter/__init__.py +0 -0
  31. {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/__init__.py +0 -0
  32. {py_predicate-0.1 → py_predicate-0.3}/predicate/optimizer/all_optimizer.py +0 -0
  33. {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.1
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.12
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 :: 3 - Alpha
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
- # Example
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
- filtered = [x for x in range(10) if x >= 2 and x <= 3]
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
- ge_2 = ge_p(2)
57
- le_3 = le_p(3)
70
+ from predicate import ge_p, le_p
58
71
 
59
- between_2_and_3 = ge_2 & le_3
60
- filtered = [x for x in range(10) if between_2_and_3(x)]
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
+ ![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
+ # 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
- AllPredicate,
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
- NotPredicate,
21
- OrPredicate,
22
- Predicate,
23
- XorPredicate,
38
+ PredicateFactory,
24
39
  )
25
- from predicate.optimizer.predicate_optimizer import optimize
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
- def add_node(name: str, *, label: str):
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 AlwaysTruePredicate()
42
+ return always_true_p
34
43
 
35
44
 
36
45
  @negate.register
37
46
  def negate_is_true(_predicate: AlwaysTruePredicate) -> Predicate:
38
- return AlwaysFalsePredicate()
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 IsNotNonePredicate()
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 IsNonePredicate()
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