Skip to content

Commit

Permalink
Refactored _expression into a package
Browse files Browse the repository at this point in the history
  • Loading branch information
pavel-kirienko committed May 14, 2019
1 parent dbb7547 commit 28e0fe8
Show file tree
Hide file tree
Showing 15 changed files with 897 additions and 822 deletions.
815 changes: 0 additions & 815 deletions pydsdl/_expression.py

This file was deleted.

19 changes: 19 additions & 0 deletions pydsdl/_expression/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#
# Copyright (C) 2018-2019 UAVCAN Development Team <uavcan.org>
# This software is distributed under the terms of the MIT License.
#

from ._any import Any
from ._any import UndefinedOperatorError, UndefinedAttributeError, InvalidOperandError

from ._primitive import Primitive, Rational, Boolean, String

from ._container import Container, Set

from ._operator import OperatorOutput, BinaryOperator, AttributeOperator
from ._operator import positive, negative
from ._operator import logical_not, logical_or, logical_and
from ._operator import equal, not_equal, less_or_equal, greater_or_equal, less, greater
from ._operator import bitwise_and, bitwise_xor, bitwise_or
from ._operator import add, subtract, multiply, divide, modulo, power
from ._operator import attribute
110 changes: 110 additions & 0 deletions pydsdl/_expression/_any.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#
# Copyright (C) 2018-2019 UAVCAN Development Team <uavcan.org>
# This software is distributed under the terms of the MIT License.
#

import abc
from .. import _error


class InvalidOperandError(_error.InvalidDefinitionError):
pass


class UndefinedOperatorError(InvalidOperandError):
"""Thrown when there is no matching operator for the supplied arguments."""
def __init__(self) -> None:
super(UndefinedOperatorError, self).__init__('The requested operator is not defined for the provided arguments')


class UndefinedAttributeError(InvalidOperandError):
"""Thrown when the requested attribute does not exist."""
def __init__(self) -> None:
super(UndefinedAttributeError, self).__init__('Invalid attribute name')


class Any(abc.ABC):
"""
This abstract class represents an arbitrary intrinsic DSDL expression value.
"""
# This attribute must be specified in the derived classes.
# It contains the name of the data type implemented by the class.
TYPE_NAME = None # type: str

@abc.abstractmethod
def __hash__(self) -> int:
raise NotImplementedError # pragma: no cover

@abc.abstractmethod
def __eq__(self, other: object) -> bool:
raise NotImplementedError # pragma: no cover

@abc.abstractmethod
def __str__(self) -> str:
"""Must return a DSDL spec-compatible textual representation of the contained value suitable for printing."""
raise NotImplementedError # pragma: no cover

def __repr__(self) -> str:
return self.TYPE_NAME + '(' + str(self) + ')'

#
# Unary operators.
#
def _logical_not(self) -> 'Boolean': raise UndefinedOperatorError

def _positive(self) -> 'Any': raise UndefinedOperatorError

def _negative(self) -> 'Any': raise UndefinedOperatorError

#
# Binary operators.
# The types of the operators defined here must match the specification.
# Make sure to use least generic types in the derived classes - Python allows covariant return types.
#
def _logical_or(self, right: 'Any') -> 'Boolean': raise UndefinedOperatorError
def _logical_and(self, right: 'Any') -> 'Boolean': raise UndefinedOperatorError

def _equal(self, right: 'Any') -> 'Boolean': raise UndefinedOperatorError # pragma: no branch
def _less_or_equal(self, right: 'Any') -> 'Boolean': raise UndefinedOperatorError
def _greater_or_equal(self, right: 'Any') -> 'Boolean': raise UndefinedOperatorError
def _less(self, right: 'Any') -> 'Boolean': raise UndefinedOperatorError
def _greater(self, right: 'Any') -> 'Boolean': raise UndefinedOperatorError

def _bitwise_or(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _bitwise_or_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

def _bitwise_xor(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _bitwise_xor_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

def _bitwise_and(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _bitwise_and_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

def _add(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _add_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

def _subtract(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _subtract_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

def _multiply(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _multiply_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

def _divide(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _divide_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

def _modulo(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _modulo_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

def _power(self, right: 'Any') -> 'Any': raise UndefinedOperatorError
def _power_right(self, left: 'Any') -> 'Any': raise UndefinedOperatorError

#
# Attribute access operator. It is a binary operator as well, but its semantics is slightly different.
# Implementations must invoke super()._attribute() when they encounter an unknown attribute, to allow
# the parent classes to handle the requested attribute as a fallback option.
#
def _attribute(self, name: 'String') -> 'Any': raise UndefinedAttributeError


# This import must be located at the bottom to break the circular dependency in the type annotations above.
# We must import specific names as opposed to the whole module because the latter breaks MyPy.
from ._primitive import Boolean, String # noqa
237 changes: 237 additions & 0 deletions pydsdl/_expression/_container.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#
# Copyright (C) 2018-2019 UAVCAN Development Team <uavcan.org>
# This software is distributed under the terms of the MIT License.
#

import abc
import typing
import functools
from . import _any, _primitive, _operator


_O = typing.TypeVar('_O')


# noinspection PyAbstractClass
class Container(_any.Any):
@property
@abc.abstractmethod
def element_type(self) -> typing.Type[_any.Any]:
raise NotImplementedError # pragma: no cover

@abc.abstractmethod
def __iter__(self) -> typing.Iterator[typing.Any]:
raise NotImplementedError # pragma: no cover


class Set(Container):
TYPE_NAME = 'set'

# noinspection PyProtectedMember
class _Decorator:
@staticmethod
def homotypic_binary_operator(inferior: typing.Callable[['Set', 'Set'], _O]) \
-> typing.Callable[['Set', 'Set'], _O]:
def wrapper(self: 'Set', other: 'Set') -> _O:
assert isinstance(self, Set) and isinstance(other, Set)
if self.element_type == other.element_type:
return inferior(self, other)
else:
raise _any.InvalidOperandError('The requested binary operator is defined only for sets '
'that share the same element type. The different types are: %r, %r' %
(self.element_type.TYPE_NAME, other.element_type.TYPE_NAME))
return wrapper

def __init__(self, elements: typing.Iterable[_any.Any]):
list_of_elements = list(elements) # type: typing.List[_any.Any]
del elements
if len(list_of_elements) < 1:
raise _any.InvalidOperandError('Zero-length sets are currently not permitted because '
'of associated type deduction issues. This may change later.')

element_types = set(map(type, list_of_elements))
if len(element_types) != 1:
# This also weeds out covariant sets, although our barbie-size type system is unaware of that.
raise _any.InvalidOperandError('Heterogeneous sets are not permitted')

# noinspection PyTypeChecker
self._element_type = list(element_types)[0] # type: typing.Type[_any.Any]
self._value = frozenset(list_of_elements) # type: typing.FrozenSet[_any.Any]

if not issubclass(self._element_type, _any.Any):
raise ValueError('Invalid element type: %r' % self._element_type)

def __iter__(self) -> typing.Iterator[typing.Any]:
return iter(self._value)

@property
def element_type(self) -> typing.Type[_any.Any]:
return self._element_type

def __hash__(self) -> int:
return hash(self._value)

def __eq__(self, other: object) -> bool:
if isinstance(other, Set):
return self._value == other._value
else:
return NotImplemented

def __str__(self) -> str:
return '{%s}' % ', '.join(map(str, self._value)) # This is recursive.

#
# Set algebra implementation.
#
@_Decorator.homotypic_binary_operator
def _is_equal_to(self, right: 'Set') -> bool:
return self._value == right._value

@_Decorator.homotypic_binary_operator
def _is_superset_of(self, right: 'Set') -> bool:
return self._value.issuperset(right._value)

@_Decorator.homotypic_binary_operator
def _is_subset_of(self, right: 'Set') -> bool:
return self._value.issubset(right._value)

@_Decorator.homotypic_binary_operator
def _is_proper_superset_of(self, right: 'Set') -> bool:
return self._is_superset_of(right) and not self._is_equal_to(right)

@_Decorator.homotypic_binary_operator
def _is_proper_subset_of(self, right: 'Set') -> bool:
return self._is_subset_of(right) and not self._is_equal_to(right)

@_Decorator.homotypic_binary_operator
def _create_union_with(self, right: 'Set') -> 'Set':
return Set(self._value.union(right._value))

@_Decorator.homotypic_binary_operator
def _create_intersection_with(self, right: 'Set') -> 'Set':
return Set(self._value.intersection(right._value))

@_Decorator.homotypic_binary_operator
def _create_disjunctive_union_with(self, right: 'Set') -> 'Set':
return Set(self._value.symmetric_difference(right._value))

#
# Set comparison.
#
def _equal(self, right: _any.Any) -> _primitive.Boolean:
if isinstance(right, Set):
return _primitive.Boolean(self._is_equal_to(right))
else:
raise _any.UndefinedOperatorError

def _less_or_equal(self, right: _any.Any) -> _primitive.Boolean:
if isinstance(right, Set):
return _primitive.Boolean(self._is_subset_of(right))
else:
raise _any.UndefinedOperatorError

def _greater_or_equal(self, right: _any.Any) -> _primitive.Boolean:
if isinstance(right, Set):
return _primitive.Boolean(self._is_superset_of(right))
else:
raise _any.UndefinedOperatorError

def _less(self, right: _any.Any) -> _primitive.Boolean:
if isinstance(right, Set):
return _primitive.Boolean(self._is_proper_subset_of(right))
else:
raise _any.UndefinedOperatorError

def _greater(self, right: _any.Any) -> _primitive.Boolean:
if isinstance(right, Set):
return _primitive.Boolean(self._is_proper_superset_of(right))
else:
raise _any.UndefinedOperatorError

#
# Set algebra operators that yield a new set.
#
def _bitwise_or(self, right: _any.Any) -> 'Set':
if isinstance(right, Set):
return self._create_union_with(right)
else:
raise _any.UndefinedOperatorError

def _bitwise_xor(self, right: _any.Any) -> 'Set':
if isinstance(right, Set):
return self._create_disjunctive_union_with(right)
else:
raise _any.UndefinedOperatorError

def _bitwise_and(self, right: _any.Any) -> 'Set':
if isinstance(right, Set):
return self._create_intersection_with(right)
else:
raise _any.UndefinedOperatorError

#
# Elementwise application.
# https://stackoverflow.com/questions/55148139/referring-to-a-pure-virtual-method
#
def _elementwise(self,
impl: typing.Callable[[_any.Any, _any.Any], _any.Any],
other: _any.Any,
swap: bool = False) -> 'Set':
if not isinstance(other, Set):
return Set((impl(other, x) if swap else impl(x, other)) for x in self)
else:
raise _any.UndefinedOperatorError

def _add(self, right: _any.Any) -> 'Set':
return self._elementwise(_operator.add, right)

def _add_right(self, left: _any.Any) -> 'Set':
return self._elementwise(_operator.add, left, swap=True)

def _subtract(self, right: _any.Any) -> 'Set':
return self._elementwise(_operator.subtract, right)

def _subtract_right(self, left: _any.Any) -> 'Set':
return self._elementwise(_operator.subtract, left, swap=True)

def _multiply(self, right: _any.Any) -> 'Set':
return self._elementwise(_operator.multiply, right)

def _multiply_right(self, left: _any.Any) -> 'Set':
return self._elementwise(_operator.multiply, left, swap=True)

def _divide(self, right: _any.Any) -> 'Set':
return self._elementwise(_operator.divide, right)

def _divide_right(self, left: _any.Any) -> 'Set':
return self._elementwise(_operator.divide, left, swap=True)

def _modulo(self, right: _any.Any) -> 'Set':
return self._elementwise(_operator.modulo, right)

def _modulo_right(self, left: _any.Any) -> 'Set':
return self._elementwise(_operator.modulo, left, swap=True)

def _power(self, right: _any.Any) -> 'Set':
return self._elementwise(_operator.power, right)

def _power_right(self, left: _any.Any) -> 'Set':
return self._elementwise(_operator.power, left, swap=True)

#
# Attributes
#
def _attribute(self, name: '_primitive.String') -> _any.Any:
if name.native_value == 'min':
out = functools.reduce(lambda a, b: a if _operator.less(a, b) else b, self)
assert isinstance(out, self.element_type)
elif name.native_value == 'max':
out = functools.reduce(lambda a, b: a if _operator.greater(a, b) else b, self)
assert isinstance(out, self.element_type)
elif name.native_value == 'count': # "size" and "length" can be ambiguous, "cardinality" is long
out = _primitive.Rational(len(self._value))
else:
out = super(Set, self)._attribute(name) # Hand over up the inheritance chain, this is important

assert isinstance(out, _any.Any)
return out
Loading

0 comments on commit 28e0fe8

Please sign in to comment.