21
21
import inspect
22
22
import io
23
23
import logging
24
+ import os
24
25
import re
25
26
import sys
26
27
import textwrap
@@ -286,14 +287,22 @@ class LoggingIntercept(object):
286
287
----------
287
288
output: io.TextIOBase
288
289
the file stream to send log messages to
290
+
289
291
module: str
290
- the target logger name to intercept
292
+ the target logger name to intercept. `logger` and `module` are
293
+ mutually exclusive.
294
+
291
295
level: int
292
296
the logging level to intercept
297
+
293
298
formatter: logging.Formatter
294
299
the formatter to use when rendering the log messages. If not
295
300
specified, uses `'%(message)s'`
296
301
302
+ logger: logging.Logger
303
+ the target logger to intercept. `logger` and `module` are
304
+ mutually exclusive.
305
+
297
306
Examples
298
307
--------
299
308
>>> import io, logging
@@ -306,17 +315,36 @@ class LoggingIntercept(object):
306
315
307
316
"""
308
317
309
- def __init__ (self , output = None , module = None , level = logging .WARNING , formatter = None ):
318
+ def __init__ (
319
+ self ,
320
+ output = None ,
321
+ module = None ,
322
+ level = logging .WARNING ,
323
+ formatter = None ,
324
+ logger = None ,
325
+ ):
310
326
self .handler = None
311
327
self .output = output
312
- self .module = module
328
+ if logger is not None :
329
+ if module is not None :
330
+ raise ValueError (
331
+ "LoggingIntercept: only one of 'module' and 'logger' is allowed"
332
+ )
333
+ self ._logger = logger
334
+ else :
335
+ self ._logger = logging .getLogger (module )
313
336
self ._level = level
314
337
if formatter is None :
315
338
formatter = logging .Formatter ('%(message)s' )
316
339
self ._formatter = formatter
317
340
self ._save = None
318
341
319
342
def __enter__ (self ):
343
+ # Get the logger for the scope we will be overriding
344
+ logger = self ._logger
345
+ self ._save = logger .level , logger .propagate , logger .handlers
346
+ if self ._level is None :
347
+ self ._level = logger .getEffectiveLevel ()
320
348
# Set up the handler
321
349
output = self .output
322
350
if output is None :
@@ -326,23 +354,25 @@ def __enter__(self):
326
354
self .handler .setFormatter (self ._formatter )
327
355
self .handler .setLevel (self ._level )
328
356
# Register the handler with the appropriate module scope
329
- logger = logging .getLogger (self .module )
330
- self ._save = logger .level , logger .propagate , logger .handlers
331
357
logger .handlers = []
332
- logger .propagate = 0
358
+ logger .propagate = False
333
359
logger .setLevel (self .handler .level )
334
360
logger .addHandler (self .handler )
335
361
return output
336
362
337
363
def __exit__ (self , et , ev , tb ):
338
- logger = logging . getLogger ( self .module )
364
+ logger = self ._logger
339
365
logger .removeHandler (self .handler )
340
366
self .handler = None
341
367
logger .setLevel (self ._save [0 ])
342
368
logger .propagate = self ._save [1 ]
343
369
assert not logger .handlers
344
370
logger .handlers .extend (self ._save [2 ])
345
371
372
+ @property
373
+ def module (self ):
374
+ return self ._logger .name
375
+
346
376
347
377
class LogStream (io .TextIOBase ):
348
378
"""
@@ -357,6 +387,8 @@ def __init__(self, level, logger):
357
387
self ._buffer = ''
358
388
359
389
def write (self , s : str ) -> int :
390
+ if not s :
391
+ return 0
360
392
res = len (s )
361
393
if self ._buffer :
362
394
s = self ._buffer + s
@@ -369,3 +401,76 @@ def write(self, s: str) -> int:
369
401
def flush (self ):
370
402
if self ._buffer :
371
403
self .write ('\n ' )
404
+
405
+ def redirect_streams (self , redirects ):
406
+ """Redirect StreamHandler objects to the original file descriptors
407
+
408
+ This utility method for py:class:`~pyomo.common.tee.capture_output`
409
+ locates any StreamHandlers that would process messages from the
410
+ logger assigned to this :py:class:`LogStream` that would write
411
+ to the file descriptors redirected by `capture_output` and
412
+ yields context managers that will redirect those StreamHandlers
413
+ back to duplicates of the original file descriptors.
414
+
415
+ """
416
+ found = 0
417
+ logger = self ._logger
418
+ while logger :
419
+ for handler in logger .handlers :
420
+ found += 1
421
+ if not isinstance (handler , logging .StreamHandler ):
422
+ continue
423
+ try :
424
+ fd = handler .stream .fileno ()
425
+ except (AttributeError , OSError ):
426
+ fd = None
427
+ if fd not in redirects :
428
+ continue
429
+ yield _StreamRedirector (handler , redirects [fd ].original_fd )
430
+ if not logger .propagate :
431
+ break
432
+ else :
433
+ logger = logger .parent
434
+ if not found :
435
+ fd = logging .lastResort .stream .fileno ()
436
+ if not redirects :
437
+ yield _LastResortRedirector (fd )
438
+ elif fd in redirects :
439
+ yield _LastResortRedirector (redirects [fd ].original_fd )
440
+
441
+
442
+ class _StreamRedirector (object ):
443
+ def __init__ (self , handler , fd ):
444
+ self .handler = handler
445
+ self .fd = fd
446
+ self .orig_stream = None
447
+
448
+ def __enter__ (self ):
449
+ self .orig_stream = self .handler .stream
450
+ self .handler .stream = os .fdopen (
451
+ os .dup (self .fd ), mode = "w" , closefd = True
452
+ ).__enter__ ()
453
+
454
+ def __exit__ (self , et , ev , tb ):
455
+ try :
456
+ self .handler .stream .__exit__ (et , ev , tb )
457
+ finally :
458
+ self .handler .stream = self .orig_stream
459
+
460
+
461
+ class _LastResortRedirector (object ):
462
+ def __init__ (self , fd ):
463
+ self .fd = fd
464
+ self .orig_stream = None
465
+
466
+ def __enter__ (self ):
467
+ self .orig = logging .lastResort
468
+ logging .lastResort = logging .StreamHandler (
469
+ os .fdopen (os .dup (self .fd ), mode = "w" , closefd = True ).__enter__ ()
470
+ )
471
+
472
+ def __exit__ (self , et , ev , tb ):
473
+ try :
474
+ logging .lastResort .stream .close ()
475
+ finally :
476
+ logging .lastResort = self .orig
0 commit comments