mrv.interface
Covered: 126 lines
Missed: 254 lines
Skipped 169 lines
Percent: 33 %
  2
"""Contains interface definitions """
  3
__docformat__ = "restructuredtext"
  5
from collections import deque as Deque
  6
import logging
  7
log = logging.getLogger('mrv.interface')
  9
__all__ = ("Interface", "iDagItem", "iDuplicatable", "iChoiceDialog", "iPrompt", 
 10
           "iProgressIndicator")
 12
class Interface( object ):
 13
	"""Base for all interfaces.
 14
	All interfaces should derive from here."""
 18
	__slots__ = tuple()
 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 
 23
			to support
 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 )
 26
			will not work"""
 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
 34
	will work natively.
 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)
 47
	__slots__ = tuple()
 52
	_sep = None
 57
	def isRoot( self ):
 58
		""":return: True if this path is the root of the DAG """
 59
		return self ==  self.root()
 61
	def root( self ):
 62
		""":return: the root of the DAG - it has no further parents"""
 63
		parents = self.parentDeep( )
 64
		if not parents:
 65
			return self
 66
		return parents[-1]
 68
	def basename( self ):
 69
		""":return: basename of this path, '/hello/world' -> 'world'"""
 70
		return str(self).split( self._sep )[-1]
 72
	def parent( self ):
 73
		"""
 74
		:return: parent of this path, '/hello/world' -> '/hello' or None if this path
 75
			is the dag's root"""
 76
		tokens =  str(self).split( self._sep )
 77
		if len( tokens ) <= 2:		# its already root
 78
			return None
 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( ) )
 86
		return out
 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"""
 99
		out = []
100
		if order == self.kOrder_DepthFirst:
101
			def depthSearch( child ):
102
				if not predicate( c ):
103
					return
104
				children = child.children( predicate = predicate )
105
				for c in children:
106
					depthSearch( c )
107
				out.append( child )
110
			depthSearch( self )
112
		elif order == self.kOrder_BreadthFirst:
113
			childstack = Deque( [ self ] )
114
			while childstack:
115
				item = childstack.pop( )
116
				if not predicate( item ):
117
					continue
118
				children = item.children( predicate = predicate )
120
				childstack.extendleft( children )
121
				out.extend( children )
124
		return out
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"""
145
		curpath = self
146
		while True:
147
			parent = curpath.parent( )
148
			if not parent:
149
				raise StopIteration
151
			if predicate( parent ):
152
				yield parent
154
			curpath = parent
161
	@classmethod
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 ):
168
			item += sep
169
		return item
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
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 """
192
	__slots__ = tuple()
194
	def __copyTo( self, instance, *args, **kwargs ):
195
		"""Internal Method with minimal checking"""
197
		mrorev = instance.__class__.mro()
198
		mrorev.reverse()
202
		for base in mrorev:
203
			if base is iDuplicatable:
204
				continue
208
			try:
209
				copyFromFunc = base.__dict__[ 'copyFrom' ]
210
				copyFromFunc( instance, self, *args, **kwargs )
211
			except KeyError:
212
				pass
213
			except TypeError,e:
214
				raise
218
		return instance
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"""
249
		try:
250
			createInstFunc = getattr( self, 'createInstance' )
251
			instance = createInstFunc( *args, **kwargs )
252
		except TypeError,e:
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`
278
		if possible.
279
		As more checking is performed, this method performs worse than `copyTo`"""
281
		mrorev = instance.__class__.mro()
282
		mrorev.reverse()
284
		own_bases = self.__class__.mro()
285
		own_bases.reverse()
289
		for base in mrorev:
290
			if base is iDuplicatable or base not in own_bases:
291
				continue
293
			try:
294
				copyFromFunc = base.__dict__[ 'copyFrom' ]
295
				copyFromFunc( instance, self, *args, **kwargs )
296
			except KeyError:
297
				pass
298
			except TypeError,e: 
299
				raise AssertionError( "The subclass method %s.%s must support *args and or **kwargs if the superclass does, error: %s" % (base, copyFromFunc,e) )
301
		return instance
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
316
		:param kwargs:
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 ) )
325
		assert self.message
327
		self.choices = kwargs.get( "c", kwargs.get( "choices", None ) )
328
		assert self.choices
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] ) )
338
	def choice( self ):
339
		"""Make the choice
341
		:return: name of the choice made by the user, the type shall equal the type given
342
			as button names
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
359
		:param kwargs:
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 ) )
373
		self._kwargs = kwargs
375
	def prompt( self ):
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
409
	def begin( self ):
410
		"""intiialize the progress indicator before calling `set` """
411
		self.__progress = self.__min		# refresh
413
	def end( self ):
414
		"""indicate that you are done with the progress indicator - this must be your last
415
		call to the interface"""
416
		pass
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"""
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
436
		if not omit_refresh:
437
			self.refresh( message = message )
439
	def setRange( self, min, max ):
440
		"""set the range within we expect our progress to occour"""
441
		self.__min = min
442
		self.__max = max
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
456
		be interrupted"""
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)
481
		if begin:
482
			self.begin()
487
	def get( self ):
488
		""":return: the current progress value
490
		:note: if set to relative mode, values will range
491
			from 0.0 to 100.0.
492
			Values will always be within the ones returned by `range`"""
493
		p = self.value()
494
		mn,mx = self.range()
495
		if self.roundRobin():
496
			p = p % mx
498
		if not self.isRelative():
499
			return min( max( p, mn ), mx )
503
		return min( max( ( p - mn ) / float( mx - mn ), 0.0 ), 1.0 ) * 100.0
505
	def value( self ):
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
513
	def range( self ):
514
		""":return: tuple( min, max ) value"""
515
		return ( self.__min, self.__max )
517
	def roundRobin( self ):
518
		""":return: True if roundRobin mode is enabled"""
519
		return self.__rr
521
	def prefix( self, value ):
522
		"""
523
		:return: a prefix indicating the progress according to the current range
524
			and given value """
525
		prefix = ""
526
		if self.isRelative():
527
			prefix = "%i%%" % value
528
		else:
529
			mn,mx = self.range()
530
			prefix = "%i/%i" % ( value, mx )
532
		return prefix
534
	def isAbortable( self ):
535
		""":return: True if the process may be cancelled"""
536
		return self.__may_abort
538
	def isRelative( self ):
539
		"""
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"""
546
		return False