class KnowledgeEngine:
    """
    This represents a clips' ``module``, wich is an ``inference engine``
    holding a set of ``rules`` (as :obj:`pyknow.rule.Rule` objects),
    an ``agenda`` (as :obj:`pyknow.agenda.Agenda` object)
    and a ``fact-list`` (as :obj:`pyknow.factlist.FactList` objects)

    This could be considered, when inherited from, as the
    ``knowlege-base``.
    """
    from pyknow.matchers import ReteMatcher as __matcher__
    from pyknow.strategies import DepthStrategy as __strategy__

    def __init__(self):
        self.running = False
        self.facts = FactList()
        self.agenda = Agenda()

        if (isinstance(self.__matcher__, type)
                and issubclass(self.__matcher__, abstract.Matcher)):
            self.matcher = self.__matcher__(self)
        else:
            raise TypeError("__matcher__ must be a subclass of Matcher")

        if (isinstance(self.__strategy__, type)
                and issubclass(self.__strategy__, abstract.Strategy)):
            self.strategy = self.__strategy__()
        else:
            raise TypeError("__strategy__ must be a subclass of Strategy")

    @staticmethod
    def _get_real_modifiers(**modifiers):
        for k, v in modifiers.items():
            if k.startswith('_') and k[1:].isnumeric():
                yield (int(k[1:]), v)
            else:
                yield (k, v)

    def modify(self, declared_fact, **modifiers):
        """

        Modifies a fact.

        Facts are inmutable in Clips, thus, as documented in clips
        reference manual, this retracts a fact and then re-declares it

        `modifiers` must be a Mapping object containing keys and values
        to be changed.

        To allow modifying positional facts, the user can pass a string
        containing the symbol "_" followed by the numeric index
        (starting with 0). Ex::

            >>> ke.modify(my_fact, _0="hello", _1="world", other_key="!")

        """
        self.retract(declared_fact)

        newfact = declared_fact.copy()
        newfact.update(dict(self._get_real_modifiers(**modifiers)))

        return self.declare(newfact)

    def duplicate(self, template_fact, **modifiers):
        """Create a new fact from an existing one."""

        newfact = template_fact.copy()
        newfact.update(dict(self._get_real_modifiers(**modifiers)))

        return self.declare(newfact)

    @DefFacts(order=-1)
    def _declare_initial_fact(self):
        yield InitialFact()

    def _get_by_type(self, wanted_type):
        for _, obj in inspect.getmembers(self):
            if isinstance(obj, wanted_type):
                obj.ke = self
                yield obj

    def get_rules(self):
        """Return the existing rules."""
        return list(self._get_by_type(Rule))

    def get_deffacts(self):
        """Return the existing deffacts sorted by the internal order"""
        return sorted(self._get_by_type(DefFacts), key=lambda d: d.order)

    def get_activations(self):
        """
        Return activations
        """
        return self.matcher.changes(*self.facts.changes)

    def retract(self, idx_or_declared_fact):
        """
        Retracts a specific fact, using its index

        .. note::
            This updates the agenda
        """
        self.facts.retract(idx_or_declared_fact)

        if not self.running:
            added, removed = self.get_activations()
            self.strategy.update_agenda(self.agenda, added, removed)

    def run(self, steps=float('inf')):
        """
        Execute agenda activations
        """

        self.running = True
        activation = None
        execution = 0
        while steps > 0 and self.running:

            added, removed = self.get_activations()
            self.strategy.update_agenda(self.agenda, added, removed)

            if watchers.worth('AGENDA', 'DEBUG'):  # pragma: no cover
                for idx, act in enumerate(self.agenda.activations):
                    watchers.AGENDA.debug(
                        "%d: %r %r",
                        idx,
                        act.rule.__name__,
                        ", ".join(str(f) for f in act.facts))

            activation = self.agenda.get_next()

            if activation is None:
                break
            else:
                steps -= 1
                execution += 1

                watchers.RULES.info(
                    "FIRE %s %s: %s",
                    execution,
                    activation.rule.__name__,
                    ", ".join(str(f) for f in activation.facts))

                activation.rule(
                    self,
                    **{k: v
                       for k, v in activation.context.items()
                       if not k.startswith('__')})

        self.running = False

    def halt(self):
        self.running = False

    def reset(self, **kwargs):
        """
        Performs a reset as per CLIPS behaviour (resets the
        agenda and factlist and declares InitialFact())

        Any keyword argument passed to `reset` will be passed to @DefFacts
        which have those arguments on their signature.

        .. note:: If persistent facts have been added, they'll be
                  re-declared.
        """

        self.agenda = Agenda()
        self.facts = FactList()

        self.matcher.reset()

        deffacts = []
        for deffact in self.get_deffacts():
            signature = inspect.signature(deffact)
            if not any(p.kind == inspect.Parameter.VAR_KEYWORD
                       for p in signature.parameters.values()):
                # There is not **kwargs defined. Pass only the defined
                # names.
                args = set(signature.parameters.keys())
                deffacts.append(
                    deffact(**{k: v for k, v in kwargs.items()
                               if k in args}))
            else:
                deffacts.append(deffact(**kwargs))

        # Declare all facts yielded by deffacts
        self.__declare(*chain.from_iterable(deffacts))

        self.running = False

    def __declare(self, *facts):
        """
        Internal declaration method. Used for ``declare`` and ``deffacts``
        """
        if any(f.has_field_constraints() for f in facts):
            raise TypeError(
                "Declared facts cannot contain conditional elements")
        elif any(f.has_nested_accessor() for f in facts):
            raise KeyError(
                "Cannot declare facts containing double underscores as keys.")
        else:
            last_inserted = None
            for fact in facts:
                last_inserted = self.facts.declare(fact)

            if not self.running:
                added, removed = self.get_activations()
                self.strategy.update_agenda(self.agenda, added, removed)

            return last_inserted

    def declare(self, *facts):
        """
        Declare from inside a fact, equivalent to ``assert`` in clips.

        .. note::

            This updates the agenda.
        """

        if not self.facts:
            watchers.ENGINE.warning("Declaring fact before reset()")
        return self.__declare(*facts) class Fact(OperableCE, Bindable, dict, metaclass=Validable):
    """Base Fact class"""

    def __init__(self, *args, **kwargs):
        self.update(dict(chain(enumerate(args), kwargs.items())))
        self.__defaults = dict()

    def __missing__(self, key):
        if key not in self.__fields__:
            raise KeyError(key)
        else:
            default = self.__fields__[key].default
            if default is Field.NODEFAULT:
                raise KeyError(key)
            elif key in self.__defaults:
                return self.__defaults[key]
            elif isinstance(default, collections.abc.Callable):
                return self.__defaults.setdefault(key, default())
            else:
                return self.__defaults.setdefault(key, default)

    def __setitem__(self, key, value):
        if self.__factid__ is None:
            super().__setitem__(key, freeze(value))
        else:
            raise RuntimeError("A fact can't be modified after declaration.")

    def validate(self):
        for name, field in self.__fields__.items():
            if name in self:
                try:
                    field.validate(self[name])
                except Exception as exc:
                    raise ValueError(
                        "Invalid value on field %r for fact %r"
                        % (name, self))
            elif field.mandatory:
                raise ValueError(
                    "Mandatory field %r is not defined for fact %r"
                    % (name, self))
            else:
                pass

    def update(self, mapping):
        for k, v in mapping.items():
            self[k] = v

    def as_dict(self):
        """Return a dictionary containing this `Fact` data."""
        return {k: unfreeze(v)
                for k, v in self.items()
                if not self.is_special(k)}

    def copy(self):
        """Return a copy of this `Fact`."""
        content = [(k, v) for k, v in self.items()]

        intidx = [(k, v) for k, v in content if isinstance(k, int)]
        args = [v for k, v in sorted(intidx)]

        kwargs = {k: v
                  for k, v in content
                  if not isinstance(k, int) and not self.is_special(k)}
        return self.__class__(*args, **kwargs)

    def has_field_constraints(self):
        return any(isinstance(v, ConditionalElement) for v in self.values())

    def has_nested_accessor(self):
        return any(("__" in str(k).strip('__') for k in self.keys()))

    @staticmethod
    def is_special(key):
        return (isinstance(key, str)
                and key.startswith('__')
                and key.endswith('__'))

    @property
    def __bind__(self):
        return self.get('__bind__', None)

    @__bind__.setter
    def __bind__(self, value):
        super().__setitem__('__bind__', value)

    @property
    def __factid__(self):
        return self.get('__factid__', None)

    @__factid__.setter
    def __factid__(self, value):
        super().__setitem__('__factid__', value)

    @classmethod
    def from_iter(cls, pairs):
        obj = cls()
        obj.update(dict(pairs))
        return obj

    def __str__(self):  # pragma: no cover
        if self.__factid__ is None:
            return "<Undeclared Fact> %r" % self
        else:
            return "<f-%d>" % self.__factid__

    def __repr__(self):  # pragma: no cover
        return "{}({})".format(
            self.__class__.__name__,
            ", ".join(
                (repr(v) if isinstance(k, int) else "{}={!r}".format(k, v)
                 for k, v in self.items()
                 if not self.is_special(k))))

    def __hash__(self):
        try:
            return self._hash
        except AttributeError:
            self._hash = hash(frozenset(self.items()))
            return self._hash

    def __eq__(self, other):
        return (self.__class__ == other.__class__
                and super().__eq__(other)) class Rule(ConditionalElement):
    """
    Base ``CE``, all ``CE`` are to derive from this class.

    This class is used as a decorator, thus provoking __call__
    to be called twice:

    #. The first call is when the decorator is been created. At this
       point we assign the function decorated to ``self._wrapped`` and
       return ``self`` to be called the second time.

    #. The second call is to execute the decorated function, se we
       pass all the arguments along.
    """

    def __new__(cls, *args, salience=0):
        obj = super(Rule, cls).__new__(cls, *args)

        obj._wrapped = None
        obj._wrapped_args = []
        obj._wrapped_self = None
        obj.salience = salience

        return obj

    def new_conditions(self, *args):
        """
        Generate a new rule with the same attributes but with the given
        conditions.

        """
        obj = self.__class__(*args, salience=self.salience)

        if self._wrapped:
            obj = obj(self._wrapped)

        return obj

    def __hash__(self):
        return hash(tuple(self)
                    + (self._wrapped, self._wrapped_self, self.salience))

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            self_data = tuple(self) + (self._wrapped,
                                       self._wrapped_self,
                                       self.salience)
            other_data = tuple(other) + (other._wrapped,
                                         other._wrapped_self,
                                         other.salience)
            return self_data == other_data
        else:
            return False

    def __call__(self, *args, **kwargs):
        """
        Make method checks if it's the first call, and update wrapper.
        Othersise execute the RHS.
        """
        if self._wrapped is None:
            if not args:
                raise AttributeError("Mandatory function not provided.")
            else:
                self._wrapped = args[0]

                signature = inspect.signature(self._wrapped)
                if not any(p.kind == inspect.Parameter.VAR_KEYWORD
                           for p in signature.parameters.values()):
                    # There is not **kwargs defined. Pass only the defined
                    # names.
                    self._wrapped_args = set(signature.parameters.keys())

                return update_wrapper(self, self._wrapped)
        else:
            if self._wrapped_args:
                kwargs = {k: v for k, v in kwargs.items()
                          if k in self._wrapped_args}
            if self._wrapped_self is None:
                return self._wrapped(*args, **kwargs)
            else:
                return self._wrapped(self._wrapped_self, *args, **kwargs)

    def __repr__(self):  # pragma: no cover
        return "%s => %r" % (super().__repr__(), self._wrapped)

    def __get__(self, instance, owner):
        self._wrapped_self = instance
        return self TRUTH = P(bool) LT = _from_operator2(op.lt) LE = _from_operator2(op.le) EQ = _from_operator2(op.eq) NE = _from_operator2(op.ne) GE = _from_operator2(op.ge) GT = _from_operator2(op.gt) IS = _from_operator2(op.is_) IS_NOT = _from_operator2(op.is_not) CONTAINS = _from_operator2(op.contains) def BETWEEN(a, b):
    """
    The BETWEEN operator selects values within a given range.
    The BETWEEN operator is inclusive: begin and end values are included.

    """
    if any((isinstance(x, ConditionalElement) for x in (a, b))):
        raise TypeError(
            "A ConditionalElement can't be used as an operator condition.")
    else:
        return P(lambda x: a <= x <= b) CALL = _CALL() def REGEX(pattern, flags=0):
    """Regular expression matching."""
    return P(lambda x: re.match(pattern, x, flags=flags)) def LIKE(pattern):
    """Unix shell-style wildcards. Case-sensitive"""
    return P(lambda x: fnmatch.fnmatchcase(x, pattern)) def ILIKE(pattern):
    """Unix shell-style wildcards. Case-insensitive"""
    return P(lambda x: fnmatch.fnmatch(x.lower(), pattern.lower()))
import pytest
import re
from pyknow import KnowledgeEngine, Rule, Fact
from pyknow.operator import TRUTH, LT, LE, EQ, NE, GE, GT, IS, IS_NOT
from pyknow.operator import CONTAINS, BETWEEN, CALL, REGEX, LIKE, ILIKE



@pytest.mark.parametrize("operator,x,expected", [
    (TRUTH, 1, True),
    (TRUTH, 0, False),
    (LT(3), 2, True),
    (LT(3), 3, False),
    (LE(3), 3, True),
    (LE(3), 4, False),
    (EQ(3), 3, True),
    (EQ(3), 2, False),
    (NE(3), 2, True),
    (NE(3), 3, False),
    (GE(3), 3, True),
    (GE(3), 2, False),
    (GT(3), 4, True),
    (GT(3), 3, False),
    (IS(None), None, True),
    (IS(True), False, False),
    (IS_NOT(None), False, True),

    (IS_NOT(None), None, False),