1
2 """All kinds of utility methods and classes that are used in more than one modules """
3 import networkx as nx
4 from collections import deque as Deque
5 import weakref
6 import inspect
7 import itertools
8 from interface import iDuplicatable
9
10 from path import make_path
11
12 import os
13 import logging
14 log = logging.getLogger("mrv.maya.ui.util")
15
16 __docformat__ = "restructuredtext"
17 __all__ = ("decodeString", "decodeStringOrList", "capitalize", "uncapitalize",
18 "pythonIndex", "copyClsMembers", "packageClasses", "iterNetworkxGraph",
19 "Call", "CallAdv", "WeakInstFunction", "Event", "EventSender",
20 "InterfaceMaster", "Singleton", "CallOnDeletion",
21 "DAGTree", "PipeSeparatedFile", "MetaCopyClsMembers", "And", "Or",
22 "list_submodules", "list_subpackages")
26 """ :return: int,float or str from string valuestr - a string that encodes a
27 numeric value or a string
28
29 :raise TypeError: if the type could not be determined"""
30
31 if not isinstance(valuestr, basestring):
32 raise TypeError("Invalid value type: only int, long, float and str are allowed", valuestr)
33
34 types = (long, float)
35 for numtype in types:
36 try:
37 val = numtype(valuestr)
38
39
40 if val != float(valuestr):
41 continue
42
43 return val
44 except (ValueError,TypeError):
45 continue
46
47
48
49 return valuestr
50
52 """
53 :return: as `decodeString`, but returns a list of appropriate values if
54 the input argument is a list or tuple type"""
55 if isinstance(valuestrOrList , (list, tuple)):
56 return [decodeString(valuestr) for valuestr in valuestrOrList]
57
58 return decodeString(valuestrOrList)
59
61 """:return: s with first letter capitalized"""
62 return s[0].upper() + s[1:]
63
65 """:return: ``s`` with first letter lower case
66 :param preserveAcronymns: enabled ensures that 'NTSC' does not become 'nTSC'
67 :note: from pymel
68 """
69 try:
70 if preserveAcronymns and s[0:2].isupper():
71 return s
72 except IndexError: pass
73
74 return s[0].lower() + s[1:]
75
77 """Compute the actual index based on the given index and array length, thus
78 -1 will result in the last array element's index"""
79 if index > -1: return index
80 return length + index
81
82 -def copyClsMembers(sourcecls, destcls, overwritePrefix = None, forbiddenMembers = list(), copyNamespaceGlobally=None):
83 """Copy the members or sourcecls to destcls while ignoring member names in forbiddenMembers
84 It will only copy mebers of this class, not its base classes
85
86 :param sourcecls: class whose members should be copied
87 :param destcls: class to receive members from sourcecls
88 :param overwritePrefix: if None, existing members on destcls will not be overwritten, if string,
89 the original method will be stored in a name like prefix+originalname (allowing you to access the
90 original method lateron)
91 :param copyNamespaceGlobally: if not None, the variable contains the name of the namespace as string
92 whose methods should also be copied into the global namespace, possibly overwriting existing ones.
93 For instance, 'nsmethod' will be available as obj.nsmethod and as obj.method if the namespace value was 'ns.
94 The forbiddenMembers list is applied to the original as well as the global name
95 :note: this can be useful if you cannot inherit from a class directly because you would get
96 method resolution order problems
97 :note: see also the `MetaCopyClsMembers` meta class"""
98 for orig_name,member in sourcecls.__dict__.iteritems():
99 names = [orig_name]
100 if copyNamespaceGlobally is not None and orig_name.startswith(copyNamespaceGlobally):
101 names.append(orig_name[len(copyNamespaceGlobally):])
102
103
104 for name in names:
105 if name in forbiddenMembers:
106 continue
107 try:
108
109 if hasattr(destcls, name):
110 if not overwritePrefix:
111 continue
112 morig = getattr(destcls, name)
113 type.__setattr__(destcls, overwritePrefix+name, morig)
114 type.__setattr__(destcls, name, member)
115 except TypeError:
116 pass
117
118
119
120 -def packageClasses(importBase, packageFile, predicate = lambda x: True):
121 """
122 :return: all classes of modules of the given package file that additionally
123 match given predicate
124 :param importBase: longest import base path whose submodules contain the classes to import
125 :param packageFile: the filepath to the package, as given in your __file__ variables
126 :param predicate: receives the class and returns True if it is a class you are looking for"""
127 from glob import glob
128 import os
129
130 packageDir = os.path.dirname(packageFile)
131
132
133 basenameNoExt = lambda n: os.path.splitext(os.path.split(n)[1])[0]
134 pymodules = itertools.chain(glob(os.path.join(packageDir, "*.py")), glob(os.path.join(packageDir, "*.pyc")))
135 pymodules = [basenameNoExt(m) for m in pymodules
136 if not os.path.basename(m).startswith('_')]
137
138 outclasses = []
139 classAndCustom = lambda x: inspect.isclass(x) and predicate(x)
140 for modulename in pymodules:
141 modobj = __import__("%s.%s" % (importBase, modulename), globals(), locals(), [''])
142 for name,obj in inspect.getmembers(modobj, predicate = classAndCustom):
143 outclasses.append(obj)
144
145 return outclasses
146
147 -def iterNetworkxGraph(graph, startItem, direction = 0, prune = lambda i,g: False,
148 stop = lambda i,g: False, depth = -1, branch_first=True,
149 visit_once = True, ignore_startitem=1):
150 """:return: iterator yielding pairs of depth, item
151 :param direction: specifies search direction, either :
152 0 = items being successors of startItem
153 1 = items being predecessors of startItem
154 :param prune: return True if item d,i in graph g should be pruned from result.
155 d is the depth of item i
156 :param stop: return True if item d,i in graph g, d is the depth of item i
157 stop the search in that direction. It will not be returned.
158 :param depth: define at which level the iteration should not go deeper
159 if -1, there is no limit
160 if 0, you would only get startitem.
161 i.e. if 1, you would only get the startitem and the first level of predessessors/successors
162 :param branch_first: if True, items will be returned branch first, otherwise depth first
163 :param visit_once: if True, items will only be returned once, although they might be encountered
164 several times
165 :param ignore_startitem: if True, the startItem will be ignored and automatically pruned from
166 the result
167 :note: this is an adjusted version of `dge.iterShells`"""
168 visited = set()
169 stack = Deque()
170 stack.append((0 , startItem))
171
172 def addToStack(stack, lst, branch_first, dpth):
173 if branch_first:
174 reviter = ((dpth , lst[i]) for i in range(len(lst)-1,-1,-1))
175 stack.extendleft(reviter)
176 else:
177 stack.extend((dpth,item) for item in lst)
178
179
180
181 directionfunc = graph.successors
182 if direction == 1:
183 directionfunc = graph.predecessors
184
185 while stack:
186 d, item = stack.pop()
187
188 if item in visited:
189 continue
190
191 if visit_once:
192 visited.add(item)
193
194 oitem = (d, item)
195 if stop(oitem, graph):
196 continue
197
198 skipStartItem = ignore_startitem and (item == startItem)
199 if not skipStartItem and not prune(oitem, graph):
200 yield oitem
201
202
203 nd = d + 1
204 if depth > -1 and nd > depth:
205 continue
206
207 addToStack(stack, directionfunc(item), branch_first, nd)
208
209
210
211
212 -class Call(object):
213 """Call object encapsulating any code, thus providing a simple facade for it
214 :note: derive from it if a more complex call is required"""
215 __slots__ = ("func", "args", "kwargs")
216
217 - def __init__(self, func, *args,**kwargs):
218 """Initialize object with function to call once this object is called"""
219 self.func = func
220 self.args = args
221 self.kwargs = kwargs
222
224 """Execute the stored function on call
225 :note: having ``args`` and ``kwargs`` set makes it more versatile"""
226 return self.func(*self.args, **self.kwargs)
227
230 """Advanced call class providing additional options:
231
232 merge_args:
233 if True, default True, incoming arguments will be prepended before the static ones
234 merge_kwargs:
235 if True, default True, incoming kwargs will be merged into the static ones """
236 __slots__ = ("merge_args", "merge_kwargs")
237
238 - def __init__(self, func, *args, **kwargs):
243
244 - def __call__(self, *inargs, **inkwargs):
255
258 """Create a proper weak instance to an instance function by weakly binding
259 the instance, not the bound function object.
260 When called, the weakreferenced instance pointer will be retrieved, if possible,
261 to finally make the call. If it could not be retrieved, the call
262 will do nothing."""
263 __slots__ = ("_weakinst", "_clsfunc")
264
266 self._weakinst = weakref.ref(instancefunction.im_self)
267 self._clsfunc = instancefunction.im_func
268
270 return hash(self) == hash(other)
271
274
276 """
277 :raise LookupError: if the instance referred to by the instance method
278 does not exist anymore"""
279 inst = self._weakinst()
280 if inst is None:
281 raise LookupError("Instance for call to %s has been deleted as it is weakly bound" % self._clsfunc.__name__)
282
283 return self._clsfunc(inst, *args, **kwargs)
284
287 """Descriptor allowing to easily setup callbacks for classes derived from
288 EventSender"""
289 _inst_event_attr = '__events__'
290
291
292
293
294 use_weakref = True
295
296
297
298 remove_on_error = False
299
300
301
302 sender_as_argument = None
303
304
305
306
307 _curSender = None
308
310 """
311 :param kwargs:
312 * weak: if True, default class configuration use_weakref, weak
313 references will be created for event handlers, if False it will be strong
314 references
315 * remove_failed: if True, defailt False, failed callback handlers
316 will be removed silently
317 * sender_as_argument - see class member"""
318 self.use_weakref = kwargs.get("weak", self.__class__.use_weakref)
319 self.remove_on_error = kwargs.get("remove_failed", self.__class__.remove_on_error)
320 self.sender_as_argument = kwargs.get("sender_as_argument", self.__class__.sender_as_argument)
321 self._last_inst_ref = None
322
324 """
325 :return: an eventfunction suitable to be used as key in our instance
326 event set"""
327 if self.use_weakref:
328 if inspect.ismethod(eventfunc):
329 eventfunc = WeakInstFunction(eventfunc)
330 else:
331 eventfunc = weakref.ref(eventfunc)
332
333
334 return eventfunc
335
337 """:return: event function from the given eventkey as stored in our events set.
338 :note: this is required as the event might be weakreffed or not"""
339 if self.use_weakref:
340 if isinstance(eventkey, WeakInstFunction):
341 return eventkey
342 else:
343 return eventkey()
344
345
346 return eventkey
347
349 """:return: The last instance that retrieved us"""
350 if self._last_inst_ref is None or self._last_inst_ref() is None:
351 raise TypeError("Cannot send events through class descriptor")
352 return self._last_inst_ref()
353
354 - def __set__(self, inst, eventfunc):
357
358 - def __get__(self, inst, cls = None):
359 """Always return self, but keep the instance in case
360 we someone wants to send an event."""
361 if inst is not None:
362 inst = weakref.ref(inst)
363
364 self._last_inst_ref = inst
365 return self
366
368 """:return: function set of the given instance containing functions of our event"""
369 if not hasattr(inst, self._inst_event_attr):
370 setattr(inst, self._inst_event_attr, dict())
371
372
373 ed = getattr(inst, self._inst_event_attr)
374 try:
375 return ed[self]
376 except KeyError:
377 return ed.setdefault(self, set())
378
379
380
381 - def send(self, *args, **kwargs):
382 """Send our event using the given args
383
384 :note: if an event listener is weak referenced and goes out of scope
385 :note: will catch all event exceptions trown by the methods called
386 :return: False if at least one event call threw an exception, true otherwise"""
387 inst = self._get_last_instance()
388 callbackset = self._getFunctionSet(inst)
389 success = True
390 failed_callbacks = list()
391
392 sas = inst.sender_as_argument
393 if self.sender_as_argument is not None:
394 sas = self.sender_as_argument
395
396
397
398 Event._curSender = inst
399
400
401
402 for function in callbackset.copy():
403 try:
404 func = self._key_to_func(function)
405 if func is None:
406 log.warn("Listener for callback of %s was not available anymore" % self)
407 failed_callbacks.append(function)
408 continue
409
410
411 try:
412 if sas:
413 func(inst, *args, **kwargs)
414 else:
415 func(*args, **kwargs)
416 except LookupError, e:
417
418 if inst.reraise_on_error:
419 raise
420 log.error(str(e))
421 failed_callbacks.append(function)
422
423 except Exception, e :
424 if self.remove_on_error:
425 failed_callbacks.append(function)
426
427 if inst.reraise_on_error:
428 raise
429 log.error(str(e))
430 success = False
431
432
433
434 for function in failed_callbacks:
435 callbackset.remove(function)
436
437 Event._curSender = None
438 return success
439
440
441 __call__ = send
442
444 """remove the given function from this event
445 :note: will not raise if eventfunc does not exist"""
446 inst = self._get_last_instance()
447 eventfunc = self._func_to_key(eventfunc)
448 try:
449 self._getFunctionSet(inst).remove(eventfunc)
450 except KeyError:
451 pass
452
460
466 """Base class for all classes that want to provide a common callback interface
467 to supply event information to clients.
468
469 **Usage**:
470 Derive from this class and define your callbacks like:
471
472 >>> event = Event()
473 >>> # Call it using
474 >>> self.event.send([*args][,**kwargs]])
475
476 >>> # Users register using
477 >>> yourinstance.event = callable
478
479 >>> # and deregister using
480 >>> yourinstance.event.remove(callable)
481
482 :note: if use_weakref is True, we will weakref the eventfunction, and deal
483 properly with instance methods which would go out of scope immediatly otherwise
484
485 :note: using weak-references to ensure one does not keep objects alive,
486 see `Event.use_weakref`"""
487 __slots__ = tuple()
488
489
490
491
492 sender_as_argument = False
493
494
495
496 reraise_on_error = False
497
498
499 @classmethod
501 """:return: list of event ids that exist on our class"""
502 return [name for name,member in inspect.getmembers(cls, lambda m: isinstance(m, Event))]
503
505 """Remove all event receivers for all events registered in this instance.
506
507 :note: This usually doesn't need to be called directly, but might be useful
508 in conjunction with other system that do not release your strongly bound
509 instance"""
510
511 for en in self.listEventNames():
512 event = getattr(self, en)
513 for key in event._getFunctionSet(self).copy():
514 event.remove(event._key_to_func(key))
515
516
517
519 """:return: instance which sent the event you are currently processing
520 :raise ValueError: if no event is currently in progress"""
521 if Event._curSender is None:
522 raise ValueError("Cannot return sender as no event is being sent")
523 return Event._curSender
524
527 """Base class making the derived class an interface provider, allowing interfaces
528 to be set, queried and used including build-in use"""
529 __slots__ = ("_idict",)
530
531 im_provide_on_instance = True
532
533
534
536 """Descriptor handing out interfaces from our interface dict
537 They allow access to interfaces directly through the InterfaceMaster without calling
538 extra functions"""
539
541 self.iname = interfacename
542
543 - def __get__(self, inst, cls = None):
544
545 if inst is None:
546 return self
547
548 try:
549 return inst.interface(self.iname)
550 except KeyError:
551 raise AttributeError("Interface %s does not exist" % self.iname)
552
554 raise ValueError("Cannot set interfaces through the instance - use the setInterface method instead")
555
556
558 """Utility class passing all calls to the stored InterfaceBase, updating the
559 internal caller-id"""
567
569 self.__ibase._current_caller_id = self.__callerid
570 return getattr(self.__ibase, attr)
571
576
577
579 """If your interface class is derived from this base, you get access to
580 access to call to the number of your current caller.
581
582 :note: You can register an InterfaceBase with several InterfaceMasters and
583 share the caller count respectively"""
584 __slots__ = ("_current_caller_id", "_num_callers")
588
590 """:return: number possible callers"""
591 return self._num_callers
592
594 """Return the number of the caller that called your interface method
595
596 :note: the return value of this method is undefined if called if the
597 method has been called by someone not being an official caller (like yourself)"""
598 return self._current_caller_id
599
601 """Called once our interface has been given to a new caller.
602 The caller has not made a call yet, but its id can be queried"""
603 pass
604
606 """Called once our interface is about to be removed from the current
607 caller - you will not receive a call from it anymore """
608 pass
609
610
611
612
614 """Initialize the interface base with some tracking variables"""
615 self._idict = dict()
616
617
618
619 - def copyFrom(self, other, *args, **kwargs):
620 """Copy all interface from other to self, use they duplciate method if
621 possibly """
622 for ifname, ifinst in other._idict.iteritems():
623 myinst = ifinst
624 if hasattr(ifinst, "duplicate"):
625 myinst = ifinst.duplicate()
626
627 self.setInterface(ifname, myinst)
628
629
630
631
633 """Set the given interfaceInstance to be handed out once an interface
634 with interfaceName is requested from the provider base
635
636 :param interfaceName: should start with i..., i.e. names would be iInterface
637 The name can be used to refer to the interface later on
638 :param interfaceInstance: instance to be handed out once an interface with the
639 given name is requested by the InterfaceMaster or None
640 if None, the interface will effectively be deleted
641 :raise ValueError: if given InterfaceBase has a master already """
642 if interfaceInstance is None:
643
644 try:
645 del(self._idict[interfaceName])
646 except KeyError:
647 pass
648
649
650 if self.im_provide_on_instance:
651 try:
652 delattr(self.__class__, interfaceName)
653 except AttributeError:
654 pass
655
656
657
658 else:
659 self._idict[interfaceName] = interfaceInstance
660
661
662 if self.im_provide_on_instance:
663 setattr(self.__class__, interfaceName, self.InterfaceDescriptor(interfaceName))
664
665
666
668 """:return: an interface registered with interfaceName
669 :raise ValueError: if no such interface exists"""
670 try:
671 iinst = self._idict[interfaceName]
672
673
674 if isinstance(iinst, self.InterfaceBase):
675 return self._InterfaceHandler(iinst)
676 else:
677 return iinst
678 except KeyError:
679 raise ValueError("Interface %s does not exist" % interfaceName)
680
682 """:return: list of names indicating interfaces available at our InterfaceMaster"""
683 return self._idict.keys()
684
689 """ Singleton classes can be derived from this class,
690 you can derive from other classes as long as Singleton comes first (and class doesn't override __new__) """
697
700 """Call the given callable object once this object is being deleted
701 Its usefull if you want to assure certain code to run once the parent scope
702 of this object looses focus"""
703 __slots__ = "callableobj"
706
710
713 """Adds utility functions to DirectedTree allowing to handle a directed tree like a dag
714 :note: currently this tree does not support instancing
715 :todo: add instancing support"""
716
718 """ :return: list of children of given node n """
719 return list(self.children_iter(n))
720
722 """ :return: iterator with children of given node n"""
723 return (e[1] for e in self.out_edges_iter(n))
724
726 """:return: parent of node n
727 :note: currently there is only one parent, as instancing is not supported yet"""
728 for parent in self.predecessors_iter(n):
729 return parent
730 return None
731
733 """:return: iterator returning all parents of node n"""
734 while True:
735 p = self.parent(n)
736 if p is None:
737 raise StopIteration()
738 yield p
739 n = p
740
742 """:return: the root node of this dag tree
743 :param startnode: if None, the first node will be used to get the root from
744 (good for single rooted dags), otherwise this node will be used to get the root from
745 - thus it must exist in the dag tree"""
746 if startnode is None:
747 startnode = self.nodes_iter().next()
748
749 root = None
750 for parent in self.parent_iter(startnode):
751 root = parent
752
753 return root
754
756 """Write ourselves in hierarchy file format to the given output_path.
757
758 :param root: The root of the written file, nodes above it will not be serialized.
759 :note: Directories are expected to exist
760 :raise ValueError: If an node's string representation contains a newline or
761 starts with a tab
762 :note: works best with strings as nodes, which may not contain newlines"""
763 fp = open(output_path, "wb")
764 for depth, item in iterNetworkxGraph(self, root, branch_first=False, ignore_startitem=False):
765 itemstr = str(item)
766 if itemstr.startswith("\t") or "\n" in itemstr:
767 raise ValueError("Item %r contained characters unsupported by the hierarchy file format")
768
769 fp.write("%s%s\n" % ("\t"*depth, itemstr))
770
771 fp.close()
772
775 """Read and write simple pipe separated files.
776
777 The number of column must remain the same per line
778 **Format**:
779
780 val11 | val2 | valn
781 ...
782 """
783 kSeparator = '|'
784 __slots__ = ("_fileobj", "_columncount", "_formatstr")
785
787 """Initialize the instance
788
789 :param fileobj: fileobject where new lines will be written to or read from
790 It must already be opened for reading and/or writing respectively"""
791 self._fileobj = fileobj
792 self._columncount = None
793
795 """Start reading the file"""
796
798 """Generator reading one line after another, returning the stripped columns
799
800 :return: tuple of stripped column strings
801 :raise ValueError: if the column count changes between the lines"""
802 for line in self._fileobj:
803 if not len(line.strip()):
804 continue
805
806 tokens = [item.strip() for item in line.split(self.kSeparator)]
807 if not self._columncount:
808 self._columncount = len(tokens)
809
810 if self._columncount != len(tokens):
811 raise ValueError("Columncount changed between successive lines")
812
813 yield tuple(tokens)
814
815
817 """intiialize the writing process
818
819 :param columnSizes: list of ints defining the size in characters for each column you plan to feed
820 :note: When done writing, you have to close the file object yourself (there is no endWriting method here)"""
821 columnTokens = ["%%-%is" % csize for csize in columnSizes]
822 self._formatstr = ((self.kSeparator + " ").join(columnTokens)) + "\n"
823
825 """Write the list of tokens to the file accordingly
826
827 :param tokens: one token per column that you want to write
828 :raise TypeError: If column count changed between successive calls"""
829 self._fileobj.write(self._formatstr % tokens)
830
860
861
862
863
864
865 -class And(object):
866 """For use with python's filter method, simulates logical AND
867 Usage: filter(And(f1,f2,fn), sequence)"""
868 __slots__ = "functions"
870 """args must contain the filter methods to be AND'ed
871 To append functions after creation, simply access the 'functions' attribute
872 directly as a list"""
873 self.functions = list(args)
874
876 """Called during filter function, return true if all functions return true"""
877 val = True
878 for func in self.functions:
879 val = val and func(*args, **kwargs)
880 if not val:
881 return val
882
883 return val
884
885
886 -class Or(object):
887 """For use with python's filter method, simulates logical OR
888 Usage: filter(Or(f1,f2,fn), sequence) """
889 __slots__ = "functions"
891 """args must contain the filter methods to be AND'ed"""
892 self.functions = args
893
895 """Called during filter function, return true if all functions return true"""
896 val = False
897 for func in self.functions:
898 val = val or func(*args, **kwargs)
899 if val:
900 return val
901
902 return val
903
909 """
910 :return: set(submodule_name, ...) list of submodule names that could
911 be imported using __import__
912 :param path: module path containing the submodules"""
913 file_package = make_path(path).dirname()
914
915
916 cut_ext = lambda f: str(os.path.splitext(os.path.basename(f))[0])
917 modules = set()
918 for glob in ("*.py", "*.pyc"):
919 modules.update(set(map(cut_ext, file_package.files(glob))))
920
921 return modules
922
926
927
928