bincombo 0.2.0
Make any type binary-combinable with a single line of code.
License: MIT
Requires Python: >=3.7,<4.0

bincombo
========

Make any type binary-combinable with a single line of code.

The most obvious use case arguably is the creation of binary expressions over custom
types, but ``bincombo`` is not limited to combination of boolean values.
By providing appropriate map/reduce operations, any type of data of the discrete
members of a combination can be aggregated to form a combined result.

The implementation solely relies on inheritance, no meta programming is involved.
You are free to use your own metaclasses, should you wish to do so.

Suppose you have a custom type ``Check``, which accepts or rejects values depending
on the result of the invocation of some callable.
You can make instances of it combinable using the binary operators `&` (and) and `|`
(or) and also support the unary `~` (invert) by using the ``combinable()`` decorator::

    import bincombo

    @bincombo.combinable(methods=("check",))
    class Check:
        __slots__ = ("checker",)

        def __init__(self, checker):
            self.checker = checker

        def check(self, value):
            return self.checker(value)

Now, ``Check`` objects can be binary-combined::

    c1 = Check(lambda v: isinstance(v, int) and v >= 42)
    c2 = Check(lambda v: isinstance(v, str))
    c3 = Check(lambda v: "hello" in v)
    c = c1 | c2 & ~c3
    c.check(41)  # False
    c.check(42)  # True
    c.check("hello, world!")  # False
    c.check("hey, world!")  # True

The ``combinable()`` decorator creates a number of types needed to represent discrete
checks and combinations thereof.
These are stored in a ``Config`` object, which is aavailable as class attribute
``BIN_CONFIG`` of ``Check``.
It can be worth storing these types as module attributes alongside your ``Check``
class to have them at hand for explicit use or type checking::

    BaseCheck = Check.BIN_CONFIG.base_type
    CheckCombo = Check.BIN_CONFIG.combo_type
    AllChecks = Check.BIN_CONFIG.and_type
    AnyCheck = Check.BIN_CONFIG.or_type

All types in this module have ``__slots__`` defined for smaller memory footprints
and improved lookup times, as have the types created by ``combinable()``.
It is recommended to also equip your own type with ``__slots__`` if possible to
benefit from entirely ``__dict__``-less objects.

To gain a better understanding of how all the types relate or to further customize
them, here is how you would make ``Check`` binary-combinable without using the
``combinable()`` helper::

    # This is a base class from which both the discrete Check type and the type
    # representing a combination of Check objects will inherit.
    # It can be used, for instance, to test whether some object is Check-like
    # using isinstance(obj, BaseCheck).
    class BaseCheck:
        __slots__ = ()

    # Check should support all three operations.
    feature_mixins = (
        bincombo.AndSupportMixin, bincombo.OrSupportMixin, bincombo.InvertSupportMixin
    )

    # This is the type that would normally be returned by combinable() decorator.
    # Here it replaces the original Check, but you could also give it a different name.
    class Check(*feature_mixins, Check, BaseCheck):
        __slots__ = ()

    # Objects of this type represent (possibly negated) AND/OR combinations of
    # Check objects.
    class CheckCombo(*feature_mixins, bincombo.Combo, BaseCheck):
        __slots__ = ()

        # Create a proxy check() method that queries an individual combo member.
        # bincombo will call this method for all members, combining the returned
        # boolean values using and/or, depending on the combination type, and possibly
        # invert the final result.
        @bincombo.combine_members
        def check(self, member, value):
            return member.check(value)

    # This is an AND combination.
    class AllChecks(bincombo.AndComboMixin, CheckCombo):
        __slots__ = ()

    # This is an OR combination.
    class AnyCheck(bincombo.OrComboMixin, CheckCombo):
        __slots__ = ()

    # Finally, bincombo has to be taught all the types just created.
    # By attaching the Config object to BaseCheck as a class attribute, both Check
    # and CheckCombo instances will have it available due to inheritance.
    BaseCheck.BIN_CONFIG = bincombo.Config(
        BaseCheck, Check, CheckCombo, AllChecks, AnyCheck
    )

Further customization of the combining abilities is possible, the documentations of
``combinable()``, ``combine_members()`` and ``Config`` have more information.