[Python-checkins] [3.7] bpo-38830: Correct slot signature in Qt example. (GH-17220) (GH-17222)

Vinay Sajip webhook-mailer at python.org
Mon Nov 18 07:24:20 EST 2019


https://github.com/python/cpython/commit/9a4c5c30d93278e420a7dadafbaa35a5b52325ec
commit: 9a4c5c30d93278e420a7dadafbaa35a5b52325ec
branch: 3.7
author: Vinay Sajip <vinay_sajip at yahoo.co.uk>
committer: GitHub <noreply at github.com>
date: 2019-11-18T12:24:16Z
summary:

[3.7] bpo-38830: Correct slot signature in Qt example. (GH-17220) (GH-17222)

(cherry picked from commit 5383956583bb758f3828513bcdd011871f24a0e8)

files:
M Doc/howto/logging-cookbook.rst

diff --git a/Doc/howto/logging-cookbook.rst b/Doc/howto/logging-cookbook.rst
index a9222ab6ce38e..6ab1e39f0652b 100644
--- a/Doc/howto/logging-cookbook.rst
+++ b/Doc/howto/logging-cookbook.rst
@@ -2584,3 +2584,399 @@ In this case, the message #5 printed to ``stdout`` doesn't appear, as expected.
 Of course, the approach described here can be generalised, for example to attach
 logging filters temporarily. Note that the above code works in Python 2 as well
 as Python 3.
+
+
+.. _starter-template:
+
+A CLI application starter template
+----------------------------------
+
+Here's an example which shows how you can:
+
+* Use a logging level based on command-line arguments
+* Dispatch to multiple subcommands in separate files, all logging at the same
+  level in a consistent way
+* Make use of simple, minimal configuration
+
+Suppose we have a command-line application whose job is to stop, start or
+restart some services. This could be organised for the purposes of illustration
+as a file ``app.py`` that is the main script for the application, with individual
+commands implemented in ``start.py``, ``stop.py`` and ``restart.py``. Suppose
+further that we want to control the verbosity of the application via a
+command-line argument, defaulting to ``logging.INFO``. Here's one way that
+``app.py`` could be written::
+
+    import argparse
+    import importlib
+    import logging
+    import os
+    import sys
+
+    def main(args=None):
+        scriptname = os.path.basename(__file__)
+        parser = argparse.ArgumentParser(scriptname)
+        levels = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
+        parser.add_argument('--log-level', default='INFO', choices=levels)
+        subparsers = parser.add_subparsers(dest='command',
+                                           help='Available commands:')
+        start_cmd = subparsers.add_parser('start', help='Start a service')
+        start_cmd.add_argument('name', metavar='NAME',
+                               help='Name of service to start')
+        stop_cmd = subparsers.add_parser('stop',
+                                         help='Stop one or more services')
+        stop_cmd.add_argument('names', metavar='NAME', nargs='+',
+                              help='Name of service to stop')
+        restart_cmd = subparsers.add_parser('restart',
+                                            help='Restart one or more services')
+        restart_cmd.add_argument('names', metavar='NAME', nargs='+',
+                                 help='Name of service to restart')
+        options = parser.parse_args()
+        # the code to dispatch commands could all be in this file. For the purposes
+        # of illustration only, we implement each command in a separate module.
+        try:
+            mod = importlib.import_module(options.command)
+            cmd = getattr(mod, 'command')
+        except (ImportError, AttributeError):
+            print('Unable to find the code for command \'%s\'' % options.command)
+            return 1
+        # Could get fancy here and load configuration from file or dictionary
+        logging.basicConfig(level=options.log_level,
+                            format='%(levelname)s %(name)s %(message)s')
+        cmd(options)
+
+    if __name__ == '__main__':
+        sys.exit(main())
+
+And the ``start``, ``stop`` and ``restart`` commands can be implemented in
+separate modules, like so for starting::
+
+    # start.py
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    def command(options):
+        logger.debug('About to start %s', options.name)
+        # actually do the command processing here ...
+        logger.info('Started the \'%s\' service.', options.name)
+
+and thus for stopping::
+
+    # stop.py
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    def command(options):
+        n = len(options.names)
+        if n == 1:
+            plural = ''
+            services = '\'%s\'' % options.names[0]
+        else:
+            plural = 's'
+            services = ', '.join('\'%s\'' % name for name in options.names)
+            i = services.rfind(', ')
+            services = services[:i] + ' and ' + services[i + 2:]
+        logger.debug('About to stop %s', services)
+        # actually do the command processing here ...
+        logger.info('Stopped the %s service%s.', services, plural)
+
+and similarly for restarting::
+
+    # restart.py
+    import logging
+
+    logger = logging.getLogger(__name__)
+
+    def command(options):
+        n = len(options.names)
+        if n == 1:
+            plural = ''
+            services = '\'%s\'' % options.names[0]
+        else:
+            plural = 's'
+            services = ', '.join('\'%s\'' % name for name in options.names)
+            i = services.rfind(', ')
+            services = services[:i] + ' and ' + services[i + 2:]
+        logger.debug('About to restart %s', services)
+        # actually do the command processing here ...
+        logger.info('Restarted the %s service%s.', services, plural)
+
+If we run this application with the default log level, we get output like this:
+
+.. code-block:: shell-session
+
+    $ python app.py start foo
+    INFO start Started the 'foo' service.
+
+    $ python app.py stop foo bar
+    INFO stop Stopped the 'foo' and 'bar' services.
+
+    $ python app.py restart foo bar baz
+    INFO restart Restarted the 'foo', 'bar' and 'baz' services.
+
+The first word is the logging level, and the second word is the module or
+package name of the place where the event was logged.
+
+If we change the logging level, then we can change the information sent to the
+log. For example, if we want more information:
+
+.. code-block:: shell-session
+
+    $ python app.py --log-level DEBUG start foo
+    DEBUG start About to start foo
+    INFO start Started the 'foo' service.
+
+    $ python app.py --log-level DEBUG stop foo bar
+    DEBUG stop About to stop 'foo' and 'bar'
+    INFO stop Stopped the 'foo' and 'bar' services.
+
+    $ python app.py --log-level DEBUG restart foo bar baz
+    DEBUG restart About to restart 'foo', 'bar' and 'baz'
+    INFO restart Restarted the 'foo', 'bar' and 'baz' services.
+
+And if we want less:
+
+.. code-block:: shell-session
+
+    $ python app.py --log-level WARNING start foo
+    $ python app.py --log-level WARNING stop foo bar
+    $ python app.py --log-level WARNING restart foo bar baz
+
+In this case, the commands don't print anything to the console, since nothing
+at ``WARNING`` level or above is logged by them.
+
+.. _qt-gui:
+
+A Qt GUI for logging
+--------------------
+
+A question that comes up from time to time is about how to log to a GUI
+application. The `Qt <https://www.qt.io/>`_ framework is a popular
+cross-platform UI framework with Python bindings using `PySide2
+<https://pypi.org/project/PySide2/>`_ or `PyQt5
+<https://pypi.org/project/PyQt5/>`_ libraries.
+
+The following example shows how to log to a Qt GUI. This introduces a simple
+``QtHandler`` class which takes a callable, which should be a slot in the main
+thread that does GUI updates. A worker thread is also created to show how you
+can log to the GUI from both the UI itself (via a button for manual logging)
+as well as a worker thread doing work in the background (here, just logging
+messages at random levels with random short delays in between).
+
+The worker thread is implemented using Qt's ``QThread`` class rather than the
+:mod:`threading` module, as there are circumstances where one has to use
+``QThread``, which offers better integration with other ``Qt`` components.
+
+The code should work with recent releases of either ``PySide2`` or ``PyQt5``.
+You should be able to adapt the approach to earlier versions of Qt. Please
+refer to the comments in the code snippet for more detailed information.
+
+.. code-block:: python3
+
+    import datetime
+    import logging
+    import random
+    import sys
+    import time
+
+    # Deal with minor differences between PySide2 and PyQt5
+    try:
+        from PySide2 import QtCore, QtGui, QtWidgets
+        Signal = QtCore.Signal
+        Slot = QtCore.Slot
+    except ImportError:
+        from PyQt5 import QtCore, QtGui, QtWidgets
+        Signal = QtCore.pyqtSignal
+        Slot = QtCore.pyqtSlot
+
+
+    logger = logging.getLogger(__name__)
+
+
+    #
+    # Signals need to be contained in a QObject or subclass in order to be correctly
+    # initialized.
+    #
+    class Signaller(QtCore.QObject):
+        signal = Signal(str, logging.LogRecord)
+
+    #
+    # Output to a Qt GUI is only supposed to happen on the main thread. So, this
+    # handler is designed to take a slot function which is set up to run in the main
+    # thread. In this example, the function takes a string argument which is a
+    # formatted log message, and the log record which generated it. The formatted
+    # string is just a convenience - you could format a string for output any way
+    # you like in the slot function itself.
+    #
+    # You specify the slot function to do whatever GUI updates you want. The handler
+    # doesn't know or care about specific UI elements.
+    #
+    class QtHandler(logging.Handler):
+        def __init__(self, slotfunc, *args, **kwargs):
+            super(QtHandler, self).__init__(*args, **kwargs)
+            self.signaller = Signaller()
+            self.signaller.signal.connect(slotfunc)
+
+        def emit(self, record):
+            s = self.format(record)
+            self.signaller.signal.emit(s, record)
+
+    #
+    # This example uses QThreads, which means that the threads at the Python level
+    # are named something like "Dummy-1". The function below gets the Qt name of the
+    # current thread.
+    #
+    def ctname():
+        return QtCore.QThread.currentThread().objectName()
+
+
+    #
+    # Used to generate random levels for logging.
+    #
+    LEVELS = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR,
+              logging.CRITICAL)
+
+    #
+    # This worker class represents work that is done in a thread separate to the
+    # main thread. The way the thread is kicked off to do work is via a button press
+    # that connects to a slot in the worker.
+    #
+    # Because the default threadName value in the LogRecord isn't much use, we add
+    # a qThreadName which contains the QThread name as computed above, and pass that
+    # value in an "extra" dictionary which is used to update the LogRecord with the
+    # QThread name.
+    #
+    # This example worker just outputs messages sequentially, interspersed with
+    # random delays of the order of a few seconds.
+    #
+    class Worker(QtCore.QObject):
+        @Slot()
+        def start(self):
+            extra = {'qThreadName': ctname() }
+            logger.debug('Started work', extra=extra)
+            i = 1
+            # Let the thread run until interrupted. This allows reasonably clean
+            # thread termination.
+            while not QtCore.QThread.currentThread().isInterruptionRequested():
+                delay = 0.5 + random.random() * 2
+                time.sleep(delay)
+                level = random.choice(LEVELS)
+                logger.log(level, 'Message after delay of %3.1f: %d', delay, i, extra=extra)
+                i += 1
+
+    #
+    # Implement a simple UI for this cookbook example. This contains:
+    #
+    # * A read-only text edit window which holds formatted log messages
+    # * A button to start work and log stuff in a separate thread
+    # * A button to log something from the main thread
+    # * A button to clear the log window
+    #
+    class Window(QtWidgets.QWidget):
+
+        COLORS = {
+            logging.DEBUG: 'black',
+            logging.INFO: 'blue',
+            logging.WARNING: 'orange',
+            logging.ERROR: 'red',
+            logging.CRITICAL: 'purple',
+        }
+
+        def __init__(self, app):
+            super(Window, self).__init__()
+            self.app = app
+            self.textedit = te = QtWidgets.QPlainTextEdit(self)
+            # Set whatever the default monospace font is for the platform
+            f = QtGui.QFont('nosuchfont')
+            f.setStyleHint(f.Monospace)
+            te.setFont(f)
+            te.setReadOnly(True)
+            PB = QtWidgets.QPushButton
+            self.work_button = PB('Start background work', self)
+            self.log_button = PB('Log a message at a random level', self)
+            self.clear_button = PB('Clear log window', self)
+            self.handler = h = QtHandler(self.update_status)
+            # Remember to use qThreadName rather than threadName in the format string.
+            fs = '%(asctime)s %(qThreadName)-12s %(levelname)-8s %(message)s'
+            formatter = logging.Formatter(fs)
+            h.setFormatter(formatter)
+            logger.addHandler(h)
+            # Set up to terminate the QThread when we exit
+            app.aboutToQuit.connect(self.force_quit)
+
+            # Lay out all the widgets
+            layout = QtWidgets.QVBoxLayout(self)
+            layout.addWidget(te)
+            layout.addWidget(self.work_button)
+            layout.addWidget(self.log_button)
+            layout.addWidget(self.clear_button)
+            self.setFixedSize(900, 400)
+
+            # Connect the non-worker slots and signals
+            self.log_button.clicked.connect(self.manual_update)
+            self.clear_button.clicked.connect(self.clear_display)
+
+            # Start a new worker thread and connect the slots for the worker
+            self.start_thread()
+            self.work_button.clicked.connect(self.worker.start)
+            # Once started, the button should be disabled
+            self.work_button.clicked.connect(lambda : self.work_button.setEnabled(False))
+
+        def start_thread(self):
+            self.worker = Worker()
+            self.worker_thread = QtCore.QThread()
+            self.worker.setObjectName('Worker')
+            self.worker_thread.setObjectName('WorkerThread')  # for qThreadName
+            self.worker.moveToThread(self.worker_thread)
+            # This will start an event loop in the worker thread
+            self.worker_thread.start()
+
+        def kill_thread(self):
+            # Just tell the worker to stop, then tell it to quit and wait for that
+            # to happen
+            self.worker_thread.requestInterruption()
+            if self.worker_thread.isRunning():
+                self.worker_thread.quit()
+                self.worker_thread.wait()
+            else:
+                print('worker has already exited.')
+
+        def force_quit(self):
+            # For use when the window is closed
+            if self.worker_thread.isRunning():
+                self.kill_thread()
+
+        # The functions below update the UI and run in the main thread because
+        # that's where the slots are set up
+
+        @Slot(str, logging.LogRecord)
+        def update_status(self, status, record):
+            color = self.COLORS.get(record.levelno, 'black')
+            s = '<pre><font color="%s">%s</font></pre>' % (color, status)
+            self.textedit.appendHtml(s)
+
+        @Slot()
+        def manual_update(self):
+            # This function uses the formatted message passed in, but also uses
+            # information from the record to format the message in an appropriate
+            # color according to its severity (level).
+            level = random.choice(LEVELS)
+            extra = {'qThreadName': ctname() }
+            logger.log(level, 'Manually logged!', extra=extra)
+
+        @Slot()
+        def clear_display(self):
+            self.textedit.clear()
+
+
+    def main():
+        QtCore.QThread.currentThread().setObjectName('MainThread')
+        logging.getLogger().setLevel(logging.DEBUG)
+        app = QtWidgets.QApplication(sys.argv)
+        example = Window(app)
+        example.show()
+        sys.exit(app.exec_())
+
+    if __name__=='__main__':
+        main()



More information about the Python-checkins mailing list