comparison cmd2/cmd2.py @ 100:3de2a3eb765a

oops, renesting directory
author catherine@dellzilla
date Mon, 29 Sep 2008 12:48:29 -0400
parents cmd2.py@199a08e3ae72
children e4daf715fc31
comparison
equal deleted inserted replaced
99:00898931969b 100:3de2a3eb765a
1 """Variant on standard library's cmd with extra features.
2
3 To use, simply import cmd2.Cmd instead of cmd.Cmd; use precisely as though you
4 were using the standard library's cmd, while enjoying the extra features.
5
6 Searchable command history (commands: "hi", "li", "run")
7 Load commands from file, save to file, edit commands in file
8 Multi-line commands
9 Case-insensitive commands
10 Special-character shortcut commands (beyond cmd's "@" and "!")
11 Settable environment parameters
12 Parsing commands with `optparse` options (flags)
13 Redirection to file with >, >>; input from file with <
14
15 Note that redirection with > and | will only work if `self.stdout.write()`
16 is used in place of `print`. The standard library's `cmd` module is
17 written to use `self.stdout.write()`,
18
19 - Catherine Devlin, Jan 03 2008 - catherinedevlin.blogspot.com
20
21 CHANGES:
22 As of 0.3.0, options should be specified as `optparse` options. See README.txt.
23 flagReader.py options are still supported for backward compatibility
24 """
25 import cmd, re, os, sys, optparse, subprocess, tempfile, pyparsing, doctest
26 from optparse import make_option
27 __version__ = '0.3.7'
28
29 class OptionParser(optparse.OptionParser):
30 def exit(self, status=0, msg=None):
31 self.values._exit = True
32 if msg:
33 print msg
34
35 def error(self, msg):
36 """error(msg : string)
37
38 Print a usage message incorporating 'msg' to stderr and exit.
39 If you override this in a subclass, it should not return -- it
40 should either exit or raise an exception.
41 """
42 raise
43
44 def options(option_list):
45 def option_setup(func):
46 optionParser = OptionParser()
47 for opt in option_list:
48 optionParser.add_option(opt)
49 optionParser.set_usage("%s [options] arg" % func.__name__.strip('do_'))
50 def newFunc(instance, arg):
51 try:
52 opts, arg = optionParser.parse_args(arg.split())
53 arg = ' '.join(arg)
54 except (optparse.OptionValueError, optparse.BadOptionError,
55 optparse.OptionError, optparse.AmbiguousOptionError,
56 optparse.OptionConflictError), e:
57 print e
58 optionParser.print_help()
59 return
60 if hasattr(opts, '_exit'):
61 return None
62 result = func(instance, arg, opts)
63 return result
64 newFunc.__doc__ = '%s\n%s' % (func.__doc__, optionParser.format_help())
65 return newFunc
66 return option_setup
67
68 class PasteBufferError(EnvironmentError):
69 if sys.platform[:3] == 'win':
70 errmsg = """Redirecting to or from paste buffer requires pywin32
71 to be installed on operating system.
72 Download from http://sourceforge.net/projects/pywin32/"""
73 else:
74 errmsg = """Redirecting to or from paste buffer requires xclip
75 to be installed on operating system.
76 On Debian/Ubuntu, 'sudo apt-get install xclip' will install it."""
77 def __init__(self):
78 Exception.__init__(self, self.errmsg)
79
80 '''check here if functions exist; otherwise, stub out'''
81 pastebufferr = """Redirecting to or from paste buffer requires %s
82 to be installed on operating system.
83 %s"""
84 if subprocess.mswindows:
85 try:
86 import win32clipboard
87 def getPasteBuffer():
88 win32clipboard.OpenClipboard(0)
89 try:
90 result = win32clipboard.GetClipboardData()
91 except TypeError:
92 result = '' #non-text
93 win32clipboard.CloseClipboard()
94 return result
95 def writeToPasteBuffer(txt):
96 win32clipboard.OpenClipboard(0)
97 win32clipboard.EmptyClipboard()
98 win32clipboard.SetClipboardText(txt)
99 win32clipboard.CloseClipboard()
100 except ImportError:
101 def getPasteBuffer():
102 raise OSError, pastebufferr % ('pywin32', 'Download from http://sourceforge.net/projects/pywin32/')
103 setPasteBuffer = getPasteBuffer
104 else:
105 can_clip = False
106 try:
107 subprocess.check_call('xclip -o -sel clip', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
108 can_clip = True
109 except AttributeError: # check_call not defined, Python < 2.5
110 teststring = 'Testing for presence of xclip.'
111 xclipproc = subprocess.Popen('xclip -sel clip', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
112 xclipproc.stdin.write(teststring)
113 xclipproc.stdin.close()
114 xclipproc = subprocess.Popen('xclip -o -sel clip', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
115 if xclipproc.stdout.read() == teststring:
116 can_clip = True
117 except (subprocess.CalledProcessError, OSError, IOError):
118 pass
119 if can_clip:
120 def getPasteBuffer():
121 xclipproc = subprocess.Popen('xclip -o -sel clip', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
122 return xclipproc.stdout.read()
123 def writeToPasteBuffer(txt):
124 xclipproc = subprocess.Popen('xclip -sel clip', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
125 xclipproc.stdin.write(txt)
126 xclipproc.stdin.close()
127 # but we want it in both the "primary" and "mouse" clipboards
128 xclipproc = subprocess.Popen('xclip', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
129 xclipproc.stdin.write(txt)
130 xclipproc.stdin.close()
131 else:
132 def getPasteBuffer():
133 raise OSError, pastebufferr % ('xclip', 'On Debian/Ubuntu, install with "sudo apt-get install xclip"')
134 setPasteBuffer = getPasteBuffer
135
136 pyparsing.ParserElement.setDefaultWhitespaceChars(' \t')
137 def parseSearchResults(pattern, s):
138 generator = pattern.scanString(s)
139 try:
140 result, start, stop = generator.next()
141 result['before'], result['after'] = s[:start], s[stop:]
142 result['upToIncluding'] = s[:stop]
143 except StopIteration:
144 result = pyparsing.ParseResults('')
145 result['before'] = s
146 return result
147
148 class Cmd(cmd.Cmd):
149 caseInsensitive = True
150 multilineCommands = []
151 continuationPrompt = '> '
152 shortcuts = {'?': 'help', '!': 'shell', '@': 'load'}
153 excludeFromHistory = '''run r list l history hi ed edit li eof'''.split()
154 defaultExtension = 'txt'
155 defaultFileName = 'command.txt'
156 editor = os.environ.get('EDITOR')
157 _STOP_AND_EXIT = 2
158 if not editor:
159 if sys.platform[:3] == 'win':
160 editor = 'notepad'
161 else:
162 for editor in ['gedit', 'kate', 'vim', 'emacs', 'nano', 'pico']:
163 if not os.system('which %s' % (editor)):
164 break
165
166 settable = ['prompt', 'continuationPrompt', 'defaultFileName', 'editor', 'caseInsensitive']
167 _TO_PASTE_BUFFER = 1
168 def do_cmdenvironment(self, args):
169 self.stdout.write("""
170 Commands are %(casesensitive)scase-sensitive.
171 Commands may be terminated with: %(terminators)s
172 Settable parameters: %(settable)s
173 """ %
174 { 'casesensitive': ('not ' and self.caseInsensitive) or '',
175 'terminators': self.terminatorPattern,
176 'settable': ' '.join(self.settable)
177 })
178
179 def do_help(self, arg):
180 cmd.Cmd.do_help(self, arg)
181 try:
182 fn = getattr(self, 'do_' + arg)
183 if fn and fn.optionParser:
184 fn.optionParser.print_help(file=self.stdout)
185 except AttributeError:
186 pass
187
188 def __init__(self, *args, **kwargs):
189 cmd.Cmd.__init__(self, *args, **kwargs)
190 self.history = History()
191
192 def do_shortcuts(self, args):
193 """Lists single-key shortcuts available."""
194 result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in self.shortcuts.items())
195 self.stdout.write("Single-key shortcuts for other commands:\n%s\n" % (result))
196
197 terminatorPattern = ((pyparsing.Literal(';') ^ pyparsing.Literal('\n\n'))
198 ^ (pyparsing.Literal('\nEOF') + pyparsing.lineEnd))('terminator')
199 argSeparatorPattern = pyparsing.Word(pyparsing.printables)('command') \
200 + pyparsing.SkipTo(pyparsing.StringEnd())('args')
201 filenamePattern = pyparsing.Word(pyparsing.alphanums + '#$-_~{},.!:\\/')
202 integerPattern = pyparsing.Word(pyparsing.nums).setParseAction( lambda s,l,t: [ int(t[0]) ] )
203 pipePattern = pyparsing.Literal('|')('pipe') + pyparsing.restOfLine('pipeTo')
204 redirectOutPattern = (pyparsing.Literal('>>') ^ '>')('output') \
205 + pyparsing.Optional(filenamePattern)('outputTo')
206 redirectInPattern = pyparsing.Literal('<')('input') \
207 + pyparsing.Optional(filenamePattern)('inputFrom')
208 punctuationPattern = pipePattern ^ redirectInPattern ^ redirectOutPattern
209 for p in (terminatorPattern, pipePattern, redirectInPattern, redirectOutPattern, punctuationPattern):
210 p.ignore(pyparsing.sglQuotedString)
211 p.ignore(pyparsing.dblQuotedString)
212
213 def parsed(self, s):
214 '''
215 >>> c = Cmd()
216 >>> r = c.parsed('quotes "are > ignored" < inp.txt')
217 >>> r.statement, r.input, r.inputFrom, r.output, r.outputFrom
218 ('quotes "are > ignored" ', '<', 'inp.txt', '', '')
219 >>> r = c.parsed('very complex; < from.txt >> to.txt etc.')
220 >>> r.statement, r.terminator, r.input, r.inputFrom, r.output, r.outputTo
221 ('very complex;', ';', '<', 'from.txt', '>>', 'to.txt')
222 >>> c.parsed('nothing to parse').statement
223 'nothing to parse'
224 >>> r = c.parsed('ignore > within a terminated statement; > out.txt')
225 >>> r.statement, r.terminator, r.input, r.inputFrom, r.output, r.outputTo
226 ('ignore > within a terminated statement;', ';', '', '', '>', 'out.txt')
227 >>> r = c.parsed('send it to | sort | wc')
228 >>> r.statement, r.pipe, r.pipeTo
229 ('send it to ', '|', ' sort | wc')
230 >>> r = c.parsed('got from < thisfile.txt plus blah blah')
231 >>> r.statement, r.input, r.inputFrom
232 ('got from ', '<', 'thisfile.txt')
233 '''
234 if isinstance(s, pyparsing.ParseResults):
235 return s
236 result = (pyparsing.SkipTo(pyparsing.StringEnd()))('fullStatement').parseString(s)
237 if s[0] in self.shortcuts:
238 s = self.shortcuts[s[0]] + ' ' + s[1:]
239 result['statement'] = s
240 result['parseable'] = s
241 result += parseSearchResults(self.terminatorPattern, s)
242 if result.terminator:
243 result['statement'] = result.upToIncluding
244 result['unterminated'] = result.before
245 result['parseable'] = result.after
246 else:
247 result += parseSearchResults(self.punctuationPattern, s)
248 result['statement'] = result['unterminated'] = result.before
249 result += parseSearchResults(self.pipePattern, result.parseable)
250 result += parseSearchResults(self.redirectInPattern, result.parseable)
251 result += parseSearchResults(self.redirectOutPattern, result.parseable)
252 result += parseSearchResults(self.argSeparatorPattern, result.statement)
253 if self.caseInsensitive:
254 result['command'] = result.command.lower()
255 result['statement'] = '%s %s' % (result.command, result.args)
256 return result
257
258 def extractCommand(self, statement):
259 try:
260 (command, args) = statement.split(None,1)
261 except ValueError:
262 (command, args) = statement, ''
263 if self.caseInsensitive:
264 command = command.lower()
265 return command, args
266
267 def onecmd(self, line, assumeComplete=False):
268 """Interpret the argument as though it had been typed in response
269 to the prompt.
270
271 This may be overridden, but should not normally need to be;
272 see the precmd() and postcmd() methods for useful execution hooks.
273 The return value is a flag indicating whether interpretation of
274 commands by the interpreter should stop.
275
276 """
277 line = line.strip()
278 if not line:
279 return
280 statement = self.parsed(line)
281 while (statement.command in self.multilineCommands) and not \
282 (statement.terminator or assumeComplete):
283 statement = self.parsed('%s\n%s' % (statement.fullStatement,
284 self.pseudo_raw_input(self.continuationPrompt)))
285
286 statekeeper = None
287 stop = 0
288 if statement.input:
289 if statement.inputFrom:
290 try:
291 newinput = open(statement.inputFrom, 'r').read()
292 except OSError, e:
293 print e
294 return 0
295 else:
296 newinput = getPasteBuffer()
297 start, end = self.redirectInPattern.scanString(statement.fullStatement).next()[1:]
298 return self.onecmd('%s%s%s' % (statement.fullStatement[:start],
299 newinput, statement.fullStatement[end:]))
300 if statement.pipe and statement.pipeTo:
301 redirect = subprocess.Popen(statement.pipeTo, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
302 statekeeper = Statekeeper(self, ('stdout',))
303 self.stdout = redirect.stdin
304 elif statement.output:
305 statekeeper = Statekeeper(self, ('stdout',))
306 if statement.outputTo:
307 mode = 'w'
308 if statement.output == '>>':
309 mode = 'a'
310 try:
311 self.stdout = open(statement.outputTo, mode)
312 except OSError, e:
313 print e
314 return 0
315 else:
316 statekeeper = Statekeeper(self, ('stdout',))
317 self.stdout = tempfile.TemporaryFile()
318 if statement.output == '>>':
319 self.stdout.write(getPasteBuffer())
320 stop = cmd.Cmd.onecmd(self, statement.statement)
321 try:
322 if statement.command not in self.excludeFromHistory:
323 self.history.append(statement.fullStatement)
324 finally:
325 if statekeeper:
326 if statement.output and not statement.outputTo:
327 self.stdout.seek(0)
328 writeToPasteBuffer(self.stdout.read())
329 elif statement.pipe:
330 for result in redirect.communicate():
331 statekeeper.stdout.write(result or '')
332 self.stdout.close()
333 statekeeper.restore()
334
335 return stop
336
337 def pseudo_raw_input(self, prompt):
338 """copied from cmd's cmdloop; like raw_input, but accounts for changed stdin, stdout"""
339
340 if self.use_rawinput:
341 try:
342 line = raw_input(prompt)
343 except EOFError:
344 line = 'EOF'
345 else:
346 self.stdout.write(prompt)
347 self.stdout.flush()
348 line = self.stdin.readline()
349 if not len(line):
350 line = 'EOF'
351 else:
352 if line[-1] == '\n': # this was always true in Cmd
353 line = line[:-1]
354 return line
355
356 def cmdloop(self, intro=None):
357 """Repeatedly issue a prompt, accept input, parse an initial prefix
358 off the received input, and dispatch to action methods, passing them
359 the remainder of the line as argument.
360 """
361
362 # An almost perfect copy from Cmd; however, the pseudo_raw_input portion
363 # has been split out so that it can be called separately
364
365 self.preloop()
366 if self.use_rawinput and self.completekey:
367 try:
368 import readline
369 self.old_completer = readline.get_completer()
370 readline.set_completer(self.complete)
371 readline.parse_and_bind(self.completekey+": complete")
372 except ImportError:
373 pass
374 try:
375 if intro is not None:
376 self.intro = intro
377 if self.intro:
378 self.stdout.write(str(self.intro)+"\n")
379 stop = None
380 while not stop:
381 if self.cmdqueue:
382 line = self.cmdqueue.pop(0)
383 else:
384 line = self.pseudo_raw_input(self.prompt)
385 line = self.precmd(line)
386 stop = self.onecmd(line)
387 stop = self.postcmd(stop, line)
388 self.postloop()
389 finally:
390 if self.use_rawinput and self.completekey:
391 try:
392 import readline
393 readline.set_completer(self.old_completer)
394 except ImportError:
395 pass
396 return stop
397
398 def do_EOF(self, arg):
399 return True
400 do_eof = do_EOF
401
402 def clean(self, s):
403 """cleans up a string"""
404 if self.caseInsensitive:
405 return s.strip().lower()
406 return s.strip()
407
408 def showParam(self, param):
409 param = self.clean(param)
410 if param in self.settable:
411 val = getattr(self, param)
412 self.stdout.write('%s: %s\n' % (param, str(getattr(self, param))))
413
414 def do_quit(self, arg):
415 return self._STOP_AND_EXIT
416 do_exit = do_quit
417 do_q = do_quit
418
419 def do_show(self, arg):
420 'Shows value of a parameter'
421 if arg.strip():
422 self.showParam(arg)
423 else:
424 for param in self.settable:
425 self.showParam(param)
426
427 def do_set(self, arg):
428 'Sets a parameter'
429 try:
430 paramName, val = arg.split(None, 1)
431 paramName = self.clean(paramName)
432 if paramName not in self.settable:
433 raise NotSettableError
434 currentVal = getattr(self, paramName)
435 val = cast(currentVal, self.parsed(val).unterminated)
436 setattr(self, paramName, val)
437 self.stdout.write('%s - was: %s\nnow: %s\n' % (paramName, currentVal, val))
438 except (ValueError, AttributeError, NotSettableError), e:
439 self.do_show(arg)
440
441 def do_shell(self, arg):
442 'execute a command as if at the OS prompt.'
443 os.system(arg)
444
445 def do_history(self, arg):
446 """history [arg]: lists past commands issued
447
448 no arg -> list all
449 arg is integer -> list one history item, by index
450 arg is string -> string search
451 arg is /enclosed in forward-slashes/ -> regular expression search
452 """
453 if arg:
454 history = self.history.get(arg)
455 else:
456 history = self.history
457 for hi in history:
458 self.stdout.write(hi.pr())
459 def last_matching(self, arg):
460 try:
461 if arg:
462 return self.history.get(arg)[-1]
463 else:
464 return self.history[-1]
465 except:
466 return None
467 def do_list(self, arg):
468 """list [arg]: lists last command issued
469
470 no arg -> list absolute last
471 arg is integer -> list one history item, by index
472 - arg, arg - (integer) -> list up to or after #arg
473 arg is string -> list last command matching string search
474 arg is /enclosed in forward-slashes/ -> regular expression search
475 """
476 try:
477 self.stdout.write(self.last_matching(arg).pr())
478 except:
479 pass
480 do_hi = do_history
481 do_l = do_list
482 do_li = do_list
483
484 def do_ed(self, arg):
485 """ed: edit most recent command in text editor
486 ed [N]: edit numbered command from history
487 ed [filename]: edit specified file name
488
489 commands are run after editor is closed.
490 "set edit (program-name)" or set EDITOR environment variable
491 to control which editing program is used."""
492 if not self.editor:
493 print "please use 'set editor' to specify your text editing program of choice."
494 return
495 filename = self.defaultFileName
496 buffer = ''
497 try:
498 arg = int(arg)
499 buffer = self.last_matching(arg)
500 except:
501 if arg:
502 filename = arg
503 else:
504 buffer = self.last_matching(arg)
505
506 if buffer:
507 f = open(filename, 'w')
508 f.write(buffer or '')
509 f.close()
510
511 os.system('%s %s' % (self.editor, filename))
512 self.do__load(filename)
513 do_edit = do_ed
514
515 saveparser = (pyparsing.Optional(pyparsing.Word(pyparsing.nums)^'*')("idx") +
516 pyparsing.Optional(pyparsing.Word(pyparsing.printables))("fname") +
517 pyparsing.stringEnd)
518 def do_save(self, arg):
519 """`save [N] [filename.ext]`
520 Saves command from history to file.
521 N => Number of command (from history), or `*`;
522 most recent command if omitted"""
523
524 try:
525 args = self.saveparser.parseString(arg)
526 except pyparsing.ParseException:
527 print self.do_save.__doc__
528 return
529 fname = args.fname or self.defaultFileName
530 if args.idx == '*':
531 saveme = '\n\n'.join(self.history[:])
532 elif args.idx:
533 saveme = self.history[int(args.idx)-1]
534 else:
535 saveme = self.history[-1]
536 try:
537 f = open(fname, 'w')
538 f.write(saveme)
539 f.close()
540 print 'Saved to %s' % (fname)
541 except Exception, e:
542 print 'Error saving %s: %s' % (fname, str(e))
543
544 def do_load(self, fname=None):
545 """Runs command(s) from a file."""
546 if fname is None:
547 fname = self.defaultFileName
548 keepstate = Statekeeper(self, ('stdin','use_rawinput','prompt','continuationPrompt'))
549 if isinstance(fname, file):
550 self.stdin = fname
551 else:
552 try:
553 self.stdin = open(fname, 'r')
554 except IOError, e:
555 try:
556 self.stdin = open('%s.%s' % (fname, self.defaultExtension), 'r')
557 except IOError:
558 print 'Problem opening file %s: \n%s' % (fname, e)
559 keepstate.restore()
560 return
561 self.use_rawinput = False
562 self.prompt = self.continuationPrompt = ''
563 stop = self.cmdloop()
564 self.stdin.close()
565 keepstate.restore()
566 self.lastcmd = ''
567 return (stop == self._STOP_AND_EXIT) and self._STOP_AND_EXIT
568 do__load = do_load # avoid an unfortunate legacy use of do_load from sqlpython
569
570 def do_run(self, arg):
571 """run [arg]: re-runs an earlier command
572
573 no arg -> run most recent command
574 arg is integer -> run one history item, by index
575 arg is string -> run most recent command by string search
576 arg is /enclosed in forward-slashes/ -> run most recent by regex
577 """
578 'run [N]: runs the SQL that was run N commands ago'
579 runme = self.last_matching(arg)
580 print runme
581 if runme:
582 runme = self.precmd(runme)
583 stop = self.onecmd(runme)
584 stop = self.postcmd(stop, runme)
585 do_r = do_run
586
587 def fileimport(self, statement, source):
588 try:
589 f = open(source)
590 except IOError:
591 self.stdout.write("Couldn't read from file %s\n" % source)
592 return ''
593 data = f.read()
594 f.close()
595 return data
596
597 class HistoryItem(str):
598 def __init__(self, instr):
599 str.__init__(self, instr)
600 self.lowercase = self.lower()
601 self.idx = None
602 def pr(self):
603 return '-------------------------[%d]\n%s\n' % (self.idx, str(self))
604
605 class History(list):
606 rangeFrom = re.compile(r'^([\d])+\s*\-$')
607 def append(self, new):
608 new = HistoryItem(new)
609 list.append(self, new)
610 new.idx = len(self)
611 def extend(self, new):
612 for n in new:
613 self.append(n)
614 def get(self, getme):
615 try:
616 getme = int(getme)
617 if getme < 0:
618 return self[:(-1 * getme)]
619 else:
620 return [self[getme-1]]
621 except IndexError:
622 return []
623 except (ValueError, TypeError):
624 getme = getme.strip()
625 mtch = self.rangeFrom.search(getme)
626 if mtch:
627 return self[(int(mtch.group(1))-1):]
628 if getme.startswith(r'/') and getme.endswith(r'/'):
629 finder = re.compile(getme[1:-1], re.DOTALL | re.MULTILINE | re.IGNORECASE)
630 def isin(hi):
631 return finder.search(hi)
632 else:
633 def isin(hi):
634 return (getme.lower() in hi.lowercase)
635 return [itm for itm in self if isin(itm)]
636
637 class NotSettableError(Exception):
638 pass
639
640 def cast(current, new):
641 """Tries to force a new value into the same type as the current."""
642 typ = type(current)
643 if typ == bool:
644 try:
645 return bool(int(new))
646 except ValueError, TypeError:
647 pass
648 try:
649 new = new.lower()
650 except:
651 pass
652 if (new=='on') or (new[0] in ('y','t')):
653 return True
654 if (new=='off') or (new[0] in ('n','f')):
655 return False
656 else:
657 try:
658 return typ(new)
659 except:
660 pass
661 print "Problem setting parameter (now %s) to %s; incorrect type?" % (current, new)
662 return current
663
664 class Statekeeper(object):
665 def __init__(self, obj, attribs):
666 self.obj = obj
667 self.attribs = attribs
668 self.save()
669 def save(self):
670 for attrib in self.attribs:
671 setattr(self, attrib, getattr(self.obj, attrib))
672 def restore(self):
673 for attrib in self.attribs:
674 setattr(self.obj, attrib, getattr(self, attrib))
675
676 if __name__ == '__main__':
677 doctest.testmod()