diff --git a/.travis.yml b/.travis.yml index dfc40fe..ef79aef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "3.5" - "3.6" - "3.7" + - "3.8" install: - pip install pipenv - pipenv install --dev --ignore-pipfile --skip-lock diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 21b1a98..c9d3bff 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ CHANGELOG --------- +1.9.3 +~~~~~ + +* Resolve #11 that was introduced with #7, that caused the same behavior + under different conditions. + + 1.9.2 ~~~~~ diff --git a/Pipfile b/Pipfile index df6362b..9c18b4f 100644 --- a/Pipfile +++ b/Pipfile @@ -12,9 +12,11 @@ pycodestyle = "*" pydocstyle = "*" codecov = "*" twine = "*" +ipython = "*" [packages] tox = "*" +experta = {editable = true,path = "."} [requires] -python_version = "3.4" +python_version = "3.7" diff --git a/docs/examples/EXISTS.ipynb b/docs/examples/EXISTS.ipynb new file mode 100644 index 0000000..f4f07f1 --- /dev/null +++ b/docs/examples/EXISTS.ipynb @@ -0,0 +1,166 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# EXISTS Conditional Element\n", + "\n", + "With exists you can test if a group of patterns is satisfied by at least one set of facts.\n", + "\n", + "The rule will be fired once.\n", + "\n", + "Please note that you although you can use MATCH to pattern match between matches inside the EXISTS group, **those matches can't be used as parameters in the RHS of the rule** (because more than one group of patterns can match and we are only firing once). Trying to use the matched field as parameter will result in a **TypeError**.\n", + "\n", + "This is a direct translation to Experta of [this](https://www.csie.ntu.edu.tw/~sylee/courses/clips/bpg/node5.4.6.html) Clips example about the EXISTS Conditional Element" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from experta import *" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "class Goal(Fact):\n", + " pass\n", + "\n", + "class Hero(Fact):\n", + " name = Field(str)\n", + " status = Field(str, default=\"unoccupied\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class KE(KnowledgeEngine):\n", + " @DefFacts()\n", + " def goal_and_heroes(self):\n", + " yield Goal('save-the-day')\n", + " yield Hero(name=\"Death Defying Man\")\n", + " yield Hero(name=\"Stupendous Man\")\n", + " yield Hero(name=\"Incredible Man\")\n", + " @Rule(\n", + " Goal('save-the-day'),\n", + " EXISTS(\n", + " Hero(status=\"unoccupied\")\n", + " )\n", + " )\n", + " def save_the_day(self):\n", + " print(\"The day is saved\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "ke = KE()\n", + "ke.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0: save_the_day {InitialFact(), Goal('save-the-day')}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ke.agenda" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "FactList([(0, InitialFact()),\n", + " (1, Goal('save-the-day')),\n", + " (2, Hero(name='Death Defying Man')),\n", + " (3, Hero(name='Stupendous Man')),\n", + " (4, Hero(name='Incredible Man'))])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ke.facts" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Implement (matches ) function" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The day is saved\n" + ] + } + ], + "source": [ + "ke.run()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.4.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/experta/__init__.py b/experta/__init__.py index cc006b2..7e4c06f 100644 --- a/experta/__init__.py +++ b/experta/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.9.2' +__version__ = '1.9.3' try: from .conditionalelement import AND, OR, NOT, TEST, EXISTS, FORALL diff --git a/experta/agenda.py b/experta/agenda.py index 3c378cd..d477750 100644 --- a/experta/agenda.py +++ b/experta/agenda.py @@ -15,10 +15,10 @@ def __init__(self): def __repr__(self): # pragma: no cover return "\n".join( "{idx}: {rule} {facts}".format(idx=idx, - rule=getattr(act.activation.rule, + rule=getattr(act.rule, '__name__', '[anonymous]'), - facts=act.activation.facts) + facts=act.facts) for idx, act in enumerate(self.activations)) def get_next(self): diff --git a/experta/strategies.py b/experta/strategies.py index f9fb2a0..1b3d473 100644 --- a/experta/strategies.py +++ b/experta/strategies.py @@ -17,7 +17,8 @@ def _update_agenda(self, agenda, added, removed): act.key = self.get_key(act) try: idx = bisect.bisect_left(agenda.activations, act) - del agenda.activations[idx] + if agenda.activations[idx] == act: + del agenda.activations[idx] except IndexError: pass diff --git a/setup.cfg b/setup.cfg index 4df2014..86323d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,7 @@ classifiers = Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 Programming Language :: Python :: Implementation :: PyPy authors = Roberto Abdelkader Martínez Pérez authors_email = robertomartinezp@gmail.com diff --git a/tests/unit/test_engine.py b/tests/unit/test_engine.py index 9ff7ce5..045a360 100644 --- a/tests/unit/test_engine.py +++ b/tests/unit/test_engine.py @@ -986,3 +986,36 @@ def first(self, f): ke.declare(Fact(s=False)) ke.run() assert executed == [first, second] + +def test_last_activation_is_executed_2(): + from experta import KnowledgeEngine, Rule, Fact, AS, DefFacts + from experta import NOT, W, MATCH + + executed = None + + class KE(KnowledgeEngine): + @DefFacts() + def _initial_action(self): + yield Fact(action="greet") + + @Rule(Fact(action='greet'), + NOT(Fact(name=W()))) + def ask_name(self): + self.declare(Fact(name="foo")) + + @Rule(Fact(action='greet'), + NOT(Fact(location=W()))) + def ask_location(self): + self.declare(Fact(location="bar")) + + @Rule(Fact(action='greet'), + Fact(name=MATCH.name), + Fact(location=MATCH.location)) + def greet(self, name, location): + nonlocal executed + executed = (name, location) + + ke = KE() + ke.reset() + ke.run() + assert executed == ("foo", "bar") diff --git a/tox.ini b/tox.ini index b96af7f..299e833 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35, py36, py37, pypy3 +envlist = py35, py36, py37, py38, pypy3 [testenv] deps = pipenv