@@ -194,10 +194,12 @@ class capture_output(object):
194
194
195
195
capture_fd : bool
196
196
197
- If True, we will also redirect the low-level file descriptors
198
- associated with stdout (1) and stderr (2) to the ``output``.
199
- This is useful for capturing output emitted directly to the
200
- process stdout / stderr by external compiled modules.
197
+ If True, we will also redirect the process file descriptors
198
+ ``1`` (stdout), ``2`` (stderr), and the file descriptors from
199
+ ``sys.stdout.fileno()`` and ``sys.stderr.fileno()`` to the
200
+ ``output``. This is useful for capturing output emitted
201
+ directly to the process stdout / stderr by external compiled
202
+ modules.
201
203
202
204
Returns
203
205
-------
@@ -231,19 +233,59 @@ def __enter__(self):
231
233
sys .stdout = self .tee .STDOUT
232
234
sys .stderr = self .tee .STDERR
233
235
if self .capture_fd :
234
- self .fd_redirect = (
235
- redirect_fd (1 , self .tee .STDOUT .fileno (), synchronize = False ),
236
- redirect_fd (2 , self .tee .STDERR .fileno (), synchronize = False ),
236
+ tee_fd = (self .tee .STDOUT .fileno (), self .tee .STDERR .fileno ())
237
+ self .fd_redirect = []
238
+ for i in range (2 ):
239
+ # Redirect the standard process file descriptor (1 or 2)
240
+ self .fd_redirect .append (
241
+ redirect_fd (i + 1 , tee_fd [i ], synchronize = False )
242
+ )
243
+ # Redirect the file descriptor currently associated with
244
+ # sys.stdout / sys.stderr
245
+ try :
246
+ fd = self .old [i ].fileno ()
247
+ except (AttributeError , OSError ):
248
+ pass
249
+ else :
250
+ if fd != i + 1 :
251
+ self .fd_redirect .append (
252
+ redirect_fd (fd , tee_fd [i ], synchronize = False )
253
+ )
254
+ for fdr in self .fd_redirect :
255
+ fdr .__enter__ ()
256
+ # We have an issue where we are (very aggressively)
257
+ # commandeering the terminal. This is what we intend, but the
258
+ # side effect is that any errors generated by this module (e.g.,
259
+ # because the user gave us an invalid output stream) get
260
+ # completely suppressed. So, we will make an exception to the
261
+ # output that we are catching and let messages logged to THIS
262
+ # logger to still be emitted.
263
+ if self .capture_fd :
264
+ # Because we are also comandeering the FD that underlies
265
+ # self.old[1], we cannot just write to that stream and
266
+ # instead open a new stream to the original FD.
267
+ #
268
+ # Note that we need to duplicate the FD from the redirector,
269
+ # as it will close the (temporary) `original_fd` descriptor
270
+ # when it restores the actual original descriptor
271
+ self .temp_log_stream = os .fdopen (
272
+ os .dup (self .fd_redirect [- 1 ].original_fd ), mode = "w" , closefd = True
237
273
)
238
- self .fd_redirect [0 ].__enter__ ()
239
- self .fd_redirect [1 ].__enter__ ()
274
+ else :
275
+ self .temp_log_stream = self .old [1 ]
276
+ self .temp_log_handler = logging .StreamHandler (self .temp_log_stream )
277
+ logger .addHandler (self .temp_log_handler )
278
+ self ._propagate = logger .propagate
279
+ logger .propagate = False
240
280
return self .output_stream
241
281
242
282
def __exit__ (self , et , ev , tb ):
283
+ # Restore any file descriptors we comandeered
243
284
if self .fd_redirect is not None :
244
- self .fd_redirect [ 1 ]. __exit__ ( et , ev , tb )
245
- self . fd_redirect [ 0 ] .__exit__ (et , ev , tb )
285
+ for fdr in reversed ( self .fd_redirect ):
286
+ fdr .__exit__ (et , ev , tb )
246
287
self .fd_redirect = None
288
+ # Check and restore sys.stderr / sys.stdout
247
289
FAIL = self .tee .STDOUT is not sys .stdout
248
290
self .tee .__exit__ (et , ev , tb )
249
291
if self .output_stream is not self .output :
@@ -252,6 +294,15 @@ def __exit__(self, et, ev, tb):
252
294
self .old = None
253
295
self .tee = None
254
296
self .output_stream = None
297
+ # Clean up our temporary override of the local logger
298
+ self .temp_log_handler .flush ()
299
+ logger .removeHandler (self .temp_log_handler )
300
+ if self .capture_fd :
301
+ self .temp_log_stream .flush ()
302
+ self .temp_log_stream .close ()
303
+ logger .propagate = self ._propagate
304
+ self .temp_log_stream = None
305
+ self .temp_log_handler = None
255
306
if FAIL :
256
307
raise RuntimeError ('Captured output does not match sys.stdout.' )
257
308
@@ -378,47 +429,66 @@ def writeOutputBuffer(self, ostreams, flush):
378
429
if not ostring :
379
430
return
380
431
381
- for local_stream , user_stream in ostreams :
432
+ for stream in ostreams :
382
433
try :
383
- written = local_stream .write (ostring )
434
+ written = stream .write (ostring )
384
435
except :
385
- written = 0
436
+ my_repr = "<%s.%s @ %s>" % (
437
+ stream .__class__ .__module__ ,
438
+ stream .__class__ .__name__ ,
439
+ hex (id (stream )),
440
+ )
441
+ if my_repr in ostring :
442
+ # In the case of nested capture_outputs, all the
443
+ # handlers are left on the logger. We want to make
444
+ # sure that we don't create an infinite loop by
445
+ # re-printing a message *this* object generated.
446
+ continue
447
+ et , e , tb = sys .exc_info ()
448
+ msg = "Error writing to output stream %s:\n %s: %s\n " % (
449
+ my_repr ,
450
+ et .__name__ ,
451
+ e ,
452
+ )
453
+ if getattr (stream , 'closed' , False ):
454
+ msg += "Output stream closed before all output was written to it."
455
+ else :
456
+ msg += "Is this a writeable TextIOBase object?"
457
+ logger .error (
458
+ f"{ msg } \n The following was left in the output buffer:\n "
459
+ f" { ostring !r} "
460
+ )
461
+ continue
386
462
if flush or (written and not self .buffering ):
387
- local_stream .flush ()
388
- if local_stream is not user_stream :
389
- user_stream .flush ()
463
+ stream .flush ()
390
464
# Note: some derived file-like objects fail to return the
391
465
# number of characters written (and implicitly return None).
392
466
# If we get None, we will just assume that everything was
393
467
# fine (as opposed to tossing the incomplete write error).
394
468
if written is not None and written != len (ostring ):
469
+ my_repr = "<%s.%s @ %s>" % (
470
+ stream .__class__ .__module__ ,
471
+ stream .__class__ .__name__ ,
472
+ hex (id (stream )),
473
+ )
474
+ if my_repr in ostring [written :]:
475
+ continue
395
476
logger .error (
396
- "Output stream (%s) closed before all output was "
397
- "written to it. The following was left in "
398
- "the output buffer:\n \t %r" % (local_stream , ostring [written :])
477
+ "Incomplete write to output stream %s.\n The following was "
478
+ "left in the output buffer:\n %r" % (my_repr , ostring [written :])
399
479
)
400
480
401
481
402
482
class TeeStream (object ):
403
483
def __init__ (self , * ostreams , encoding = None , buffering = - 1 ):
404
- self .ostreams = []
484
+ self .ostreams = ostreams
405
485
self .encoding = encoding
406
486
self .buffering = buffering
407
487
self ._stdout = None
408
488
self ._stderr = None
409
489
self ._handles = []
410
490
self ._active_handles = []
411
491
self ._threads = []
412
- for user_stream in ostreams :
413
- try :
414
- fileno = user_stream .fileno ()
415
- except :
416
- self .ostreams .append ((user_stream , user_stream ))
417
- continue
418
- local_stream = os .fdopen (
419
- os .dup (fileno ), mode = getattr (user_stream , 'mode' , None ), closefd = True
420
- )
421
- self .ostreams .append ((local_stream , user_stream ))
422
492
423
493
@property
424
494
def STDOUT (self ):
@@ -499,9 +569,6 @@ def close(self, in_exception=False):
499
569
self ._active_handles .clear ()
500
570
self ._stdout = None
501
571
self ._stderr = None
502
- for local , orig in self .ostreams :
503
- if orig is not local :
504
- local .close ()
505
572
506
573
def __enter__ (self ):
507
574
return self
0 commit comments