Pysimplegui: [Question] Is there any way to get python's logging library to print to the Output element?

Created on 4 Jun 2020  路  21Comments  路  Source: PySimpleGUI/PySimpleGUI

Operating System: macOS High Sierra
Python version: Python 3.7
PySimpleGUI Version: 4.19.0 Released 5-May-2020

Your Experience Levels In Months or Years

5 years. Python programming experience
5 years. Programming experience overall
no. Have used another Python GUI Framework (tkinter, Qt, etc) previously (yes/no is fine)?

You have completed these steps:

  • [x] Read instructions on how to file an Issue
  • [x] Searched through main docs http://www.PySimpleGUI.org for your problem
  • [ ] Searched through the readme for your specific port if not PySimpleGUI (Qt, WX, Remi)
  • [x] Looked for Demo Programs that are similar to your goal http://www.PySimpleGUI.com
  • [x] Note that there are also Demo Programs under each port on GitHub
  • [x] Run your program outside of your debugger (from a command line)
  • [x] Searched through Issues (open and closed) to see if already reported

Description of Question

First I'd just like to say thank you for this package. I really like the way that the layout works.

I'm wondering if there's any way to get the text logged using python's logging library to print to (preferably) the Output element.

I have a fairly big project's worth of code that is full of many logging statements. I need to share this project with lots of Windows users who would get on much better with a GUI. The logging statements are quite important to show to the users, so I'd prefer them to be on the same window as the main GUI.

Code To Duplicate

import PySimpleGUI as sg
import logging

log_file = 'run_log.txt'

# Logging setup to send one format of logs to a log file and one to stdout:
logging.basicConfig(
    level=logging.DEBUG,
    format='%(name)s, %(asctime)s, [%(levelname)s], %(message)s',
    filename=log_file,
    filemode='w')

ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(logging.Formatter('%(name)s, [%(levelname)s], %(message)s'))
logging.getLogger('').addHandler(ch)

layout = [
    [sg.Text('Number:'), sg.Input(key='input')],
    [sg.Button('Run')],
    [sg.Output(size=(100,10), key='log')],
    [sg.Button('Exit')],
]

window = sg.Window('Window Title', layout)

while True:  # Event Loop
    event, values = window.read()
    if event == sg.WIN_CLOSED or event == 'Exit':
        break
    if event == 'Run':
        logging.info('Running...')

window.close()

This outputs root, [INFO], Running... to the command line/output in PyCharm (and similar text is sent to the log file), (rather than to the output window, which is what I'd like). I was wondering if there was a way to reroute the stdout to the Output window?

I did look in the documentation, but couldn't see anything. Sorry if I missed something.

Demo Programs documentation

Most helpful comment

Over the weekend there was a new Demo Program:
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Graph_Elem_Image_Album.py

I was wondering the same thing.... how to scale images down to a particular size without writing to disk. This demo contains 2 functions that help do this. One is for files, the other for the bytes objects that are Base64 encoded.

Take a look at this function in particular:

def get_img_filename(f, resize=None):
    """Generate image data using PIL
    """
    img = PIL.Image.open(f)
    cur_width, cur_height = img.size
    if resize:
        new_width, new_height = resize
        if cur_width > cur_height:
            new_height = int(new_height * cur_height/cur_width)
        else:
            new_width = int(new_width * cur_width/cur_height)
        img = img.resize((new_width, new_height), PIL.Image.ANTIALIAS)
    bio = io.BytesIO()
    img.save(bio, format="PNG")
    del img
    return bio.getvalue()

It should return to you a bytes object that you can pass to the image.update as a data parameter.

All 21 comments

It is not processed mainly under PySimpleGUI, and you need to build a handler by yourself. To update output, I create a buffer for all output content. Format of output is defined in emit().

You can do it like this:

class Handler(logging.StreamHandler):

    def __init__(self):
        logging.StreamHandler.__init__(self)

    def emit(self, record):
        global buffer
        buffer = f'{buffer}\n{str(record)}'.strip()
        window['log'].update(value=buffer)

buffer = ''
ch = Handler()

Thanks this worked perfectly 馃槃. I've ever looked at the inner workings of handlers before. It was easy to update to get the formatting I wanted.

class Handler(logging.StreamHandler):

    def __init__(self):
        logging.StreamHandler.__init__(self)

    def emit(self, record):
        global buffer
        record = f'{record.name}, [{record.levelname}], {record.message}'
        buffer = f'{buffer}\n{record}'.strip()
        window['log'].update(value=buffer)

log_file = 'run_log.txt'

logging.basicConfig(
    level=logging.DEBUG,
    format='%(name)s, %(asctime)s, [%(levelname)s], %(message)s',
    filename=log_file,
    filemode='w')

buffer = ''
ch = Handler()
ch.setLevel(logging.INFO)
logging.getLogger('').addHandler(ch)

layout = [
    [sg.Text('Number:'), sg.Input(key='input')],
    [sg.Button('Run')],
    [sg.Output(size=(100,10), key='log')],
    [sg.Button('Exit')],
]

window = sg.Window('Window Title', layout)

while True:  # Event Loop
    event, values = window.read()
    if event == sg.WIN_CLOSED or event == 'Exit':
        break
    if event == 'Run':
        logging.info('Running...')

window.close()

I want to keep this opened. Jason came up with something important. Maybe I should add to the Cookbook? Or it could be turned into a demo program. I think it's certainly worth keeping and not losing.

@jason990420
I have a similar problem, but I am not familiar with handlers.
I would like to display scaled images (png, jpg, ,,,) with an image element. The way I do it now is that I save a temporary image to disk and read it from there. Is it possible to write it to a buffer and get it back from there. Should be faster.
A demo code modified from the cookbook looks like this. I do some other stuff with my images, therefore the special intermediate image format.
Thanks for your help, your last advice was excellent!

#!/usr/bin/env python
import PySimpleGUI as sg
import os
import numpy as np
from skimage import io, img_as_float
from skimage import transform as tf
import time

def main():

    # Get the folder containing the images from the user
    folder = sg.popup_get_folder('Image folder to open')
    if folder is None:
        sg.popup_cancel('Cancelling')
        return

    # get list of PNG, JPG or GIF files in folder
    image_files = [folder + '\\' + f for f in os.listdir(folder)
                if '.png' in f or '.jpg' in f or '.gif' in f]

    if len(image_files) == 0:
        sg.popup('No PNG images in folder')
        return

    # define layout, show and read the window
    col = [[sg.Text(image_files[0], size=(80, 3), key='filename')],
           [sg.Button('Next', size=(8, 2)), sg.Button('Prev', size=(8, 2)),
               sg.Text('File 1 of {}'.format(len(image_files)),
               size=(15, 1), key='filenum')],
           [sg.Image(filename=image_files[0], key='image', size=(500,500)),
            sg.Sizer(0,500)]]

    window = sg.Window('Image Browser', [[sg.Col(col)]],
            return_keyboard_events=True,
            location=(0, 0),
            use_default_focus=False)

    # loop reading the user input and displaying image, filename
    i = 0
    while True:

        event, values = window.read()
        # --------------------- Button & Keyboard ---------------------
        if event is None:
            break
        elif event in ('Next', 'MouseWheel:Down', 'Down:40', 'Next:34') and i < len(image_files)-1:
            i += 1
        elif event in ('Prev', 'MouseWheel:Up', 'Up:38', 'Prior:33') and i > 0:
            i -= 1
        elif event == 'Exit':
            break

        if event == 'Read':
            filename = folder + '/' + values['listbox'][0]
        else:
            t = time.time()
            filename = image_files[i]
            image = img_as_float(io.imread(filename))
            (y,x) = image.shape[:2]
            print(x,y)
            scale = 500.0/max(y, x, 500)
            if len(image.shape) == 3:
                multichannel = True
            else:
                multichannel = False
            im = tf.rescale(image, scale, multichannel=multichannel)
            im = np.clip(im, 0.0, 1.0)
            t1 = time.time()  - t
            # I save a temporary scaled png image for showing it and load it again
            io.imsave('tmp.png', (im * 255).astype(np.uint8))
        # update window with new image
        window['image'].update(filename='tmp.png')
        t2 = time.time() - t - t1
        print(scale, t1, t2)
        # update window with filename
        window['filename'].update(filename)
        # update page display
        window['filenum'].update('File {} of {}'.format(i+1, len(image_files)))

    window.close()

if __name__ == '__main__':
    main()

Over the weekend there was a new Demo Program:
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Graph_Elem_Image_Album.py

I was wondering the same thing.... how to scale images down to a particular size without writing to disk. This demo contains 2 functions that help do this. One is for files, the other for the bytes objects that are Base64 encoded.

Take a look at this function in particular:

def get_img_filename(f, resize=None):
    """Generate image data using PIL
    """
    img = PIL.Image.open(f)
    cur_width, cur_height = img.size
    if resize:
        new_width, new_height = resize
        if cur_width > cur_height:
            new_height = int(new_height * cur_height/cur_width)
        else:
            new_width = int(new_width * cur_width/cur_height)
        img = img.resize((new_width, new_height), PIL.Image.ANTIALIAS)
    bio = io.BytesIO()
    img.save(bio, format="PNG")
    del img
    return bio.getvalue()

It should return to you a bytes object that you can pass to the image.update as a data parameter.

This is what I call fast response! I was trying something similar without success. I looked at the demo. I am sure I can use this for my program. This should simplify my code. Next I have to see how I can get my FITS images into the same format (https://docs.astropy.org/en/stable/io/fits/). Thank you very much.

I was able to flip through folders of images and also used the same function to resize things like the Base64 icon that's built into PySimpleGUI. I dunno if it's a good function / technique or not, but it seems to work.

The function shown above for resizing images works only for square graphics. I suggest to change it the following way:

G_SIZE = (800,600)

def get_img_data(data, resize=None):
    """Generate PIL.Image data using PIL
    """
    img = PIL.Image.open(io.BytesIO(base64.b64decode(data)))
    cur_width, cur_height = img.size
    if resize:
        new_width, new_height = resize
        scale = min(new_height/cur_height, new_width/cur_width)
        img = img.resize((cur_width*scale, cur_height*scale), PIL.Image.ANTIALIAS)
    bio = io.BytesIO()
    img.save(bio, format="PNG")
    del img
    return bio.getvalue()

Thanks for your tip

Yes, as you said. I think it's a bug in old code and only correct when width = height.
for example, I want to size (3, 5) to (6, 10), it will get (3, 10), or (5, 3) to (10, 6), it will get (10, 3).
It means size ration changed, not 2 times as request.

I have no idea why to fix the ratio for it, not kept it unchanged and decided by user, like resize work in Image package. Sometime we may need to stretch the image with different ratio, and after that we can excatly know the size of image after resize.

Ah, I see what you mean. It's only working when the output size is square?

I hacked it together without testing a non-square output format. Sorry about that.

I'll try out your algorithm today! Thank you!

I ended up splitting out the resize. Needed to cast as int before calling resize.

Here's the new code for the image part. Will check in the new demo shortly.

def get_img_filename(f, resize=None):
    """
    Resizes an image file and returns the result as a byte string which can be used to update a PySimpleGUI Element
    """
    img = PIL.Image.open(f)
    return resize_pil_image(img, resize)


def get_img_data(data, resize=None):
    """Generate PIL.Image data using PIL
    """
    img = PIL.Image.open(io.BytesIO(base64.b64decode(data)))
    return resize_pil_image(img, resize)

def resize_pil_image(img, resize=None):
    """
    Resizes a PIL image to a new size or just converts to PNG as a byte string
    :param img:
    :param resize:
    :return:
    """
    cur_width, cur_height = img.size
    if resize:
        new_width, new_height = resize
        scale = min(new_height/cur_height, new_width/cur_width)
        img = img.resize((int(cur_width*scale), int(cur_height*scale)), PIL.Image.ANTIALIAS)
    bio = io.BytesIO()
    img.save(bio, format="PNG")
    del img
    return bio.getvalue()

my fault. I corrected get_image_filename to int image size when I noticed the bug, but copied the uncorrected und untested get_image_data in my comment. :-(
Using one function for resizing avoids this mistake. As you would say: keep it simple

Well, now that you went and tossed out that "Simple" word, that meant time to go back in and "do it right".

There should be one function for this and it should take either a filename or a bytes object. So, I made this function to replace the entire lot of functions:

def convert_to_bytes(file_or_bytes, resize=None):
    '''
    Will convert into bytes and optionally resize an image that is a file or a base64 bytes object.
    :param file_or_bytes: either a string filename or a bytes base64 image object
    :type file_or_bytes:  (Union[str, bytes])
    :param resize:  optional new size
    :type resize: (Tuple[int, int] or None)
    :return: (bytes) a byte-string object
    :rtype: (bytes)
    '''
    if isinstance(file_or_bytes, str):
        img = PIL.Image.open(file_or_bytes)
    else:
        img = PIL.Image.open(io.BytesIO(base64.b64decode(file_or_bytes)))

    cur_width, cur_height = img.size
    if resize:
        new_width, new_height = resize
        scale = min(new_height/cur_height, new_width/cur_width)
        img = img.resize((int(cur_width*scale), int(cur_height*scale)), PIL.Image.ANTIALIAS)
    bio = io.BytesIO()
    img.save(bio, format="PNG")
    del img
    return bio.getvalue()

You can pass in a filename or a bytes string and it'll return a bytes string that you can use to update an Image element or draw on a Graph element.

Now I should make sure this thing get put somewhere useful, like maybe in the Cookbook.

However, I still don't quite know what I'm doing, so until I get a little more verification that it actually works I'll hold off.

It is not processed mainly under PySimpleGUI, and you need to build a handler by yourself. To update output, I create a buffer for all output content. Format of output is defined in emit().

You can do it like this:

class Handler(logging.StreamHandler):

    def __init__(self):
        logging.StreamHandler.__init__(self)

    def emit(self, record):
        global buffer
        buffer = f'{buffer}\n{str(record)}'.strip()
        window['log'].update(value=buffer)

buffer = ''
ch = Handler()

Could you explain if and why this is better practice then simply calling an existing StreamHandler class with .TKOut like I've seen other PSG apps (like mine) do? For example, with an output element referenced by key "_output_":

viewHandler= logging.StreamHandler(window["_output_"].TKOut)
formatter= logging.Formatter("%(levelname)s: %(message)s")
viewHandler.setFormatter(formatter)
viewHandler.setLevel(logging.ERROR)
parentLogger.addHandler(viewHandler)

Sorry about that I just find a working solution and provide a suggestion. Not sure which one is best, anyway, I am not good at PySimpleGUI, also at logging. :-) So I cannot discuss other issue with you in detail. Just try to find solution or answer about PySimpleGUI issues here.

Thanks this worked perfectly 馃槃. I've ever looked at the inner workings of handlers before. It was easy to update to get the formatting I wanted.

I tried this solution, but it does not seem to work ... It just exists and says: Process finished with exit code 1
after a while. Does someone have an idea?
Here's how my code looks:

import logging

import PySimpleGUI as sg

import crawler as crawler
import iocParser
import mispParser
import postProcessing

# MISP KEY
MISP_URL = ""
MISP_KEY = ""


class Handler(logging.StreamHandler):

    def __init__(self):
        logging.StreamHandler.__init__(self)

    def emit(self, record):
        global buffer
        record = f'{record.name}, [{record.levelname}], {record.message}'
        buffer = f'{buffer}\n{record}'.strip()
        window['log'].update(value=buffer)


def generate(misp_url: str, misp_key: str, misp_cert: bool, numberOfArticlesPerBlog: int, features: int) -> None:
    """ starts the import client

    Args:
        misp_url: the URL of the local MISP instance
        misp_key: a valid API key for the current MISP instance
        misp_cert: specifies if the certificate of the server should be validated
        numberOfArticlesPerBlog: specifies how many articles we need to crawl from each blog
        features: specifies how many features the IOC objects need to have

    Returns:
        returns nothing

    """
    logging.basicConfig(level=logging.DEBUG)

    # get article URLs for every blog
    logging.info(f"Crawling {numberOfArticlesPerBlog} articles from each blog")
    crawled_urls = crawler.crawl_blogs(numberOfArticlesPerBlog)

    # parse articles
    logging.info("Parsing the blog posts...")
    api_filename = iocParser.parse(crawled_urls, parser="api")

    # post process api result
    logging.info("Filtering...")
    postProcessing.postProcess(api_filename, features)

    # create Events and Attributes
    logging.info("Creating events and attributes...")
    mispParser.processIOC(api_filename, misp_url, misp_key, misp_cert)
    logging.info("Finished.")


# logging
log_file = 'run_log.txt'

logging.basicConfig(
    level=logging.DEBUG,
    format='%(name)s, %(asctime)s, [%(levelname)s], %(message)s',
    filename=log_file,
    filemode='w')
buffer = ''
ch = Handler()
ch.setLevel(logging.INFO)
logging.getLogger('').addHandler(ch)

# GUI
sg.theme('DarkBlue3')  # Add some color to the window

# Very basic window.

layout = [
    [sg.Text('MISP infos')],
    [sg.Text('MISP URL', size=(15, 1)), sg.InputText(default_text=MISP_URL)],
    [sg.Text('MISP KEY', size=(15, 1)), sg.InputText(MISP_KEY)],
    [sg.Checkbox('Verify certificates?', default=False)],
    [sg.Text('Generator info')],
    [sg.Text('Number of articles per source'), sg.Spin([i for i in range(1, 1000)], initial_value=1)],
    [sg.Text('Number of min. features per post'), sg.Spin([i for i in range(1, 15)], initial_value=3)],
    [sg.Text('Log:')],
    [sg.Output(size=(100, 10), key='log')],
    [sg.Button('Run'), sg.Button('Exit')]
]

window = sg.Window('IOC Generator', layout)

# Event Loop
while True:
    event, values = window.read()
    if event == sg.WIN_CLOSED or event == 'Exit':
        break
    if event == 'Run':
        logging.info('Running...')
        generate(values[0], values[1], values[2], values[3], values[4])
        logging.info('Finished...')

window.close()

I should document this better, but you may find that the Multiline's recent changes make it superior to the Output element. You can now redirect stdout when creating the Multiline. That means it will work just like the Output Element in terms of print statements going to it. The Multiline has a lot more other features that you can then apply to it. Perhaps try swapping out the Output with a Multiline.

Can you post the entire error that's output and your environment (basically the stuff in the form) since nothing is known about your setup?

@PySimpleGUI thanks for the fast responds :)

I filled out the form below. Also i replaced Output with Multiline and used the print command.
Now it works and i can see the printed statements!
Still i got two things related to that:

  1. The print statements are only shown after the program is finished not in "real time".
  2. Print statements from other (selfwritten) modules/files used are not shown. How do i "import" the log windows in to a diffrent module?

Operating System

MacOS BigSur Beta

Python version

3.8

PySimpleGUI Port and Version


4.28.0 Released 3-Aug-2020

Your Experience Levels In Months or Years

1 years Python programming experience
5 years Programming experience overall
Have used another Python GUI Framework (tkinter, Qt, etc) previously (yes/no is fine)?
yes, a little Qt

You have completed these steps:

  • [X] Read instructions on how to file an Issue
  • [X] Searched through main docs http://www.PySimpleGUI.org for your problem
  • [ ] Searched through the readme for your specific port if not PySimpleGUI (Qt, WX, Remi)
  • [X] Looked for Demo Programs that are similar to your goal http://www.PySimpleGUI.com
  • [X] Note that there are also Demo Programs under each port on GitHub
  • [X] Run your program outside of your debugger (from a command line)
  • [X] Searched through Issues (open and closed) to see if already reported
  • [ ] Try again by upgrading your PySimpleGUI.py file to use the current one on GitHub. Your problem may have already been fixed but is not yet on PyPI.

Also here's now the code:

import logging

import PySimpleGUI as sg

import crawler as crawler
import iocParser
import mispParser
import postProcessing

# MISP KEY
MISP_URL = ""
MISP_KEY = ""

# logging
print = lambda *args, **kwargs: window['log'].print(*args, **kwargs)


def generate(misp_url: str, misp_key: str, misp_cert: bool, numberOfArticlesPerBlog: int, features: int) -> None:
    [...]    


# GUI
sg.theme('DarkBlue3')  # Add some color to the window

# Very basic window.  Return values using auto numbered keys

layout = [
    [sg.Text('MISP infos')],
    [sg.Text('MISP URL', size=(15, 1)), sg.InputText(default_text=MISP_URL)],
    [sg.Text('MISP KEY', size=(15, 1)), sg.InputText(MISP_KEY)],
    [sg.Checkbox('Verify certificates?', default=False)],
    [sg.Text('Generator info')],
    [sg.Text('Number of articles per source'), sg.Spin([i for i in range(1, 1000)], initial_value=1)],
    [sg.Text('Number of min. features per post'), sg.Spin([i for i in range(1, 15)], initial_value=3)],
    [sg.Text('Log:')],
    [sg.Multiline(size=(100, 10), key='log')],
    [sg.Button('Run'), sg.Button('Exit')]
]

window = sg.Window('IOC Generator', layout)

# Event Loop
while True:
    event, values = window.read()
    if event == sg.WIN_CLOSED or event == 'Exit':
        break
    if event == 'Run':
        print('Running...')
        generate(values[0], values[1], values[2], values[3], values[4])
        print('Finished...')

window.close()

If you make a change to a window or its elements, you need to either call window.read or window.refresh in order to see those changes. There should be something in the docs about updating elements and the need to perform these kinds of calls.

I don't know what to tell you about other modules. In theory, all stdout should go to that window. Maybe they're logging to stderr and you need to re-route that too? Not sure on this one.

Ok, thank you very much :) seems i did a sloppy job reading the docs 馃槄

I don't see in your code that you're re-routing stdout. Take a look at the definition of the Multiline definition for those parameters you need to set. They will show up in the docstrings if you're using PyCharm. If not, they're in the docs:
https://pysimplegui.readthedocs.io/en/latest/call%20reference/#multiline-element

Was this page helpful?
0 / 5 - 0 ratings