jacquev6/LowVoltage

View on GitHub
LowVoltage/actions/expressions.py

Summary

Maintainability
F
3 days
Test Coverage
# coding: utf8

# Copyright 2014-2015 Vincent Jacques <vincent@vincent-jacques.net>

import LowVoltage.testing as _tst


class _Boolean(object):
    def __and__(self, other):
        return _BooleanExpression(self, "AND", other)

    def __or__(self, other):
        return _BooleanExpression(self, "OR", other)

    def __invert__(self):
        return _BooleanNegation(self)


class _BooleanExpression(_Boolean):
    def __init__(self, left, operator, right):
        if not isinstance(left, _Boolean):
            raise TypeError
        if not isinstance(operator, basestring):
            raise TypeError
        if not isinstance(right, _Boolean):
            raise TypeError
        self.__left = left
        self.__operator = operator
        self.__right = right

    def bool(self):
        return "({}) {} ({})".format(self.__left.bool(), self.__operator, self.__right.bool())


class _BooleanNegation(_Boolean):
    def __init__(self, operand):
        if not isinstance(operand, _Boolean):
            raise TypeError
        self.__operand = operand

    def bool(self):
        return "NOT ({})".format(self.__operand.bool())


class _ComparisonExpression(_Boolean):
    def __init__(self, left, operator, right):
        if not isinstance(left, _Atom):
            raise TypeError
        if not isinstance(operator, basestring):
            raise TypeError
        if not isinstance(right, _Atom):
            raise TypeError
        self.__left = left
        self.__operator = operator
        self.__right = right

    def bool(self):
        return "{}{}{}".format(self.__left.atom(), self.__operator, self.__right.atom())


class _Atom(object):
    def __eq__(self, other):
        return _ComparisonExpression(self, "=", other)

    def __ne__(self, other):
        return _ComparisonExpression(self, "<>", other)

    def __lt__(self, other):
        return _ComparisonExpression(self, "<", other)

    def __le__(self, other):
        return _ComparisonExpression(self, "<=", other)

    def __gt__(self, other):
        return _ComparisonExpression(self, ">", other)

    def __ge__(self, other):
        return _ComparisonExpression(self, ">=", other)


class Attr(_Atom):
    def __init__(self, name):
        if not isinstance(name, basestring):
            raise TypeError
        self.__name = name

    def atom(self):
        return self.__name


class Val(_Atom):
    def __init__(self, label):
        if not isinstance(label, basestring):
            raise TypeError
        self.__label = label

    def atom(self):
        return ":{}".format(self.__label)


class In(_Boolean):
    def __init__(self, elem, set):
        if not isinstance(elem, _Atom):
            raise TypeError
        if not all(isinstance(elem, _Atom) for elem in set):
            raise TypeError
        self.__elem = elem
        self.__set = set

    def bool(self):
        return "{} IN ({})".format(self.__elem.atom(), ", ".join(elem.atom() for elem in self.__set))


class Between(_Boolean):
    def __init__(self, elem, low, high):
        if not isinstance(elem, _Atom):
            raise TypeError
        if not isinstance(low, _Atom):
            raise TypeError
        if not isinstance(high, _Atom):
            raise TypeError
        self.__elem = elem
        self.__low = low
        self.__high = high

    def bool(self):
        return "{} BETWEEN {} AND {}".format(self.__elem.atom(), self.__low.atom(), self.__high.atom())


class AttributeExists(_Boolean):
    def __init__(self, name):
        if not isinstance(name, basestring):
            raise TypeError
        self.__name = name

    def bool(self):
        return "attribute_exists({})".format(self.__name)


class Contains(_Boolean):
    def __init__(self, left, right):
        if not isinstance(left, _Atom):
            raise TypeError
        if not isinstance(right, _Atom):
            raise TypeError
        self.__left = left
        self.__right = right

    def bool(self):
        return "contains({}, {})".format(self.__left.atom(), self.__right.atom())


class BeginsWith(_Boolean):
    def __init__(self, left, right):
        if not isinstance(left, _Atom):
            raise TypeError
        if not isinstance(right, _Atom):
            raise TypeError
        self.__left = left
        self.__right = right

    def bool(self):
        return "begins_with({}, {})".format(self.__left.atom(), self.__right.atom())


class ConditionExpressionUnitTests(_tst.UnitTests):
    def test_atoms_comparison(self):
        self.assertEqual((Attr("a") == Attr("b")).bool(), "a=b")
        self.assertEqual((Attr("a") == Val("b")).bool(), "a=:b")
        self.assertEqual((Val("a") == Attr("b")).bool(), ":a=b")
        self.assertEqual((Val("a") == Val("b")).bool(), ":a=:b")

    def test_comparisons(self):
        self.assertEqual((Attr("a") == Attr("b")).bool(), "a=b")
        self.assertEqual((Attr("a") != Attr("b")).bool(), "a<>b")
        self.assertEqual((Attr("a") < Attr("b")).bool(), "a<b")
        self.assertEqual((Attr("a") <= Attr("b")).bool(), "a<=b")
        self.assertEqual((Attr("a") > Attr("b")).bool(), "a>b")
        self.assertEqual((Attr("a") >= Attr("b")).bool(), "a>=b")

    def test_boolean_algebra(self):
        # Too many parentheses are needed because of lower priority of bitwise operators ("&", "|" and "~"),
        # but we cannot override keywords ("and", "or" and "not") that would have higher priority.
        self.assertEqual(((Attr("a") == Attr("b")) & (Attr("c") == Attr("d"))).bool(), "(a=b) AND (c=d)")
        self.assertEqual(((Attr("a") == Attr("b")) | (Attr("c") == Attr("d"))).bool(), "(a=b) OR (c=d)")
        self.assertEqual((~((Attr("a") == Attr("b")) | (Attr("c") == Attr("d")))).bool(), "NOT ((a=b) OR (c=d))")

    def test_functions(self):
        # Erf, we cannot redefine list.__contains__ so we cannot obtain the syntax:
        # Attr("a") in [Val("b"), Val("c")]
        # And anyway the result of __contains__ is converted to a boolean by "in".
        # So we cannot implement Attr("a") in Set(Val("b"), Val("c")) either.
        self.assertEqual(In(Attr("a"), [Val("b"), Val("c")]).bool(), "a IN (:b, :c)")
        self.assertEqual(Between(Attr("a"), Val("b"), Val("c")).bool(), "a BETWEEN :b AND :c")
        self.assertEqual(AttributeExists("a").bool(), "attribute_exists(a)")
        self.assertEqual(Contains(Val("a"), Attr("b")).bool(), "contains(:a, b)")
        self.assertEqual(BeginsWith(Val("a"), Attr("b")).bool(), "begins_with(:a, b)")

    def test_missing_parentheses_in_boolean_algebra(self):
        # But at least, missing parentheses are caught early
        with self.assertRaises(TypeError):
            Attr("a") == Attr("b") & Attr("c") == Attr("d")
        with self.assertRaises(TypeError):
            (Attr("a") == Attr("b")) & Attr("c") == Attr("d")
        with self.assertRaises(TypeError):
            Attr("a") == Attr("b") & (Attr("c") == Attr("d"))

    def test_plain_wrong_types(self):
        with self.assertRaises(TypeError):
            Attr("a") & (Attr("c") == Attr("d"))
        with self.assertRaises(TypeError):
            (Attr("a") == Attr("c")) & Attr("d")
        with self.assertRaises(TypeError):
            (Attr("a") == Attr("c")) == Attr("d")
        with self.assertRaises(TypeError):
            In("a", [Attr("a")])
        with self.assertRaises(TypeError):
            In(Attr("a"), ["a"])
        with self.assertRaises(TypeError):
            _BooleanExpression(Attr("a"), "==", Attr("a") == Attr("b"))
        with self.assertRaises(TypeError):
            _BooleanExpression(Attr("a") == Attr("b"), 42, Attr("a") == Attr("b"))
        with self.assertRaises(TypeError):
            _BooleanNegation(Attr("a"))
        with self.assertRaises(TypeError):
            _ComparisonExpression("a", "==", Attr("b"))
        with self.assertRaises(TypeError):
            _ComparisonExpression(Attr("a"), 42, Attr("b"))
        with self.assertRaises(TypeError):
            _ComparisonExpression(Attr("a"), "==", "b")
        with self.assertRaises(TypeError):
            Attr(42)
        with self.assertRaises(TypeError):
            Val(42)
        with self.assertRaises(TypeError):
            Between("a", Attr("b"), Attr("c"))
        with self.assertRaises(TypeError):
            Between(Attr("a"), "b", Attr("c"))
        with self.assertRaises(TypeError):
            Between(Attr("a"), Attr("b"), "c")
        with self.assertRaises(TypeError):
            AttributeExists(Attr("a"))
        with self.assertRaises(TypeError):
            Contains("a", Attr("b"))
        with self.assertRaises(TypeError):
            Contains(Attr("a"), "b")
        with self.assertRaises(TypeError):
            BeginsWith("a", Attr("b"))
        with self.assertRaises(TypeError):
            BeginsWith(Attr("a"), "b")