clipspyx 0.1.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 (40) hide show
  1. clipspyx-0.1.0/LICENSE.txt +12 -0
  2. clipspyx-0.1.0/PKG-INFO +103 -0
  3. clipspyx-0.1.0/README.md +84 -0
  4. clipspyx-0.1.0/clipspyx/__init__.py +68 -0
  5. clipspyx-0.1.0/clipspyx/_clipspyx/__init__.py +11 -0
  6. clipspyx-0.1.0/clipspyx/agenda.py +415 -0
  7. clipspyx-0.1.0/clipspyx/classes.py +893 -0
  8. clipspyx-0.1.0/clipspyx/common.py +213 -0
  9. clipspyx-0.1.0/clipspyx/dsl/__init__.py +7 -0
  10. clipspyx-0.1.0/clipspyx/dsl/codegen.py +83 -0
  11. clipspyx-0.1.0/clipspyx/dsl/define.py +124 -0
  12. clipspyx-0.1.0/clipspyx/dsl/ir.py +216 -0
  13. clipspyx-0.1.0/clipspyx/dsl/parser.py +526 -0
  14. clipspyx-0.1.0/clipspyx/dsl/rule.py +139 -0
  15. clipspyx-0.1.0/clipspyx/dsl/template.py +153 -0
  16. clipspyx-0.1.0/clipspyx/dsl/types.py +41 -0
  17. clipspyx-0.1.0/clipspyx/dsl/visualize.py +485 -0
  18. clipspyx-0.1.0/clipspyx/environment.py +228 -0
  19. clipspyx-0.1.0/clipspyx/facts.py +780 -0
  20. clipspyx-0.1.0/clipspyx/functions.py +484 -0
  21. clipspyx-0.1.0/clipspyx/modules.py +270 -0
  22. clipspyx-0.1.0/clipspyx/routers.py +306 -0
  23. clipspyx-0.1.0/clipspyx/tables.py +133 -0
  24. clipspyx-0.1.0/clipspyx/values.py +164 -0
  25. clipspyx-0.1.0/clipspyx.egg-info/PKG-INFO +103 -0
  26. clipspyx-0.1.0/clipspyx.egg-info/SOURCES.txt +38 -0
  27. clipspyx-0.1.0/clipspyx.egg-info/dependency_links.txt +1 -0
  28. clipspyx-0.1.0/clipspyx.egg-info/requires.txt +9 -0
  29. clipspyx-0.1.0/clipspyx.egg-info/top_level.txt +1 -0
  30. clipspyx-0.1.0/pyproject.toml +43 -0
  31. clipspyx-0.1.0/setup.cfg +4 -0
  32. clipspyx-0.1.0/tests/test_70x.py +616 -0
  33. clipspyx-0.1.0/tests/test_agenda.py +170 -0
  34. clipspyx-0.1.0/tests/test_classes.py +273 -0
  35. clipspyx-0.1.0/tests/test_dsl.py +1479 -0
  36. clipspyx-0.1.0/tests/test_environment.py +199 -0
  37. clipspyx-0.1.0/tests/test_facts.py +251 -0
  38. clipspyx-0.1.0/tests/test_functions.py +107 -0
  39. clipspyx-0.1.0/tests/test_modules.py +78 -0
  40. clipspyx-0.1.0/tests/test_visualize.py +715 -0
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2016-2023, Matteo Cafasso
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: clipspyx
3
+ Version: 0.1.0
4
+ Summary: CLIPS Python bindings via CFFI
5
+ License-Expression: BSD-3-Clause
6
+ Project-URL: Homepage, https://github.com/inferal-oss/clipspyx
7
+ Project-URL: Repository, https://github.com/inferal-oss/clipspyx
8
+ Project-URL: Issues, https://github.com/inferal-oss/clipspyx/issues
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.txt
12
+ Provides-Extra: 64x
13
+ Requires-Dist: clipspyx-ffi-64x==0.1.0; extra == "64x"
14
+ Provides-Extra: 70x
15
+ Requires-Dist: clipspyx-ffi-70x==0.1.0; extra == "70x"
16
+ Provides-Extra: dsl
17
+ Requires-Dist: libcst>=1.0; extra == "dsl"
18
+ Dynamic: license-file
19
+
20
+ # clipspyx
21
+
22
+ CLIPS Python bindings via CFFI. Fork of [clipspy](https://github.com/noxdafox/clipspy).
23
+
24
+ ## Building
25
+
26
+ clipspyx compiles CLIPS source directly into the CFFI extension. No separate `libclips` build step.
27
+
28
+ ### Prerequisites
29
+
30
+ - Python 3.10+
31
+ - C compiler (gcc/clang on Linux/macOS, MSVC on Windows)
32
+ - CLIPS source (auto-checked out from orphan branch, or provide your own)
33
+
34
+ ### Install for development
35
+
36
+ ```bash
37
+ # Install with 64x backend (CLIPS 6.4x)
38
+ uv sync --extra 64x
39
+
40
+ # Install with 70x backend (CLIPS 7.0x)
41
+ uv sync --extra 70x
42
+ ```
43
+
44
+ ### Switching backends
45
+
46
+ The 64x and 70x backends conflict with each other. Always use `uv sync` to switch:
47
+
48
+ ```bash
49
+ # Switch to 64x
50
+ uv sync --extra 64x
51
+
52
+ # Switch to 70x
53
+ uv sync --extra 70x
54
+ ```
55
+
56
+ **Important:** `uv run --extra 64x` does *not* remove a previously installed 70x backend
57
+ (and vice versa). Only `uv sync --extra <variant>` correctly installs the requested
58
+ backend and removes the conflicting one.
59
+
60
+ ### CLIPS source override
61
+
62
+ ```bash
63
+ # Use an arbitrary CLIPS source directory
64
+ CLIPS_SOURCE_DIR=/path/to/clips/core uv sync --extra 64x
65
+ ```
66
+
67
+ ### Build distributable wheels
68
+
69
+ ```bash
70
+ uv run scripts/build-backend.py 64x # builds clipspyx-ffi + clipspyx-ffi-64x wheels
71
+ uv run scripts/build-backend.py 70x # builds clipspyx-ffi + clipspyx-ffi-70x wheels
72
+ ```
73
+
74
+ ### CLIPS Source Management
75
+
76
+ CLIPS source is stored in git orphan branches (`clips-64x`, `clips-70x`).
77
+ Sync from SVN:
78
+
79
+ ```bash
80
+ uv run scripts/sync-svn.py 64x # syncs core/ -> orphan branch clips-64x
81
+ uv run scripts/sync-svn.py 70x # syncs core/ -> orphan branch clips-70x
82
+ ```
83
+
84
+ ## Usage
85
+
86
+ ```python
87
+ import clipspyx
88
+
89
+ env = clipspyx.Environment()
90
+ env.build('(defrule hello (initial-fact) => (println "Hello CLIPS!"))')
91
+ env.reset()
92
+ env.run()
93
+ ```
94
+
95
+ ## Testing
96
+
97
+ ```bash
98
+ uv run pytest tests/ -v
99
+ ```
100
+
101
+ ## License
102
+
103
+ BSD-3-Clause. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,84 @@
1
+ # clipspyx
2
+
3
+ CLIPS Python bindings via CFFI. Fork of [clipspy](https://github.com/noxdafox/clipspy).
4
+
5
+ ## Building
6
+
7
+ clipspyx compiles CLIPS source directly into the CFFI extension. No separate `libclips` build step.
8
+
9
+ ### Prerequisites
10
+
11
+ - Python 3.10+
12
+ - C compiler (gcc/clang on Linux/macOS, MSVC on Windows)
13
+ - CLIPS source (auto-checked out from orphan branch, or provide your own)
14
+
15
+ ### Install for development
16
+
17
+ ```bash
18
+ # Install with 64x backend (CLIPS 6.4x)
19
+ uv sync --extra 64x
20
+
21
+ # Install with 70x backend (CLIPS 7.0x)
22
+ uv sync --extra 70x
23
+ ```
24
+
25
+ ### Switching backends
26
+
27
+ The 64x and 70x backends conflict with each other. Always use `uv sync` to switch:
28
+
29
+ ```bash
30
+ # Switch to 64x
31
+ uv sync --extra 64x
32
+
33
+ # Switch to 70x
34
+ uv sync --extra 70x
35
+ ```
36
+
37
+ **Important:** `uv run --extra 64x` does *not* remove a previously installed 70x backend
38
+ (and vice versa). Only `uv sync --extra <variant>` correctly installs the requested
39
+ backend and removes the conflicting one.
40
+
41
+ ### CLIPS source override
42
+
43
+ ```bash
44
+ # Use an arbitrary CLIPS source directory
45
+ CLIPS_SOURCE_DIR=/path/to/clips/core uv sync --extra 64x
46
+ ```
47
+
48
+ ### Build distributable wheels
49
+
50
+ ```bash
51
+ uv run scripts/build-backend.py 64x # builds clipspyx-ffi + clipspyx-ffi-64x wheels
52
+ uv run scripts/build-backend.py 70x # builds clipspyx-ffi + clipspyx-ffi-70x wheels
53
+ ```
54
+
55
+ ### CLIPS Source Management
56
+
57
+ CLIPS source is stored in git orphan branches (`clips-64x`, `clips-70x`).
58
+ Sync from SVN:
59
+
60
+ ```bash
61
+ uv run scripts/sync-svn.py 64x # syncs core/ -> orphan branch clips-64x
62
+ uv run scripts/sync-svn.py 70x # syncs core/ -> orphan branch clips-70x
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ ```python
68
+ import clipspyx
69
+
70
+ env = clipspyx.Environment()
71
+ env.build('(defrule hello (initial-fact) => (println "Hello CLIPS!"))')
72
+ env.reset()
73
+ env.run()
74
+ ```
75
+
76
+ ## Testing
77
+
78
+ ```bash
79
+ uv run pytest tests/ -v
80
+ ```
81
+
82
+ ## License
83
+
84
+ BSD-3-Clause. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,68 @@
1
+ # Copyright (c) 2016-2025, Matteo Cafasso
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+
14
+ # 3. Neither the name of the copyright holder nor the names of its contributors
15
+ # may be used to endorse or promote products derived from this software without
16
+ # specific prior written permission.
17
+
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
+ # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
22
+ # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
23
+ # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
24
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+
31
+ __author__ = 'Matteo Cafasso'
32
+ __version__ = '0.1.0'
33
+ __license__ = 'BSD-3'
34
+
35
+
36
+ __all__ = ('CLIPSError',
37
+ 'CLIPS_MAJOR',
38
+ 'Environment',
39
+ 'Router',
40
+ 'LoggingRouter',
41
+ 'ImpliedFact',
42
+ 'TemplateFact',
43
+ 'Template',
44
+ 'Instance',
45
+ 'InstanceName',
46
+ 'Class',
47
+ 'Strategy',
48
+ 'SalienceEvaluation',
49
+ 'Verbosity',
50
+ 'ClassDefaultMode',
51
+ 'TemplateSlotDefaultType',
52
+ 'Symbol',
53
+ 'InstanceName',
54
+ 'SaveMode')
55
+
56
+
57
+ from clipspyx.environment import Environment
58
+ from clipspyx.classes import Instance, Class
59
+ from clipspyx.values import Symbol, InstanceName
60
+ from clipspyx.routers import Router, LoggingRouter
61
+ from clipspyx.facts import ImpliedFact, TemplateFact, Template
62
+ from clipspyx.common import SaveMode, Strategy, SalienceEvaluation, Verbosity
63
+ from clipspyx.common import CLIPSError, ClassDefaultMode, TemplateSlotDefaultType
64
+ from clipspyx.common import CLIPS_MAJOR
65
+
66
+ if CLIPS_MAJOR >= 7:
67
+ from clipspyx.tables import Deftable
68
+ __all__ = __all__ + ('Deftable',)
@@ -0,0 +1,11 @@
1
+ """Redirect imports to the appropriate CLIPS FFI backend."""
2
+ try:
3
+ from clipspyx_ffi_70x._clipspyx import lib, ffi # noqa: F401
4
+ except ImportError:
5
+ try:
6
+ from clipspyx_ffi_64x._clipspyx import lib, ffi # noqa: F401
7
+ except ImportError:
8
+ raise ImportError(
9
+ "No CLIPS FFI backend found. "
10
+ "Install clipspyx[64x] or clipspyx[70x]."
11
+ ) from None
@@ -0,0 +1,415 @@
1
+ # Copyright (c) 2016-2025, Matteo Cafasso
2
+ # All rights reserved.
3
+
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+
7
+ # 1. Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+
14
+ # 3. Neither the name of the copyright holder nor the names of its contributors
15
+ # may be used to endorse or promote products derived from this software without
16
+ # specific prior written permission.
17
+
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
+ # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
22
+ # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
23
+ # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
24
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ """This module contains the definition of:
31
+
32
+ * Agenda class
33
+ * Rule class
34
+ * Activation class
35
+
36
+ """
37
+
38
+ import clipspyx
39
+
40
+ from clipspyx.modules import Module
41
+ from clipspyx.common import environment_builder
42
+ from clipspyx.common import CLIPSError, Strategy, SalienceEvaluation, Verbosity
43
+
44
+ from clipspyx._clipspyx import lib, ffi
45
+
46
+
47
+ class Rule:
48
+ """A CLIPS rule.
49
+
50
+ In CLIPS, Rules are defined via the (defrule) statement.
51
+
52
+ """
53
+
54
+ __slots__ = '_env', '_name'
55
+
56
+ def __init__(self, env: ffi.CData, name: str):
57
+ self._env = env
58
+ self._name = name.encode()
59
+
60
+ def __hash__(self):
61
+ return hash(self._ptr())
62
+
63
+ def __eq__(self, rule):
64
+ return self._ptr() == rule._ptr()
65
+
66
+ def __str__(self):
67
+ string = lib.DefrulePPForm(self._ptr())
68
+ string = ffi.string(string).decode() if string != ffi.NULL else ''
69
+
70
+ return ' '.join(string.split())
71
+
72
+ def __repr__(self):
73
+ string = lib.DefrulePPForm(self._ptr())
74
+ string = ffi.string(string).decode() if string != ffi.NULL else ''
75
+
76
+ return "%s: %s" % (self.__class__.__name__, ' '.join(string.split()))
77
+
78
+ def _ptr(self) -> ffi.CData:
79
+ rule = lib.FindDefrule(self._env, self._name)
80
+ if rule == ffi.NULL:
81
+ raise CLIPSError(self._env, 'Rule <%s> not defined' % self.name)
82
+
83
+ return rule
84
+
85
+ @property
86
+ def name(self) -> str:
87
+ """Rule name."""
88
+ return self._name.decode()
89
+
90
+ @property
91
+ def module(self) -> Module:
92
+ """The module in which the Rule is defined.
93
+
94
+ Equivalent to the CLIPS (defrule-module) function.
95
+
96
+ """
97
+ name = ffi.string(lib.DefruleModule(self._ptr())).decode()
98
+
99
+ return Module(self._env, name)
100
+
101
+ @property
102
+ def deletable(self) -> bool:
103
+ """True if the Rule can be deleted."""
104
+ return lib.DefruleIsDeletable(self._ptr())
105
+
106
+ @property
107
+ def watch_firings(self) -> bool:
108
+ """Whether or not the Rule firings are being watched."""
109
+ return lib.DefruleGetWatchFirings(self._ptr())
110
+
111
+ @watch_firings.setter
112
+ def watch_firings(self, flag: bool):
113
+ """Whether or not the Rule firings are being watched."""
114
+ lib.DefruleSetWatchFirings(self._ptr(), flag)
115
+
116
+ @property
117
+ def watch_activations(self) -> bool:
118
+ """Whether or not the Rule Activations are being watched."""
119
+ return lib.DefruleGetWatchActivations(self._ptr())
120
+
121
+ @watch_activations.setter
122
+ def watch_activations(self, flag: bool):
123
+ """Whether or not the Rule Activations are being watched."""
124
+ lib.DefruleSetWatchActivations(self._ptr(), flag)
125
+
126
+ def matches(self, verbosity: Verbosity = Verbosity.TERSE):
127
+ """Shows partial matches and activations.
128
+
129
+ Returns a tuple containing the combined sum of the matches
130
+ for each pattern, the combined sum of partial matches
131
+ and the number of activations.
132
+
133
+ The verbosity parameter controls how much to output:
134
+
135
+ * Verbosity.VERBOSE: detailed matches are printed to stdout
136
+ * Verbosity.SUCCINT: a brief description is printed to stdout
137
+ * Verbosity.TERSE: (default) nothing is printed to stdout
138
+
139
+ """
140
+ value = clipspyx.values.clips_value(self._env)
141
+
142
+ lib.Matches(self._ptr(), verbosity, value)
143
+
144
+ return clipspyx.values.python_value(self._env, value)
145
+
146
+ def refresh(self):
147
+ """Refresh the Rule.
148
+
149
+ Equivalent to the CLIPS (refresh) function.
150
+
151
+ """
152
+ lib.Refresh(self._ptr())
153
+
154
+ def add_breakpoint(self):
155
+ """Add a breakpoint for the Rule.
156
+
157
+ Equivalent to the CLIPS (add-break) function.
158
+
159
+ """
160
+ lib.SetBreak(self._ptr())
161
+
162
+ def remove_breakpoint(self):
163
+ """Remove a breakpoint for the Rule.
164
+
165
+ Equivalent to the CLIPS (remove-break) function.
166
+
167
+ """
168
+ if not lib.RemoveBreak(self._env, self._ptr()):
169
+ raise CLIPSError("No breakpoint set")
170
+
171
+ def undefine(self):
172
+ """Undefine the Rule.
173
+
174
+ Equivalent to the CLIPS (undefrule) function.
175
+
176
+ The object becomes unusable after this method has been called.
177
+
178
+ """
179
+ if not lib.Undefrule(self._ptr(), self._env):
180
+ raise CLIPSError(self._env)
181
+
182
+
183
+ class Activation:
184
+ """When all the constraints of a Rule are satisfied,
185
+ the Rule becomes active.
186
+
187
+ Activations are organized within the CLIPS Agenda.
188
+
189
+ """
190
+
191
+ def __init__(self, env: ffi.CData, act: ffi.CData):
192
+ self._env = env
193
+ self._act = act
194
+ self._pp = activation_pp_string(self._env, self._act)
195
+ self._rule_name = ffi.string(lib.ActivationRuleName(self._act))
196
+
197
+ def __hash__(self):
198
+ return hash(self._act)
199
+
200
+ def __eq__(self, act):
201
+ return self._act == act._act
202
+
203
+ def __str__(self):
204
+ return ' '.join(self._pp.split())
205
+
206
+ def __repr__(self):
207
+ return "%s: %s" % (self.__class__.__name__, ' '.join(self._pp.split()))
208
+
209
+ def _assert_is_active(self):
210
+ """As the engine does not provide means to find activations,
211
+ the existence of the pointer in the activations list is tested instead.
212
+
213
+ """
214
+ activations = []
215
+ activation = lib.GetNextActivation(self._env, ffi.NULL)
216
+
217
+ while activation != ffi.NULL:
218
+ activations.append(activation)
219
+ activation = lib.GetNextActivation(self._env, activation)
220
+
221
+ if self._act not in activations:
222
+ raise CLIPSError(
223
+ self._env, "Activation %s not in the agenda" % self.name)
224
+
225
+ @property
226
+ def name(self) -> str:
227
+ """Activation Rule name."""
228
+ return self._rule_name.decode()
229
+
230
+ @property
231
+ def salience(self) -> int:
232
+ """Activation salience value."""
233
+ self._assert_is_active()
234
+ return lib.ActivationGetSalience(self._act)
235
+
236
+ @salience.setter
237
+ def salience(self, salience: int):
238
+ """Activation salience value."""
239
+ self._assert_is_active()
240
+ lib.ActivationSetSalience(self._act, salience)
241
+
242
+ def delete(self):
243
+ """Remove the activation from the agenda."""
244
+ self._assert_is_active()
245
+ lib.DeleteActivation(self._act)
246
+
247
+
248
+ class Agenda:
249
+ """In CLIPS, when all the conditions to activate a rule are met,
250
+ The Rule action is placed within the Agenda.
251
+
252
+ The CLIPS Agenda is responsible of sorting the Rule Activations
253
+ according to their salience and the conflict resolution strategy.
254
+
255
+ .. note::
256
+
257
+ All the Agenda methods are accessible through the Environment class.
258
+
259
+ """
260
+
261
+ def __init__(self, env: ffi.CData):
262
+ self._env = env
263
+
264
+ @property
265
+ def agenda_changed(self) -> bool:
266
+ """True if any rule activation changes have occurred."""
267
+ value = lib.GetAgendaChanged(self._env)
268
+ lib.SetAgendaChanged(self._env, False)
269
+
270
+ return value
271
+
272
+ @property
273
+ def focus(self) -> Module:
274
+ """The module associated with the current focus.
275
+
276
+ Equivalent to the CLIPS (get-focus) function.
277
+
278
+ """
279
+ current_focus = lib.GetFocus(self._env)
280
+ if current_focus != ffi.NULL:
281
+ name = ffi.string(lib.DefmoduleName(current_focus)).decode()
282
+
283
+ return Module(self._env, name)
284
+
285
+ return None
286
+
287
+ @focus.setter
288
+ def focus(self, module: Module):
289
+ """The module associated with the current focus.
290
+
291
+ Equivalent to the CLIPS (get-focus) function.
292
+
293
+ """
294
+ return lib.Focus(module._ptr())
295
+
296
+ @property
297
+ def strategy(self) -> Strategy:
298
+ """The current conflict resolution strategy.
299
+
300
+ Equivalent to the CLIPS (get-strategy) function.
301
+
302
+ """
303
+ return Strategy(lib.GetStrategy(self._env))
304
+
305
+ @strategy.setter
306
+ def strategy(self, value: Strategy):
307
+ """The current conflict resolution strategy.
308
+
309
+ Equivalent to the CLIPS (get-strategy) function.
310
+
311
+ """
312
+ lib.SetStrategy(self._env, Strategy(value))
313
+
314
+ @property
315
+ def salience_evaluation(self) -> SalienceEvaluation:
316
+ """The salience evaluation behavior.
317
+
318
+ Equivalent to the CLIPS (get-salience-evaluation) command.
319
+
320
+ """
321
+ return SalienceEvaluation(lib.GetSalienceEvaluation(self._env))
322
+
323
+ @salience_evaluation.setter
324
+ def salience_evaluation(self, value: SalienceEvaluation):
325
+ """The salience evaluation behavior.
326
+
327
+ Equivalent to the CLIPS (get-salience-evaluation) command.
328
+
329
+ """
330
+ lib.SetSalienceEvaluation(self._env, SalienceEvaluation(value))
331
+
332
+ def rules(self) -> iter:
333
+ """Iterate over the defined Rules."""
334
+ rule = lib.GetNextDefrule(self._env, ffi.NULL)
335
+
336
+ while rule != ffi.NULL:
337
+ name = ffi.string(lib.DefruleName(rule)).decode()
338
+ yield Rule(self._env, name)
339
+
340
+ rule = lib.GetNextDefrule(self._env, rule)
341
+
342
+ def find_rule(self, name: str) -> Rule:
343
+ """Find a Rule by name."""
344
+ defrule = lib.FindDefrule(self._env, name.encode())
345
+ if defrule == ffi.NULL:
346
+ raise LookupError("Rule '%s' not found" % name)
347
+
348
+ return Rule(self._env, name)
349
+
350
+ def reorder(self, module: Module = None):
351
+ """Reorder the Activations in the Agenda.
352
+
353
+ If no Module is specified, the agendas of all modules are reordered.
354
+
355
+ To be called after changing the conflict resolution strategy.
356
+
357
+ """
358
+ if module is not None:
359
+ lib.ReorderAgenda(module._ptr())
360
+ else:
361
+ lib.ReorderAllAgendas(self._env)
362
+
363
+ def refresh(self, module: Module = None):
364
+ """Recompute the salience values of the Activations on the Agenda
365
+ and then reorder the agenda.
366
+
367
+ Equivalent to the CLIPS (refresh-agenda) function.
368
+
369
+ If no Module is specified, the agendas of all modules are refreshed.
370
+
371
+ """
372
+ if module is not None:
373
+ lib.RefreshAgenda(module._ptr())
374
+ else:
375
+ lib.RefreshAllAgendas(self._env)
376
+
377
+ def activations(self) -> iter:
378
+ """Iterate over the Activations in the Agenda."""
379
+ activation = lib.GetNextActivation(self._env, ffi.NULL)
380
+
381
+ while activation != ffi.NULL:
382
+ yield Activation(self._env, activation)
383
+
384
+ activation = lib.GetNextActivation(self._env, activation)
385
+
386
+ def delete_activations(self):
387
+ """Delete all activations in the agenda."""
388
+ if not lib.DeleteActivation(self._env, ffi.NULL):
389
+ raise CLIPSError(self._env)
390
+
391
+ def clear_focus(self):
392
+ """Remove all modules from the focus stack.
393
+
394
+ Equivalent to the CLIPS (clear-focus-stack) function.
395
+
396
+ """
397
+ lib.ClearFocusStack(self._env)
398
+
399
+ def run(self, limit: int = None) -> int:
400
+ """Runs the activations in the agenda.
401
+
402
+ If limit is not None, the first activations up to limit will be run.
403
+
404
+ Returns the number of activation which were run.
405
+
406
+ """
407
+ return lib.Run(self._env, limit if limit is not None else -1)
408
+
409
+
410
+ def activation_pp_string(env: ffi.CData, ist: ffi.CData) -> str:
411
+ builder = environment_builder(env, 'string')
412
+ lib.SBReset(builder)
413
+ lib.ActivationPPForm(ist, builder)
414
+
415
+ return ffi.string(builder.contents).decode()