Package: collateral
CollateralThis tool package provides a simple way to manipulate several objects with similar behaviors in parallel.>>>importcollateralasll>>>help(ll)#doctest: +SKIPMotivationOften, in software development, we define objects that should behave the same way as known objects.
Typically, a class implementingcollections.abc.MutableMappingis expected to behave similarly todict.
When developing such objects, we might want to quickly check this behavior similarity (or dissimilarity)
in an interactive way, without having to write down many automatic tests,
or, in contrast, to write down tests that compares behaviors.
The collateral package has been designed for this purpose.Just give to the constructor a sequence of objects (typically 2), calledcollaterals,
and then manipulate them in parallel through the resultingCollateral object, as if there was only one object.>>>#What is the difference between lists and tuples?>>>C=ll.Collateral([3,4,3],(3,4,3))>>>C.count(3)Collateral(2,2)>>>C[0]Collateral(3,3)>>>C[-1]Collateral(3,3)>>>C[1:2]Collateral([4],(4,))>>>C.index(4)Collateral(1,1)>>>iter(C)Collateral(<list_iteratorobjectat0x...>,<tuple_iteratorobjectat0x...>)How it worksIntuitively, the methods and attributes of a Collateral object
are the methods and attributes of its collaterals,
which are applied pointwise on each of them,
in order to form a new Collateral object that gathers the results.
Hence, getting an attribute namedattr(or calling a method namedmeth) is the same asgettingattrfrom (or callingmethon) each of the collaterals;gathering the results in a newCollateralobject.Methods/attributes with such behavior are saidpointwise.>>>C.count(3)#both have a `count` method that do the sameCollateral(2,2)>>>C[2]#both support indexingCollateral(3,3)>>>D=ll.Collateral([3,4,4],[4,3,4])>>>D.__add__([2,3])#in D, both collaterals are list whence may be added to listCollateral([3,4,4,2,3],[4,3,4,2,3])>>>D+[2,3]#equivalent to, but more pleasant than, the previous exampleCollateral([3,4,4,2,3],[4,3,4,2,3])There are a few special cases:Procedures;Attributes/methods with protected names;Transversal attributes/methods.Pointwise methods and attributesA method/attribute exists in a Collateral object
if it is a method/attribute of all of its collaterals.
An attribute name which correspond to callable values in each of the collaterals
results in a method within the Collateral object gathering the attributes of the collaterals.
In contrast, if for some of the collaterals the attribute is not callable,
then its Collateral counterpart will be a property returning the Collateral object
that gathers the corresponding attributes from each of the collaterals
(some of them might be callable).Furthermore, when called, pointwise methods handle their Collateral parameters (keyworded or not) in a special way.
Indeed, when some parameter is itself a Collateral object,
its collaterals specify pointwise the parameter to pass to the inner call of the method on collaterals of our main Collateral object.>>>F=ll.Collateral(dict(shape="circle",color="red"),dict(SHAPE="square",color="blue"))>>>K=ll.Collateral("color","background")>>>F.get(K,"white")==ll.Collateral("red","white")#returns TrueTrue>>>K=ll.Collateral("shape","SHAPE")>>>F[K]==ll.Collateral("circle","square")#returns TrueTrueProceduresAprocedureis a function which returns nothing (e.g.,list.append).
Unfortunately, this is undistinguishable from functions returningNonein Python.
When a method name correspond to a procedure method in each of the collaterals of some Collateral object,
its pointwise counterpart in that Collateral object will always return a Collateral object
which gathers as manyNones as there are collaterals.
Such Collateral object are not really interesting and will most often pollute the output of an interactive interpreter.
For this reason, the package aims to make each pointwise counterpart of procedures a procedure.
This is handled dynamically, by replacing outputs that Collateral consisting ofNonevalues only by, simply,None.
Yet, asNoneis also a possible return value (e.g., indict.get),
it is possible to indicate that some special method names are not procedure,
or that some special function are not procedures (see Setting up section below).>>>D.append(None)#will return nothing (i.e., `None`), because append is a procedure>>>D.pop()#will return a Collateral object gathering two `None` (that have just been added) because `pop` is known as a non-procedure name (by default)Collateral(None,None)Protected namesSome pointwise attributes cannot be defined with their original name,
either because the name is already used by some attribute of the Collateral base class
(e.g.,__class__,__slots__,__getattr___),
or because the corresponding method is a special method whose return type is forced
(e.g.,__hash__and__int__should return anint,__bool__should return abool,__repr__should return astr).
When defining a pointwise method for a so-named method,
a prefix (default is_collateral_, see Setting up section below) is prepended to the name
(as many times (usually once) as needed in order to obtain a non-protected name).>>>E=ll.Collateral((3,4,4),(4,3,4))>>>E._collateral___hash__()#returns the Collateral object gathering the hashes of the collaterals of ECollateral(-2504614661605197030,3996966733163272653)>>>E._collateral___repr__()#returns the Collateral object gathering the repr of the collaterals of ECollateral('(3, 4, 4)','(4, 3, 4)')>>>E.__int__#is not definedTraceback(mostrecentcalllast):....collateral.exception.CollateralAttributeError:("'tuple' object has no attribute '__int__'",Collateral(AttributeError("'tuple' object has no attribute '__int__'"),AttributeError("'tuple' object has no attribute '__int__'")))>>>E.__class__#is the Collateral class of E, not the Collateral object gathering the classes of the collaterals of E<class'collateral.collateral.Collateral'>Transversal attributesAtransversalattribute/method is a Collateral attribute/method which is not pointwise.
Such attributes and methods are defined in Collateral objects in order to ease their use.
For instance, there will always be an__hash__function,
which returns a hash (int) of the Collateral object based on the hashes of its collaterals
(and not a new Collateral object gathering these hashes of the collaterals)
or raised aTypeErrorif some of the collaterals is not hashable.
Other methods such as__repr__,_repr_pretty_,__eq__, and__dir__are also defined.>>>isinstance(hash(E),int)True>>>repr(D)'Collateral([3, 4, 4], [4, 3, 4])'Most importantly, Collateral objects all have a'collaterals'class attribute
which is the tuple of its collaterals.
(By the way, Collateral objects are instances of their own dynamically factored singleton type, so yes,'collaterals'is aclassattribute.)>>>C.collaterals#returns the tuple of collaterals of C([3,4,3],(3,4,3))>>>C.collaterals[0]#returns the first collateral of C[3,4,3]>>>len(C.collaterals)#returns 2, namely the number of collaterals of C2The attribute type is nottuple, but a subclass of it,
which provides a few additional methods, for manipulating the collaterals tuple.
Among these methods,
some are aggregating functions (e.g.,min,max,reduce,all_equal,all,any),
while some are aimed to produce a Collateral object from the given collaterals
(e.g.,map,filter,enumerate,call,join,add,drop).>>>C.collaterals.map(list).collaterals.map(len).collaterals.min()3>>>C.collaterals.add([])Collateral([3,4,3],(3,4,3),[])>>>C.collaterals.add([]).collaterals.filter()Collateral([3,4,3],(3,4,3))>>>C.collaterals.filter(lambdae:isinstance(e,tuple))Collateral((3,4,3))(Actually,map,enumerate, andcallare a kind of pointwise methods.)More pointwise functionsThe package provides thefunctionsmodules
in which many pointwise functions are defined.
Most of them are pointwise counterparts of builtin functions
(e.g.,len,int,hash,enumerate),
with same name.
Others are Collateral specific pointwise functions,
likeapply(which call a function on each collaterals),and_,or_, andnot_.>>>C=ll.Collateral(1,2,0,3,None)>>>ll.functions.apply(print,C,pre_args="prefix:",post_args=":suffix",sep='\t')#print each collaterals with prefix 'prefix:\t' and suffix '\t:suffix' #doctest: +NORMALIZE_WHITESPACEprefix:1:suffixprefix:2:suffixprefix:0:suffixprefix:3:suffixprefix:None:suffixCollateral(None,None,None,None,None)>>>ll.functions.apply(bool,C)#returns Collateral(True, True, False, True, False)Collateral(True,True,False,True,False)>>>ll.functions.or_(C,True)#returns Collateral(1, 2, True, 3, True)Collateral(1,2,True,3,True)>>>ll.functions.and_(C,True)#returns Collateral(True, True, 0, True, False)Collateral(True,True,0,True,None)>>>ll.functions.not_(C)#returns Collateral(False, False, True, False, True)Collateral(False,False,True,False,True)Examples:>>>importcollections>>>classMyDict(collections.abc.Mapping):...def__init__(self,source_dict=(),/):...self._dict=dict(source_dict)...def__getitem__(self,k):...returnself._dict[k]...def__iter__(self):...returniter(self._dict)...def__len__(self):...returnlen(self._dict)...def__repr__(self):...returnf"MyDict({self._dict!r})">>>d={3:True,"foo":{2:None},True:"foo"}>>>md=MyDict(d)>>>C=ll.Collateral(d,md)>>>CCollateral({3:True,'foo':{2:None},True:'foo'},MyDict({3:True,'foo':{2:None},True:'foo'}))>>>C.keys()#returns ll.Collateral(d.keys(), md.keys())Collateral(dict_keys([3,'foo',True]),KeysView(MyDict({3:True,'foo':{2:None},True:'foo'})))>>>C.values()#returns ll.Collateral(d.values(), md.values())Collateral(dict_values([True,{2:None},'foo']),ValuesView(MyDict({3:True,'foo':{2:None},True:'foo'})))>>>C[3]#returns ll.Collateral(d[3], md[3])Collateral(True,True)>>>C[True]#returns ll.Collateral(d[True], md[True])Collateral('foo','foo')>>>C.__init__()#call d.__init__({}) (no effect) and md.__init__({}) (clear) and returns None>>>C#see the divergence of __init__Collateral({3:True,'foo':{2:None},True:'foo'},MyDict({}))>>>C.get(3,False)#3 is still a key of d but not of md (because of the divergence of __init__)Collateral(True,False)>>>C["bar"]=0#setitem does not exist for mdTraceback(mostrecentcalllast):...TypeError:'Collateral'objectdoesnotsupportitemassignment>>>ll.keep_errors#function decorator that replaces raising by returning<functionkeep_errorsat0x...>>>>C.collaterals.map(ll.keep_errors(lambdax:x.__setitem__("bar",0)))Collateral(None,AttributeError("'MyDict' object has no attribute '__setitem__'"))>>>CCollateral({3:True,'foo':{2:None},True:'foo','bar':0},MyDict({}))>>>hash(C)#raise an exception since neither dict nor MyDict objects are hashableTraceback(mostrecentcalllast):...TypeError:unhashabletype:'dict'>>>hC=ll.keep_errors(hash)(C)#returns a Collateral gathering the exception (or the result) raised when calling hash on each of the collaterals>>>hCTypeError("unhashable type: 'dict'")>>>C.collaterals.map(hash,keep_errors=True)Collateral(TypeError("unhashable type: 'dict'"),TypeError("unhashable type: 'MyDict'"))