Monday, March 1, 2021

Learn PyQt: PDF Report generator — Generate custom PDF reports using reportlab & pdfrw

If your job involves generating PDF reports, invoices, etc. you have probably thought about automating that with Python. Python has some great libraries for working with PDF files, allowing you to read and write PDFs from scripts. But you can also use these libraries as the basic of simple GUI tools, giving you an easy way to auto-fill or edit PDF reports on the desktop.

In this tutorial we'll be using two libraries to create a custom PDF report filler. The data will be collected using a Qt form: just edit the fields, press "Generate" to get the filled out form in the folder. The two libraries we'll be using here are --

  • reportlab which allows you to create PDFs using text and drawing primitives
  • pdfrw a library for reading and extracting pages from existing PDFs

While we could use reportlab to draw the entire PDF, it's easier to design a template using external tools and then simply overlay the dynamic content on this. We can use pdfrw to read our template PDF and then extract a page, onto which we can then draw using reportlab. That allows us to overlay custom information (from our app) directly onto an existing PDF template, which we save under a new name.

In this example we're entering the fields manually, but you can modify the application to read the data for the PDF from an external CSV file & generate multiple PDFs from it.

Template PDF

For testing I've created a custom TPS report template using Google Docs and downloaded the page as PDF. The page contains a number of fields which are to be filled. In this tutorial, we'll write a PyQt form which a user can fill in and then write that data out onto the PDF at the correct place.

TPS Report

The template is in A4 format. Save it in the same folder as your script.

If you have another template you'd prefer to use, feel free to use that. Just remember that you'll need to adjust the positions of the form fields when writing it.

Laying out the Form view

Qt includes a QFormLayout layout which simplifies the process of generating simple form layouts. It works similarly to a grid, but you can add rows of elements together and strings are converted automatically to QLabel objects. Our skeleton application, including the full layout matching the template form (more or less) is shown below.

python
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox

class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)


app = QApplication([])
w = Window()
w.show()
app.exec()
python
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)


app = QApplication([])
w = Window()
w.show()
app.exec_()
python
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)


app = QApplication([])
w = Window()
w.show()
app.exec()
python
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)


app = QApplication([])
w = Window()
w.show()
app.exec_()

When writing tools to replace/automate paper forms, it's usually a good idea to try and mimic the layout of the paper form so it's familiar.

The above will give us the following layout in a window when run. You can already type things into the fields, but pressing the button won't do anything yet -- we haven't written the code to generate the PDF or hooked it up to the button.

The form layout

Generating a PDF

For PDF generation using a base template, we'll be combining reportlab and PdfReader. The process is as follows --

  1. Read in the template.pdf file using PdfReader, and extract the first page only.
  2. Create a reportlab Canvas object
  3. Use pdfrw.toreportlab.makerl to generate a canvas object then add it to the Canvas with canvas.doForm()
  4. Draw out custom bits on the Canvas
  5. Save the PDF to file

The code is shown below, this doesn't require Qt, you can save to a file and run as-is. When run the resulting PDF will be saved as result.pdf in the same folder.

python
from reportlab.pdfgen.canvas import Canvas
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl

outfile = "result.pdf"

template = PdfReader("template.pdf", decompress=False).pages[0]
template_obj = pagexobj(template)

canvas = Canvas(outfile)

xobj_name = makerl(canvas, template_obj)
canvas.doForm(xobj_name)

ystart = 443

# Prepared by
canvas.drawString(170, ystart, "My name here")

canvas.save()

Since the process of generating the PDF is doing IO, it may take some time (e.g. if we loading files off network drives). Because of this, it is better to handle this in a separate thread. We'll define this custom thread runner next.

Running the generation in a separate thread

Since each generation is an isolated job, it makes sense to use Qt's QRunner framework to handle the process -- this also makes it simple later to for example add customizable templates per job. We're using the same approach seen in the Multithreading tutorial where we use a subclass of QRunner to hold our custom run code, and implement runner-specific signals on a separate subclass of QObject.

python
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    file_saved_as = pyqtSignal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)
python
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot

from reportlab.pdfgen.canvas import Canvas

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    file_saved_as = Signal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)
python
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    file_saved_as = pyqtSignal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)

python
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot

from reportlab.pdfgen.canvas import Canvas

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    file_saved_as = Signal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)

We've defined two signals here:

  • file_saved_as which emits the filename of the saved PDF file (on success)
  • error which emits errors as a string for debugging

We need a QThreadPool to add run our custom runner on. We can add this onto our MainWindowin the __init__ block.

python
class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

Now we have the generator QRunner defined, we just need to implement the generate method to create the runner, pass it the data from our form fields and the start the generation running.

python
def generate(self):
    self.generate_btn.setDisabled(True)
    data = {
        'name': self.name.text(),
        'program_type': self.program_type.text(),
        'product_code': self.product_code.text(),
        'customer': self.customer.text(),
        'vendor': self.vendor.text(),
        'n_errors': str(self.n_errors.value()),
        'comments': self.comments.toPlainText()
    }
    g = Generator(data)
    g.signals.file_saved_as.connect(self.generated)
    g.signals.error.connect(print)  # Print errors to console.
    self.threadpool.start(g)

def generated(self, outfile):
    pass28

In this code we first disable the generate_btn so the user can't press the button multiple times while the generation is taking place. We then construct a dictionary of data from our widgets, using the .text() method to get the text from QLineEdit widgets, .value() to get the value from the QSpinBox and .toPlainText() to get the plain text representation of the QTextEdit. We convert the numeric value to a string, since we are placing text.

To actually generate the PDF we create an instance of the Generator runner we just defined, passing in the dictionary of data. We connect the file_saved_as signal to our generated method (defined at the bottom, but not doing anything yet) and the error signal to the standard Python print function: this will automatically print any errors to the console.

Finally, we take our Generator instance and pass it to our threadpool's .start() method to queue it to run (it should start immediately). We can then hook this method up to our button in the __init__ of our main window e.g.

python
    self.generate_btn.pressed.connect(self.generate)

If you run the app now, pressing the button will trigger the generation of the PDF and the result will be saved as result.pdf in the same folder as you started the app. So far we've only placed a single block of text on the page, so let's complete the generator to write all our fields in the correct place.

Completing the generator

Next we need to finish the text placement on the template. The trick here is to work out what the per-line spacing is for your template (depends on the font size etc.) and then calculate positions relative to the first line. The y coordinates increase up the page (so 0,0 is the bottom left) so in our code before, we define the ystart for the top line and then subtract 28 for each line.

python

ystart = 443

# Prepared by
canvas.drawString(170, ystart, self.data['name'])

# Date: Todays date
today = datetime.today()
canvas.drawString(410, ystart, today.strftime('%F'))

# Device/Program Type
canvas.drawString(230, ystart-28, self.data['program_type'])

# Product code
canvas.drawString(175, ystart-(2*28), self.data['product_code'])

# Customer
canvas.drawString(315, ystart-(2*28), self.data['customer'])

# Vendor
canvas.drawString(145, ystart-(3*28), self.data['vendor'])

ystart = 250

# Program Language
canvas.drawString(210, ystart, "Python")

canvas.drawString(430, ystart, self.data['n_errors'])

Wrapping

For most of our form fields we can just output the text as-is, since there are no line breaks. If the text entered is too long, then it will overflow -- but if we wanted we can limit this on the fields themselves by setting a max length in characters, e.g.

python
field.setMaxLength(25)

For the comments field, things are a little more tricky. The field can be much longer, and lines need to be wrapped over multiple lines in the template. The field also accepts line breaks (by pressing Enter) which cause problems when written out to the PDF.

Line breaks show up as black squares

As you can see in the above screenshot, the line breaks appear as black squares in the text. The good news is that just removing the line breaks will make it easier to wrap: we can just wrap each line to a specified number of characters.

Since the characters are variable width this isn't perfect, but it shouldn't matter. If we wrap for a line-full of the widest characters (W) any real line will fit.

Python comes with the textwrap library built in, which we can use to wrap our text, once we've stripped the newlines.

python
import textwrap
comments = comments.replace('\n', ' ')
lines = textwrap.wrap(comments, width=80)

But we need to account for the first line being shorter, which we can do by wrapping first to the shorter length, re-joining the remainder, and re-wrapping it, e.g.

python
import textwrap
comments = comments.replace('\n', ' ')
lines = textwrap.wrap(comments, width=65) # 45
first_line = lines[0]
remainder = ' '.join(lines[1:])

lines = textwrap.wrap(remainder, 75) # 55
lines = lines[:4]  # max lines, not including the first.

The comment markers on the wrap lines (45 & 55) show the wrap length needed to fit a line of Ws into the space. This is the shortest possible line, but not realistic. The values used should work with most normal text.

To do this properly we should calculate the actual size of each length of text in the document font and use that to inform the wrapper.

Once we have the lines prepared, we can print them onto the PDF by iterating through the list and decrementing the y position for each time. The spacing between the lines in our template document is 28.

python
comments = self.data['comments'].replace('\n', ' ')
if comments:
    lines = textwrap.wrap(comments, width=65) # 45
    first_line = lines[0]
    remainder = ' '.join(lines[1:])

    lines = textwrap.wrap(remainder, 75) # 55
    lines = lines[:4]  # max lines, not including the first.

    canvas.drawString(155, 223, first_line)
    for n, l in enumerate(lines, 1):
        canvas.drawString(80, 223 - (n*28), l)

This gives the following result with some sample lorem ipsum text.

Lorem ipsum wrapped

Automatically showing the result

When the file is created our runner returns the filename of the created file in a signal (currently it is always the same). It would be nice to present the resulting PDF to the user automatically, so they can check if everything looks good. On Windows we can use os.startfile to open a file with the default launcher for that type -- in this case opening the PDF with the default PDF viewer.

Since this isn't available on other platforms, we catch the error and instead show a QMessageBox

python

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")

Complete code

The complete code for PyQt5, PySide2, PyQt6 or PySide6 is shown below.

python
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

import os

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    file_saved_as = pyqtSignal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            # Date: Todays date
            today = datetime.today()
            canvas.drawString(410, ystart, today.strftime('%F'))

            # Device/Program Type
            canvas.drawString(230, ystart-28, self.data['program_type'])

            # Product code
            canvas.drawString(175, ystart-(2*28), self.data['product_code'])

            # Customer
            canvas.drawString(315, ystart-(2*28), self.data['customer'])

            # Vendor
            canvas.drawString(145, ystart-(3*28), self.data['vendor'])

            ystart = 250

            # Program Language
            canvas.drawString(210, ystart, "Python")

            canvas.drawString(430, ystart, self.data['n_errors'])

            comments = self.data['comments'].replace('\n', ' ')
            if comments:
                lines = textwrap.wrap(comments, width=65) # 45
                first_line = lines[0]
                remainder = ' '.join(lines[1:])

                lines = textwrap.wrap(remainder, 75) # 55
                lines = lines[:4]  # max lines, not including the first.

                canvas.drawString(155, 223, first_line)
                for n, l in enumerate(lines, 1):
                    canvas.drawString(80, 223 - (n*28), l)

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def generate(self):
        self.generate_btn.setDisabled(True)
        data = {
            'name': self.name.text(),
            'program_type': self.program_type.text(),
            'product_code': self.product_code.text(),
            'customer': self.customer.text(),
            'vendor': self.vendor.text(),
            'n_errors': str(self.n_errors.value()),
            'comments': self.comments.toPlainText()
        }
        g = Generator(data)
        g.signals.file_saved_as.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")


app = QApplication([])
w = Window()
w.show()
app.exec_()
python
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot

from reportlab.pdfgen.canvas import Canvas

import os

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    file_saved_as = Signal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            # Date: Todays date
            today = datetime.today()
            canvas.drawString(410, ystart, today.strftime('%F'))

            # Device/Program Type
            canvas.drawString(230, ystart-28, self.data['program_type'])

            # Product code
            canvas.drawString(175, ystart-(2*28), self.data['product_code'])

            # Customer
            canvas.drawString(315, ystart-(2*28), self.data['customer'])

            # Vendor
            canvas.drawString(145, ystart-(3*28), self.data['vendor'])

            ystart = 250

            # Program Language
            canvas.drawString(210, ystart, "Python")

            canvas.drawString(430, ystart, self.data['n_errors'])

            comments = self.data['comments'].replace('\n', ' ')
            if comments:
                lines = textwrap.wrap(comments, width=65) # 45
                first_line = lines[0]
                remainder = ' '.join(lines[1:])

                lines = textwrap.wrap(remainder, 75) # 55
                lines = lines[:4]  # max lines, not including the first.

                canvas.drawString(155, 223, first_line)
                for n, l in enumerate(lines, 1):
                    canvas.drawString(80, 223 - (n*28), l)

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def generate(self):
        self.generate_btn.setDisabled(True)
        data = {
            'name': self.name.text(),
            'program_type': self.program_type.text(),
            'product_code': self.product_code.text(),
            'customer': self.customer.text(),
            'vendor': self.vendor.text(),
            'n_errors': str(self.n_errors.value()),
            'comments': self.comments.toPlainText()
        }
        g = Generator(data)
        g.signals.file_saved_as.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")


app = QApplication([])
w = Window()
w.show()
app.exec_()
python
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

import os

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    file_saved_as = pyqtSignal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data:The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            # Date: Todays date
            today = datetime.today()
            canvas.drawString(410, ystart, today.strftime('%F'))

            # Device/Program Type
            canvas.drawString(230, ystart-28, self.data['program_type'])

            # Product code
            canvas.drawString(175, ystart-(2*28), self.data['product_code'])

            # Customer
            canvas.drawString(315, ystart-(2*28), self.data['customer'])

            # Vendor
            canvas.drawString(145, ystart-(3*28), self.data['vendor'])

            ystart = 250

            # Program Language
            canvas.drawString(210, ystart, "Python")

            canvas.drawString(430, ystart, self.data['n_errors'])

            comments = self.data['comments'].replace('\n', ' ')
            if comments:
                lines = textwrap.wrap(comments, width=65) # 45
                first_line = lines[0]
                remainder = ' '.join(lines[1:])

                lines = textwrap.wrap(remainder, 75) # 55
                lines = lines[:4]  # max lines, not including the first.

                canvas.drawString(155, 223, first_line)
                for n, l in enumerate(lines, 1):
                    canvas.drawString(80, 223 - (n*28), l)

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def generate(self):
        self.generate_btn.setDisabled(True)
        data = {
            'name': self.name.text(),
            'program_type': self.program_type.text(),
            'product_code': self.product_code.text(),
            'customer': self.customer.text(),
            'vendor': self.vendor.text(),
            'n_errors': str(self.n_errors.value()),
            'comments': self.comments.toPlainText()
        }
        g = Generator(data)
        g.signals.file_saved_as.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")


app = QApplication([])
w = Window()
w.show()
app.exec()
python
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot

from reportlab.pdfgen.canvas import Canvas

import os

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    file_saved_as = Signal(str)


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data:The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            outfile = "result.pdf"

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            canvas = Canvas(outfile)

            xobj_name = makerl(canvas, template_obj)
            canvas.doForm(xobj_name)

            ystart = 443

            # Prepared by
            canvas.drawString(170, ystart, self.data['name'])

            # Date: Todays date
            today = datetime.today()
            canvas.drawString(410, ystart, today.strftime('%F'))

            # Device/Program Type
            canvas.drawString(230, ystart-28, self.data['program_type'])

            # Product code
            canvas.drawString(175, ystart-(2*28), self.data['product_code'])

            # Customer
            canvas.drawString(315, ystart-(2*28), self.data['customer'])

            # Vendor
            canvas.drawString(145, ystart-(3*28), self.data['vendor'])

            ystart = 250

            # Program Language
            canvas.drawString(210, ystart, "Python")

            canvas.drawString(430, ystart, self.data['n_errors'])

            comments = self.data['comments'].replace('\n', ' ')
            if comments:
                lines = textwrap.wrap(comments, width=65) # 45
                first_line = lines[0]
                remainder = ' '.join(lines[1:])

                lines = textwrap.wrap(remainder, 75) # 55
                lines = lines[:4]  # max lines, not including the first.

                canvas.drawString(155, 223, first_line)
                for n, l in enumerate(lines, 1):
                    canvas.drawString(80, 223 - (n*28), l)

            canvas.save()

        except Exception as e:
            self.signals.error.emit(str(e))
            return

        self.signals.file_saved_as.emit(outfile)


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

        self.name = QLineEdit()
        self.program_type = QLineEdit()
        self.product_code = QLineEdit()
        self.customer = QLineEdit()
        self.vendor = QLineEdit()
        self.n_errors = QSpinBox()
        self.n_errors.setRange(0, 1000)
        self.comments = QTextEdit()

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow("Name", self.name)
        layout.addRow("Program Type", self.program_type)
        layout.addRow("Product Code", self.product_code)
        layout.addRow("Customer", self.customer)
        layout.addRow("Vendor", self.vendor)
        layout.addRow("No. of Errors", self.n_errors)

        layout.addRow("Comments", self.comments)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def generate(self):
        self.generate_btn.setDisabled(True)
        data = {
            'name': self.name.text(),
            'program_type': self.program_type.text(),
            'product_code': self.product_code.text(),
            'customer': self.customer.text(),
            'vendor': self.vendor.text(),
            'n_errors': str(self.n_errors.value()),
            'comments': self.comments.toPlainText()
        }
        g = Generator(data)
        g.signals.file_saved_as.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self, outfile):
        self.generate_btn.setDisabled(False)
        try:
            os.startfile(outfile)
        except Exception:
            # If startfile not available, show dialog.
            QMessageBox.information(self, "Finished", "PDF has been generated")


app = QApplication([])
w = Window()
w.show()
app.exec_()

Generating from a CSV file

In the above example you need to type the data to fill in manually. This is fine if you don't have a lot of PDFs to generate, but not so much fun if you have an entire CSV file worth of data to generate reports for. In the example below, rather than present a list of form fields to the user we just ask for a source CSV file from which PDFs can be generated -- each row in the file generates a separate PDF file using the data in the file.

python
from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

from reportlab.pdfgen.canvas import Canvas

import os, csv

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    finished = pyqtSignal()


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            filename, _ = os.path.splitext(self.data['sourcefile'])
            folder = os.path.dirname(self.data['sourcefile'])

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            with open(self.data['sourcefile'], 'r', newline='') as f:
                reader = csv.DictReader(f)

                for n, row in enumerate(reader, 1):
                    fn = f'{filename}-{n}.pdf'
                    outfile = os.path.join(folder, fn)
                    canvas = Canvas(outfile)

                    xobj_name = makerl(canvas, template_obj)
                    canvas.doForm(xobj_name)

                    ystart = 443

                    # Prepared by
                    canvas.drawString(170, ystart, row.get('name', ''))

                    # Date: Todays date
                    today = datetime.today()
                    canvas.drawString(410, ystart, today.strftime('%F'))

                    # Device/Program Type
                    canvas.drawString(230, ystart-28, row.get('program_type', ''))

                    # Product code
                    canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))

                    # Customer
                    canvas.drawString(315, ystart-(2*28), row.get('customer', ''))

                    # Vendor
                    canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))

                    ystart = 250

                    # Program Language
                    canvas.drawString(210, ystart, "Python")

                    canvas.drawString(430, ystart, row.get('n_errors', ''))

                    comments = row.get('comments', '').replace('\n', ' ')
                    if comments:
                        lines = textwrap.wrap(comments, width=65) # 45
                        first_line = lines[0]
                        remainder = ' '.join(lines[1:])

                        lines = textwrap.wrap(remainder, 75) # 55
                        lines = lines[:4]  # max lines, not including the first.

                        canvas.drawString(155, 223, first_line)
                        for n, l in enumerate(lines, 1):
                            canvas.drawString(80, 223 - (n*28), l)

                    canvas.save()

        except Exception as e:

            self.signals.error.emit(str(e))
            return

        self.signals.finished.emit()


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

        self.sourcefile = QLineEdit()
        self.sourcefile.setDisabled(True)  # must use the file finder to select a valid file.

        self.file_select = QPushButton("Select CSV...")
        self.file_select.pressed.connect(self.choose_csv_file)

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow(self.sourcefile, self.file_select)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def choose_csv_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
        if filename:
            self.sourcefile.setText(filename)

    def generate(self):
        if not self.sourcefile.text():
            return  # If the field is empty, ignore.

        self.generate_btn.setDisabled(True)

        data = {
            'sourcefile': self.sourcefile.text(),
        }
        g = Generator(data)
        g.signals.finished.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self):
        self.generate_btn.setDisabled(False)
        QMessageBox.information(self, "Finished", "PDFs have been generated")


app = QApplication([])
w = Window()
w.show()
app.exec()
python
from PySide2.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PySide2.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot


from reportlab.pdfgen.canvas import Canvas

import os, csv

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    finished = Signal()


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            filename, _ = os.path.splitext(self.data['sourcefile'])
            folder = os.path.dirname(self.data['sourcefile'])

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            with open(self.data['sourcefile'], 'r', newline='') as f:
                reader = csv.DictReader(f)

                for n, row in enumerate(reader, 1):
                    fn = f'{filename}-{n}.pdf'
                    outfile = os.path.join(folder, fn)
                    canvas = Canvas(outfile)

                    xobj_name = makerl(canvas, template_obj)
                    canvas.doForm(xobj_name)

                    ystart = 443

                    # Prepared by
                    canvas.drawString(170, ystart, row.get('name', ''))

                    # Date: Todays date
                    today = datetime.today()
                    canvas.drawString(410, ystart, today.strftime('%F'))

                    # Device/Program Type
                    canvas.drawString(230, ystart-28, row.get('program_type', ''))

                    # Product code
                    canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))

                    # Customer
                    canvas.drawString(315, ystart-(2*28), row.get('customer', ''))

                    # Vendor
                    canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))

                    ystart = 250

                    # Program Language
                    canvas.drawString(210, ystart, "Python")

                    canvas.drawString(430, ystart, row.get('n_errors', ''))

                    comments = row.get('comments', '').replace('\n', ' ')
                    if comments:
                        lines = textwrap.wrap(comments, width=65) # 45
                        first_line = lines[0]
                        remainder = ' '.join(lines[1:])

                        lines = textwrap.wrap(remainder, 75) # 55
                        lines = lines[:4]  # max lines, not including the first.

                        canvas.drawString(155, 223, first_line)
                        for n, l in enumerate(lines, 1):
                            canvas.drawString(80, 223 - (n*28), l)

                    canvas.save()

        except Exception as e:

            self.signals.error.emit(str(e))
            return

        self.signals.finished.emit()


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

        self.sourcefile = QLineEdit()
        self.sourcefile.setDisabled(True)  # must use the file finder to select a valid file.

        self.file_select = QPushButton("Select CSV...")
        self.file_select.pressed.connect(self.choose_csv_file)

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow(self.sourcefile, self.file_select)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def choose_csv_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
        if filename:
            self.sourcefile.setText(filename)

    def generate(self):
        if not self.sourcefile.text():
            return  # If the field is empty, ignore.

        self.generate_btn.setDisabled(True)

        data = {
            'sourcefile': self.sourcefile.text(),
        }
        g = Generator(data)
        g.signals.finished.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self):
        self.generate_btn.setDisabled(False)
        QMessageBox.information(self, "Finished", "PDFs have been generated")


app = QApplication([])
w = Window()
w.show()
app.exec_()
python
from PyQt6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot


from reportlab.pdfgen.canvas import Canvas

import os, csv

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = pyqtSignal(str)
    finished = pyqtSignal()


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        try:
            filename, _ = os.path.splitext(self.data['sourcefile'])
            folder = os.path.dirname(self.data['sourcefile'])

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            with open(self.data['sourcefile'], 'r', newline='') as f:
                reader = csv.DictReader(f)

                for n, row in enumerate(reader, 1):
                    fn = f'{filename}-{n}.pdf'
                    outfile = os.path.join(folder, fn)
                    canvas = Canvas(outfile)

                    xobj_name = makerl(canvas, template_obj)
                    canvas.doForm(xobj_name)

                    ystart = 443

                    # Prepared by
                    canvas.drawString(170, ystart, row.get('name', ''))

                    # Date: Todays date
                    today = datetime.today()
                    canvas.drawString(410, ystart, today.strftime('%F'))

                    # Device/Program Type
                    canvas.drawString(230, ystart-28, row.get('program_type', ''))

                    # Product code
                    canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))

                    # Customer
                    canvas.drawString(315, ystart-(2*28), row.get('customer', ''))

                    # Vendor
                    canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))

                    ystart = 250

                    # Program Language
                    canvas.drawString(210, ystart, "Python")

                    canvas.drawString(430, ystart, row.get('n_errors', ''))

                    comments = row.get('comments', '').replace('\n', ' ')
                    if comments:
                        lines = textwrap.wrap(comments, width=65) # 45
                        first_line = lines[0]
                        remainder = ' '.join(lines[1:])

                        lines = textwrap.wrap(remainder, 75) # 55
                        lines = lines[:4]  # max lines, not including the first.

                        canvas.drawString(155, 223, first_line)
                        for n, l in enumerate(lines, 1):
                            canvas.drawString(80, 223 - (n*28), l)

                    canvas.save()

        except Exception as e:

            self.signals.error.emit(str(e))
            return

        self.signals.finished.emit()


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

        self.sourcefile = QLineEdit()
        self.sourcefile.setDisabled(True)  # must use the file finder to select a valid file.

        self.file_select = QPushButton("Select CSV...")
        self.file_select.pressed.connect(self.choose_csv_file)

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow(self.sourcefile, self.file_select)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def choose_csv_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
        if filename:
            self.sourcefile.setText(filename)

    def generate(self):
        if not self.sourcefile.text():
            return  # If the field is empty, ignore.

        self.generate_btn.setDisabled(True)

        data = {
            'sourcefile': self.sourcefile.text(),
        }
        g = Generator(data)
        g.signals.finished.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self):
        self.generate_btn.setDisabled(False)
        QMessageBox.information(self, "Finished", "PDFs have been generated")


app = QApplication([])
w = Window()
w.show()
app.exec()
python
from PySide6.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal, Slot


from reportlab.pdfgen.canvas import Canvas

import os, csv

import textwrap
from datetime import datetime

from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl


class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """
    error = Signal(str)
    finished = Signal()


class Generator(QRunnable):
    """
    Worker thread

    Inherits from QRunnable to handle worker thread setup, signals
    and wrap-up.

    :param data: The data to add to the PDF for generating.
    """

    def __init__(self, data):
        super().__init__()
        self.data = data
        self.signals = WorkerSignals()

    @Slot()
    def run(self):
        try:
            filename, _ = os.path.splitext(self.data['sourcefile'])
            folder = os.path.dirname(self.data['sourcefile'])

            template = PdfReader("template.pdf", decompress=False).pages[0]
            template_obj = pagexobj(template)

            with open(self.data['sourcefile'], 'r', newline='') as f:
                reader = csv.DictReader(f)

                for n, row in enumerate(reader, 1):
                    fn = f'{filename}-{n}.pdf'
                    outfile = os.path.join(folder, fn)
                    canvas = Canvas(outfile)

                    xobj_name = makerl(canvas, template_obj)
                    canvas.doForm(xobj_name)

                    ystart = 443

                    # Prepared by
                    canvas.drawString(170, ystart, row.get('name', ''))

                    # Date: Todays date
                    today = datetime.today()
                    canvas.drawString(410, ystart, today.strftime('%F'))

                    # Device/Program Type
                    canvas.drawString(230, ystart-28, row.get('program_type', ''))

                    # Product code
                    canvas.drawString(175, ystart-(2*28), row.get('product_code', ''))

                    # Customer
                    canvas.drawString(315, ystart-(2*28), row.get('customer', ''))

                    # Vendor
                    canvas.drawString(145, ystart-(3*28), row.get('vendor', ''))

                    ystart = 250

                    # Program Language
                    canvas.drawString(210, ystart, "Python")

                    canvas.drawString(430, ystart, row.get('n_errors', ''))

                    comments = row.get('comments', '').replace('\n', ' ')
                    if comments:
                        lines = textwrap.wrap(comments, width=65) # 45
                        first_line = lines[0]
                        remainder = ' '.join(lines[1:])

                        lines = textwrap.wrap(remainder, 75) # 55
                        lines = lines[:4]  # max lines, not including the first.

                        canvas.drawString(155, 223, first_line)
                        for n, l in enumerate(lines, 1):
                            canvas.drawString(80, 223 - (n*28), l)

                    canvas.save()

        except Exception as e:

            self.signals.error.emit(str(e))
            return

        self.signals.finished.emit()


class Window(QWidget):

    def __init__(self):
        super().__init__()

        self.threadpool = QThreadPool()

        self.sourcefile = QLineEdit()
        self.sourcefile.setDisabled(True)  # must use the file finder to select a valid file.

        self.file_select = QPushButton("Select CSV...")
        self.file_select.pressed.connect(self.choose_csv_file)

        self.generate_btn = QPushButton("Generate PDF")
        self.generate_btn.pressed.connect(self.generate)

        layout = QFormLayout()
        layout.addRow(self.sourcefile, self.file_select)
        layout.addRow(self.generate_btn)

        self.setLayout(layout)

    def choose_csv_file(self):
        filename, _ = QFileDialog.getOpenFileName(self, "Select a file", filter="CSV files (*.csv)")
        if filename:
            self.sourcefile.setText(filename)

    def generate(self):
        if not self.sourcefile.text():
            return  # If the field is empty, ignore.

        self.generate_btn.setDisabled(True)

        data = {
            'sourcefile': self.sourcefile.text(),
        }
        g = Generator(data)
        g.signals.finished.connect(self.generated)
        g.signals.error.connect(print)  # Print errors to console.
        self.threadpool.start(g)

    def generated(self):
        self.generate_btn.setDisabled(False)
        QMessageBox.information(self, "Finished", "PDFs have been generated")


app = QApplication([])
w = Window()
w.show()
app.exec_()

You can run this app using the template.pdf and this example CSV file to generate a few TPS reports.

Things to notice --

  • We now generate multiple files, so it doesn't make much sense to open them when they're finished. Instead, we always show the "complete" message, and only once. The signal file_saved_as has been renamed to finished and we've removed the filename str since it's no longer used.
  • The QLineEdit to get the filename is disabled so it's not possible to edit directly: the only way to set a source CSV file is to select the file directly, ensuring it's there.
  • We auto-generate the output filenames, based on the import filename and the current row number. The filename is taken from the input CSV: with a CSV named tps.csv files will be named tps-1.pdf, tps-2.pdf etc. Files are written out to the folder the source CSV is in.
  • Since some rows/files might miss required fields, we use .get() on the row dictionary with a default empty string.

Possible improvements

If you feel like improving on this code, there are a few things you could try

  • Make the template and output file location configurable -- use a Qt file dialogs
  • Load the field positions from a file alongside the template (JSON) so you can use the same form with multiple templates
  • Make the fields configurable -- this gets quite tricky, but you particular types (str, datetime, int, etc.) can have specific widgets assigned to them

Share your ideas in the forum.

See the complete PyQt5 tutorial, from first steps to complete applications with Python & Qt5.



from Planet Python
via read more

Python Software Foundation: Happy Anniversary to Python and the Python Software Foundation!

For two decades the PSF has worked on:
  • diversity and inclusion efforts
  • establishing and enforcing a code of conduct for our community
  • making PyCon more accessible
  • improving the services we provide to the community 
  • increasing our support of developers and volunteers
  • supporting Python conferences, workshops, and other events around the world.

As we celebrate the PSF’s 20th and Python’s 30th anniversaries, we want to welcome everyone to become a PSF member. It’s important to us to have a membership that reflects our community. We want our community to be more diverse: whoever you are, and whatever your background, we welcome you. We are working to support a community that is based on mutual respect, acceptance, and encouragement. 

Everyone who uses and supports Python is invited to join us at any of the following membership levels:

Basic Member - it’s free!

Being part of the PSF means being part of the Python community. In 2014, we changed the bylaws to make the PSF an open membership organization, so that everyone who uses and supports Python can join. You can sign up as a basic member here.

Supporting Member - $99/year

Supporting members make an annual donation to the PSF to sustain the Foundation and support the Python community. They are eligible to vote for candidates for the PSF Board of Directors, changes in the PSF bylaws, and other matters related to the foundation. To become a Supporting member, follow the instructions here

Managing Member 

Managing members are people who dedicate at least five hours per month working to support the Python ecosystem - by organizing Python events, managing or contributing to PSF projects, running infrastructure, participating in one of the PSF's working groups etc. For details on signing up as a contributing member, refer to this form.

Contributing Member 

Contributing members are people who dedicate at least five hours per month working on projects that advance the mission of the PSF, where the work relates to the creation or maintenance of open source software available to the public at no charge. Contributing members are eligible to vote. For details on signing up as a contributing member, refer to this form.

Help us celebrate and spread the word!

We’d love to hear your stories about what being part of the Python community means to you. Post a picture or a memory with our hashtag (#psfanniversary) and we’ll retweet.

We at the PSF want to thank everyone in our community for all that you do. Your support is what makes Python and its community so extraordinary.










from Planet Python
via read more

Stack Abuse: Python: Check if Variable is a List

Introduction

Python is a dynamically typed language, and the variable data types are inferred without explicit intervention by the developer.

If we had code that needed a list but lacked type hints, which are optional, how can we avoid errors if the variable used is not a list?

In this tutorial, we'll take a look at how to check if a variable is a list in Python, using the type() and isinstance() functions, as well as the is operator:

Developers usually use type() and is, though, these can be limited in certain contexts, in which case, it's better to use the isinstance() function.

Check if Variable is a List with type()

The built-in type() function can be used to return the data type of an object. Let's create a Dictionary, Tuple and List and use the type() function to check if a variable is a list or not:

grocery_list = ["milk", "cereal", "ice-cream"]
aDict = {"username": "Daniel", "age": 27, "gender": "Male"}
aTuple = ("apple", "banana", "cashew")

# Prints the type of each variable
print("The type of grocery_list is ", type(grocery_list))
print("The type of aDict is ", type(aDict))
print("The type of aTuple is ", type(aTuple))

This results in:

The type of grocery_list is  <class 'list'>
The type of aDict is  <class 'dict'>
The type of aTuple is  <class 'tuple'>

Now, to alter code flow programatically, based on the results of this function:

a_list = [1, 2, 3, 4, 5]

# Checks if the variable "a_list" is a list
if type(a_list) == list:
    print("Variable is a list.")
else:
    print("Variable is not a list.")

This results in:

"Variable is a list."

Check if Variable is a List with is Operator

The is operator is used to compare identities in Python. That is to say, it's used to check if two objects refer to the same location in memory.

The result of type(variable) will always point to the same memory location as the class of that variable. So, if we compare the results of the type() function on our variable with the list class, it'll return True if our variable is a list.

Let's take a look at the is operator:

a_list = [1, 2, 3, 4, 5]
print(type(a_list) is list)

This results in:

True

Since this might look off to some, let's do a sanity check for this approach, and compare the IDs of the objects in memory as well:

print("Memory address of 'list' class:", id(list))
print("Memory address of 'type(a_list)':", id(type(a_list)))

Now, these should return the same number:

Memory address of 'list' class: 4363151680
Memory address of 'type(a_list)': 4363151680

Note: You'll need to keep any subtypes in mind if you've opted for this approach. If you compare the type() result of any list sub-type, with the list class, it'll return False, even though the variable is-a list, although, a sub-class of it.

This shortfall of the is operator is fixed in the next approach - using the isinstance() function.

Check if Variable is a List with isinstance()

The isinstance() function is another built-in function that allows you to check the data type of a variable. The function takes two arguments - the variable we're checking the type for, and the type we're looking for.

This function also takes sub-classes into consideration, so any list sub-classes will also return True for being an instance of the list.

Let's try this out with a regular list and a UserList from the collections framework:

from collections import UserList

regular_list = [1, 2, 3, 4, 5]
user_list = [6, 7, 8, 9, 10]

# Checks if the variable "a_list" is a list
if isinstance(regular_list, list):
    print("'regular_list' is a list.")
else:
    print("'regular_list' is not a list.")

# Checks if the variable "a_string" is a list
if isinstance(user_list, list):
    print("'user_list' is a list.")
else:
    print("'user_list' is not a list.")

Running this code results in:

'regular_list' is a list.
'user_list' is a list.

Conclusion

Python is a dynamically typed language, and sometimes, due to user-error, we might deal with an unexpected data type.

In this tutorial, we've gone over three ways to check if a variable is a list in Python - the type() function, the is operator and isinstance() function.



from Planet Python
via read more

John Ludhi/nbshare.io: Opinion Mining Aspect Level Sentiment Analysis

Opinion Mining - Aspect Level Sentiment Analysis

Aspect level sentiment analysis employs multiple machine learning processes. The first is parsing the sentence to extract the relation between words and be able to identify the aspects of a review. The second is analysing the sentiment of the adjectives used to describe the aspects.

This can be done automatically using Azure's Text Analytics service. All we need to do is to create a free account on microsoft azure and create a text analytics service: link

  1. Once you create and login to your account go to azure portal.
  2. Search for Text Analytics and create a new service.
  3. It will ask for a resource group, click on "create new"
  4. Choose the free tier which works fine for personal experimentation.
  5. Once the service is created, go to your resources and look for Keys and Endpoints, copy the keys and put them in the following cell.
In [1]:
KEY = "PUT THE KEY HERE"
ENDPOINT = "PUT THE ENDPOINT HERE"

This function is just header to authenticate your credentials and connect with Azure. We can the communicate with the Azure ML service through the client object.

In [ ]:
from azure.ai.textanalytics import TextAnalyticsClient
from azure.core.credentials import AzureKeyCredential


def authenticate_client():
    ta_credential = AzureKeyCredential(KEY)
    text_analytics_client = TextAnalyticsClient(
        endpoint=ENDPOINT,
        credential=ta_credential)
    return text_analytics_client


client = authenticate_client() # we will interact with Azure ML via this object.

We will use Jupyter's widgets to create an interactive tool for opinion mining.

In [2]:
import ipywidgets as widgets

We will use Plotly library for interactive visualizations.

In [ ]:
import plotly.graph_objs as go
from plotly.offline import init_notebook_mode
from plotly.subplots import make_subplots
init_notebook_mode() # this line is required to be able to export the notebook as html with the plots.
In [4]:
# given three score (positive - neutral - negative) this function plots a pie chart of the three sentiments
def plot_sentiment_scores(pos, neut, neg):
    return go.Figure(go.Pie(labels=["Positive", "Neutral", "Negative"], values=[pos, neut, neg],
                            textinfo='label+percent',
                            marker=dict(colors=["#2BAE66FF", "#795750", "#C70039"])),
                     layout=dict(showlegend=False)
                     )

Sentiment Analysis using Azure's Text Analytics

Azure's Text analytics analyzes documents, not just sentences. Each document is a list of sentences. So our input must be a list of sentences.

We can use our Azure client to call the analyze_sentiment method, which will return a list of sentiment scores for each passed document. Since we are just using one document with one sentence, we are interested in the first thing it returns, which is a tuple of three values: positive, negative, and neutral sentiment scores.

In [ ]:
response = client.analyze_sentiment(documents=["This movie is fantastic"])
response
In [ ]:
response[0]

AnalyzeSentimentResult(id=0, sentiment=positive, warnings=[], statistics=None, confidence_scores=SentimentConfidenceScores(positive=1.0, neutral=0.0, negative=0.0), sentences=[SentenceSentiment(text=This movie is fantastic, sentiment=positive, confidence_scores=SentimentConfidenceScores(positive=1.0, neutral=0.0, negative=0.0), offset=0, mined_opinions=[])], is_error=False)

In [ ]:
print(f"Positive: {response[0].confidence_scores.positive}")
print(f"Neutral: {response[0].confidence_scores.neutral}")
print(f"Negative: {response[0].confidence_scores.negative}")

Positive: 1.0
Neutral: 0.0
Negative: 0.0

Let's put all of this in a function that takes a list of sentences as an input and plots the distribution of sentiment scores as a pie chart!

In [ ]:
def sentiment_analysis_example(sentences):
    document = [sentences] # we use only one document for this function
    response = client.analyze_sentiment(documents=document)[0] # we use [0] to get only the first and only document
    print("Document Sentiment: {}".format(response.sentiment))
    plot_sentiment_scores(response.confidence_scores.positive,
                          response.confidence_scores.neutral,
                          response.confidence_scores.negative
                         ).show()
    
    
    # here we plot the sentiment for each sentence in the document.
    for idx, sentence in enumerate(response.sentences):
        print("Sentence: {}".format(sentence.text))
        print("Sentence {} sentiment: {}".format(idx+1, sentence.sentiment))
        plot_sentiment_scores(sentence.confidence_scores.positive,
                          sentence.confidence_scores.neutral,
                          sentence.confidence_scores.negative
                         ).show()
In [ ]:
sentiment_analysis_example("The acting was good. The graphics however were just okayish. I did not like the ending though.")

Document Sentiment: mixed

Sentence: The acting was good.
Sentence 1 sentiment: positive

Sentence: The graphics however were just okayish.
Sentence 2 sentiment: negative

Sentence: I did not like the ending though.
Sentence 3 sentiment: negative

Aspect Level Opinion Mining Using Azure Text Analytics

Instead of just reporting the overall sentiment of a sentence, in aspect-level opinion mining, there are two main differences:

  1. We extract specific aspects in the sentences.
  2. We detect the opinion about the aspect in the text, not just a sentiment score.
In [ ]:
repsonse = client.analyze_sentiment(
    ["The food and service were unacceptable and meh, but the concierge were nice and ok"],
    show_opinion_mining=True # only addition is that we set `show_opinion_mining` to True
)[0]
In [ ]:
# now we can also access the mined_opinions in a sentence
mined_opinion = repsonse.sentences[0].mined_opinions[0]
aspect = mined_opinion.aspect
print(f"Aspect: {aspect.text}")
for opinion in mined_opinion.opinions:
    print(f"Opinion: {opinion.text}\tSentiment:{opinion.sentiment}".expandtabs(12))
    # p.s. we use expandtabs because unacceptable is longer than 8 characters
    # , so we want the \t to consider it one long word

Aspect: food Opinion:
unacceptable Sentiment:negative
Opinion: meh Sentiment:mixed

Let's make this more visual

In [ ]:
def plot_sentiment_gauge(pos_score, title, domain=[0, 1]):
    fig = go.Figure(go.Indicator(
        mode="gauge+number",
        value=pos_score,
        gauge={'axis': {'range': [0, 1]}},
        domain={'x': domain, 'y': [0, 1]},
        title={'text': f"{title}", "font":dict(size=14)}), layout=dict(width=800, height=600, margin=dict(l=150,r=150)))
    return fig
In [ ]:
def sentiment_analysis_with_opinion_mining_example(sentences,
                                                   document_level=True,
                                                   sentence_level=True,
                                                   aspect_level=True,
                                                   opinion_level=True):

    document = [sentences]

    response = client.analyze_sentiment(document, show_opinion_mining=True)[0]

    if document_level:  # plotting overall document sentiment
        print("Document Sentiment: {}".format(response.sentiment))
        plot_sentiment_scores(response.confidence_scores.positive,
                              response.confidence_scores.neutral,
                              response.confidence_scores.negative
                              ).show()
    if not(sentence_level or aspect_level or opinion_level):
        # no need to continue if no plots are needed
        return response
    
    for sentence in response.sentences:
        if sentence_level:  # plotting the overall sentence sentiment
            print(f"Sentence: {sentence.text}")
            print(f"Sentence sentiment: {sentence.sentiment}")
            plot_sentiment_scores(
                sentence.confidence_scores.positive,
                sentence.confidence_scores.neutral,
                sentence.confidence_scores.negative).show()

        for mined_opinion in sentence.mined_opinions:
            aspect = mined_opinion.aspect

            if aspect_level:  # plotting the sentiment of the aspect
                plot_sentiment_gauge(
                    aspect.confidence_scores.positive, f"Aspect ({aspect.text})").show()

            if opinion_level:
                opinions = mined_opinion.opinions
                n = len(opinions)
                gauges = list()
                for i, opinion in enumerate(opinions, start=1):
                    gauges.append(plot_sentiment_gauge(
                        opinion.confidence_scores.positive, f"Opinion ({opinion.text})",
                        # this is just to show the plots next to each other
                        domain=[(i-1)/n, i/n]
                    ).data[0])

                go.Figure(gauges, layout=go.Layout(
                    height=600, width=800, autosize=False)).show()
    return response
In [ ]:
response = sentiment_analysis_with_opinion_mining_example(
    "The food and service were unacceptable and meh, but the concierge were nice and ok",
    document_level=False, sentence_level=False
)






Text Analytics using Jupyter widgets

Now let's create some jupyter widgets to interact with this function.

In [ ]:
# some text to get the input
text = widgets.Textarea(placeholder="Enter your text here")
# checkboxes to select different levels of analysis
document_cb = widgets.Checkbox(value=True, description="Document Level")
sentence_cb = widgets.Checkbox(value=True, description="Sentence Level")
aspect_cb = widgets.Checkbox(value=True, description="Aspect Level")
opinion_cb = widgets.Checkbox(value=True, description="Opinion Level")

# some button to trigger the analysis
btn = widgets.Button(description="Analyse")

# some place to show the output on
out = widgets.Output()

def analysis(b):
    with out:
        out.clear_output()
        sentences = text.value # get the input sentences from the Textarea widget
        # pass the input sentences to our `sentiment_analysis_example` function
        sentiment_analysis_with_opinion_mining_example(sentences,
                                                       document_level=document_cb.value,
                                                       sentence_level=sentence_cb.value,
                                                       aspect_level=aspect_cb.value,
                                                       opinion_level=opinion_cb.value
                                                      )

btn.on_click(analysis) # bind the button with the `sentiment_analysis` function

# put all widgets together in a tool
checkboxes = widgets.VBox([document_cb, sentence_cb, aspect_cb,opinion_cb])
tool = widgets.VBox([widgets.HBox([text, checkboxes]), btn, out]) 
# give a default value for the text
text.value = "The food and service were unacceptable and meh, but the concierge were nice and ok"
tool



from Planet Python
via read more

Tryton News: Newsletter for March 2021

Here’s a sneak peak at the improvements that landed during the last month.

Changes for the User

We now show the carrier on the shipment list so it’s possible to prioritize shipments based on the carrier.

We’ve added a wizard to make it easy to add lots to stock moves. The sequence to use for the lot number can be configured for each product.

We ensure the unit prices for stock moves are up to date when their invoices are posted or their moves are done.

The account move lines created by a statement now have the statement line as their origin. This makes it simpler to audit the accounts.

We now use the menu path from which a window was opened as its name.

We now warn the user when they try to post a statement with cancelled or paid invoices and then remove them from the statement.

A delivery usage checkbox has been added to contact mechanisms just like for addresses. It can be used, for example, to indicate which email address to send notifications related to deliveries.

The clients now display the revision on the dialog. This is useful, for example, when opening the party dialog from the invoice when the history is activated. This way the user can see from which date the information is displayed.

It is easy to get lost when quickly opening consecutive dialog fields. To improve the situation, the clients now display breadcrumbs in the title showing the browsing path to the dialog.

We’ve added the new identifiers from python-stdnum 1.15.

We no longer create accounting moves for stock when the amount involved is 0.

There is now a scheduled task that can be configured to fetch currency rates at a specific frequency. By default it gets the rates from the European Central Bank.

New Modules

Changes for the System Administrator

We’ve added device cookie support to the clients. This allows these clients to not be affected by the brute force attack protection.

Changes for the Developer

It is now possible to send emails with different “FROM” addresses for the envelope and header.

All the warnings can be skipped automatically by adding a single key named _skip_warnings to the context.

We’ve added the trigonometric functions to the SQLite back-end.

Any fields that are loaded eagerly are no longer instantiated automatically but instead the id is just stored in the cache. The instantiation is done only if the field is actually accessed. This improves the performance of some operations by up to 13%, but the actual improvements you can expect will depend a lot on of the number of fields the model has.

It is now possible to define help text for each selection value. However, at the moment only the web client can display it.

We made the ModelView.parse_view method public. This allows the XML that makes up the view to be modified by code before it is sent to the client.

It is now possible to group the report renderings by header. As the OpenDocument format only supports a single header and footer definition, this feature renders a different file for each header and places them in a zip file if needed. This is used when rendering company related reports which display the company information in the header/footer.

In order to simplify the dependencies in our web client, we replaced tempusdominus with the browser’s native input methods for types date, datetime-local and time when available.

In order to make better use of the browse cache, the getter method of Function fields is called with cache sized groups of records.

1 post - 1 participant

Read full topic



from Planet Python
via read more

Mike Driscoll: PyDev of the Week: Jonathan Hoffstadt

This week we welcome Jonathan Hoffstadt (@jhoffs1) as our PyDev of the Week! Jonathan is the co-author of Dear PyGUI. It’s a neat, new Python GUI package. You can see what else Jonathan has been working on over on Github.

Let’s spend some time getting to know Jonathan better!

Can you tell us a little about yourself (hobbies, education, etc):

I’m a mechanical engineer based in Houston, Texas. I have a bachelor’s degree in Mechanical Engineering from Louisiana State University, was a Tow Gunner in the U.S. Marines, and I’ve been working in the oil and gas industry since I graduated university.

My hobbies include chess, shooting, and programming. With programming, I find 3D graphics to be extremely interesting.

Why did you start using Python?

I’d been interested in programming since middle school after I was given a C++ for dummies book as a gift, but I did not encounter Python until university. It was there that I started using Python as a free alternative to MATLAB for assignments. It wasn’t long before I was hooked on the language.

I started using it outside of homework for anything and everything I could. This included making small games, automating tasks at internships, controlling breadboards with raspberry pi’s, and everything in between. When compared to other languages, I was amazed at how quickly you could make things happen.

I ended up using Python for courses in Finite Element Analysis and Computational Fluid Dynamics. For our senior design capstone project, my team was tasked with building an Arc Welding 3D printer. As the member with the most exposure to programming, I was responsible for the software side of the project in which I used Python to control all the mechanical devices including a robotic arm and custom electronics the team created. I also wrote my first user interface which used tkinter and pygame to wrap an open source slicing engine and provide a 3D view of tool paths and the robotic arm position.

What other programming languages do you know, and which is your favorite?

C, C++, and Java are my other primary languages, though I’ve worked with C#, Swift, and Objective-C.

The truth is that I have 2 favorite languages. C++ for large projects. Python for small projects, scripting, and just getting things done!

What projects are you working on now?

I currently spend most of my time working on Dear PyGui.

Which Python libraries are your favorite (core or 3rd party)?

My favorite Python libraries would have to be NumPy, Pillow, tqdm, json, and Nuitka.

How did your package, Dear PyGUI, come about?

Dear PyGui is a graphical user interface library I coauthored with my friend, Preston Cothren. As mechanical engineers, we use python daily for calculations, scripts, and plugins for various software used in mechanical design and analysis. We wanted an easy way to add an interface to the various scripts with minimal effort.

The first iteration of the software was called “Engineer’s Sandbox” which was commercial. Not only was it easy to create small interfaces but it also made it easy to package and distribute. It came with a built in IDE and 60 premade apps. “Sandbox” was a C++ program that embedded python into it where the graphics were created with wxWidgets. Ultimately, this project was unsuccessful, gaining only a few hundred users. You can see an image of it below:

Engineer's Sandbox (precursor to Dear PyGUI)

Six months after abandoning Engineer’s Sandbox, we revisited the idea and reassessed. We came to 3 realizations:

1. Our primary target audience (mechanical engineers) were mostly uninterested in programming.

2. The software was too restrictive and limited for developers (our second target audience). Limited widgets, layouts, limited 3rd party operability, etc.

3. Most developers prefer using open source libraries.

From these realizations, we went back to the drawing board and decided to make a full GUI library. With this iteration being open source, a Python extension (instead of a standalone software), and as easy to use as possible.

Between iterations, we fell in love with the C++ library, Dear ImGui, and so decided to use it as underlying library to build around. With Dear ImGui being an immediate mode GUI library, it allowed us to make Dear PyGui extremely dynamic when compared to other UI libraries.

Dear PyGui has continued to rapidly improve and grow in popularity since we released the first open beta in July of 2020:

Dear PyGUI Examples

What are the top three things you’ve learned as an open-source developer?

As an open-source developer, I’ve learned that:

1. It’s hard work and will make you appreciate open-source software and developers.

2. Listen to the community but also know when to say “no”.

3. Funding is difficult to find, so you should enjoy the work you are doing.

Is there anything else you’d like to say?

Yes! This is for those new to programming. I’m often asked how to learn a programming language, library, topic, etc. and my answer has always been: The best way to learn anything in programming is to just start building things.

I typically skim a book then immediately start trying to build something. As I get stuck, I go back to the book to read the relevant sections more closely. Once it’s time to refactor and optimize, I typically go back to the book and read the more advanced sections now that I’m more aware of the issues that the advanced sections try to address. I’ve found this technique helps me a lot. Although you may end up reinventing the wheel by saving the advanced topics for after you’re done, you will end up with a deeper understanding that you are unlikely to forget.

Thanks for doing the interview, Jonathan!

The post PyDev of the Week: Jonathan Hoffstadt appeared first on Mouse Vs Python.



from Planet Python
via read more

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...