1
1
import os
2
2
import re
3
3
from contextlib import contextmanager
4
+ from itertools import cycle
4
5
5
6
try :
6
- from invoke .vendor .six import raise_from , iteritems
7
+ from invoke .vendor .six import raise_from , iteritems , string_types
7
8
except ImportError :
8
- from six import raise_from , iteritems
9
+ from six import raise_from , iteritems , string_types
9
10
10
11
from .config import Config , DataProxy
11
12
from .exceptions import Failure , AuthFailure , ResponseNotAccepted
@@ -404,26 +405,55 @@ def __init__(self, config=None, **kwargs):
404
405
A Configuration object to use. Identical in behavior to `.Context`.
405
406
406
407
:param run:
407
- A data structure of `Results < .Result>`, to return from calls to
408
- the instantiated object's `~.Context.run` method (instead of
409
- actually executing the requested shell command).
408
+ A data structure indicating what ` .Result` objects to return from
409
+ calls to the instantiated object's `~.Context.run` method (instead
410
+ of actually executing the requested shell command).
410
411
411
412
Specifically, this kwarg accepts:
412
413
413
- - A single `.Result` object, which will be returned once.
414
- - An iterable of `Results <.Result>`, which will be returned on
415
- each subsequent call to ``.run``.
416
- - A map of command strings to either of the above, allowing
417
- specific call-and-response semantics instead of assuming a call
418
- order.
414
+ - A single `.Result` object.
415
+
416
+ - Remember that class's first positional argument is its stdout
417
+ - it can thus be handy to hand in expressions like
418
+ ``Result("command output here!")``.)
419
+
420
+ - A boolean; if True, yields a `.Result` whose ``exited`` is ``0``,
421
+ and if False, ``1``.
422
+ - An iterable of the above values, which will be returned on each
423
+ subsequent call to ``.run`` (the first item on the first call,
424
+ the second on the second call, etc).
425
+ - A dict mapping command strings or compiled regexen to the above
426
+ values (including an iterable), allowing specific
427
+ call-and-response semantics instead of assuming a call order.
419
428
420
429
:param sudo:
421
430
Identical to ``run``, but whose values are yielded from calls to
422
431
`~.Context.sudo`.
423
432
433
+ :param bool repeat:
434
+ A flag determining whether results yielded by this class' methods
435
+ repeat or are consumed.
436
+
437
+ For example, when a single result is indicated, it will normally
438
+ only be returned once, causing `NotImplementedError` afterwards.
439
+ But when ``repeat=True`` is given, that result is returned on
440
+ every call, forever.
441
+
442
+ Similarly, iterable results are normally exhausted once, but when
443
+ this setting is enabled, they are wrapped in `itertools.cycle`.
444
+
445
+ Default: ``False`` (for backwards compatibility reasons).
446
+
424
447
:raises:
425
448
``TypeError``, if the values given to ``run`` or other kwargs
426
- aren't individual `.Result` objects or iterables.
449
+ aren't of the expected types.
450
+
451
+ .. versionchanged:: 1.5
452
+ Added support for boolean and string result values.
453
+ .. versionchanged:: 1.5
454
+ Added support for regex dict keys.
455
+ .. versionchanged:: 1.5
456
+ Added the ``repeat`` keyword argument.
427
457
"""
428
458
# Figure out if we can support Mock in the current environment
429
459
Mock = None
@@ -434,52 +464,77 @@ def __init__(self, config=None, **kwargs):
434
464
from unittest .mock import Mock
435
465
except ImportError :
436
466
pass
437
- # TODO: would be nice to allow regexen instead of exact string matches
467
+ # Set up like any other Context would, with the config
438
468
super (MockContext , self ).__init__ (config )
469
+ # Pull out behavioral kwargs
470
+ self ._set ("__repeat" , kwargs .pop ("repeat" , False ))
471
+ # The rest must be things like run/sudo - mock Context method info
439
472
for method , results in iteritems (kwargs ):
440
- # Special convenience case: individual Result -> one-item list
441
- if (
442
- not hasattr (results , "__iter__" )
443
- and not isinstance (results , Result )
444
- # No need for explicit dict test; they have __iter__
445
- ):
473
+ # For each possible value type, normalize to iterable of Result
474
+ # objects (possibly repeating).
475
+ singletons = tuple ([Result , bool ] + list (string_types ))
476
+ if isinstance (results , dict ):
477
+ for key , value in iteritems (results ):
478
+ results [key ] = self ._normalize (value )
479
+ elif isinstance (results , singletons ) or hasattr (results , "__iter__" ):
480
+ results = self ._normalize (results )
481
+ # Unknown input value: cry
482
+ else :
446
483
err = "Not sure how to yield results from a {!r}"
447
484
raise TypeError (err .format (type (results )))
448
- # Set the return values
485
+ # Save results for use by the method
449
486
self ._set ("__{}" .format (method ), results )
450
487
# Wrap the method in a Mock, if applicable
451
488
if Mock is not None :
452
489
self ._set (method , Mock (wraps = getattr (self , method )))
453
490
491
+ def _normalize (self , value ):
492
+ # First turn everything into an iterable
493
+ if not hasattr (value , "__iter__" ) or isinstance (value , string_types ):
494
+ value = [value ]
495
+ # Then turn everything within into a Result
496
+ results = []
497
+ for obj in value :
498
+ if isinstance (obj , bool ):
499
+ obj = Result (exited = 0 if obj else 1 )
500
+ elif isinstance (obj , string_types ):
501
+ obj = Result (obj )
502
+ results .append (obj )
503
+ # Finally, turn that iterable into an iteratOR, depending on repeat
504
+ return cycle (results ) if getattr (self , "__repeat" ) else iter (results )
505
+
454
506
# TODO: _maybe_ make this more metaprogrammy/flexible (using __call__ etc)?
455
507
# Pretty worried it'd cause more hard-to-debug issues than it's presently
456
508
# worth. Maybe in situations where Context grows a _lot_ of methods (e.g.
457
509
# in Fabric 2; though Fabric could do its own sub-subclass in that case...)
458
510
459
511
def _yield_result (self , attname , command ):
460
- # NOTE: originally had this with a bunch of explicit
461
- # NotImplementedErrors, but it doubled method size, and chance of
462
- # unexpected index/etc errors seems low here.
463
512
try :
464
- # Obtain result if possible
465
- value = getattr (self , attname )
466
- # TODO: thought there's a 'better' 2x3 DictType or w/e, but can't
467
- # find one offhand
468
- if isinstance (value , dict ):
469
- if hasattr (value [command ], "__iter__" ):
470
- result = value [command ].pop (0 )
471
- elif isinstance (value [command ], Result ):
472
- result = value .pop (command )
473
- elif hasattr (value , "__iter__" ):
474
- result = value .pop (0 )
475
- elif isinstance (value , Result ):
476
- result = value
477
- delattr (self , attname )
478
- # Populate command string unless explicitly given
513
+ obj = getattr (self , attname )
514
+ # Dicts need to try direct lookup or regex matching
515
+ if isinstance (obj , dict ):
516
+ try :
517
+ obj = obj [command ]
518
+ except KeyError :
519
+ # TODO: could optimize by skipping this if not any regex
520
+ # objects in keys()?
521
+ for key , value in iteritems (obj ):
522
+ if hasattr (key , "match" ) and key .match (command ):
523
+ obj = value
524
+ break
525
+ else :
526
+ # Nope, nothing did match.
527
+ raise KeyError
528
+ # Here, the value was either never a dict or has been extracted
529
+ # from one, so we can assume it's an iterable of Result objects due
530
+ # to work done by __init__.
531
+ result = next (obj )
532
+ # Populate Result's command string with what matched unless
533
+ # explicitly given
479
534
if not result .command :
480
535
result .command = command
481
536
return result
482
- except (AttributeError , IndexError , KeyError ):
537
+ except (AttributeError , IndexError , KeyError , StopIteration ):
483
538
raise_from (NotImplementedError , None )
484
539
485
540
def run (self , command , * args , ** kwargs ):
@@ -531,4 +586,4 @@ def set_result_for(self, attname, command, result):
531
586
if not isinstance (value , dict ):
532
587
raise heck
533
588
# OK, we're good to modify, so do so.
534
- value [command ] = result
589
+ value [command ] = self . _normalize ( result )
0 commit comments