# -*- coding: utf-8 -*-
"""
This modules provides most core features of PyAlaOCL.
It defines the various functions and classes that constitutes the OCL
library.
"""
#import setup
#__version__ = setup.getVersion()
import logging
log = logging.getLogger(__name__)
__all__ = (
'floor',
'isUndefined',
'oclIsUndefined',
'isEmpty',
'notEmpty',
'implies',
'Invalid',
'oclIsKindOf',
'oclIsTypeOf',
'registerIsKindOfFunction',
'registerIsTypeOfFunction',
'Collection',
'Set',
'Bag',
'Seq',
'asSet',
'asBag',
'asSeq',
'isCollection',
'asCollection',
'emptyCollection',
'listAll',
)
import inspect
import logging
logging.getLogger('pyalaocl').addHandler(logging.NullHandler())
class InfixedOperator: # old-class style required
def __init__(self, function):
self.function = function
def __rrshift__(self, other):
return \
InfixedOperator(
lambda x, self=self, other=other: self.function(other, x))
def __rshift__(self, other):
return self.function(other)
def __call__(self, value1, value2):
return self.function(value1, value2)
class PostfixedOperator:
def __init__(self, function):
self.function = function
def __rrshift__(self, other):
return self.function(other)
def _isEmpty(value):
if value is None:
return True
else:
try:
empty =value.isEmpty()
except AttributeError:
try:
l = len(value)
except (TypeError,AttributeError):
return False
else:
return l == 0
else:
return empty
implies = InfixedOperator(lambda x,y: y if x else True)
isEmpty = PostfixedOperator(_isEmpty)
notEmpty = PostfixedOperator(lambda x: not _isEmpty(x))
[docs]def floor(r):
""" Return the largest integer which is not greater than the parameter.
"""
import math
return math.floor(r)
[docs]def isUndefined(value):
"""
Indicates if the given parameter is undefined (None) or not.
:param value: any kind of value.
:type value: any
:return: True if the value is None.
:rtype: bool
Examples:
>>> print isUndefined(3)
False
>>> print isUndefined(None)
True
"""
try:
return value is None
except:
return True # see OCL 11.3.4
def oclIsUndefined(value):
return isUndefined(value)
class Invalid(Exception):
def __init__(self,msg):
super(Invalid,self).__init__(msg)
_OCL_IS_KIND_OF_DELEGATES = []
_OCL_IS_TYPE_OF_DELEGATES = []
def registerIsKindOfFunction(function):
global _OCL_IS_KIND_OF_DELEGATES
if function not in _OCL_IS_KIND_OF_DELEGATES:
_OCL_IS_KIND_OF_DELEGATES.append(function)
def registerIsTypeOfFunction(function):
global _OCL_IS_TYPE_OF_DELEGATES
if function not in _OCL_IS_TYPE_OF_DELEGATES:
_OCL_IS_TYPE_OF_DELEGATES.append(function)
[docs]def oclIsKindOf(value1,value2):
"""
Evaluates to True if the type of the value is *exactly* the type given as
a second parameter is an instance of type or one of its subtypes directly
or indirectly. Use the method oclIsTypeOf if you want to check if a value
is exactly of a given type.
:param value: A scalar value, a collection or an object.
:type value: Any
:param aType: The type to check the value against
(e.g. int, float, str, unicode, bool or a class)
:type aType: type
:return: True if value is compatible with the type aType.
:rtype: bool
Examples:
>>> print oclIsKindOf(3,int)
True
>>> print oclIsKindOf("3",int)
False
>>> print oclIsKindOf(2.5,float)
True
>>> print oclIsKindOf("hello",basestring)
True
>>> print oclIsKindOf(True,bool)
True
>>> class Person(object): pass
>>> print oclIsKindOf(Person(),Person)
True
>>> print oclIsKindOf(Person(),object)
True
>>>
"""
if inspect.isclass(value2) and isinstance(value1, value2):
return True
else:
for is_kind_of_function in _OCL_IS_KIND_OF_DELEGATES:
if is_kind_of_function(value1, value2):
return True
return False
[docs]def oclIsTypeOf(value1,value2):
"""
Return True if the type of the value is *exactly* the type given as a
second parameter. This function does not take into account sub-typing
relationships. If this is what is intended, use oclIsKindOf instead.
:param value: A scalar value, a collection or an object.
:type value: Any
:param aType: The type to check the value against
(e.g. int, float, str, unicode, bool or a class)
:type aType: type
:return: True if value is compatible with the type aType.
:rtype: bool
Examples:
>>> print oclIsTypeOf("hello",str)
True
>>> print oclIsTypeOf("hello",basestring)
False
>>> print oclIsTypeOf(u"çüabè",unicode)
True
"""
if type(value1) == value2:
return True
else:
for is_type_of_function in _OCL_IS_TYPE_OF_DELEGATES:
if is_type_of_function(value1, value2):
return True
return False
def evaluate(value,expression):
"""
Evaluate an expression on a given value
:param value:
:type value:
:param expression:
:type expression:
:return:
:rtype:
Examples:
>>> evaluate(1,lambda x:x*2)
2
>>> evaluate('hello',len)
5
>>> evaluate('hello',str.islower)
True
>>> evaluate('hello','islower')
True
>>> evaluate('hello','len(_)')
5
>>> evaluate('hello','_.islower()')
True
>>> class A(object):
... a = 3
... def __init__(self):
... self.b = 10
... def c(self):
... return 25
>>> evaluate(A(),'a')
3
>>> evaluate(A(),'b')
10
>>> evaluate(A(),'c')
25
"""
if callable(expression):
return expression(value)
elif isinstance(expression,(str,unicode)):
try:
r = getattr(value,expression)
except AttributeError:
if '_' in expression:
_ = value
return eval(expression)
else:
msg = "evaluate(): %s is not an attribute of the type %s" \
% (expression,type(value))
raise Invalid(msg)
if callable(r):
return r()
else:
return r
def evaluatePredicate(value,predicate):
r = evaluate(value,predicate)
t = type(r)
if t is not bool:
msg = "Predicate expected. Returned a value of type" \
" %s instead of a boolean" % t
raise Invalid(msg)
else:
return r
def flatten(value):
"""
Return an OCL collection with all the elements at the first level.
:param value: The collection to be flatten
:rtype value: iterable[iterable]
:return: A flatten collection.
:rtype: Seq
"""
try:
return value.flatten()
except NameError:
if isCollection(value):
flat = []
for e in value:
flat.extend(flatten(e))
return flat
else:
return [value]
#==============================================================================
# Collections
#==============================================================================
from abc import ABCMeta, abstractmethod
# noinspection PyClassicStyleClass
class GenericCollection: # old-class style required
"""
Class used both to define brand new OCL collection (classes under
Collection) but also to define JavaCollectionExtension. Due to restriction
of class instrumentation we use old-style class, hence object is not the
base class.
"""
def __init__(self):
pass
def __len__(self):
"""
Return the size of the collection.
Not in OCL but pythonic.
:return: The number of elements in the collection
:rtype: int
Examples:
>>> len(Set(2,2,3))
2
>>> len(Bag(1,1,1))
3
>>> len(Set(Set()))
1
>>> len(Set())
0
"""
return self.size()
def isEmpty(self):
return self.size() == 0
def notEmpty(self):
return not self.isEmpty()
def includes(self,value):
"""
Return True if the value is in the collection.
:param value: Any kind of value.
:type value: any
:return: True if the element is in set, False otherwise.
:rtype: bool
Examples:
>>> Set(1,3,"a").includes("3")
False
>>> Set(1,3,"a").includes(3)
True
>>> Set(Set()).includes(Set())
True
>>> Set().includes(Set())
False
>>> 3 in Set(2,3,1)
True
>>> "hello" in Set("a","b")
False
>>> Bag(10,"a",3,3,10,10).includes(10)
True
>>> Bag(2).includes(5)
False
>>> 2 in Bag(2,2)
True
>>> Seq(10,2,2,3).includes(3)
True
>>> 2 in Seq(1,0,1)
False
"""
return value in self
def excludes(self,value):
return value not in self
def includesAll(self,elements):
for e in elements:
if e not in self:
return False
return True
def excludesAll(self,elements):
for e in elements:
if e in self:
return False
return True
def __or__(self,anyCollection):
return self.union(anyCollection)
def any(self,predicate):
"""
Return any element in the collection that satisfy the predicate.
This operation is non deterministic as various elements may satisfy
the predicate.
If not element satisfies the predicate an exception is raised.
See OCL-11.9.1
:param predicate: A predicate, that is a function returning a boolean.
:type predicate: X->bool
:return: Any element satisfying the predicate.
:rtype X:
Examples:
>>> Set(1,2,-5,-10).any(lambda x:x<0) in [-5,-10]
True
>>> Set(1,2).any(lambda x:x<0)
Traceback (most recent call last):
...
Invalid: .any(...) failed: No such element.
"""
# noinspection PyTypeChecker
for e in self:
if evaluatePredicate(e,predicate):
return e
raise Invalid(".any(...) failed: No such element.")
def max(self):
return max(self)
def min(self):
return min(self)
def sum(self):
"""
Sum of the number in the collection.
Examples:
>>> Set(1,0.5,-5).sum()
-3.5
>>> Set().sum()
0
"""
return sum(self)
def selectByKind(self,aType):
return self.select(lambda e:oclIsKindOf(e,aType))
def selectByType(self,aType):
return self.select(lambda e:oclIsTypeOf(e,aType))
def reject(self,predicate):
"""
Discard from the set all elements that satisfy the predicate.
:param predicate: A predicate, that is a function returning a boolean.
:return: The set without the rejected elements.
:rtype set:
Examples:
>>> Set(2,3,2.5,-5).reject(lambda e:e>2) == Set(2,-5)
True
>>> Set(Set(1,2,3,4),Set()).reject(lambda e:e.size()>3) \
== Set(Set())
True
"""
return self.select(lambda e:not evaluatePredicate(e,predicate))
def collect(self,expression):
return self.collectNested(expression).flatten()
def __getattr__(self,name):
"""
:param name:
:return:
Examples:
>>> class P(object):
... def __init__(self,x):
... self.a = x
>>> P1 = P(1)
>>> P4 = P(4)
>>> P1.a
1
>>> P4.a
4
>>> Set(P1,P4).a == Bag(1,4)
True
"""
return self.collect(lambda e:getattr(e,name))
def forAll(self,predicate):
"""
Return True if the predicate given as parameter is satisfied by all
elements of the collection.
:param predicate: A predicate, that is a function returning a boolean.
:type predicate: X->bool
:return: Whether or not the predicate is satisfied by all elements.
:rtype bool:
Examples:
>>> Set(2,3,5,-5).forAll(lambda e:e>=0)
False
>>> Set(2,3,5).forAll(lambda e:e>=0)
True
>>> Set().forAll(lambda e:e>=0)
True
>>> Bag(4,4,4).forAll(lambda e:e==4)
True
>>> Seq(Bag(1),Set(2),Seq(3)).forAll(lambda e:e.size()==1)
True
"""
# noinspection PyTypeChecker
for e in self:
if not evaluatePredicate(e,predicate):
return False
return True
def exists(self,predicate):
"""
Return True if the predicate given as parameter is satisfied by at
least one element of the collection.
:param predicate: A predicate, that is a function returning a boolean.
:type predicate: X->bool
:return: Whether or not the predicate is satisfied by at least one
element.
:rtype bool:
Examples:
>>> Set(2,3,5,-5).exists(lambda e:e<0)
True
>>> Set(2,3,5).exists(lambda e:e<0)
False
>>> Set().exists(lambda e:e>=0)
False
>>> Bag(Set(),Set(),Set(2),Set(3)).exists(lambda e:e.size()==1)
True
"""
# noinspection PyTypeChecker
for e in self:
if evaluatePredicate(e,predicate):
return True
return False
def one(self,predicate):
"""
Return True if the predicate given as parameter is satisfied by at
one and only one element in the collection.
:param predicate: A predicate, that is a function returning a boolean.
:type predicate: X->bool
:return: Whether or not the predicate is satisfied by exactly one
element.
:rtype bool:
Examples:
>>> Set(2,3,5,-5).one(lambda e:e<0)
True
>>> Bag(2,3,5,-5,-5).one(lambda e:e<0)
False
>>> Set().one(lambda e:e>=0)
False
>>> Seq().one(lambda e:e>=0)
False
>>> Seq(1).one(lambda e:e>=0)
True
>>> Bag(Set(2),Set(),Set(3),Set()).one(lambda e:e.size()==0)
False
"""
foundOne = False
# noinspection PyTypeChecker
for e in self:
found = evaluatePredicate(e,predicate)
if found and foundOne:
return False
elif found:
foundOne = True
return foundOne
def closure(self,expression):
"""
Return the transitive closure of the expression for all element in
the collection.
See OCL (section 7.6.5.
:param expression: The expression to be applied again and again.
:type: X->X
:return: A set representing the transitive closure including the
source elements/
:type: Seq[X]
Examples:
>>> def f(x):
... successors = {1:[2], 2:[1, 2, 3], 3:[4], 4:[], \
5:[5], 6:[5], 7:[5, 7]}
... return successors[x]
>>> Set(1).closure(f) == Seq(1,2,3,4)
True
>>> Set(5).closure(f) == Seq(5)
True
>>> Seq(6,6,3).closure(f) == Seq(6,3,5,4)
True
"""
# FIXME: returns always a sequence, but the type changes in OCL.
from collections import deque
sources = list(self)
to_visit = deque(sources)
visited = []
while len(to_visit) != 0:
current = to_visit.popleft()
if current not in visited:
result = evaluate(current,expression)
if isCollection(result):
successors = listAll(result)
else:
successors = [result]
# print "visited %s -> %s" % (current,successors)
for s in successors:
if s not in visited:
to_visit.append(s)
visited.append(current)
return Seq.new(visited)
def iterate(self):
# FIXME: Not implemented (See 7.6.6)
raise NotImplementedError()
def isUnique(self,expression):
return not self.collect(expression).hasDuplicates()
from collections import deque
[docs]class Collection(object, GenericCollection):
"""
Base class for OCL collections.
Collections are either:
* sets (Set),
* ordered set (OrderedSet)
* bags (Bag),
* sequences (Seq)
"""
__metaclass__ = ABCMeta
@abstractmethod
def size(self):
pass
@abstractmethod
def count(self,element):
pass
@abstractmethod
def including(self,value):
pass
@abstractmethod
def excluding(self,value):
pass
@abstractmethod
def union(self,value):
pass
@abstractmethod
def select(self,expression):
pass
@abstractmethod
def flatten(self):
pass
@abstractmethod
def collectNested(self,expression):
pass
@abstractmethod
def hasDuplicates(self):
pass
@abstractmethod
def duplicates(self):
pass
@abstractmethod
def selectWithCount(self,number):
pass
@abstractmethod
def sortedBy(self,expression):
pass
def asCollection(self):
return self
@abstractmethod
def emptyCollection(self):
return self
@abstractmethod
def asSet(self):
pass
@abstractmethod
def asBag(self):
pass
@abstractmethod
def asSeq(self):
pass
@abstractmethod
def __str__(self):
pass
def __repr__(self):
return self.__str__()
@abstractmethod
def __eq__(self,value):
pass
def __ne__(self,value):
return not self.__eq__(value)
@abstractmethod
def __hash__(self):
pass
@abstractmethod
def __contains__(self,item):
pass
@abstractmethod
def __iter__(self):
pass
#------------------------------------------------------------------------------
# OCL Sets
#------------------------------------------------------------------------------
[docs]def asSet(collection):
"""
Convert the given collection to a Set
:param collection:
:return:
:rtype: Set
"""
try:
return collection.asSet()
except AttributeError:
return Set.new(collection)
[docs]class Set(Collection):
"""
Set of elements.
This class mimics OCL Sets. Being a set, there are no duplicates and no
ordering of elements. By contrast to OCL Sets, here a set can contain
any kind of elements at the same time. OCL sets are homogeneous,
all elements being of the same type (or at least same supertype).
"""
def __init__(self,*args):
"""
Create a set from some elements.
Eliminate duplicates if any.
Examples:
>>> Set(10,"a",3,10,10) == Set(10,"a",3)
True
>>> Set() <> Set(10,"a",3)
True
>>> Set(10,10).size()
1
>>> Set(Set()).size()
1
>>> Set("hello").size()
1
>>> Set(Set(2),Set(2)).size()
1
"""
# We cannot have Counter here. So list is ok (see listAll)
super(Set, self).__init__()
self.theSet = set(list(args))
@classmethod
def new(cls,anyCollection=()):
newSet = cls()
newSet.theSet = set(anyCollection)
return newSet
def emptyCollection(self):
return Set.new()
[docs] def size(self):
"""
Return the size of the set.
:return: The size of the set.
:rtype: int
Examples:
>>> Set(1,4,2,1,1).size()
3
>>> Set().size()
0
"""
return len(self.theSet)
[docs] def isEmpty(self):
"""
Examples:
>>> Set().isEmpty()
True
>>> Set(Set()).isEmpty()
False
>>> Set(2,3).isEmpty()
False
"""
return False if self.theSet else True
[docs] def count(self,value):
"""
Return the number of occurrence of the value in the set (0 or 1).
:param value: The element to search in the set.
:type value: any
:return: 1 if the element is in the set, 0 otherwise.
:rtype: bool
Examples:
>>> Set(1,3,"a").count("3")
0
>>> Set(1,3,3,3).count(3)
1
"""
return 1 if value in self.theSet else 0
def includes(self,value):
return value in self.theSet
[docs] def including(self,value):
"""
Add the element to the set if not already there.
:param value: The element to add to the set.
:type value: any
:return: A set including this element.
:rtype: Set
Examples:
>>> Set(1,3,"a").including("3") == Set(1,"3",3,"a")
True
>>> Set(1,3,3,3).including(3) == Set(3,1)
True
"""
fresh = set(self.theSet)
fresh.add(value)
return Set.new(fresh)
[docs] def excluding(self,value):
"""
Excludes a value from the set (if there).
:param value: The element to add to the set.
:type value: any
:return: A set including this element.
:rtype: Set
Examples:
>>> Set(1,3,"a").excluding("3") == Set(1,3,"a")
True
>>> Set(1,3,3,3).excluding(3) == Set(1)
True
"""
fresh = set(self.theSet)
fresh.discard(value)
return Set.new(fresh)
[docs] def union(self,anyCollection):
"""
Add all elements from the collection given to the set.
:param anyCollection: A collection of values to be added to this set.
:type anyCollection: collection
:return: A set including all values added plus previous set elements.
:rtype: Set
Examples:
>>> Set(1,3,'a').union([2,3,2]) == Set(1,3,"a",2)
True
>>> Set(1,3,3,3).union(Set(2,1,8)) == Set(1,2,3,8)
True
>>> Set().union(Set()) == Set()
True
>>> Set(1,3) | [2,3] == Set(1,2,3)
True
"""
assert isCollection(anyCollection), \
'Any collection expected, but found %s' % anyCollection
# We don't need to take special care with Counter as we remove
# duplicates
fresh = set(self.theSet)
fresh = fresh | set(anyCollection)
return Set.new(fresh)
def __or__(self,anyCollection):
return self.union(anyCollection)
[docs] def intersection(self,anyCollection):
"""
Retain only elements in the intersection between this set and the
given collection.
:param anyCollection: A collection of values to be added to this set.
:type anyCollection: collection
:return: A set including all values added plus previous set elements.
:rtype: Set
Examples:
>>> Set(1,3,"a").intersection(["a","a",8]) == Set("a")
True
>>> Set(1,3,3,3).intersection(Set(1,3)) == Set(1,3)
True
>>> Set(2).intersection(Set()) == Set()
True
>>> Set(2) & Set(3,2) == Set(2)
True
"""
assert isCollection(anyCollection), \
'Any collection expected, but found %s' % anyCollection
# Don't need to take special care with Counter as we remove duplicates
fresh = set(self.theSet)
fresh = fresh & set(anyCollection)
return Set.new(fresh)
def __and__(self,anyCollection):
return self.intersection(anyCollection)
[docs] def difference(self,anyCollection):
"""
Remove from the set all values in the collection.
:param anyCollection: Any collection of values to be discarded from
this set.
:type anyCollection: collection
:return: This set without the values in the collection.
:rtype: Set
Examples:
>>> Set(1,3,"a").difference([2,3,2,'z']) == Set(1,"a")
True
>>> Set(1,3,3,3).difference(Set(1,3)) == Set()
True
>>> Set().difference(Set()) == Set()
True
>>> Set(1,3) - [2,3] == Set(1)
True
"""
assert isCollection(anyCollection), \
'Any collection expected, but found %s' % anyCollection
fresh = set(self.theSet)
# No need for take special care with Counter as we remove duplicates
fresh = fresh - set(anyCollection)
return Set.new(fresh)
def __sub__(self,anyCollection):
return self.difference(anyCollection)
[docs] def symmetricDifference(self,anyCollection):
"""
Return the elements that are either in one set but not both sets.
In fact this method accept any collection, but it is first converted
to a set.
:param anyCollection: A collection to make the difference with.
:type anyCollection: collection
:return: The symmetric difference.
:rtype: Set
Examples:
>>> Set(1,2).symmetricDifference(Set(3,2)) == Set(1,3)
True
>>> Set(Set()).symmetricDifference(Set()) == Set(Set())
True
"""
assert isCollection(anyCollection), \
'Any collection expected, but found %s' % anyCollection
fresh = set(self.theSet)
other_set = set(anyCollection)
fresh = (fresh | other_set) - (fresh & other_set)
return Set.new(fresh)
[docs] def hasDuplicates(self):
"""
Return always False for sets.
This method is an extension to OCL. It makes is defined on sets just
for consistency but is more useful for Bags or Sequences.
:return: True
:rtype: bool
"""
return False
[docs] def duplicates(self):
""" Return always an empty bag for a set """
return Bag.new()
[docs] def selectWithCount(self, number):
""" Return the set if 1 is selected as count. Otherwise return
an empty set because there is no duplicated elements in a set.
:param number:
:type number:
:return:
:rtype:
"""
if number==1:
return self
else:
return Set()
[docs] def flatten(self):
"""
If the set is a set of collections, then return the set-union of all
its elements.
:return: Set
:rtype: Set
Examples:
>>> Set(Set(2)).flatten() == Set(2)
True
>>> Set(Set(Set(2)),Set(2)).flatten() == Set(2)
True
>>> Set(Set(2,3),Set(4),Set(),Bag("a"),Bag(2,2)).flatten() \
== Set(2,3,4,"a")
True
#>>> Set().flatten() == Set()
# True
# >>> Set(2,3).flatten() == Set(2,3)
# True
# >>> Set(2,Set(3),Set(Set(2))).flatten() == Set(2,3)
# True
"""
fresh = set()
for e in self.theSet:
if isCollection(e):
flat_set = set(flatten(e))
else:
flat_set = {e}
fresh = fresh | flat_set
return Set.new(fresh)
[docs] def select(self,predicate):
"""
Retain in the set only the elements satisfying the expression.
:param predicate: A predicate, that is a function returning a boolean.
:return: The set with only the selected elements.
:rtype Set:
Examples:
>>> Set(2,3,2.5,-5).select(lambda e:e>2) == Set(3,2.5)
True
>>> Set(Set(1,2,3,4),Set()).select(lambda e:e.size()>3) \
== Set(Set(1,2,3,4))
True
"""
return Set.new(set([e for e in self if evaluatePredicate(e,predicate)]))
[docs] def collectNested(self,expression):
"""
Return a bag of values resulting from the evaluation of the given expression
on all elements of the set.
The transformation from this set to a bag is due to the fact that
the expression can generate duplicates.
:param expression: A function returning any kind of value.
:type expression: X -> Y
:return: The bag of values produced.
:rtype Bag[Y]:
Examples:
>>> Set(2,3,5,-5).collectNested(lambda e:e*e) == Bag(25,25,4,9)
True
>>> Set(2,3).collectNested(lambda e:Bag(e,e)) \
== Bag(Bag(2,2),Bag(3,3))
True
"""
return Bag.new(map((lambda e:evaluate(e,expression)),self.theSet))
def sortedBy(self,expression):
# FIXME: should return a OrderedSet
return \
Seq.new(sorted(self.theSet,key=(lambda e:evaluate(e,expression))))
def asSet(self):
return self
def asBag(self):
return asBag(self.theSet)
def asSeq(self):
return asSeq(self.theSet)
def __str__(self):
"""
Return a string representation of the set where elements are
separated by ", ".
The result is non deterministic as there is no ordering between
elements.
:return: A string.
:rtype: str
Examples:
>>> str(Set())
'Set()'
>>> str(Set(3))
'Set(3)'
>>> str(Set(3,2)).startswith('Set(')
True
"""
body = ", ".join(map(str,self.theSet))
return "Set(%s)" % body
def __repr__(self):
return self.__str__()
def __eq__(self,value):
"""
Return true if the value given is a Set and has exactly the same
elements.
:param value: Any value, but succeed only for sets.
:type value: any
:return: True if "value" is a set with the same elements.
:rtype: bool
Examples:
>>> Set() == []
False
>>> Set() == Set()
True
>>> Set(2,3,3) == Set(3,2)
True
>>> Set(2,"3",4) == Set(2,4,3)
False
>>> Set("hello") == Set("hello")
True
>>> Set(Set(1)) == Set(Set(1))
True
>>> Set(Set(1),Set(2,1)) == Set(Set(1,2),Set(1))
True
"""
if not isinstance(value,Set):
return False
return self.theSet == value.theSet
def __ne__(self,value):
return not self.__eq__(value)
def __hash__(self):
return hash(frozenset(self.theSet))
def __iter__(self):
""" Make Sets iterable for pythonic usage.
:return: the iterator for this Set
"""
return self.theSet.__iter__()
def __contains__(self,item):
return item in self.theSet
#------------------------------------------------------------------------------
# OCL Bags
#------------------------------------------------------------------------------
[docs]def asBag(anyCollection):
"""
:param anyCollection:
:return:
"""
try:
return anyCollection.asBag()
except AttributeError:
return Bag.new(anyCollection)
from collections import Counter
class Bag(Collection):
def __init__(self,*args):
"""
Create a bag from some elements.
Examples:
>>> Bag(10,"a",3,10,10) == Bag(10,10,"a",3,10)
True
>>> Bag(2) <> Bag(2,2,2)
True
>>> Bag(3,3,4) == Bag(3,4,3)
True
>>> Bag(2,3) == Bag(3,2)
True
>>> Bag(Set(2,3),Set(3,2)).size()
2
"""
super(Bag,self).__init__()
# We cannot have Counter here. So list is ok (see listAll)
self.theCounter = Counter(list(args))
@classmethod
def new(cls,anyCollection=()):
newBag = Bag()
if isinstance(anyCollection,Counter):
newBag.theCounter = anyCollection.copy()
# Remove the 0 and negative elements from the counter. This
# weird trick is indicated in python documentation for Counter.
newBag.theCounter += Counter()
elif isinstance(anyCollection,Bag):
newBag.theCounter = anyCollection.theBag.copy()
else:
newBag.theCounter = Counter(listAll(anyCollection))
return newBag
def emptyCollection(self):
return Bag.new()
def size(self):
"""
Return the total number of elements in the bag.
:rtype! int
Examples:
>>> Bag(10,"a",3,3,10,10).size()
6
>>> Bag(2).size()
1
>>> Bag().size()
0
"""
return sum(self.theCounter.values())
def count(self,value):
"""
Return the number of occurrences of a given value within the bag.
Examples:
>>> Bag(10,"a",3,3,10,10).count(10)
3
>>> Bag(2).count(5)
0
>>> Bag().count(2)
0
>>> Bag(Set(1),Set(1)).count(Set(1))
2
"""
return self.theCounter[value]
# def __getitem__(self,key):
# return self.theCounter[key]
def including(self,value):
"""
Add a value into the bag.
:param value: The value to be added.
:type: any
:return: The bag with one more occurrence of the value.
:rtype: Bag
Examples:
>>> Bag(10,10,2,10).including(10) == Bag(10,10,10,10,2)
True
>>> Bag(10,10,2,10).including("a") == Bag(10,10,10,2,'a')
True
>>> Bag().including(34) == Bag(34)
True
"""
fresh = self.theCounter.copy()
fresh[value] += 1
return Bag.new(fresh)
def excluding(self,value):
"""
Remove *all* elements corresponding to the given value from the bag.
:param value: Any value within the bag or not.
:type: any
:return: The bag without any occurrence of 'value'.
:rtype: Bag
Examples:
>>> Bag(10,10,2,10).excluding(10) == Bag(2)
True
>>> Bag(10,10,2,10).excluding("a") == Bag(10,10,10,2)
True
>>> Bag().excluding(34) == Bag()
True
"""
fresh = self.theCounter.copy()
del fresh[value]
return Bag.new(fresh)
def union(self,anyCollection):
"""
Add to the bag all values in the collection given as a parameter.
Examples:
>>> Bag(10,"a",3,3,10,10).union(Bag(10,10,"b")) \
== Bag("b","a",3,3,10,10,10,10,10)
True
>>> Bag(2,4).union([2,4]) == Bag(2,2,4,4)
True
>>> Bag().union([1]) == Bag(1)
True
>>> Bag(3,3) | Set(3,3,3,2) == Bag(3,3,3,2)
True
>>> Bag(2,3,1) | Bag(3,3,2,4) == Bag(3,3,3,2,2,1,4)
True
>>> Bag(2,3,1) | Counter([3,3,2,4]) == Bag(3,3,3,2,2,1,4)
True
"""
assert isCollection(anyCollection), \
'Any collection expected, but found %s' % anyCollection
fresh = self.theCounter.copy()
fresh.update(listAll(anyCollection))
return Bag.new(fresh)
def intersection(self,anyCollection):
"""
Retain only elements that are in common with the given collection.
Examples:
>>> Bag(10,"a",3,3,10,10).intersection(Bag(10,10,"b")) == Bag(10,10)
True
>>> Bag(2,4).intersection(Bag(2,4)) == Bag(2,4)
True
>>> Bag() & [1] == Bag()
True
>>> Bag(3,3) & Set(3,3,3,2) == Bag(3)
True
"""
assert isCollection(anyCollection), \
'Any collection expected, but found %s' % anyCollection
return Bag.new(self.theCounter & Counter(list(anyCollection)))
def __and__(self,anyCollection):
return self.intersection(anyCollection)
def sum(self):
"""
Return the sum of all elements in a bag including duplicates.
:return: the sum of all elements .
:rtype: int
Examples:
>>> Bag().sum()
0
>>> Bag(3,3,2,3).sum()
11
"""
return sum([e * n for (e,n) in self.theCounter.items()])
def flatten(self):
"""
If the bag is a bag of collection then return the bag union of all
its elements.
:return: the sum of all elements .
:rtype: int
Examples:
>>> Bag(Bag(2),Bag(3,3)).flatten() == Bag(2,3,3)
True
>>> Bag(Bag(),Bag(),Bag(3,2),Set(3)).flatten() == Bag(3,2,3)
True
"""
counter = Counter()
for (e,n) in self.theCounter.items():
if isCollection(e):
coll = e.flatten()
else:
coll = [e]
for x in coll:
counter[x] += n
self.theCounter = counter
return self
def select(self,predicate):
"""
Retain in the bag only the elements that satisfy the predicate.
:param predicate: A predicate, that is a function returning a boolean.
:return: The bag with only the selected elements.
:rtype Bag:
Examples:
>>> Bag(2,3,2,3,-1,-2).select(lambda e:e>=0) == Bag(2,2,3,3)
True
>>> Bag().select(lambda e:True) == Bag()
True
"""
fresh = \
Counter(dict([(e,n) for (e,n) in self.theCounter.items()
if evaluatePredicate(e,predicate)]))
return Bag.new(fresh)
def collectNested(self,expression):
"""
Return a bag of values resulting from the evaluation of the given
expression on all elements of the bag.
It is assumed that the expression has no side effect; this
expression is not called for each occurrence but only one for a
given value. This is an optimisation for bags.
:param expression: A function returning any kind of value.
:type expression: X -> Y
:return: The bag of values produced.
:rtype Bag[Y]:
Examples:
>>> Bag(2,2,3,5,-5).collectNested(lambda e:e*e) == Bag(4,4,9,25,25)
True
>>> Bag(2,2).collectNested(lambda e:Bag(e,e)) \
== Bag(Bag(2,2),Bag(2,2))
True
"""
results = [(evaluate(e,expression),n)
for (e,n) in self.theCounter.items()]
fresh = Counter()
for (r,n) in results:
fresh[r] += n
return Bag.new(fresh)
def hasDuplicates(self):
"""
Return True if this bag has at least one element with more than one
occurrence.
This is not an OCL operation. It is provided here just for convenience.
:return: True if there are some duplicates in the bag.
:rtype: bool
Examples:
>>> Bag().hasDuplicates()
False
>>> Bag(2,3).hasDuplicates()
False
>>> Bag(2,2,1,3,3).hasDuplicates()
True
"""
for n in self.theCounter.values():
if n > 1:
return True
return False
def duplicates(self):
"""
>>> Bag().duplicates() == Bag()
True
>>> Bag(2,3).duplicates() == Bag()
True
>>> Bag(2,2,1,3,3).duplicates() == Bag(2,2,3,3)
True
"""
new_counter = \
Counter(dict([(e,n) for (e,n) in self.theCounter.items() if n>=2]))
return Bag.new(new_counter)
def selectWithCount(self, number):
""" Select in the bag only the elements that have exactly the
exact number of element specified.
:param number:
:type number:
:return:
:rtype:
>>> Bag().selectWithCount(2) == Bag()
True
>>> Bag(2,3).selectWithCount(2) == Bag()
True
>>> Bag(2,2,1,3,3).selectWithCount(2) == Bag(2,2,3,3)
True
>>> Bag(2,2,9,3,3).selectWithCount(1) == Bag(9)
True
"""
new_counter = \
Counter(
dict([(e, n) for (e, n) in self.theCounter.items()
if n == number]))
return Bag.new(new_counter)
def sortedBy(self,expression):
r = []
s = sorted(self.theCounter.keys(),key=lambda e:evaluate(e,expression))
for key in s:
r += [key] * self.theCounter[key]
# FIXME: Should be an ordered set
return r
def asSet(self):
return Set.new(self.theCounter.keys())
def asBag(self):
return self
def asSeq(self):
# A list with duplicates is wanted, so use elements().
return Seq.new(list(self.theCounter.elements()))
def __str__(self):
def show_element((value,count)):
return str(value)+('' if count==1 else '*'+str(count))
body = ", ".join(map(
show_element,
self.theCounter.items()))
return 'Bag(%s)' % str(body)
def __repr__(self):
return self.__str__()
def __eq__(self,value):
"""
Return True only if the value is a Bag with the same elements and
number of occurrences.
:param value: Any value.
:type value: any
:return: True if the value is equals to this bag.
:rtype: bool
Examples:
>>> Bag(1,2,2) == Bag(2,2,1)
True
>>> Bag() == Bag()
True
>>> Bag(Bag(2,2)) == Bag(2,2)
False
>>> Bag(Set(2))==Bag(Set(2))
True
"""
if not isinstance(value,Bag):
return False
return self.theCounter == value.theCounter
def __ne__(self,value):
return not self.__eq__(value)
def __hash__(self):
return hash(tuple(self.theCounter.items()))
def __iter__(self):
""" Make Bags iterable for pythonic usage.
:return: the iterator for this Bag
:rtype: iterator
Examples:
>>> list(Bag())
[]
>>> sorted(list(Bag(1,1,"a","b",1)))
[1, 1, 1, 'a', 'b']
"""
return self.theCounter.elements().__iter__()
def __contains__(self,value):
return self.theCounter[value] > 0
#------------------------------------------------------------------------------
# OCL Sequences
#------------------------------------------------------------------------------
[docs]def asSeq(anyCollection):
"""
Convert the given collection to a Seq
:param anyCollection:
:return:
:rtype: Seq
"""
try:
return anyCollection.asSeq()
except AttributeError:
return Seq.new(anyCollection)
class Seq(Collection):
def __init__(self,*args):
"""
Create a Seq from some elements or from one collection.
Examples:
>>> Seq(10,"a",3,10,10) == Seq(10,10,"a",3,10)
False
>>> Seq(2) <> Seq(2,2,2)
True
>>> Seq(3,3,4) == Seq(3,4,3)
False
>>> Seq() == Seq()
True
>>> Seq() == Set()
False
>>> Seq(Seq(1,2)) == Seq(Seq(1),Seq(2))
False
"""
super(Seq,self).__init__()
# no worry with args being a Counter
self.theList = list(args)
@classmethod
def new(cls,anyCollection=()):
newSeq = Seq()
newSeq.theList = listAll(anyCollection)
return newSeq
def emptyCollection(self):
return Seq.new()
def size(self):
return len(self.theList)
def isEmpty(self):
return False if self.theList else True
def count(self,element):
return self.theList.count(element)
def includes(self,element):
return element in self.theList
def including(self,value):
self.theList.append(value)
return self
def excluding(self,value):
"""
Excludes all occurrence of the value from the sequence (if there).
:param value: The element to add to the set.
:type value: any
:return: A set including this element.
:rtype: Set
Examples:
>>> Seq(1,3,"a").excluding("3") == Seq(1,3,"a")
True
>>> Seq(1,3,3,2,3).excluding(3) == Seq(1,2)
True
>>> Seq().excluding(23) == Seq()
True
"""
return Seq.new([e for e in self.theList if e != value])
def select(self,predicate):
return Seq.new([e for e in self.theList
if evaluatePredicate(e,predicate)])
def hasDuplicates(self):
"""
Indicates if there duplicated elements in the sequence.
This method is an extension to OCL. I
:return: True
:rtype: bool
"""
return Bag.new(self).hasDuplicates()
def duplicates(self):
return Bag.new(self).duplicates()
def selectWithCount(self,number):
"""
Select only the elements that have exactly the number of occurences
specified and return these elements in the original order.
:param number:
:type number:
:return:
:rtype:
>>> Seq(1,2,2,1,1,5,3,2,5).selectWithCount(1) == Seq(3)
True
>>> Seq(1,2,2,1,1,5,3,2,5).selectWithCount(3) == Seq(1,2,2,1,1,2)
True
>>> Seq(1,2,2,1,1,5,3,2,5).selectWithCount(2) == Seq(5,5)
True
>>> Seq().selectWithCount(3) == Seq()
True
>>> Seq(2).selectWithCount(0) == Seq()
True
"""
keep = Bag.new(self).selectWithCount(number)
return Seq.new([e for e in self.theList if e in keep ])
def flatten(self):
r = []
for e in self.theList:
if isCollection(e):
flat_list = listAll(flatten(e))
else:
flat_list = [e]
r = r + flat_list
self.theList = r
return self
def collectNested(self,expression):
return Seq.new(map((lambda e:evaluate(e,expression)),self.theList))
def sortedBy(self,expression):
return \
Seq.new(sorted(self.theList,key=(lambda e:evaluate(e,expression))))
def union(self,anyCollection):
assert isCollection(anyCollection), \
'Any collection expected, but found %s' % anyCollection
return Seq.new(self.theList + listAll(anyCollection))
def __add__(self,anyCollection):
return self.union(anyCollection)
def append(self,value):
fresh = list(self.theList)
fresh.append(value)
return Seq.new(fresh)
def prepend(self,value):
fresh = list(self.theList)
fresh.insert(0,value)
return Seq.new(fresh)
def subSequence(self,lower,upper):
try:
return Seq.new(self.theList[lower - 1:upper])
except:
msg = ".subSequence(%s,%s) failed: No such element."
raise Invalid(msg % (lower,upper))
def at(self,index):
"""
Return the nth element of the sequence starting from 1.
Note: In OCL the 1st element is at the index 1 while in python this
is at 0. Both the OCL 'at' and python [] operators can be used,
but remember the different way to index elements.
Examples:
>>> Seq(1,2,3,4).at(1)
1
>>> Seq(1,2,3,4)[0]
1
:param index: The index of the element to return, starting at:
* 1 for the OCL 'at' operator.
* 0 for the [] python operator.
:type: int
:return: The element at that position.
:rtype: any
"""
try:
return self.theList[index - 1]
except:
raise Invalid(".at(%s) failed: No such element." % index)
def __getitem__(self,item):
return self.theList[item]
def asSet(self):
return Set.new(self.theList)
def asBag(self):
return Bag.new(self.theList)
def asSeq(self):
return self
def first(self):
try:
return self.theList[0]
except:
raise Invalid(".first() failed: No such element.")
def last(self):
try:
return self.theList[-1]
except:
raise Invalid(".last() failed: No such element.")
def __str__(self):
body = ", ".join(map(str,self.theList))
return 'Seq(%s)' % body
def __repr__(self):
return self.__str__()
def __eq__(self,value):
if not isinstance(value,Seq):
return False
return self.theList == value.theList
def __hash__(self):
return hash(tuple(self.theList))
def __contains__(self,item):
return item in self.theList
def __iter__(self):
return self.theList.__iter__()
#==============================================================================
# Conversions
#==============================================================================
import collections
class ConversionRule(object):
def __init__(self,language,sourceType,collectionType):
self.language = language
self.sourceType = sourceType
self.collectionType = collectionType
def accept(self,value):
return isinstance(value,self.sourceType)
def asCollection(self,value):
return self.collectionType.new(value)
def emptyCollection(self):
return self.collectionType.new()
from collections import OrderedDict
class Converter(object):
def __init__(self):
self.rules = OrderedDict()
self.language_collections = OrderedDict()
self.all_collections = []
def registerConversionRules(self,language,conversionList):
for (source,target) in conversionList:
rule = ConversionRule(language,source,target)
self.rules[source] = rule
if language not in self.language_collections:
self.language_collections[language] = []
self.language_collections[language].append(source)
self.all_collections.append(source)
def _registerActualTypeRule(self,source,rule):
self.registerConversionRules(
self,rule.language,[(source,rule.collectionType)])
return self.rules[source]
def isCollection(self,value,language=None):
if isinstance(value,basestring):
return False
if language == None:
collections = self.all_collections
else:
collections = self.language_collections[language]
return isinstance(value,tuple(collections))
def findRule(self,value):
"""
Return the type of the OCL collection corresponding to the given type.
Raise an exception if no conversion is possible for the given type
:param aType: The type to be converted.
:type aType: type
:return: A collection type.
:rtype: type < Collection
:raise: ValueError if there is no correspondance possible.
"""
valueType = type(value)
# check if we have some chance and the type is already registered
if valueType in self.rules:
return self.rules[valueType]
else:
# no chance. We have to check if this is a subtype.
for rule in self.rules.values():
if rule.accept(value):
return self._registerActualTypeRule(self,valueType,rule)
msg = "getConversionRule(): Can't convert a value of type %s"
raise ValueError(msg % valueType)
def asCollection(self,value):
try:
return value.asCollection()
except AttributeError:
return self.findRule(value).asCollection(value)
def emptyCollection(self,value):
try:
return value.emptyCollection()
except NameError:
return self.findRule(value).emptyCollection(value)
def listAll(self,value):
"""
Return all the elements of the collection as a list.
This takes into account the Counter specificity: instead of using
list and the standard enumeration on this collection this function
use the "elements()" method. Otherwise occurrences are eliminated.
"""
if isinstance(value,collections.Counter):
return list(value.elements())
else:
return list(value)
CONVERTER = Converter()
pythonConversionRules = [ # Order is very important
(set,Set),
(frozenset,Set),
(collections.Counter,Bag),
(list,Seq),
(tuple,Seq),
(collections.deque,Seq),
(collections.Iterable,Seq),
(collections.Iterable,Seq),
]
CONVERTER.registerConversionRules('python',pythonConversionRules)
oclConversionRules = [
(Set,Set),
(Bag,Bag),
(Seq,Seq),
]
CONVERTER.registerConversionRules('ocl',oclConversionRules)
[docs]def asCollection(anyCollection):
"""
Convert any collection into the proper (OCL) collection.
:param anyCollection: A python, java or ocl collection.
:return: The OCL collection
:rtype: Collection
Examples:
>>> asCollection({2,3}) == Set(3,2)
True
>>> asCollection(frozenset({1,5,1})) == Set(1,5)
True
>>> asCollection(Counter([1,1,3,1])) == Bag(1,1,1,3)
True
>>> asCollection(Counter({'hello':2,-1:0})) == Bag('hello','hello')
True
>>> asCollection([1,2,3,4]) == Seq(1,2,3,4)
True
>>> asCollection((1,2,3,4)) == Seq(1,2,3,4)
True
>>> asCollection(deque([1,2,3,4])) == Seq(1,2,3,4)
True
"""
return CONVERTER.asCollection(anyCollection)
def emptyCollection(anyCollection):
return CONVERTER.emptyCollection(anyCollection)
def listAll(collection):
return CONVERTER.listAll(collection)
[docs]def isCollection(value,language=None):
"""
:param value:
:param language:
:return:
>>> isCollection((2,3))
True
>>> isCollection([])
True
>>> isCollection(12)
False
>>> isCollection(Counter())
True
>>> isCollection("text")
False
"""
return CONVERTER.isCollection(value,language=language)