@@ -194,10 +194,12 @@ class capture_output(object):
194194
195195 capture_fd : bool
196196
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.
201203
202204 Returns
203205 -------
@@ -231,19 +233,59 @@ def __enter__(self):
231233 sys .stdout = self .tee .STDOUT
232234 sys .stderr = self .tee .STDERR
233235 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
237273 )
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
240280 return self .output_stream
241281
242282 def __exit__ (self , et , ev , tb ):
283+ # Restore any file descriptors we comandeered
243284 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 )
246287 self .fd_redirect = None
288+ # Check and restore sys.stderr / sys.stdout
247289 FAIL = self .tee .STDOUT is not sys .stdout
248290 self .tee .__exit__ (et , ev , tb )
249291 if self .output_stream is not self .output :
@@ -252,6 +294,15 @@ def __exit__(self, et, ev, tb):
252294 self .old = None
253295 self .tee = None
254296 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
255306 if FAIL :
256307 raise RuntimeError ('Captured output does not match sys.stdout.' )
257308
@@ -378,47 +429,66 @@ def writeOutputBuffer(self, ostreams, flush):
378429 if not ostring :
379430 return
380431
381- for local_stream , user_stream in ostreams :
432+ for stream in ostreams :
382433 try :
383- written = local_stream .write (ostring )
434+ written = stream .write (ostring )
384435 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
386462 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 ()
390464 # Note: some derived file-like objects fail to return the
391465 # number of characters written (and implicitly return None).
392466 # If we get None, we will just assume that everything was
393467 # fine (as opposed to tossing the incomplete write error).
394468 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
395476 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 :])
399479 )
400480
401481
402482class TeeStream (object ):
403483 def __init__ (self , * ostreams , encoding = None , buffering = - 1 ):
404- self .ostreams = []
484+ self .ostreams = ostreams
405485 self .encoding = encoding
406486 self .buffering = buffering
407487 self ._stdout = None
408488 self ._stderr = None
409489 self ._handles = []
410490 self ._active_handles = []
411491 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 ))
422492
423493 @property
424494 def STDOUT (self ):
@@ -499,9 +569,6 @@ def close(self, in_exception=False):
499569 self ._active_handles .clear ()
500570 self ._stdout = None
501571 self ._stderr = None
502- for local , orig in self .ostreams :
503- if orig is not local :
504- local .close ()
505572
506573 def __enter__ (self ):
507574 return self
0 commit comments