PyScore

001: # -*- python -*-
002: ###### base.py ###### python package PyScore.relational module base ######
003: 
004: # PyScore
005: # a race scoring programme
006: # written by Matt Draisey
007: # 2004 April 6
008: 
009: reloadables=[]
010: 
011: ###### base.py ###### python package PyScore.relational module base ######
012: 
013: import heapq
014: 
015: ######## ######## simple value based hashing container stuff ######## ########
016: 
017: class Entry(object):
018:     """Nominal mixin for a single entry, equality being object identity."""
019: 
020: # Objects which implement the ValueEntry interface will be sortable and
021: # usable as dictionary keys based on defining either an entry_tuple()
022: # or a canonical_tuple() which should be derived from its value.
023: #
024: # These interfaces also provide the default string representation of the entry.
025: #
026: # Defined entry_tuple()s may maintain their upper/lower case and will have
027: # case-insensative canonical_tuple()s generated automatically.
028: 
029: def canonical(x):
030:     try:
031:         return x.lower()
032:     except AttributeError:
033:         return x
034: 
035: def canonical(x):
036:     if hasattr(x,"lower"):
037:         return x.lower()
038:     else:
039:         return x
040: 
041: class ValueEntry(object):
042:     """A mixin for a single data base entry to implement value hashing."""
043: 
044:     # requires subclasses to implement entry_tuple() method
045: 
046:     def canonical_tuple(self):
047:         return tuple([canonical(x) for x in self.entry_tuple()])
048: 
049:     def __str__(self):
050:         return ":".join([str(x) for x in self.entry_tuple()])
051: 
052:     def __repr__(self):
053:         return repr(self.entry_tuple())
054: 
055:     def __cmp__(self,other):
056:         try:
057:             return cmp(self.canonical_tuple(),other.canonical_tuple())
058:         except AttributeError:
059:             return -cmp(other,self.canonical_tuple())
060: 
061:     def __hash__(self):
062:         return reduce(lambda x,y: x^hash(y),self.canonical_tuple(),0)
063: 
064: ######## ######## faster comparisons in batch operations ######## ########
065: 
066: def canonical_expansion(x):
067:     try:
068:         t=x.canonical_tuple()
069:     except AttributeError:
070:         return x
071:     else:
072:         return tuple([canonical_expansion(e) for e in t])
073: 
074: def canonical_expansion(x):
075:     if isinstance(x,tuple):
076:         return tuple([canonical_expansion(t) for t in x])
077:     else:
078:         try:
079:             t=x.canonical_tuple()
080:         except AttributeError:
081:             return x
082:         else:
083:             return canonical_expansion(t)
084: 
085: class ValueListable(object):
086:     """A mixin for a list of ValueEntries."""
087: 
088:     def sort(container):
089:         temp=[(canonical_expansion(x),x) for x in container]
090:         temp.sort()
091:         container[:]=[x for (t,x) in temp]
092: 
093: ######## ######## ######## simple enumerations ######## ######## ########
094: 
095: class Enum(object):
096:     """A master list for an enumeration."""
097: 
098:     def __init__(self,*sequence,**options):
099:         self.sequence=sequence
100:         self.casesensitive=options.pop("casesensitive",False)
101:         self.refilter=options.pop("refilter",None)
102:         self.strict=options.pop("strict",False)
103:         assert not options
104:         self.dictlowered=self.dictenum={}
105: 
106:     def create_dict(self):
107:         for (i,e) in enumerate(self.sequence):
108:             self.dictenum[e]=i
109:         if not self.casesensitive:
110:             for (i,e) in enumerate(self.sequence):
111:                 self.dictenum[e.lower()]=i
112: 
113: class Enumeration(tuple):
114:     """The base class for an enumeration."""
115: 
116:     """Each instance is a single data base entry in an enumerated list."""
117: 
118:     # ( enum, 0, i ) such that enum.sequence[i] == s
119:     # ( enum, 1, s ) where s not in enum.sequence
120: 
121:     # inherits lexicographic sorting of the base tuple type and
122:     # so the sort order supplied by the master list in the enum field
123:     
124:     # we include enum in the tuple so there is a sane ordering between
125:     # different enumerated types --- slows down by about 10%
126: 
127: 
128:     def __new__(Self,s,strict=False):
129:         assert isinstance(Self.enum,Enum)
130:         i=None
131:         if isinstance(s,str) and Self.enum.refilter:
132:             for (i,f) in Self.enum.refilter:
133:                 if f.match(s):
134:                     break
135:             else:
136:                 i=None
137:         elif isinstance(s,str) and Self.enum.dictlowered:
138:             l=s.lower()
139:             if l in Self.enum.dictlowered:
140:                 i=Self.enum.dictlowered[l]
141:         elif Self.enum.dictenum:
142:             if s in Self.enum.dictenum:
143:                 i=Self.enum.dictenum[s]
144:         else:
145:             try:
146:                 i=Self.enum.sequence.index(s)
147:             except ValueError:
148:                 pass
149:         if i<>None:
150:             try:
151:                 Self.enumcache
152:             except AttributeError:
153:                 Self.enumcache=[None]*len(Self.enum.sequence)
154:             if not Self.enumcache[i]:
155:                 Self.enumcache[i]=tuple.__new__(Self,(Self.enum,0,i))
156:             return Self.enumcache[i]
157:         else:
158:             if strict or Self.enum.strict:
159:                 raise ValueError
160:             return tuple.__new__(Self,(Self.enum,1,s))
161: 
162:     def __str__(self):
163:         """For display."""
164:         if self[1]:
165:             return str(self[2])
166:         else:
167:             return str(self[0].sequence[self[2]])
168: 
169:     def __repr__(self):
170:         """For later eval."""
171:         if self[1]:
172:             return repr(self[2])
173:         else:
174:             return repr(self[0].sequence[self[2]])
175: 
176:     def __getattr__(self,attr):
177:         """Just passing through."""
178:         if self[1]:
179:             return getattr(self[2],attr)
180:         else:
181:             return getattr(self[0].sequence[self[2]],attr)
182: 
183:     def canonical_tuple(self):
184:         return self[1:]
185: 
186: ######## ######## simple pseudorelational data base algebra ######## ########
187: 
188: ##### join to a primary key which is really an encapsulating object #####
189: 
190: class JointAfterEntry(tuple):
191:     def __getattr__(self,attr): return getattr(self[0],attr)
192:     def __setattr__(self,attr,val): setattr(self[0],attr,val)
193: 
194:     # note that __getattr__ is not called for attributes which already
195:     # exist for tuples --- in particular for comparison operators ---
196: 
197: class JointBeforeEntry(tuple):
198:     def __getattr__(self,attr): return getattr(self[-1],attr)
199:     def __setattr__(self,attr,val): setattr(self[-1],attr,val)
200: 
201: ## inline filters ##
202: 
203: def stable_filter(data):
204:     # stabilize current order of elements
205:     for entry in enumerate(data):
206:         yield JointBeforeEntry(entry)
207: 
208: def semistable_filter(data):
209:     # stabilize current order for elements that compare equally
210:     for (i,d) in enumerate(data):
211:         yield JointAfterEntry((d,i))
212: 
213: def process_column(c):
214:     """
215:     Columns for `joining` to primary keys.
216:     As 3-tuple:
217:         [0] name of attribute,
218:         [1] function of key and attribute to join,
219:         [2] function of key only if attribute doesn't exist.
220:     Functions filter out entry by raising AttributeError.
221:     Sane defaults as shorter tuple or string.
222:     """
223:     def alt(attr): raise AttributeError
224:     def filt(entry,attr): return attr
225:     if isinstance(c,str):
226:         name=c
227:     else:
228:         assert isinstance(c,tuple) and 1<=len(c)<=3
229:         name=c[0]
230:         assert isinstance(name,str)
231:         if len(c)>1:
232:             if c[1] is None:
233:                 pass
234:             elif callable(c[1]):
235:                 filt=c[1]
236:             else:
237:                 t=c[1]
238:                 def filt(entry,attr):
239:                     if attr==t: return attr
240:                     else: raise AttributeError
241:         if len(c)>2:
242:             if c[2] is None:
243:                 pass
244:             elif callable(c[2]):
245:                 alt=c[2]
246:             else:
247:                 t=c[2]
248:                 def alt(entry): return t
249:     return (name,filt,alt)
250:         
251: def adjoin_after_filter(data,columns):
252:     """Yield joint entries with key first."""
253:     assert isinstance(columns,list)
254:     columns=[process_column(c) for c in columns]
255:     for entry in data:
256:         jointentry=[entry]
257:         for c in columns:
258:             try:
259:                 try:
260:                     attr=getattr(entry,c[0])
261:                 except AttributeError:
262:                     jointentry.append(c[2](entry))
263:                 else:
264:                     jointentry.append(c[1](entry,attr))
265:             except AttributeError:
266:                 break
267:         else:
268:             yield JointAfterEntry(jointentry)
269: 
270: def adjoin_before_filter(data,columns):
271:     """Yield joint entries with key last."""
272:     assert isinstance(columns,list)
273:     columns=[process_column(c) for c in columns]
274:     for entry in data:
275:         jointentry=[]
276:         for c in columns:
277:             try:
278:                 try:
279:                     attr=getattr(entry,c[0])
280:                 except AttributeError:
281:                     jointentry.append(c[2](entry))
282:                 else:
283:                     jointentry.append(c[1](entry,attr))
284:             except AttributeError:
285:                 break
286:         else:
287:             jointentry.append(entry)
288:             yield JointBeforeEntry(jointentry)
289: adjoin_filter=adjoin_before_filter
290: 
291: def obverse_filter(data,columns):
292:     """Yield entries that would have joined."""
293:     assert isinstance(columns,list)
294:     columns=[process_column(c) for c in columns]
295:     for entry in data:
296:         for c in columns:
297:             try:
298:                 try:
299:                     attr=getattr(entry,c[0])
300:                 except AttributeError:
301:                     c[2](entry)
302:                 else:
303:                     c[1](entry,attr)
304:             except AttributeError:
305:                 break
306:         else:
307:             yield entry
308: 
309: def converse_filter(data,columns):
310:     """Yield entries that, in at least one column, fail to join."""
311:     assert isinstance(columns,list)
312:     columns=[process_column(c) for c in columns]
313:     for entry in data:
314:         for c in columns:
315:             try:
316:                 try:
317:                     attr=getattr(entry,c[0])
318:                 except AttributeError:
319:                     c[2](entry)
320:                 else:
321:                     c[1](entry,attr)
322:             except AttributeError:
323:                 break
324:         else:
325:             continue
326:         yield entry
327: 
328: ## static lists are accumulated from inline filters ##
329: 
330: def stable_list(data):
331:     return JointList([je for je in stable_filter(data)])
332: 
333: def semistable_list(data):
334:     return JointList([je for je in semistable_filter(data)])
335: 
336: def adjoin_after_list(data,columns):
337:     return JointList([je for je in adjoin_after_filter(data,columns)])
338: 
339: def adjoin_before_list(data,columns):
340:     return JointList([je for je in adjoin_before_filter(data,columns)])
341: 
342: adjoin_list=adjoin_before_list
343: 
344: def obverse_list(data,columns):
345:     return JointList([je for je in obverse_filter(data,columns)])
346: 
347: def converse_list(data,columns):
348:     return JointList([je for je in converse_filter(data,columns)])
349: 
350: ## iterable heaps are accumulated from inline filters and then heapified ##
351: 
352: def adjoin_after_heap(data,columns):
353:     joint=[
354:         (canonical_expansion(x),x,i)
355:         for (i,x) in enumerate(adjoin_before_filter(data,columns))
356:     ]
357:     heapq.heapify(joint)
358:     while joint:
359:         je=heapq.heappop(joint)[1]
360:         yield je[-1:]+je[:-1]
361: 
362: def adjoin_before_heap(data,columns):
363:     joint=[
364:         (canonical_expansion(x),x,i)
365:         for (i,x) in enumerate(adjoin_before_filter(data,columns))
366:     ]
367:     heapq.heapify(joint)
368:     while joint:
369:         yield heapq.heappop(joint)[1]
370: 
371: adjoin_heap=adjoin_before_heap
372: 
373: def obverse_heap(data,columns):
374:     joint=[
375:         (canonical_expansion(x),x,i)
376:         for (i,x) in enumerate(adjoin_before_filter(data,columns))
377:     ]
378:     heapq.heapify(joint)
379:     while joint:
380:         je=heapq.heappop(joint)[1]
381:         yield je[-1]
382: 
383: def converse_heap(data,columns):
384:     joint=[
385:         (canonical_expansion(x),x,i)
386:         for (i,x) in enumerate(converse_filter(data,columns))
387:     ]
388:     heapq.heapify(joint)
389:     while joint:
390:         yield heapq.heappop(joint)[1]
391: 
392: ## sorted lists are accumulated from inline filters and sorted in one go ##
393: 
394: def unstable_sort(data):
395:     joint=[(canonical_expansion(je),je) for je in data]
396:     joint.sort()
397:     return JointList([je for (ce,je) in joint])
398: 
399: def semistable_sort(data):
400:     joint=[
401:         (canonical_expansion(je),je)
402:         for je in semistable_filter(data)
403:     ]
404:     joint.sort()
405:     return JointList([je for (ce,je) in joint])
406: 
407: def adjoin_after_sort(data,columns):
408:     joint=[
409:         (canonical_expansion(je),je)
410:         for je in adjoin_before_filter(data,columns)
411:     ]
412:     joint.sort()
413:     return JointList([je[-1:]+je[:-1] for (ce,je) in joint])
414: 
415: def adjoin_before_sort(data,columns):
416:     joint=[
417:         (canonical_expansion(je),je)
418:         for je in adjoin_before_filter(data,columns)
419:     ]
420:     joint.sort()
421:     return JointList([je for (ce,je) in joint])
422: 
423: adjoin_sort=adjoin_before_sort
424: 
425: def obverse_sort(data,columns):
426:     joint=[
427:         (canonical_expansion(je),je)
428:         for je in adjoin_before_filter(data,columns)
429:     ]
430:     joint.sort()
431:     return JointList([je[-1] for (ce,je) in joint])
432: 
433: def converse_sort(data,columns):
434:     joint=[
435:         (canonical_expansion(je),je)
436:         for je in converse_filter(data,columns)
437:     ]
438:     joint.sort()
439:     return JointList([je for (ce,je) in joint])
440: 
441: ## hierarchies are factored using an inline filter from an ordered source ##
442: 
443: def adjoin_before_hierarchy(data,columns):
444:     joint=[
445:         (canonical_expansion(je),je)
446:         for je in adjoin_before_filter(data,columns)
447:     ]
448:     joint.sort()
449:     return hierarchy_filter(columns,[je for (ce,je) in joint])
450: 
451: #def adjoin_before_hierarchy(data,columns):
452: #       return hierarchy_filter(columns,adjoin_before_heap(data,columns))
453: 
454: adjoin_hierarchy=adjoin_before_hierarchy
455: 
456: ## dictionaries are accumulated from factored hierarchies ##
457: 
458: def adjoin_before_dictionary(data,columns):
459:     def lexicographer(c,hieriter):
460:         if c>0:
461:             return dict([
462:                 (x,lexicographer(c-1,subhierarchy))
463:                 for (x,subhierarchy) in hieriter
464:             ])
465:         else:
466:             return [e for e in hieriter]
467:     return lexicographer(len(columns),adjoin_before_hierarchy(data,columns))
468: 
469: adjoin_dictionary=adjoin_before_dictionary
470: 
471: ## polyhierarchies use several inline filters factored in step ##
472: 
473: def adjoin_before_polyhierarchy(*columns_and_data):
474:     (data,columns)=(columns_and_data[:-1],columns_and_data[-1])
475:     polyjoint=[]
476:     for d in data:
477:         joint=[
478:             (canonical_expansion(je),je)
479:             for je in adjoin_before_filter(d,columns)
480:         ]
481:         joint.sort()
482:         polyjoint+=[[je for (ce,je) in joint]]
483:     return polyhierarchy_filter(columns,*polyjoint)
484: 
485: #def adjoin_before_polyhierarchy(*columns_and_data):
486: #    (data,columns)=(columns_and_data[:-1],columns_and_data[-1])
487: #    return polyhierarchy_filter(
488: #        columns,*tuple([adjoin_before_heap(d,columns) for d in data])
489: #    )
490: 
491: adjoin_polyhierarchy=adjoin_before_polyhierarchy
492: 
493: ## polydictionaries likewise ##
494: 
495: def adjoin_before_polydictionary(*columns_and_data):
496:     (data,columns)=(columns_and_data[:-1],columns_and_data[-1])
497:     def lexicographer(c,hieriter):
498:         if c>0:
499:             return dict([
500:                 (x,lexicographer(c-1,subhierarchy))
501:                 for (x,subhierarchy) in hieriter
502:             ])
503:         else:
504:             return [
505:                 [e for e in componentiter] for componentiter in hieriter
506:             ]
507:     return lexicographer(
508:         len(columns),adjoin_before_polyhierarchy(*columns_and_data)
509:     )
510: 
511: adjoin_polydictionary=adjoin_before_polydictionary
512: 
513: ##### factor out a relation (a table) into a hierarchy #####
514: 
515: def hierarchy_filter(columns,jointiter):
516:     """
517:     Yields a pair (x, subhierarchy iterator) for each x in the first column.
518:     The Subhierarchy iterator yields likewise on remaining columns.
519:     For the final column simply yields its elements.
520:     This exhausts all the data.
521:     """
522: 
523:     jointiter=iter(jointiter)
524:     class Cursor(object): pass
525:     j=Cursor()
526:     try: j.n=jointiter.next()
527:     except StopIteration: pass
528:     def jnext():
529:         t=j.n
530:         try: j.n=jointiter.next()
531:         except StopIteration: del j.n
532:         return t
533:     def recursive_iterator(constraint):
534:         try:
535:             c=len(constraint)
536:             if c<len(columns):
537:                 while j.n[:c]==constraint:
538:                     car=j.n[c]
539:                     newconstraint=constraint+(car,)
540:                     yield (car,recursive_iterator(newconstraint))
541:                     # flush entries
542:                     while j.n[:c+1]==newconstraint:
543:                         jnext()
544:             else:
545:                 while j.n[:c]==constraint:
546:                     yield jnext()[c]
547:         except AttributeError: pass
548:     return recursive_iterator(())
549: 
550: ##### relational ideas for a decidedly non-relational result #####
551: 
552: def polyhierarchy_filter(columns,*jointiters):
553:     """
554:     Like the previous hierarchy iterator, but also performs a real
555:     join on the columns of several iters, and then returns the final
556:     and potentially incommensurable key entries in an ordered list
557:     of iterators.
558:     """
559: 
560:     nc=len(columns)
561:     niters=len(jointiters)
562:     assert niters>0
563:     jointiters=[iter(i) for i in jointiters]
564:     class Cursor(object): pass
565:     j=Cursor()
566:     j.n={}
567:     for (i,iteri) in enumerate(jointiters):
568:         try: j.n[i]=iteri.next()
569:         except StopIteration: pass
570:     def jnext(i):
571:         t=j.n[i]
572:         try: j.n[i]=jointiters[i].next()
573:         except StopIteration: del j.n[i]
574:         return t
575:     def jmin():
576:         return min(j.n.values())
577:     def recursive_iterator(constraint):
578:         c=len(constraint)
579:         if c<len(columns):
580:             def recursive_polyiterator():
581:                 try:
582:                     n=jmin()
583:                     while n[:c]==constraint:
584:                         car=n[c]
585:                         newconstraint=constraint+(car,)
586:                         yield (car,recursive_iterator(newconstraint))
587:                         # flush entries
588:                         for i in j.n.keys():
589:                             try:
590:                                 while j.n[i][:c+1]==newconstraint:
591:                                     jnext(i)
592:                             except KeyError: pass
593:                         n=jmin()
594:                 except ValueError: pass
595:             return recursive_polyiterator()
596:         else:
597:             def component_iterator(i):
598:                 try:
599:                     while j.n[i][:c]==constraint:
600:                         yield jnext(i)[c]
601:                 except KeyError: pass
602:                 #except (IndexError,KeyError): pass
603:                 # allows truncated tuples if no reasonable final component
604:             return [component_iterator(i) for i in range(niters)]
605:     return recursive_iterator(())
606: 
607: ##### simple cartesian product to augment polyhierarchy #####
608: 
609: def outer_product(componentiters):
610:     components=[list(i) for i in componentiters[:-1]]
611:     for c in components:
612:         if len(c)==0:
613:             return
614:         elif len(c)>1:
615:             break
616:     else:
617:         leading=tuple([x for [x] in components])
618:         for x in componentiters[-1]:
619:             yield JointBeforeEntry(leading+(x,))
620:         return
621: 
622:     components.append(list(componentiters[-1]))
623:     def recursive_product(product,components):
624:         if components:
625:             car=components[0]
626:             cdr=components[1:]
627:             for x in car:
628:                 for y in recursive_product(product+(x,),cdr):
629:                     yield y
630:         else:
631:             yield JointBeforeEntry(product)
632:     for y in recursive_product((),components):
633:         yield y
634: 
635: ##### relational ideas for a decidedly non-relational result #####
636: 
637: def cohierarchy_filter(columns,*jointiters):
638:     """
639:     Like the previous polyhierarchy iterator, it performs a real
640:     join on the columns of several iters; however it returns the
641:     final key entries by a single generator of all the final key
642:     entries combined into a single list, leftmost first.  These
643:     elements are returned as a pair consisting of an index to the
644:     original iters and the element itself.  This implementation
645:     has the dubious distinction of using a log order heap instead
646:     a linear min.
647:     """
648: 
649:     nc=len(columns)
650:     niters=len(jointiters)
651:     assert niters>0
652:     jointiters=[iter(i) for i in jointiters]
653:     class Cursor(object):
654:         pass
655:     j=Cursor()
656:     j.nheap=[]
657:     for (i,iteri) in enumerate(jointiters):
658:         try:
659:             nn=iteri.next()
660:             j.nheap+=[(nn[:nc],i,nn[nc])]
661:         except StopIteration:
662:             pass
663:     if not j.nheap:
664:             return iter(())
665:     heapq.heapify(j.nheap)
666:     def jnext():
667:         #try:
668:             (n,i,e)=heapq.heappop(j.nheap)
669:             try:
670:                 nn=jointiters[i].next()
671:                 heapq.heappush(j.nheap,(nn[:nc],i,nn[nc]),)
672:             except StopIteration:
673:                 pass
674:             return (n,i,e)
675:         #except IndexError:
676:         #       raise StopIteration
677:     def recursive_iterator(constraint):
678:         c=len(constraint)
679:         if c<len(columns):
680:             def recursive_polyiterator():
681:                 try:
682:                     (n,i,e)=j.nheap[0]
683:                     while n[:c]==constraint:
684:                         car=n[c]
685:                         newconstraint=constraint+(car,)
686:                         yield (car,recursive_iterator(newconstraint))
687:                         (n,i,e)=j.nheap[0]
688:                         # flush entries
689:                         while n[:c+1]==newconstraint:
690:                             jnext()
691:                             (n,i,e)=j.nheap[0]
692:                 except IndexError: #except (IndexError,StopIteration):
693:                     pass
694:             return recursive_polyiterator()
695:         else:
696:             def co_iterator():
697:                 try:
698:                     (n,i,e)=j.nheap[0]
699:                     while n==constraint:
700:                         yield (i,e)
701:                         jnext()
702:                         (n,i,e)=j.nheap[0]
703:                 except IndexError: #except (IndexError,StopIteration):
704:                     pass
705:             return co_iterator()
706:     return recursive_iterator(())
707: 
708: #### package in a nice friendly class ####
709: 
710: class Joint(object):
711:     """A mixin for the entire data base."""
712: 
713:     stable_filter=stable_filter
714:     semistable_filter=semistable_filter
715: 
716:     adjoin_after_filter=adjoin_after_filter
717:     adjoin_filter=adjoin_filter
718:     obverse_filter=obverse_filter
719:     converse_filter=converse_filter
720: 
721:     stable_list=stable_list
722:     semistable_list=stable_list
723:     adjoin_after_list=adjoin_after_list
724:     adjoin_list=adjoin_list
725:     obverse_list=obverse_list
726:     converse_list=converse_list
727: 
728:     adjoin_after_heap=adjoin_after_heap
729:     adjoin_heap=adjoin_heap
730:     obverse_heap=obverse_heap
731:     converse_heap=converse_heap
732: 
733:     unstable_sort=unstable_sort
734:     semistable_sort=semistable_sort
735:     adjoin_after_sort=adjoin_after_sort
736:     adjoin_sort=adjoin_sort
737:     obverse_sort=obverse_sort
738:     converse_sort=converse_sort
739: 
740:     adjoin_hierarchy=adjoin_hierarchy
741:     adjoin_dictionary=adjoin_dictionary
742:     adjoin_polyhierarchy=adjoin_polyhierarchy
743:     adjoin_polydictionary=adjoin_polydictionary
744: 
745: class JointList(ValueListable,list,Joint):
746:     """A list with Joint operations mixed in."""
747: 
748: class JointDict(dict):
749:     """A dict with Joint operations mixed in."""
750: 
751:     def values(self):
752:         return JointList(super(JointDict,self).values())
753: 
754:     def items(self):
755:         return JointList(super(JointDict,self).items())
756: 
757: ###### base.py ###### python package PyScore.relational module base ######