Package duplicity :: Module GnuPGInterface
[hide private]
[frames] | no frames]

Source Code for Module duplicity.GnuPGInterface

  1  """Interface to GNU Privacy Guard (GnuPG) 
  2   
  3  GnuPGInterface is a Python module to interface with GnuPG. 
  4  It concentrates on interacting with GnuPG via filehandles, 
  5  providing access to control GnuPG via versatile and extensible means. 
  6   
  7  This module is based on GnuPG::Interface, a Perl module by the same author. 
  8   
  9  Normally, using this module will involve creating a 
 10  GnuPG object, setting some options in it's 'options' data member 
 11  (which is of type Options), creating some pipes 
 12  to talk with GnuPG, and then calling the run() method, which will 
 13  connect those pipes to the GnuPG process. run() returns a 
 14  Process object, which contains the filehandles to talk to GnuPG with. 
 15   
 16  Example code: 
 17   
 18  >>> import GnuPGInterface 
 19  >>> 
 20  >>> plaintext  = "Three blind mice" 
 21  >>> passphrase = "This is the passphrase" 
 22  >>> 
 23  >>> gnupg = GnuPGInterface.GnuPG() 
 24  >>> gnupg.options.armor = 1 
 25  >>> gnupg.options.meta_interactive = 0 
 26  >>> gnupg.options.extra_args.append('--no-secmem-warning') 
 27  >>> 
 28  >>> # Normally we might specify something in 
 29  >>> # gnupg.options.recipients, like 
 30  >>> # gnupg.options.recipients = [ '0xABCD1234', 'bob@foo.bar' ] 
 31  >>> # but since we're doing symmetric-only encryption, it's not needed. 
 32  >>> # If you are doing standard, public-key encryption, using 
 33  >>> # --encrypt, you will need to specify recipients before 
 34  >>> # calling gnupg.run() 
 35  >>> 
 36  >>> # First we'll encrypt the test_text input symmetrically 
 37  >>> p1 = gnupg.run(['--symmetric'], 
 38  ...                create_fhs=['stdin', 'stdout', 'passphrase']) 
 39  >>> 
 40  >>> p1.handles['passphrase'].write(passphrase) 
 41  >>> p1.handles['passphrase'].close() 
 42  >>> 
 43  >>> p1.handles['stdin'].write(plaintext) 
 44  >>> p1.handles['stdin'].close() 
 45  >>> 
 46  >>> ciphertext = p1.handles['stdout'].read() 
 47  >>> p1.handles['stdout'].close() 
 48  >>> 
 49  >>> # process cleanup 
 50  >>> p1.wait() 
 51  >>> 
 52  >>> # Now we'll decrypt what we just encrypted it, 
 53  >>> # using the convience method to get the 
 54  >>> # passphrase to GnuPG 
 55  >>> gnupg.passphrase = passphrase 
 56  >>> 
 57  >>> p2 = gnupg.run(['--decrypt'], create_fhs=['stdin', 'stdout']) 
 58  >>> 
 59  >>> p2.handles['stdin'].write(ciphertext) 
 60  >>> p2.handles['stdin'].close() 
 61  >>> 
 62  >>> decrypted_plaintext = p2.handles['stdout'].read() 
 63  >>> p2.handles['stdout'].close() 
 64  >>> 
 65  >>> # process cleanup 
 66  >>> p2.wait() 
 67  >>> 
 68  >>> # Our decrypted plaintext: 
 69  >>> decrypted_plaintext 
 70  'Three blind mice' 
 71  >>> 
 72  >>> # ...and see it's the same as what we orignally encrypted 
 73  >>> assert decrypted_plaintext == plaintext, \ 
 74            "GnuPG decrypted output does not match original input" 
 75  >>> 
 76  >>> 
 77  >>> ################################################## 
 78  >>> # Now let's trying using run()'s attach_fhs paramter 
 79  >>> 
 80  >>> # we're assuming we're running on a unix... 
 81  >>> input = open('/etc/motd') 
 82  >>> 
 83  >>> p1 = gnupg.run(['--symmetric'], create_fhs=['stdout'], 
 84  ...                                 attach_fhs={'stdin': input}) 
 85  >>> 
 86  >>> # GnuPG will read the stdin from /etc/motd 
 87  >>> ciphertext = p1.handles['stdout'].read() 
 88  >>> 
 89  >>> # process cleanup 
 90  >>> p1.wait() 
 91  >>> 
 92  >>> # Now let's run the output through GnuPG 
 93  >>> # We'll write the output to a temporary file, 
 94  >>> import tempfile 
 95  >>> temp = tempfile.TemporaryFile() 
 96  >>> 
 97  >>> p2 = gnupg.run(['--decrypt'], create_fhs=['stdin'], 
 98  ...                               attach_fhs={'stdout': temp}) 
 99  >>> 
100  >>> # give GnuPG our encrypted stuff from the first run 
101  >>> p2.handles['stdin'].write(ciphertext) 
102  >>> p2.handles['stdin'].close() 
103  >>> 
104  >>> # process cleanup 
105  >>> p2.wait() 
106  >>> 
107  >>> # rewind the tempfile and see what GnuPG gave us 
108  >>> temp.seek(0) 
109  >>> decrypted_plaintext = temp.read() 
110  >>> 
111  >>> # compare what GnuPG decrypted with our original input 
112  >>> input.seek(0) 
113  >>> input_data = input.read() 
114  >>> 
115  >>> assert decrypted_plaintext == input_data, \ 
116             "GnuPG decrypted output does not match original input" 
117   
118  To do things like public-key encryption, simply pass do something 
119  like: 
120   
121  gnupg.passphrase = 'My passphrase' 
122  gnupg.options.recipients = [ 'bob@foobar.com' ] 
123  gnupg.run( ['--sign', '--encrypt'], create_fhs=..., attach_fhs=...) 
124   
125  Here is an example of subclassing GnuPGInterface.GnuPG, 
126  so that it has an encrypt_string() method that returns 
127  ciphertext. 
128   
129  >>> import GnuPGInterface 
130  >>> 
131  >>> class MyGnuPG(GnuPGInterface.GnuPG): 
132  ... 
133  ...     def __init__(self): 
134  ...         GnuPGInterface.GnuPG.__init__(self) 
135  ...         self.setup_my_options() 
136  ... 
137  ...     def setup_my_options(self): 
138  ...         self.options.armor = 1 
139  ...         self.options.meta_interactive = 0 
140  ...         self.options.extra_args.append('--no-secmem-warning') 
141  ... 
142  ...     def encrypt_string(self, string, recipients): 
143  ...        gnupg.options.recipients = recipients   # a list! 
144  ... 
145  ...        proc = gnupg.run(['--encrypt'], create_fhs=['stdin', 'stdout']) 
146  ... 
147  ...        proc.handles['stdin'].write(string) 
148  ...        proc.handles['stdin'].close() 
149  ... 
150  ...        output = proc.handles['stdout'].read() 
151  ...        proc.handles['stdout'].close() 
152  ... 
153  ...        proc.wait() 
154  ...        return output 
155  ... 
156  >>> gnupg = MyGnuPG() 
157  >>> ciphertext = gnupg.encrypt_string("The secret", ['0x260C4FA3']) 
158  >>> 
159  >>> # just a small sanity test here for doctest 
160  >>> import types 
161  >>> assert isinstance(ciphertext, types.StringType), \ 
162             "What GnuPG gave back is not a string!" 
163   
164  Here is an example of generating a key: 
165  >>> import GnuPGInterface 
166  >>> gnupg = GnuPGInterface.GnuPG() 
167  >>> gnupg.options.meta_interactive = 0 
168  >>> 
169  >>> # We will be creative and use the logger filehandle to capture 
170  >>> # what GnuPG says this time, instead stderr; no stdout to listen to, 
171  >>> # but we capture logger to surpress the dry-run command. 
172  >>> # We also have to capture stdout since otherwise doctest complains; 
173  >>> # Normally you can let stdout through when generating a key. 
174  >>> 
175  >>> proc = gnupg.run(['--gen-key'], create_fhs=['stdin', 'stdout', 
176  ...                                             'logger']) 
177  >>> 
178  >>> proc.handles['stdin'].write('''Key-Type: DSA 
179  ... Key-Length: 1024 
180  ... # We are only testing syntax this time, so dry-run 
181  ... %dry-run 
182  ... Subkey-Type: ELG-E 
183  ... Subkey-Length: 1024 
184  ... Name-Real: Joe Tester 
185  ... Name-Comment: with stupid passphrase 
186  ... Name-Email: joe@foo.bar 
187  ... Expire-Date: 2y 
188  ... Passphrase: abc 
189  ... %pubring foo.pub 
190  ... %secring foo.sec 
191  ... ''') 
192  >>> 
193  >>> proc.handles['stdin'].close() 
194  >>> 
195  >>> report = proc.handles['logger'].read() 
196  >>> proc.handles['logger'].close() 
197  >>> 
198  >>> proc.wait() 
199   
200   
201  COPYRIGHT: 
202   
203  Copyright (C) 2001  Frank J. Tobin, ftobin@neverending.org 
204   
205  LICENSE: 
206   
207  This library is free software; you can redistribute it and/or 
208  modify it under the terms of the GNU Lesser General Public 
209  License as published by the Free Software Foundation; either 
210  version 2.1 of the License, or (at your option) any later version. 
211   
212  This library is distributed in the hope that it will be useful, 
213  but WITHOUT ANY WARRANTY; without even the implied warranty of 
214  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
215  Lesser General Public License for more details. 
216   
217  You should have received a copy of the GNU Lesser General Public 
218  License along with this library; if not, write to the Free Software 
219  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
220  or see http://www.gnu.org/copyleft/lesser.html 
221  """ 
222   
223  import os 
224  import sys 
225  import fcntl 
226   
227  from duplicity import log 
228   
229  try: 
230      import threading 
231  except ImportError: 
232      import dummy_threading #@UnusedImport 
233      log.Warn("Threading not available -- zombie processes may appear") 
234   
235  __author__   = "Frank J. Tobin, ftobin@neverending.org" 
236  __version__  = "0.3.2" 
237  __revision__ = "$Id: duplicity.GnuPGInterface-pysrc.html,v 1.14 2011/11/25 19:41:03 loafman Exp $" 
238   
239  # "standard" filehandles attached to processes 
240  _stds = [ 'stdin', 'stdout', 'stderr' ] 
241   
242  # the permissions each type of fh needs to be opened with 
243  _fd_modes = { 'stdin':      'w', 
244                'stdout':     'r', 
245                'stderr':     'r', 
246                'passphrase': 'w', 
247                'command':    'w', 
248                'logger':     'r', 
249                'status':     'r' 
250                } 
251   
252  # correlation between handle names and the arguments we'll pass 
253  _fd_options = { 'passphrase': '--passphrase-fd', 
254                  'logger':     '--logger-fd', 
255                  'status':     '--status-fd', 
256                  'command':    '--command-fd' } 
257   
258 -class GnuPG:
259 """Class instances represent GnuPG. 260 261 Instance attributes of a GnuPG object are: 262 263 * call -- string to call GnuPG with. Defaults to "gpg" 264 265 * passphrase -- Since it is a common operation 266 to pass in a passphrase to GnuPG, 267 and working with the passphrase filehandle mechanism directly 268 can be mundane, if set, the passphrase attribute 269 works in a special manner. If the passphrase attribute is set, 270 and no passphrase file object is sent in to run(), 271 then GnuPG instnace will take care of sending the passphrase to 272 GnuPG, the executable instead of having the user sent it in manually. 273 274 * options -- Object of type GnuPGInterface.Options. 275 Attribute-setting in options determines 276 the command-line options used when calling GnuPG. 277 """ 278
279 - def __init__(self):
280 self.call = 'gpg' 281 self.passphrase = None 282 self.options = Options()
283
284 - def run(self, gnupg_commands, args=None, create_fhs=None, attach_fhs=None):
285 """Calls GnuPG with the list of string commands gnupg_commands, 286 complete with prefixing dashes. 287 For example, gnupg_commands could be 288 '["--sign", "--encrypt"]' 289 Returns a GnuPGInterface.Process object. 290 291 args is an optional list of GnuPG command arguments (not options), 292 such as keyID's to export, filenames to process, etc. 293 294 create_fhs is an optional list of GnuPG filehandle 295 names that will be set as keys of the returned Process object's 296 'handles' attribute. The generated filehandles can be used 297 to communicate with GnuPG via standard input, standard output, 298 the status-fd, passphrase-fd, etc. 299 300 Valid GnuPG filehandle names are: 301 * stdin 302 * stdout 303 * stderr 304 * status 305 * passphase 306 * command 307 * logger 308 309 The purpose of each filehandle is described in the GnuPG 310 documentation. 311 312 attach_fhs is an optional dictionary with GnuPG filehandle 313 names mapping to opened files. GnuPG will read or write 314 to the file accordingly. For example, if 'my_file' is an 315 opened file and 'attach_fhs[stdin] is my_file', then GnuPG 316 will read its standard input from my_file. This is useful 317 if you want GnuPG to read/write to/from an existing file. 318 For instance: 319 320 f = open("encrypted.gpg") 321 gnupg.run(["--decrypt"], attach_fhs={'stdin': f}) 322 323 Using attach_fhs also helps avoid system buffering 324 issues that can arise when using create_fhs, which 325 can cause the process to deadlock. 326 327 If not mentioned in create_fhs or attach_fhs, 328 GnuPG filehandles which are a std* (stdin, stdout, stderr) 329 are defaulted to the running process' version of handle. 330 Otherwise, that type of handle is simply not used when calling GnuPG. 331 For example, if you do not care about getting data from GnuPG's 332 status filehandle, simply do not specify it. 333 334 run() returns a Process() object which has a 'handles' 335 which is a dictionary mapping from the handle name 336 (such as 'stdin' or 'stdout') to the respective 337 newly-created FileObject connected to the running GnuPG process. 338 For instance, if the call was 339 340 process = gnupg.run(["--decrypt"], stdin=1) 341 342 after run returns 'process.handles["stdin"]' 343 is a FileObject connected to GnuPG's standard input, 344 and can be written to. 345 """ 346 347 if args == None: args = [] 348 if create_fhs == None: create_fhs = [] 349 if attach_fhs == None: attach_fhs = {} 350 351 for std in _stds: 352 if not attach_fhs.has_key(std) \ 353 and std not in create_fhs: 354 attach_fhs.setdefault(std, getattr(sys, std)) 355 356 handle_passphrase = 0 357 358 if self.passphrase != None \ 359 and not attach_fhs.has_key('passphrase') \ 360 and 'passphrase' not in create_fhs: 361 handle_passphrase = 1 362 create_fhs.append('passphrase') 363 364 process = self._attach_fork_exec(gnupg_commands, args, 365 create_fhs, attach_fhs) 366 367 if handle_passphrase: 368 passphrase_fh = process.handles['passphrase'] 369 passphrase_fh.write( self.passphrase ) 370 passphrase_fh.close() 371 del process.handles['passphrase'] 372 373 return process
374 375
376 - def _attach_fork_exec(self, gnupg_commands, args, create_fhs, attach_fhs):
377 """This is like run(), but without the passphrase-helping 378 (note that run() calls this).""" 379 380 process = Process() 381 382 for fh_name in create_fhs + attach_fhs.keys(): 383 if not _fd_modes.has_key(fh_name): 384 raise KeyError, \ 385 "unrecognized filehandle name '%s'; must be one of %s" \ 386 % (fh_name, _fd_modes.keys()) 387 388 for fh_name in create_fhs: 389 # make sure the user doesn't specify a filehandle 390 # to be created *and* attached 391 if attach_fhs.has_key(fh_name): 392 raise ValueError, \ 393 "cannot have filehandle '%s' in both create_fhs and attach_fhs" \ 394 % fh_name 395 396 pipe = os.pipe() 397 # fix by drt@un.bewaff.net noting 398 # that since pipes are unidirectional on some systems, 399 # so we have to 'turn the pipe around' 400 # if we are writing 401 if _fd_modes[fh_name] == 'w': pipe = (pipe[1], pipe[0]) 402 process._pipes[fh_name] = Pipe(pipe[0], pipe[1], 0) 403 404 for fh_name, fh in attach_fhs.items(): 405 process._pipes[fh_name] = Pipe(fh.fileno(), fh.fileno(), 1) 406 407 process.pid = os.fork() 408 if process.pid != 0: 409 # start a threaded_waitpid on the child 410 process.thread = threading.Thread(target=threaded_waitpid, 411 name="wait%d" % process.pid, 412 args=(process,)) 413 process.thread.start() 414 415 if process.pid == 0: self._as_child(process, gnupg_commands, args) 416 return self._as_parent(process)
417 418
419 - def _as_parent(self, process):
420 """Stuff run after forking in parent""" 421 for k, p in process._pipes.items(): 422 if not p.direct: 423 os.close(p.child) 424 process.handles[k] = os.fdopen(p.parent, _fd_modes[k]) 425 426 # user doesn't need these 427 del process._pipes 428 429 return process
430 431
432 - def _as_child(self, process, gnupg_commands, args):
433 """Stuff run after forking in child""" 434 # child 435 for std in _stds: 436 p = process._pipes[std] 437 os.dup2( p.child, getattr(sys, "__%s__" % std).fileno() ) 438 439 for k, p in process._pipes.items(): 440 if p.direct and k not in _stds: 441 # we want the fh to stay open after execing 442 fcntl.fcntl( p.child, fcntl.F_SETFD, 0 ) 443 444 fd_args = [] 445 446 for k, p in process._pipes.items(): 447 # set command-line options for non-standard fds 448 if k not in _stds: 449 fd_args.extend([ _fd_options[k], "%d" % p.child ]) 450 451 if not p.direct: os.close(p.parent) 452 453 command = [ self.call ] + fd_args + self.options.get_args() \ 454 + gnupg_commands + args 455 456 os.execvp( command[0], command )
457 458
459 -class Pipe:
460 """simple struct holding stuff about pipes we use"""
461 - def __init__(self, parent, child, direct):
462 self.parent = parent 463 self.child = child 464 self.direct = direct
465 466
467 -class Options:
468 """Objects of this class encompass options passed to GnuPG. 469 This class is responsible for determining command-line arguments 470 which are based on options. It can be said that a GnuPG 471 object has-a Options object in its options attribute. 472 473 Attributes which correlate directly to GnuPG options: 474 475 Each option here defaults to false or None, and is described in 476 GnuPG documentation. 477 478 Booleans (set these attributes to booleans) 479 480 * armor 481 * no_greeting 482 * no_verbose 483 * quiet 484 * batch 485 * always_trust 486 * rfc1991 487 * openpgp 488 * force_v3_sigs 489 * no_options 490 * textmode 491 492 Strings (set these attributes to strings) 493 494 * homedir 495 * default_key 496 * comment 497 * compress_algo 498 * options 499 500 Lists (set these attributes to lists) 501 502 * recipients (***NOTE*** plural of 'recipient') 503 * encrypt_to 504 505 Meta options 506 507 Meta options are options provided by this module that do 508 not correlate directly to any GnuPG option by name, 509 but are rather bundle of options used to accomplish 510 a specific goal, such as obtaining compatibility with PGP 5. 511 The actual arguments each of these reflects may change with time. Each 512 defaults to false unless otherwise specified. 513 514 meta_pgp_5_compatible -- If true, arguments are generated to try 515 to be compatible with PGP 5.x. 516 517 meta_pgp_2_compatible -- If true, arguments are generated to try 518 to be compatible with PGP 2.x. 519 520 meta_interactive -- If false, arguments are generated to try to 521 help the using program use GnuPG in a non-interactive 522 environment, such as CGI scripts. Default is true. 523 524 extra_args -- Extra option arguments may be passed in 525 via the attribute extra_args, a list. 526 527 >>> import GnuPGInterface 528 >>> 529 >>> gnupg = GnuPGInterface.GnuPG() 530 >>> gnupg.options.armor = 1 531 >>> gnupg.options.recipients = ['Alice', 'Bob'] 532 >>> gnupg.options.extra_args = ['--no-secmem-warning'] 533 >>> 534 >>> # no need for users to call this normally; just for show here 535 >>> gnupg.options.get_args() 536 ['--armor', '--recipient', 'Alice', '--recipient', 'Bob', '--no-secmem-warning'] 537 """ 538
539 - def __init__(self):
540 # booleans 541 self.armor = 0 542 self.no_greeting = 0 543 self.verbose = 0 544 self.no_verbose = 0 545 self.quiet = 0 546 self.batch = 0 547 self.always_trust = 0 548 self.rfc1991 = 0 549 self.openpgp = 0 550 self.force_v3_sigs = 0 551 self.no_options = 0 552 self.textmode = 0 553 554 # meta-option booleans 555 self.meta_pgp_5_compatible = 0 556 self.meta_pgp_2_compatible = 0 557 self.meta_interactive = 1 558 559 # strings 560 self.homedir = None 561 self.default_key = None 562 self.comment = None 563 self.compress_algo = None 564 self.options = None 565 566 # lists 567 self.encrypt_to = [] 568 self.recipients = [] 569 570 # miscellaneous arguments 571 self.extra_args = []
572
573 - def get_args( self ):
574 """Generate a list of GnuPG arguments based upon attributes.""" 575 576 return self.get_meta_args() + self.get_standard_args() + self.extra_args
577
578 - def get_standard_args( self ):
579 """Generate a list of standard, non-meta or extra arguments""" 580 args = [] 581 if self.homedir != None: args.extend( [ '--homedir', self.homedir ] ) 582 if self.options != None: args.extend( [ '--options', self.options ] ) 583 if self.comment != None: args.extend( [ '--comment', self.comment ] ) 584 if self.compress_algo != None: args.extend( [ '--compress-algo', self.compress_algo ] ) 585 if self.default_key != None: args.extend( [ '--default-key', self.default_key ] ) 586 587 if self.no_options: args.append( '--no-options' ) 588 if self.armor: args.append( '--armor' ) 589 if self.textmode: args.append( '--textmode' ) 590 if self.no_greeting: args.append( '--no-greeting' ) 591 if self.verbose: args.append( '--verbose' ) 592 if self.no_verbose: args.append( '--no-verbose' ) 593 if self.quiet: args.append( '--quiet' ) 594 if self.batch: args.append( '--batch' ) 595 if self.always_trust: args.append( '--always-trust' ) 596 if self.force_v3_sigs: args.append( '--force-v3-sigs' ) 597 if self.rfc1991: args.append( '--rfc1991' ) 598 if self.openpgp: args.append( '--openpgp' ) 599 600 for r in self.recipients: args.extend( [ '--recipient', r ] ) 601 for r in self.encrypt_to: args.extend( [ '--encrypt-to', r ] ) 602 603 return args
604
605 - def get_meta_args( self ):
606 """Get a list of generated meta-arguments""" 607 args = [] 608 609 if self.meta_pgp_5_compatible: args.extend( [ '--compress-algo', '1', 610 '--force-v3-sigs' 611 ] ) 612 if self.meta_pgp_2_compatible: args.append( '--rfc1991' ) 613 if not self.meta_interactive: args.extend( [ '--batch', '--no-tty' ] ) 614 615 return args
616 617
618 -class Process:
619 """Objects of this class encompass properties of a GnuPG 620 process spawned by GnuPG.run(). 621 622 # gnupg is a GnuPG object 623 process = gnupg.run( [ '--decrypt' ], stdout = 1 ) 624 out = process.handles['stdout'].read() 625 ... 626 os.waitpid( process.pid, 0 ) 627 628 Data Attributes 629 630 handles -- This is a map of filehandle-names to 631 the file handles, if any, that were requested via run() and hence 632 are connected to the running GnuPG process. Valid names 633 of this map are only those handles that were requested. 634 635 pid -- The PID of the spawned GnuPG process. 636 Useful to know, since once should call 637 os.waitpid() to clean up the process, especially 638 if multiple calls are made to run(). 639 """ 640
641 - def __init__(self):
642 self._pipes = {} 643 self.handles = {} 644 self.pid = None 645 self._waited = None 646 self.thread = None 647 self.returned = None
648
649 - def wait(self):
650 """ 651 Wait on threaded_waitpid to exit and examine results. 652 Will raise an IOError if the process exits non-zero. 653 """ 654 if self.returned == None: 655 self.thread.join() 656 if self.returned != 0: 657 raise IOError, "GnuPG exited non-zero, with code %d" % (self.returned >> 8)
658 659
660 -def threaded_waitpid(process):
661 """ 662 When started as a thread with the Process object, thread 663 will execute an immediate waitpid() against the process 664 pid and will collect the process termination info. This 665 will allow us to reap child processes as soon as possible, 666 thus freeing resources quickly. 667 """ 668 try: 669 process.returned = os.waitpid(process.pid, 0)[1] 670 except: 671 log.Debug("GPG process %d terminated before wait()" % process.pid) 672 process.returned = 0
673 674
675 -def _run_doctests():
676 import doctest, GnuPGInterface #@UnresolvedImport 677 return doctest.testmod(GnuPGInterface)
678 679 # deprecated 680 GnuPGInterface = GnuPG 681 682 if __name__ == '__main__': 683 _run_doctests() 684