Debug Tools
stderr_replacement.py
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
3 
4 
38 
39 # Warning message here
40 import sys
41 import logging
42 
43 import inspect
44 import traceback
45 
46 from logging import StreamHandler
47 
48 try:
49  unicode
50  _unicode = True
51 except NameError:
52  unicode = str
53  _unicode = False
54 
55 
56 class stderr_replacement(object):
57  """
58  In case of reloading this module, never recapture the current `sys.stderr`.
59 
60  When disabling this with unlock, it will only restore the standard behavior of the `stderr`
61  stream. However, the attached logger will cannot be detached never because someone else can
62  have a reference to its older version. This is why this is a global singleton which can
63  never dies.
64 
65  How do I duplicate sys.stdout to a log file in python?
66  https://stackoverflow.com/questions/616645/how-do-i-duplicate-sys-stdout-to-a-log-file-in-python
67 
68  How to redirect stdout and stderr to logger in Python
69  https://stackoverflow.com/questions/19425736/how-to-redirect-stdout-and-stderr-to-logger-in-python
70 
71  Set a Read-Only Attribute in Python?
72  https://stackoverflow.com/questions/24497316/set-a-read-only-attribute-in-python
73  """
74  is_active = False
75 
76  @classmethod
77  def lock(cls, logger):
78  """
79  Attach this singleton logger to the `sys.stderr` permanently.
80  """
81  global _stderr_singleton
82  global _stderr_default
83  global _stderr_default_class_type
84 
85  # On Sublime Text, the `sys.__stderr__` is None, because they already replaced `sys.stderr`
86  # by some `_LogWriter()` class, then just save the current one over there.
87  if not sys.__stderr__:
88  sys.__stderr__ = sys.stderr
89 
90  try:
91  _stderr_default
92  _stderr_default_class_type
93 
94  except NameError:
95  # sys.stdout.write( "Assigning sys.stderr to _stderr_default\n" )
96  _stderr_default = sys.stderr
97 
98  _stderr_default_class_type = type( _stderr_default )
99  # sys.stdout.write( "Assigned sys.stderr to _stderr_default: %s, %s\n" % ( _stderr_default, _stderr_default_class_type ) )
100 
101  # Recreate the sys.stderr logger when it was reset by `unlock()`
102  if not cls.is_active:
103  cls.is_active = True
104  _stderr_write = _stderr_default.write
105 
106  logger_call = logger._log_clean
107  clean_formatter = logger.clean_formatter
108 
109  global _sys_stderr_write
110  global _sys_stderr_write_hidden
111 
112  if sys.version_info <= (3,2):
113 
114  def customEmit(self, record):
115  """
116  Emit a record.
117  https://stackoverflow.com/questions/12699645/how-can-i-suppress-newline-in-python-logging-module
118 
119  If a formatter is specified, it is used to format the record.
120  The record is then written to the stream with a trailing newline. If
121  exception information is present, it is formatted using
122  traceback.print_exception and appended to the stream. If the stream
123  has an 'encoding' attribute, it is used to determine how to do the
124  output to the stream.
125  """
126  try:
127  msg = self.format(record)
128  stream = self.stream
129  fs = "%s%s"
130  if not _unicode: #if no unicode support...
131  stream.write(fs % (msg, self.terminator))
132  else:
133  try:
134  if (isinstance(msg, unicode) and
135  getattr(stream, 'encoding', None)):
136  ufs = u'%s%s'
137  try:
138  stream.write(ufs % (msg, self.terminator))
139  except UnicodeEncodeError:
140  #Printing to terminals sometimes fails. For example,
141  #with an encoding of 'cp1251', the above write will
142  #work if written to a stream opened or wrapped by
143  #the codecs module, but fail when writing to a
144  #terminal even when the codepage is set to cp1251.
145  #An extra encoding step seems to be needed.
146  stream.write((ufs % (msg, self.terminator)).encode(stream.encoding))
147  else:
148  stream.write(fs % (msg, self.terminator))
149  except UnicodeError:
150  stream.write(fs % (msg.encode("UTF-8"), self.terminator))
151  self.flush()
152  except (KeyboardInterrupt, SystemExit):
153  raise
154  except:
155  self.handleError(record)
156 
157  logging.StreamHandler.terminator = '\n'
158  setattr(StreamHandler, StreamHandler.emit.__name__, customEmit)
159 
160  # Always recreate/override the internal write function used by `_sys_stderr_write`
161  def _sys_stderr_write_hidden(msg, *args, **kwargs):
162  """
163  Suppress newline in Python logging module
164  https://stackoverflow.com/questions/7168790/suppress-newline-in-python-logging-module
165  """
166 
167  try:
168  file = logger._file
169  _stderr_write( msg, *args, **kwargs )
170 
171  formatter = file.formatter
172  terminator = file.terminator
173 
174  file.formatter = clean_formatter
175  file.terminator = ""
176 
177  kwargs['extra'] = { '_duplicated_from_file': True }
178  logger_call( msg, args, kwargs )
179 
180  file.formatter = formatter
181  file.terminator = terminator
182 
183  except Exception:
184  logger.exception( "Could not write to the file: %s(%s)", file, logger )
185  cls.unlock()
186 
187  # Only create one `_sys_stderr_write` function pointer ever
188  try:
189  _sys_stderr_write
190 
191  except NameError:
192 
193  def _sys_stderr_write(*args, **kwargs):
194  """
195  Hides the actual function pointer. This allow the external function pointer to
196  be cached while the internal written can be exchanged between the standard
197  `sys.stderr.write` and our custom wrapper around it.
198  """
199  _sys_stderr_write_hidden( *args, **kwargs )
200 
201  # sys.stdout.write( "_stderr_default: %s\n" % _stderr_default )
202  # sys.stdout.write( "_stderr_default.__dict__: %s\n" % dir( _stderr_default ) )
203  # sys.stdout.write( "(inspect) _stderr_default.__init__: %s\n" % str( inspect.getfullargspec( _stderr_default.__init__ ) ) )
204 
205  try:
206  # Only create one singleton instance ever
207  # del _stderr_singleton
208  _stderr_singleton
209 
210  except NameError:
211 
212  class stderr_replament_hidden(_stderr_default_class_type):
213  """
214  Which special methods bypasses __getattribute__ in Python?
215  https://stackoverflow.com/questions/12872695/which-special-methods-bypasses-getattribute-in-python
216  """
217 
218  if hasattr( _stderr_default, "__abstractmethods__" ):
219  __abstractmethods__ = _stderr_default.__abstractmethods__
220 
221  if hasattr( _stderr_default, "__base__" ):
222  __base__ = _stderr_default.__base__
223 
224  if hasattr( _stderr_default, "__bases__" ):
225  __bases__ = _stderr_default.__bases__
226 
227  if hasattr( _stderr_default, "__basicsize__" ):
228  __basicsize__ = _stderr_default.__basicsize__
229 
230  if hasattr( _stderr_default, "__call__" ):
231  __call__ = _stderr_default.__call__
232 
233  if hasattr( _stderr_default, "__class__" ):
234  __class__ = _stderr_default.__class__
235 
236  if hasattr( _stderr_default, "__delattr__" ):
237  __delattr__ = _stderr_default.__delattr__
238 
239  if hasattr( _stderr_default, "__dict__" ):
240  __dict__ = _stderr_default.__dict__
241 
242  if hasattr( _stderr_default, "__dictoffset__" ):
243  __dictoffset__ = _stderr_default.__dictoffset__
244 
245  if hasattr( _stderr_default, "__dir__" ):
246  __dir__ = _stderr_default.__dir__
247 
248  if hasattr( _stderr_default, "__doc__" ):
249  __doc__ = _stderr_default.__doc__
250 
251  if hasattr( _stderr_default, "__eq__" ):
252  __eq__ = _stderr_default.__eq__
253 
254  if hasattr( _stderr_default, "__flags__" ):
255  __flags__ = _stderr_default.__flags__
256 
257  if hasattr( _stderr_default, "__format__" ):
258  __format__ = _stderr_default.__format__
259 
260  if hasattr( _stderr_default, "__ge__" ):
261  __ge__ = _stderr_default.__ge__
262 
263  if hasattr( _stderr_default, "__getattribute__" ):
264  __getattribute__ = _stderr_default.__getattribute__
265 
266  if hasattr( _stderr_default, "__gt__" ):
267  __gt__ = _stderr_default.__gt__
268 
269  if hasattr( _stderr_default, "__hash__" ):
270  __hash__ = _stderr_default.__hash__
271 
272  if hasattr( _stderr_default, "__init__" ):
273  __init__ = _stderr_default.__init__
274 
275  if hasattr( _stderr_default, "__init_subclass__" ):
276  __init_subclass__ = _stderr_default.__init_subclass__
277 
278  if hasattr( _stderr_default, "__instancecheck__" ):
279  __instancecheck__ = _stderr_default.__instancecheck__
280 
281  if hasattr( _stderr_default, "__itemsize__" ):
282  __itemsize__ = _stderr_default.__itemsize__
283 
284  if hasattr( _stderr_default, "__le__" ):
285  __le__ = _stderr_default.__le__
286 
287  if hasattr( _stderr_default, "__lt__" ):
288  __lt__ = _stderr_default.__lt__
289 
290  if hasattr( _stderr_default, "__module__" ):
291  __module__ = _stderr_default.__module__
292 
293  if hasattr( _stderr_default, "__mro__" ):
294  __mro__ = _stderr_default.__mro__
295 
296  if hasattr( _stderr_default, "__name__" ):
297  __name__ = _stderr_default.__name__
298 
299  if hasattr( _stderr_default, "__ne__" ):
300  __ne__ = _stderr_default.__ne__
301 
302  if hasattr( _stderr_default, "__new__" ):
303  __new__ = _stderr_default.__new__
304 
305  if hasattr( _stderr_default, "__prepare__" ):
306  __prepare__ = _stderr_default.__prepare__
307 
308  if hasattr( _stderr_default, "__qualname__" ):
309  __qualname__ = _stderr_default.__qualname__
310 
311  if hasattr( _stderr_default, "__reduce__" ):
312  __reduce__ = _stderr_default.__reduce__
313 
314  if hasattr( _stderr_default, "__reduce_ex__" ):
315  __reduce_ex__ = _stderr_default.__reduce_ex__
316 
317  if hasattr( _stderr_default, "__repr__" ):
318  __repr__ = _stderr_default.__repr__
319 
320  if hasattr( _stderr_default, "__setattr__" ):
321  __setattr__ = _stderr_default.__setattr__
322 
323  if hasattr( _stderr_default, "__sizeof__" ):
324  __sizeof__ = _stderr_default.__sizeof__
325 
326  if hasattr( _stderr_default, "__str__" ):
327  __str__ = _stderr_default.__str__
328 
329  if hasattr( _stderr_default, "__subclasscheck__" ):
330  __subclasscheck__ = _stderr_default.__subclasscheck__
331 
332  if hasattr( _stderr_default, "__subclasses__" ):
333  __subclasses__ = _stderr_default.__subclasses__
334 
335  if hasattr( _stderr_default, "__subclasshook__" ):
336  __subclasshook__ = _stderr_default.__subclasshook__
337 
338  if hasattr( _stderr_default, "__text_signature__" ):
339  __text_signature__ = _stderr_default.__text_signature__
340 
341  if hasattr( _stderr_default, "__weakrefoffset__" ):
342  __weakrefoffset__ = _stderr_default.__weakrefoffset__
343 
344  if hasattr( _stderr_default, "mro" ):
345  mro = _stderr_default.mro
346 
347  def __init__(self):
348  """
349  Override any super class `type( _stderr_default )` constructor, so we can
350  instantiate any kind of `sys.stderr` replacement object, in case it was
351  already replaced by something else like on Sublime Text with `_LogWriter()`.
352 
353  Assures all attributes were statically replaced just above. This should happen in case
354  some new attribute is added to the python language.
355 
356  This also ignores the only two methods which are not equal, `__init__()` and `__getattribute__()`.
357  """
358  different_methods = ("__init__", "__class__", "__getattribute__")
359  attributes_to_check = set( dir( object ) + dir( type ) )
360 
361  for attribute in attributes_to_check:
362 
363  if attribute not in different_methods \
364  and hasattr( _stderr_default, attribute ):
365 
366  base_class_attribute = super( _stderr_default_class_type, self ).__getattribute__( attribute )
367  target_class_attribute = _stderr_default.__getattribute__( attribute )
368 
369  if base_class_attribute != target_class_attribute:
370  sys.stderr.write( " The base class attribute `%s` is different from the target class:\n%s\n%s\n\n" % ( attribute, base_class_attribute, target_class_attribute ) )
371 
372  def __getattribute__(self, item):
373  # sys.stdout.write( "__getattribute__, item: %s: %s\n" % ( item, _sys_stderr_write ) )
374 
375  if item == 'write':
376  return _sys_stderr_write
377 
378  try:
379  return _stderr_default.__getattribute__( item )
380 
381  except AttributeError:
382  return super( _stderr_default_class_type, _stderr_default ).__getattribute__( item )
383 
384  # sys.stdout.write( "_stderr_default: %s\n" % _stderr_default )
385  # sys.stdout.write( "inspect.getmro(_stderr_default): %s\n" % str( inspect.getmro( type( _stderr_default ) ) ) )
386  # sys.stdout.write( "inspect.getmro(stderr_replacement): %s\n" % str( inspect.getmro( stderr_replament_hidden ) ) )
387  # sys.stdout.write( "traceback.format_stack():\n%s\n" % "".join( traceback.format_stack() ) )
388  _stderr_singleton = stderr_replament_hidden()
389 
390  # sys.stdout.write( "_stderr_singleton: ")
391  # sys.stdout.write( "%s\n" % _stderr_singleton )
392  sys.stderr = _stderr_singleton
393 
394  # sys.stdout.write( "(inspect) signature: %s\n" % str( inspect.signature( _stderr_singleton.write ) ) )
395  # sys.stdout.write( "(inspect) fullargspec: %s\n" % str( inspect.getfullargspec( _stderr_singleton.write ) ) )
396  # sys.stdout.write( "(_stderr_singleton 6): %s\n" % _stderr_singleton )
397 
398  # sys.stdout.write( "Locking... _sys_stderr_write_hidden: %s\n" % _sys_stderr_write_hidden )
399  return cls
400 
401  @classmethod
402  def unlock(cls):
403  """
404  Detach this `stderr` writer from `sys.stderr` and allow the next call to `lock()` create
405  a new writer for the stderr.
406  """
407 
408  if cls.is_active:
409  global _sys_stderr_write_hidden
410  # sys.stdout.write( "Unlocking... _sys_stderr_write_hidden: %s\n" % _sys_stderr_write_hidden )
411 
412  cls.is_active = False
413  _sys_stderr_write_hidden = _stderr_default.write
414