Skip to content

Commit 5ce6325

Browse files
committed
Add context to temporarily enable/disable showing stream data in console
The `preditor.contexts.OverrideConsoleStreams` context manager can enable or disable echoing tracebacks, stdout, stderr, results for consoles.
1 parent fc8557d commit 5ce6325

File tree

5 files changed

+214
-11
lines changed

5 files changed

+214
-11
lines changed

examples/output_console.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@
1212
from Qt.QtWidgets import QApplication, QMainWindow
1313

1414
import preditor
15+
from preditor.contexts import OverrideConsoleStreams
1516

1617
logger_a = logging.getLogger("logger_a")
1718
logger_a_child = logging.getLogger("logger_a.child")
1819
logger_b = logging.getLogger("logger_b")
1920
logger_c = logging.getLogger("logger_c")
2021

22+
# Define a custom logging level name. This should be discouraged but logging
23+
# supports it so PrEditor should as well.
24+
SUPER_INFO_LEVEL = logging.INFO - 5
25+
logging.addLevelName(SUPER_INFO_LEVEL, "SUPER_INFO")
26+
2127

2228
class ExampleApp(QMainWindow):
2329
def __init__(self, parent=None):
@@ -29,6 +35,11 @@ def __init__(self, parent=None):
2935
self.uiClearBTN.released.connect(self.clear_all)
3036
self.uiAllLoggingDebugBTN.released.connect(self.all_logging_level_debug)
3137
self.uiAllLoggingWarningBTN.released.connect(self.all_logging_level_warning)
38+
self.uiAllLoggingPrintBTN.released.connect(self.all_logging_print)
39+
self.uiAllLoggingRaiseBTN.released.connect(self.all_logging_raise)
40+
self.uiAllLoggingChangeHandlersBTN.released.connect(
41+
self.all_logging_change_handlers
42+
)
3243
self.uiLoggingCriticalBTN.released.connect(self.level_critical)
3344
self.uiLoggingErrorBTN.released.connect(self.level_error)
3445
self.uiLoggingWarningBTN.released.connect(self.level_warning)
@@ -76,6 +87,24 @@ def __init__(self, parent=None):
7687
# Configure uiStderr to only show stderr text
7788
self.uiStderr.stream_echo_stderr = True
7889

90+
def all_logging_change_handlers(self):
91+
"""Demonstrate temporarily changing logging_handlers for uiAllLog.
92+
93+
This changes uiAllLog to only show a subset of loggers not all of them.
94+
It sends the same logging messages as the "Send Logging Message" Button,
95+
but due to the updated filtering it shows very different results.
96+
"""
97+
handlers = [
98+
# Only show logger_a warnings
99+
"logger_a_child,level=WARNING",
100+
# And enable showing a custom logging level message. This is normally
101+
# hidden unless the logging level is set to debug or SUPER_INFO.
102+
# Note that we can pass SUPER_INFO because of the `addLevelName` call.
103+
"logger_c,level=SUPER_INFO",
104+
]
105+
with OverrideConsoleStreams(self.uiAllLog, logging_handlers=handlers):
106+
self.send_logging()
107+
79108
def all_logging_level_debug(self):
80109
"""Update this widget to show up to debug messages for all loggers.
81110
Hide the PyQt loggers as they just clutter the output for this demo.
@@ -101,6 +130,18 @@ def all_logging_level_warning(self):
101130
"logger_b,fmt=[%(levelname)s:%(name)s] %(message)s",
102131
]
103132

133+
def all_logging_print(self):
134+
"""Show temporarily enabling showing prints inside multiple consoles."""
135+
with OverrideConsoleStreams([self.uiAllLog, self.uiSelectLog], stdout=True):
136+
print("This print is also shown in All logging and Select Logging.")
137+
print("This print is NOT shown in All logging and Select Logging.")
138+
139+
def all_logging_raise(self):
140+
"""Show temporarily enabling showing tracebacks in uiAllLog."""
141+
with OverrideConsoleStreams(self.uiAllLog, tracebacks=True):
142+
print("This print is NOT shown in All logging.")
143+
raise RuntimeError("This Exception is also shown in All logging.")
144+
104145
def clear_all(self):
105146
"""Clear the text from all consoles"""
106147
self.uiAllLog.clear()
@@ -147,6 +188,11 @@ def send_logging(self):
147188
logger_b.debug("A debug msg for logger_b")
148189
logger_c.warning("A warning msg for logger_c")
149190
logger_c.debug("A debug msg for logger_c")
191+
logger_c.log(
192+
SUPER_INFO_LEVEL,
193+
"A custom logging level msg for logger_c that is normally hidden "
194+
"unless using debug or the custom level.",
195+
)
150196
logging.root.warning("A warning msg for logging.root")
151197
logging.root.debug("A debug msg for logging.root")
152198

examples/output_console.ui

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>667</width>
9+
<width>861</width>
1010
<height>941</height>
1111
</rect>
1212
</property>
@@ -29,22 +29,39 @@
2929
<widget class="OutputConsole" name="uiAllLog"/>
3030
</item>
3131
<item>
32-
<layout class="QVBoxLayout" name="verticalLayout">
33-
<item>
32+
<layout class="QGridLayout" name="gridLayout">
33+
<item row="0" column="0">
3434
<widget class="QPushButton" name="uiAllLoggingWarningBTN">
3535
<property name="text">
3636
<string>Warning</string>
3737
</property>
3838
</widget>
3939
</item>
40-
<item>
40+
<item row="0" column="1">
4141
<widget class="QPushButton" name="uiAllLoggingDebugBTN">
4242
<property name="text">
4343
<string>Debug</string>
4444
</property>
4545
</widget>
4646
</item>
47-
<item>
47+
<item row="2" column="0" colspan="2">
48+
<widget class="Line" name="line">
49+
<property name="orientation">
50+
<enum>Qt::Horizontal</enum>
51+
</property>
52+
</widget>
53+
</item>
54+
<item row="3" column="0" colspan="2">
55+
<widget class="QPushButton" name="uiAllLoggingRaiseBTN">
56+
<property name="toolTip">
57+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Uses preditor.contexts.OverrideConsoleStreams to temporarily enable showing tracebacks in all logging and raises an exception. This enables showing errors in that console but errors raised elsewhere won't be shown in that console.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
58+
</property>
59+
<property name="text">
60+
<string>Raise and show Exception</string>
61+
</property>
62+
</widget>
63+
</item>
64+
<item row="1" column="0" colspan="2">
4865
<widget class="QLabel" name="label_2">
4966
<property name="text">
5067
<string>Set the logging level filter for this widget. This is affected by the global logging levels set at the bottom of this UI, so to see debug output you must set both to debug.</string>
@@ -54,15 +71,35 @@
5471
</property>
5572
</widget>
5673
</item>
57-
<item>
74+
<item row="4" column="0">
75+
<widget class="QPushButton" name="uiAllLoggingPrintBTN">
76+
<property name="toolTip">
77+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Uses preditor.contexts.OverrideConsoleStreams to temporarily enable showing stdout(prints). It also shows the print in Select Logging.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
78+
</property>
79+
<property name="text">
80+
<string>Show print</string>
81+
</property>
82+
</widget>
83+
</item>
84+
<item row="4" column="1">
85+
<widget class="QPushButton" name="uiAllLoggingChangeHandlersBTN">
86+
<property name="toolTip">
87+
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Sends the same logging messages as the &amp;quot;Send Logging Messages&amp;quot; button.&lt;/p&gt;&lt;p&gt;However it uses preditor.contexts.OverrideConsoleStreams to temporarily replace logging handlers for uiAllLog. This results in very different output being shown.&lt;/p&gt;&lt;p&gt;If you use the bottom Debug button to set the global logging level to debug you will also see the SUPER_INFO log message.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
88+
</property>
89+
<property name="text">
90+
<string>Logging Handlers</string>
91+
</property>
92+
</widget>
93+
</item>
94+
<item row="5" column="1">
5895
<spacer name="verticalSpacer">
5996
<property name="orientation">
6097
<enum>Qt::Vertical</enum>
6198
</property>
6299
<property name="sizeHint" stdset="0">
63100
<size>
64101
<width>20</width>
65-
<height>40</height>
102+
<height>0</height>
66103
</size>
67104
</property>
68105
</spacer>

preditor/contexts.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
from __future__ import absolute_import, print_function
1+
from __future__ import absolute_import
22

33
import logging
4+
from contextlib import contextmanager
5+
from typing import List, Tuple
46

57
_LOGGER = logging.getLogger(__name__)
68

@@ -60,7 +62,7 @@ def doMoreStuff(self):
6062
memory if there isn't a excepthook calling clearReports.
6163
"""
6264

63-
__reports__ = []
65+
__reports__: List[Tuple[str, str]] = []
6466
enabled = False
6567

6668
def __init__(self, callback, title=''):
@@ -117,3 +119,92 @@ def generateReport(cls, fmt='{result}'):
117119
result = callback()
118120
ret.append((title, fmt.format(result=result)))
119121
return ret
122+
123+
124+
@contextmanager
125+
def OverrideConsoleStreams(
126+
consoles,
127+
tracebacks=None,
128+
stdout=None,
129+
stderr=None,
130+
result=None,
131+
logging_handlers=None,
132+
):
133+
"""Override a Console's stream and logging_handlers settings.
134+
135+
This is useful if you don't want to show tracebacks or output for un-related code.
136+
137+
Example:
138+
139+
console = OutputConsole()
140+
with OverrideConsoleStreams(console, tracebacks=True)
141+
# A traceback raised here would show in console
142+
do_work_you_only_want_to_show_tracebacks_in_console()
143+
raise RuntimeError("This traceback will not be shown in console")
144+
"""
145+
# If a single console was passed, convert it into a list
146+
try:
147+
iter(consoles)
148+
except TypeError:
149+
consoles = [consoles]
150+
151+
# Store the current state and apply changes to the consoles
152+
current = []
153+
for console in consoles:
154+
current.append(
155+
(
156+
console,
157+
None if tracebacks is None else console.stream_echo_tracebacks,
158+
None if stdout is None else console.stream_echo_stdout,
159+
None if stderr is None else console.stream_echo_stderr,
160+
None if result is None else console.stream_echo_result,
161+
None if logging_handlers is None else console.logging_handlers,
162+
)
163+
)
164+
165+
if tracebacks is not None:
166+
# Enable/disable traceback display override
167+
console.stream_echo_tracebacks = tracebacks
168+
if tracebacks:
169+
# traceback display is enabled, make it automatically remove
170+
# itself if an exception is raised by the sys.excepthook handler.
171+
console._write_error_self_destruct = True
172+
if stdout is not None:
173+
console.stream_echo_stdout = stdout
174+
if stderr is not None:
175+
console.stream_echo_stderr = stderr
176+
if result is not None:
177+
console.stream_echo_result = result
178+
if logging_handlers is not None:
179+
console.logging_handlers = logging_handlers
180+
181+
# NOTE: This code is a horrible abomination of necessity.
182+
try:
183+
yield
184+
except Exception:
185+
# This is syntactically needed, we don't need to capture the exception,
186+
# but the else requires it. We need raise any un-handled exceptions
187+
# encountered to allow sys.excepthook to process normally.
188+
# This prevents the else code from removing the traceback print overriding
189+
# because the else/finally are processed before sys.excepthook is.
190+
raise
191+
else:
192+
# Only called when exception was not raised. Restore the original traceback
193+
# and disable the self destructing flag its not needed anymore.
194+
for item in current:
195+
if tracebacks is not None:
196+
item[0].stream_echo_tracebacks = item[1]
197+
if tracebacks:
198+
item[0]._write_error_self_destruct = False
199+
finally:
200+
# Always remove non-traceback options. This is handled before excepthook
201+
# so we can just use the simplicity of a normal try/finally statement.
202+
for item in current:
203+
if stdout is not None:
204+
item[0].stream_echo_stdout = item[2]
205+
if stderr is not None:
206+
item[0].stream_echo_stderr = item[3]
207+
if result is not None:
208+
item[0].stream_echo_result = item[4]
209+
if logging_handlers is not None:
210+
item[0].logging_handlers = item[5]

preditor/gui/console_base.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def __init__(self, parent: QWidget, controller: Optional[LoggerWindow] = None):
4848
# Optionally used to prevent the app from being marked as not responding
4949
# while processing blocking code and ensure _last_repaint_time processes.
5050
self._last_process_events_time = 0
51+
self._write_error_self_destruct = False
52+
"""If enabling tracebacks using `OverrideConsoleStreams`, this is set to True
53+
so only if write_error is called by sys.excepthook it will remove itself.
54+
"""
5155

5256
# Create the highlighter
5357
highlight = CodeHighlighter(self, 'Python')
@@ -56,6 +60,7 @@ def __init__(self, parent: QWidget, controller: Optional[LoggerWindow] = None):
5660
self.addSepNewline = False
5761
self.consoleLine = None
5862
self.mousePressPos = None
63+
self.logging_info = {}
5964

6065
self.init_actions()
6166

@@ -315,6 +320,13 @@ def init_actions(self):
315320
self.uiAddBreakACT.triggered.connect(self.add_separator)
316321

317322
def init_logging_handlers(self, attrName=None, value=None):
323+
# Ensure the old callbacks are removed so they don't keep writing.
324+
# The stream Manager will deal with if this widget is closed, but not
325+
# in the case of temporarily disabling a handler.
326+
for hi in self.logging_info.values():
327+
hi.uninstall(self.write_log)
328+
329+
# Reset and add new handlers to handle log statements
318330
self.logging_info = {}
319331
for h in self.logging_handlers:
320332
hi = HandlerInfo(h)
@@ -588,6 +600,15 @@ def write_error(self, *exc_info):
588600
for line in text:
589601
self.write(line, stream_type=StreamType.CONSOLE | StreamType.STDERR)
590602

603+
if self._write_error_self_destruct:
604+
# This should only be enabled by `OverrideConsoleStreams`. This callback
605+
# was installed temporarily and once its called by sys.excepthook it
606+
# should be removed so it doesn't get called again.
607+
from preditor.excepthooks import PreditorExceptHook
608+
609+
self._write_error_self_destruct = False
610+
PreditorExceptHook.callbacks.remove(self.write_error)
611+
591612
def write_log(self, log_data, stream_type=StreamType.CONSOLE):
592613
"""Write a logging message to the console depending on filters."""
593614
handler, record = log_data

preditor/stream/console_handler.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,22 @@ def __parse_setting__(cls, value, index):
123123
return attr_name, value
124124

125125
def install(self, callback=None, replay=False, disable_writes=False, clear=False):
126-
logger = logging.getLogger(self.name)
127-
handler, _ = plugins.add_logging_handler(logger, self.plugin)
126+
"""Add the required logging handler if needed and connect callback to it."""
127+
_logger = logging.getLogger(self.name)
128+
handler, _ = plugins.add_logging_handler(_logger, self.plugin)
128129
if handler and callback:
129130
handler.manager.add_callback(
130131
callback, replay=replay, disable_writes=disable_writes, clear=clear
131132
)
132133
return handler
133134

135+
def uninstall(self, callback):
136+
"""Remove the callback added via install, doesn't remove the logging handler."""
137+
_logger = logging.getLogger(self.name)
138+
handler, _ = plugins.add_logging_handler(_logger, self.plugin)
139+
if handler:
140+
handler.manager.remove_callback(callback)
141+
134142

135143
class ConsoleHandler(logging.Handler):
136144
"""A logging handler that writes directly to the PrEditor instance.

0 commit comments

Comments
 (0)