2
"""Contains interface definitions """
3
__docformat__ = "restructuredtext"
5
from collections import deque as Deque
7
log = logging.getLogger('mrv.interface')
9
__all__ = ("Interface", "iDagItem", "iDuplicatable", "iChoiceDialog", "iPrompt",
12
class Interface( object ):
13
"""Base for all interfaces.
14
All interfaces should derive from here."""
16
# assure we can be handled efficiently - subclasses are free not to define
17
# slots, but those who do will not inherit a __dict__ from here
20
def supports( self, interface_type ):
21
""":return: True if this instance supports the interface of the given type
22
:param interface_type: Type of the interface you require this instance
24
:note: Must be used in case you only have a weak reference of your interface
25
instance or proxy which is a case where the ordinary isinstance( obj, iInterface )
27
return isinstance( self, interface_type )
30
class iDagItem( Interface ):
31
""" Describes interface for a DAG item.
32
Its used to unify interfaces allowing to access objects in a dag like graph
33
Of the underlying object has a string representation, the defatult implementation
35
Otherwise the getParent and getChildren methods should be overwritten
37
:note: a few methods of this class are abstract and need to be overwritten
38
:note: this class expects the attribute '_sep' to exist containing the
39
separator at which your object should be split ( for default implementations ).
40
This works as the passed in pointer will belong to derived classes that can
41
define that attribute on instance or on class level"""
43
kOrder_DepthFirst, kOrder_BreadthFirst = range(2)
45
# assure we can be handled efficiently - subclasses are free not to define
46
# slots, but those who do will not inherit a __dict__ from here
50
# separator as appropriate for your class if it can be treated as string
51
# if string treatment is not possibly, override the respective method
58
""":return: True if this path is the root of the DAG """
59
return self == self.root()
62
""":return: the root of the DAG - it has no further parents"""
63
parents = self.parentDeep( )
69
""":return: basename of this path, '/hello/world' -> 'world'"""
70
return str(self).split( self._sep )[-1]
74
:return: parent of this path, '/hello/world' -> '/hello' or None if this path
76
tokens = str(self).split( self._sep )
77
if len( tokens ) <= 2: # its already root
80
return self.__class__( self._sep.join( tokens[0:-1] ) )
82
def parentDeep( self ):
83
""":return: all parents of this path, '/hello/my/world' -> [ '/hello/my','/hello' ]"""
84
return list( self.iterParents( ) )
88
def children( self , predicate = lambda x: True):
89
""":return: list of intermediate children of path, [ child1 , child2 ]
90
:param predicate: return True to include x in result
91
:note: the child objects returned are supposed to be valid paths, not just relative paths"""
92
raise NotImplementedError( )
94
def childrenDeep( self , order = kOrder_BreadthFirst, predicate=lambda x: True ):
95
""":return: list of all children of path, [ child1 , child2 ]
96
:param order: order enumeration
97
:param predicate: returns true if x may be returned
98
:note: the child objects returned are supposed to be valid paths, not just relative paths"""
100
if order == self.kOrder_DepthFirst:
101
def depthSearch( child ):
102
if not predicate( c ):
104
children = child.children( predicate = predicate )
108
# END recursive search method
112
elif order == self.kOrder_BreadthFirst:
113
childstack = Deque( [ self ] )
115
item = childstack.pop( )
116
if not predicate( item ):
118
children = item.children( predicate = predicate )
120
childstack.extendleft( children )
121
out.extend( children )
122
# END while childstack
123
# END if breadth first
126
def isPartOf( self, other ):
127
""":return: True if self is a part of other, and thus can be found in other
128
:note: operates on strings only"""
129
return str( other ).find( str( self ) ) != -1
131
def isRootOf( self, other ):
132
""":return: True other starts with self
133
:note: operates on strings
134
:note: we assume other has the same type as self, thus the same separator"""
135
selfstr = self.addSep( str( self ), self._sep )
136
other = self.addSep( str( other ), self._sep )
137
return other.startswith( selfstr )
142
def iterParents( self , predicate = lambda x : True ):
143
""":return: generator retrieving all parents up to the root
144
:param predicate: returns True for all x that you want to be returned"""
147
parent = curpath.parent( )
151
if predicate( parent ):
162
def addSep( cls, item, sep ):
163
""":return: item with separator added to it ( just once )
164
:note: operates best on strings
165
:param item: item to add separator to
166
:param sep: the separator"""
167
if not item.endswith( sep ):
171
def fullChildName( self, childname ):
172
"""Add the given name to the string version of our instance
173
:return: string with childname added like name _sep childname"""
174
sname = self.addSep( str( self ), self._sep )
176
if childname.startswith( self._sep ):
177
childname = childname[1:]
179
return sname + childname
181
#} END name generation
184
class iDuplicatable( Interface ):
185
"""Simple interface allowing any class to be properly duplicated
187
:note: to implement this interface, implement `createInstance` and
188
`copyFrom` in your class """
190
# assure we can be handled efficiently - subclasses are free not to define
191
# slots, but those who do will not inherit a __dict__ from here
194
def __copyTo( self, instance, *args, **kwargs ):
195
"""Internal Method with minimal checking"""
196
# Get reversed mro, starting at lowest base
197
mrorev = instance.__class__.mro()
200
# APPLY COPY CONSTRUCTORS !
201
##############################
203
if base is iDuplicatable:
206
# must get the actual method directly from the base ! Getattr respects the mro ( of course )
207
# and possibly looks at the base's superclass methods of the same name
209
copyFromFunc = base.__dict__[ 'copyFrom' ]
210
copyFromFunc( instance, self, *args, **kwargs )
217
# return the result !
222
def createInstance( self, *args, **kwargs ):
223
"""Create and Initialize an instance of self.__class__( ... ) based on your own data
225
:return: new instance of self
226
:note: using self.__class__ instead of an explicit class allows derived
227
classes that do not have anything to duplicate just to use your implementeation
229
:note: you must support ``args`` and ``kwargs`` if one of your iDuplicate bases does"""
230
return self.__class__(*args, **kwargs)
232
def copyFrom( self, other, *args, **kwargs ):
233
"""Copy the data from other into self as good as possible
234
Only copy the data that is unique to your specific class - the data of other
235
classes will be taken care of by them !
237
:note: you must support ``args`` and ``kwargs`` if one of your iDuplicate bases does"""
238
raise NotImplementedError( "Copy all data you know from other into self" )
242
def duplicate( self, *args, **kwargs ):
243
"""Implements a c-style copy constructor by creating a new instance of self
244
and applying the `copyFrom` methods from base to all classes implementing the copyfrom
245
method. Thus we will call the method directly on the class
247
:param args: passed to `copyFrom` and `createInstance` method to give additional directions
248
:param kwargs: see param args"""
250
createInstFunc = getattr( self, 'createInstance' )
251
instance = createInstFunc( *args, **kwargs )
254
raise AssertionError( "The subclass method %s must support *args and or **kwargs if the superclass does, error: %s" % ( createInstFunc, e ) )
258
if not ( instance.__class__ is self.__class__ ):
259
msg = "Duplicate must have same class as self, was %s, should be %s" % ( instance.__class__, self.__class__ )
260
raise AssertionError( msg )
262
return self.__copyTo( instance, *args, **kwargs )
264
def copyTo( self, instance, *args, **kwargs ):
265
"""Copy the values of ourselves onto the given instance which must be an
266
instance of our class to be compatible.
267
Only the common classes will be copied to instance
269
:return: altered instance
270
:note: instance will be altered during the process"""
271
if type( instance ) != type( self ):
272
raise TypeError( "copyTo: Instance must be of type %s but was type %s" % ( type( self ), type( instance ) ) )
273
return self.__copyTo( instance, *args, **kwargs )
275
def copyToOther( self, instance, *args, **kwargs ):
276
"""As `copyTo`, but does only require the objects to have a common base.
277
It will match the actually compatible base classes and call `copyFrom`
279
As more checking is performed, this method performs worse than `copyTo`"""
280
# Get reversed mro, starting at lowest base
281
mrorev = instance.__class__.mro()
284
own_bases = self.__class__.mro()
287
# APPLY COPY CONSTRUCTORS !
288
##############################
290
if base is iDuplicatable or base not in own_bases:
294
copyFromFunc = base.__dict__[ 'copyFrom' ]
295
copyFromFunc( instance, self, *args, **kwargs )
299
raise AssertionError( "The subclass method %s.%s must support *args and or **kwargs if the superclass does, error: %s" % (base, copyFromFunc,e) )
304
class iChoiceDialog( Interface ):
305
"""Interface allowing access to a simple confirm dialog allowing the user
306
to pick between a selection of choices, one of which he has to confirm
308
:note: for convenience, this interface contains a brief implementation as a
309
basis for subclasses, using standard input and standard ouput for communication"""
311
def __init__( self, *args, **kwargs ):
312
"""Allow the user to pick a choice
314
:note: all paramaters exist in a short and a long version for convenience, given
315
in the form short/long
317
* t/title: optional title of the choice box, quickly saying what this choice is about
318
* m/message: message to be shown, informing the user in detail what the choice is about
319
* c/choices: single item or list of items identifying the choices if used as string
320
* dc/defaultChoice: choice in set of choices to be used as default choice, default is first choice
321
* cc/cancelChoice: choice in set of choices to be used if the dialog is cancelled using esc,
322
default is last choice"""
323
self.title = kwargs.get( "t", kwargs.get( "title", "Choice Dialog" ) )
324
self.message = kwargs.get( "m", kwargs.get( "message", None ) )
327
self.choices = kwargs.get( "c", kwargs.get( "choices", None ) )
330
# internally we store a choice list
331
if not isinstance( self.choices, ( list, tuple ) ):
332
self.choices = [ self.choices ]
334
self.default_choice = kwargs.get( "dc", kwargs.get( "defaultChoice", self.choices[0] ) )
335
self.cancel_choice = kwargs.get( "cc", kwargs.get( "cancelChoice", self.choices[-1] ) )
341
:return: name of the choice made by the user, the type shall equal the type given
343
:note: this implementation always returns the default choice"""
344
log.debug(self.title)
345
log.debug("-"*len( self.title ))
346
log.debug(self.message)
347
log.debug(" | ".join(( str( c ) for c in self.choices )))
348
log.debug("answer: %s" % self.default_choice)
350
return self.default_choice
353
class iPrompt( Interface ):
354
"""Prompt a value from the user, providing a default if no input is retrieved"""
356
def __init__( self, **kwargs ):
357
"""Configure the prompt, most parameters allow short and long names
360
* m/message: Message to be presented, like "Enter your name", must be set
361
* d/default: default value to return in case there is no input
362
* cd/cancelDefault: default value if prompt is cancelled
363
* confirmToken: token to enter/hit/press to finish the prompt
364
* cancelToken: token to cancel and abort the prompt"""
365
self.msg = kwargs.pop( "m", kwargs.pop( "message", None ) )
366
assert self.msg is not None, "No Message given"
367
self.confirmDefault = kwargs.pop( "d", kwargs.pop( "default", None ) )
368
self.cancelDefault = kwargs.pop( "cd", kwargs.pop( "cancelDefault", None ) )
369
self.confirmToken = kwargs.pop( "t", kwargs.pop( "confirmToken", None ) )
370
self.cancelToken = kwargs.pop( "ct", kwargs.pop( "cancelToken", None ) )
372
# remaining arguments for subclass use
373
self._kwargs = kwargs
376
"""activate our prompt
377
:return: the prompted value
378
:note: base implementation just prints a sample text and returns the default"""
379
log.debug("%s [ %s ]:" % ( self.msg, self.confirmDefault ))
380
log.debug("Hit %s to confirm or %s to cancel" % ( self.confirmToken, self.cancelToken ))
381
return self.confirmDefault
384
class iProgressIndicator( Interface ):
385
"""Interface allowing to submit progress information
386
The default implementation just prints the respective messages
387
Additionally you may query whether the computation has been cancelled by the user
389
:note: this interface is a simple progress indicator itself, and can do some computations
390
for you if you use the get() method yourself"""
394
def __init__( self, min = 0, max = 100, is_relative = True, may_abort = False, round_robin=False, **kwargs ):
395
""":param min: the minimum progress value
396
:param max: the maximum progress value
397
:param is_relative: if True, the values given will be scaled to a range of 0-100,
398
if False no adjustments will be done
399
:param round_robin: see `setRoundRobin`
400
:param kwargs: additional arguments being ignored"""
401
self.setRange( min, max )
402
self.setRelative( is_relative )
403
self.setAbortable( may_abort )
404
self.setRoundRobin( round_robin )
405
self.__progress = min
410
"""intiialize the progress indicator before calling `set` """
411
self.__progress = self.__min # refresh
414
"""indicate that you are done with the progress indicator - this must be your last
415
call to the interface"""
418
#} END initialization
421
def refresh( self, message = None ):
422
"""Refresh the progress indicator so that it represents its values on screen.
424
:param message: message passed along by the user"""
425
# To be implemented in subclass
427
def set( self, value, message = None , omit_refresh=False ):
428
"""Set the progress of the progress indicator to the given value
430
:param value: progress value ( min<=value<=max )
431
:param message: optional message you would like to give to the user
432
:param omit_refresh: by default, the progress indicator refreshes on set,
433
if False, you have to call refresh manually after you set the value"""
434
self.__progress = value
437
self.refresh( message = message )
439
def setRange( self, min, max ):
440
"""set the range within we expect our progress to occour"""
444
def setRoundRobin( self, round_robin ):
445
"""Set if round-robin mode should be used.
446
If True, values exceeding the maximum range will be wrapped and
447
start at the minimum range"""
448
self.__rr = round_robin
450
def setRelative( self, state ):
451
"""enable or disable relative progress computations"""
452
self.__relative = state
454
def setAbortable( self, state ):
455
"""If state is True, the progress may be interrupted, if false it cannot
457
self.__may_abort = state
459
def setup( self, range=None, relative=None, abortable=None, begin=True, round_robin=None ):
460
"""Multifunctional, all in one convenience method setting all important attributes
461
at once. This allows setting up the progress indicator with one call instead of many
463
:note: If a kw argument is None, it will not be set
464
:param range: Tuple( min, max ) - start ane end of progress indicator range
465
:param relative: equivalent to `setRelative`
466
:param abortable: equivalent to `setAbortable`
467
:param round_robin: equivalent to `setRoundRobin`
468
:param begin: if True, `begin` will be called as well"""
469
if range is not None:
470
self.setRange( range[0], range[1] )
472
if relative is not None:
473
self.setRelative( relative )
475
if abortable is not None:
476
self.setAbortable( abortable )
478
if round_robin is not None:
479
self.setRoundRobin(round_robin)
488
""":return: the current progress value
490
:note: if set to relative mode, values will range
492
Values will always be within the ones returned by `range`"""
495
if self.roundRobin():
498
if not self.isRelative():
499
return min( max( p, mn ), mx )
500
# END relative handling
502
# compute the percentage
503
return min( max( ( p - mn ) / float( mx - mn ), 0.0 ), 1.0 ) * 100.0
506
""":return: current progress as it is stored internally, without regarding
507
the range or round-robin options.
509
:note: This allows you to use this instance as a counter without concern to
510
the range and round-robin settings"""
511
return self.__progress
514
""":return: tuple( min, max ) value"""
515
return ( self.__min, self.__max )
517
def roundRobin( self ):
518
""":return: True if roundRobin mode is enabled"""
521
def prefix( self, value ):
523
:return: a prefix indicating the progress according to the current range
526
if self.isRelative():
527
prefix = "%i%%" % value
530
prefix = "%i/%i" % ( value, mx )
534
def isAbortable( self ):
535
""":return: True if the process may be cancelled"""
536
return self.__may_abort
538
def isRelative( self ):
540
:return: true if internal progress computations are relative, False if
541
they are treated as absolute values"""
542
return self.__relative
544
def isCancelRequested( self ):
545
""":return: true if the operation should be aborted"""