Creating a Python package

Understand the requirements to create your own Python package.

Packaging the application

To distribute the application to users we have to package it. We are going to use the setuptools library.

Currently, the project can only be run as a script directly, that is, by running the files directly with python3 /path/to/simplemdviewer_app.py. This is not a convenient way to run a desktop application.

The goal in this page is to make the project available as a console script and as a module:

  • You'll know it is a console script when you are able to run it with simplemdviewer
  • You'll know it is a module when you are able to run it with python3 -m simplemdviewer

If you'd like to learn more about Python packaging, you'll be interested in the Python Packaging User Guide.

General structure

Let's recapitulate what the file structure of the project should be:

simplemdviewer
├── README.md
├── LICENSE.txt
├── MANIFEST.in                # To add our QML file to the Python module
├── pyproject.toml             # The main file to manage the project
├── org.kde.simplemdviewer.desktop
├── org.kde.simplemdviewer.json
├── org.kde.simplemdviewer.svg
├── org.kde.simplemdviewer.metainfo.xml
└── src/
    ├── __init__.py            # To import the src/ directory as a package
    ├── __main__.py            # To signal simplemdviewer_app as the entrypoint
    ├── simplemdviewer_app.py
    ├── md_converter.py
    └── qml/
        └── main.qml

Create a simplemdviewer/pyproject.toml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "org.kde.simplemdviewer"
version = "0.1"
authors = [{name = "Example Author", email = "example@author.org"}]
maintainers = [{name = "Example Author", email = "example@author.org"}]
description = "A simple markdown viewer"
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
    "Intended Audience :: End Users/Desktop",
    "Topic :: Utilities",
    "Programming Language :: Python",
    "Operating System :: POSIX :: Linux",
]
keywords = ["viewer converter markdown"]
urls = {Homepage = "https://mydomain.org/simplemdviewer"}
dependencies = ["markdown"]

[project.readme]
file = "README.md"
content-type = "text/markdown"

[project.scripts]
simplemdviewer = "simplemdviewer.simplemdviewer_app:main"

[tool.setuptools]
packages = ["simplemdviewer"]
package-dir = {simplemdviewer = "src"}
include-package-data = true

[tool.setuptools.data-files]
"share/applications" = ["org.kde.simplemdviewer.desktop"]
"share/icons/hicolor/scalable/apps" = ["org.kde.simplemdviewer.svg"]
"share/metainfo" = ["org.kde.simplemdviewer.metainfo.xml"]

Don't worry about the details for now. We will revisit each part as necessary.

The following four sections of the pyproject.toml are fairly straightforward:

  • [build-system] tells Python to fetch and use setuptools to build the project
  • [project] contains the general metadata for the project
  • [project.readme] specifies a default README file for the project in Markdown
  • [tool.setuptools.data-files] mentions where setuptools should install additional data files that are not typically present in a Python package

App metadata

Let's start with the metadata first so we can get it out of the way, namely the ones listed in the sections [project.readme] and [tool.setuptools.data-files].

Create a simplemdviewer/README.md:

1
2
3
# Simple Markdown Viewer

A simple Markdown viewer created with Kirigami, QML and Python.

Another important piece is the license of our project. Create a simplemdviewer/LICENSE.txt and add the text of the license of our project.

wget https://www.gnu.org/licenses/gpl-3.0.txt --output-document LICENSE.txt

Create a simplemdviewer/MANIFEST.in file with the following contents:

1
include src/qml/*.qml

This file is simply a declaration of additional source code files that should be present in the package when the application runs.

Some last pieces and we are ready to start changing the code. We are going to add:

  1. The Appstream metadata used to show the app in software stores.
  2. A Desktop Entry file to add the application to the application launcher.
  3. An application icon.

Create a new simplemdviewer/org.kde.simplemdviewer.desktop. This file is used to show our Markdown Viewer in application menus/launchers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[Desktop Entry]
Name=Simple Markdown Viewer
Name[ca]=Visualitzador senzill de Markdown
Name[cs]=Jednoduché prohlížení souborů Markdown
Name[eo]=Simpla Markdown-Vidilo
Name[es]=Sencillo visor de Markdown
Name[fr]=Afficheur simple pour langage « Markdown »
Name[it]=Visore Markdown semplice
Name[ja]=シンプルな Markdown ビューアー
Name[nl]=Eenvoudige Markdown-viewer
Name[sl]=Preprosti ogledovalnik Markdown
Name[sv]=Enkel Markdown-visning
Name[tr]=Basit Markdown Görüntüleyicisi
Name[uk]=Простий переглядач Markdown
Name[x-test]=xxSimple Markdown Viewerxx
Name[zh_TW]=簡單 Markdown 檢視器
GenericName=Markdown Viewer
GenericName[ca]=Visualitzador de Markdown
GenericName[cs]=Prohlížeč souborů Markdown
GenericName[eo]=Markdown-Vidilo
GenericName[es]=Visor de Markdown
GenericName[fr]=Afficheur pour langage « Markdown »
GenericName[it]=Visore Markdown
GenericName[ja]=Markdown ビューアー
GenericName[nl]=Markdown-viewer
GenericName[sl]=Ogledovalnik Markdown
GenericName[sv]=Markdown-visning
GenericName[tr]=Markdown Görüntüleyicisi
GenericName[uk]=Переглядач Markdown
GenericName[x-test]=xxMarkdown Viewerxx
GenericName[zh_TW]=Markdown 檢視器
Comment=A simple Markdown viewer application
Comment[ca]=Una aplicació senzilla de visualització de Markdown
Comment[cs]=Jednoduchá aplikace pro prohlížení souborů Markdown
Comment[eo]=Simpla Markdown-vidila aplikaĵo
Comment[es]=Una sencilla aplicación de visor de Markdown
Comment[fr]=Une application d'afficheur pour langage « Markdown »
Comment[it]=L'applicazione di un visore Markdown semplice
Comment[ja]=シンプルな Markdown ビューアーアプリケーション
Comment[nl]=Een eenvoudige toepassing als Markdown-viewer
Comment[sl]=Aplikacija preprostega ogledovalnika Markdown
Comment[sv]=Ett enkelt Markdown-visningsprogram
Comment[tr]=Basit bir Markdown görüntüleyici uygulama
Comment[uk]=Проста програма для перегляду Markdown
Comment[x-test]=xxA simple Markdown viewer applicationxx
Comment[zh_TW]=一個簡單的 Markdown 檢視器應用程式
Version=1.0
Exec=simplemdviewer
Icon=org.kde.simplemdviewer
Type=Application
Terminal=false
Categories=Office;
X-KDE-FormFactor=desktop;tablet;handset;

Add a new simplemdviewer/org.kde.simplemdviewer.metainfo.xml. This file is used to show the application in app stores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?xml version="1.0" encoding="utf-8"?>
<component type="desktop">
  <id>org.kde.simplemdviewer</id>
  <metadata_license>CC0-1.0</metadata_license>
  <project_license>GPL-3.0+</project_license>
  <name>Simple Markdown Viewer</name>
  <name xml:lang="ca">Visualitzador senzill de Markdown</name>
  <name xml:lang="cs">Jednoduché prohlížení souborů Markdown</name>
  <name xml:lang="eo">Simpla Markdown-Vidilo</name>
  <name xml:lang="es">Sencillo visor de Markdown</name>
  <name xml:lang="fr">Afficheur simple pour langage « Markdown »</name>
  <name xml:lang="it">Visore Markdown semplice</name>
  <name xml:lang="ja">シンプルな Markdown ビューアー</name>
  <name xml:lang="nl">Eenvoudige Markdown-viewer</name>
  <name xml:lang="sk">Jednoduchý prehliadač markupu</name>
  <name xml:lang="sl">Enostavni ogledovalnik Markdown</name>
  <name xml:lang="sv">Enkel Markdown-visning</name>
  <name xml:lang="tr">Basit Markdown Görüntüleyicisi</name>
  <name xml:lang="uk">Простий переглядач Markdown</name>
  <name xml:lang="x-test">xxSimple Markdown Viewerxx</name>
  <name xml:lang="zh-TW">簡單 Markdown 檢視器</name>
  <summary>A simple markdown viewer application</summary>
  <summary xml:lang="ca">Una aplicació senzilla de visualització de Markdown</summary>
  <summary xml:lang="eo">Simpla markdown-spektila aplikaĵo</summary>
  <summary xml:lang="es">Una sencilla aplicación de visor markdown</summary>
  <summary xml:lang="fr">Une application d'afficheur simple pour langage « Markdown »</summary>
  <summary xml:lang="it">L'applicazione di un visore Markdown semplice</summary>
  <summary xml:lang="ja">シンプルな markdown ビューアーアプリケーション</summary>
  <summary xml:lang="nl">Een eenvoudige toepassing als Markdown-viewer</summary>
  <summary xml:lang="sl">Aplikacija enostavnega ogledovalnika Markdown</summary>
  <summary xml:lang="sv">Ett enkelt Markdown-visningsprogram</summary>
  <summary xml:lang="tr">Basit bir Markdown görüntüleyici uygulama</summary>
  <summary xml:lang="uk">Проста програма для перегляду Markdown</summary>
  <summary xml:lang="x-test">xxA simple markdown viewer applicationxx</summary>
  <summary xml:lang="zh-TW">一個簡單的 Markdown 檢視器應用程式</summary>
  <description>
    <p>Simple Markdown Viewer is a showcase application for QML with Python development</p>
    <p xml:lang="ca">El visualitzador senzill de Markdown és una aplicació per a presentar el desenvolupament del QML amb el Python</p>
    <p xml:lang="eo">Simple Markdown Viewer estas montra aplikaĵo por QML kun Python-evoluo</p>
    <p xml:lang="es">Sencillo visor de Markdown es una aplicación de demostración para QML con desarrollo en Python</p>
    <p xml:lang="fr">Un afficheur simple pour langage « Markdown » est une application majeure pour QML pour les développement sous Python</p>
    <p xml:lang="it">Visore Markdown semplice è un'applicazione dimostrativa per QML nello sviluppo in Python</p>
    <p xml:lang="ja">シンプルな Markdown ビューアーは Python 開発による QML のサンプルアプリケーションです。</p>
    <p xml:lang="nl">Eenvoudige Markdown-viewer is een uitstelkast voor QML met Python ontwikkeling</p>
    <p xml:lang="sl">Enostavni ogledovalnik Markdown je predstavitvena aplikacija za razvoj QML s Pythonom</p>
    <p xml:lang="sv">Enkel Markdown-visning är ett förevisningsprogram för QML med Python-utveckling</p>
    <p xml:lang="tr">Basit Markdown Görüntüleyicisi, Python ile QML geliştirmek için bir vitrin uygulamadır</p>
    <p xml:lang="uk">Проста програма для перегляду Markdown є прикладом програми для розробки з QML за допомогою Python</p>
    <p xml:lang="x-test">xxSimple Markdown Viewer is a showcase application for QML with Python developmentxx</p>
    <p xml:lang="zh-TW">《簡單 Markdown 檢視器》是用來示範 QML 與 Python 併用開發的應用程式</p>
  </description>
  <url type="homepage">https://mydomain.org/simplemdviewer</url>
  <releases>
    <release version="0.1" date="2022-02-25">
      <description>
        <p>First release</p>
      </description>
    </release>
  </releases>
  <provides>
    <binary>simplemdviewer</binary>
  </provides>
</component>

For this tutorial the well known Markdown icon is okay.

Get the Markdown icon and save it as simplemdviewer/org.kde.simplemdviewer.svg:

wget https://upload.wikimedia.org/wikipedia/commons/4/48/Markdown-mark.svg --output-document org.kde.simplemdviewer.svg

We need the icon to be perfectly squared, which can be accomplished with Inkscape:

  1. Open org.kde.simplemdviewer.svg in Inkscape.
  2. Type Ctrl+a to select everything.
  3. On the top W: text field, type 128 and press Enter.
  4. Go to File -> Document Properties...
  5. Change the Width: text field to 128 and press Enter.
  6. Save the file.

Code changes

Before creating a console script or module, we first need to turn our existing code into a package.

Create an empty simplemdviewer/src/__init__.py file. This file just needs to be present in order to import a directory as a package.

touch __init__.py

Usually, Python packages keep their package source code directly in the root folder. Since we are using a custom package directory, namely src/, we pass it as the correct package-dir for our simplemdviewer application in the pyproject.toml:

30
31
32
33
[tool.setuptools]
packages = ["simplemdviewer"]
package-dir = {simplemdviewer = "src"}
include-package-data = true

Here, the package name is simplemdviewer, and its source code is located in simplemdviewer/src/ instead of the root folder. Now that we have a package name, we can use that for the way we import modules in our code.

Update simplemdviewer/src/simplemdviewer_app.py to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python3

import os
import sys
import signal
from PySide6.QtGui import QGuiApplication
from PySide6.QtCore import QUrl
from PySide6.QtQml import QQmlApplicationEngine
# from md_converter import MdConverter
from simplemdviewer.md_converter import MdConverter  # noqa: F401

def main():
    """Initializes and manages the application execution"""
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

    """Needed to close the app with Ctrl+C"""
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    """Needed to get proper KDE style outside of Plasma"""
    if not os.environ.get("QT_QUICK_CONTROLS_STYLE"):
        os.environ["QT_QUICK_CONTROLS_STYLE"] = "org.kde.desktop"

    base_path = os.path.abspath(os.path.dirname(__file__))
    url = QUrl(f"file://{base_path}/qml/main.qml")
    engine.load(url)

    if len(engine.rootObjects()) == 0:
        quit()

    app.exec()


if __name__ == "__main__":
    main()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env python3

import os
import sys
import signal
from PyQt6.QtGui import QGuiApplication
from PyQt6.QtCore import QUrl
from PyQt6.QtQml import QQmlApplicationEngine, qmlRegisterType
# from md_converter import MdConverter
from simplemdviewer.md_converter import MdConverter

def main():
    """Initializes and manages the application execution"""
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

    """Needed to close the app with Ctrl+C"""
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    """Needed to get proper KDE style outside of Plasma"""
    if not os.environ.get("QT_QUICK_CONTROLS_STYLE"):
        os.environ["QT_QUICK_CONTROLS_STYLE"] = "org.kde.desktop"

    qmlRegisterType(MdConverter, "org.kde.simplemdviewer", 1, 0, "MdConverter")

    base_path = os.path.abspath(os.path.dirname(__file__))
    url = QUrl(f"file://{base_path}/qml/main.qml")
    engine.load(url)

    if len(engine.rootObjects()) == 0:
        quit()

    app.exec()


if __name__ == "__main__":
    main()

Create a __main__.py file in the simplemdviewer/src/ directory:

1
2
3
from . import simplemdviewer_app

simplemdviewer_app.main()

This simply adds the contents of the current directory (src/) and imports it as a module named simplemdviewer_app, then immediately run the main() function of the application.

Make sure that you have a project script in your pyproject.toml:

27
28
[project.scripts]
simplemdviewer = "simplemdviewer.simplemdviewer_app:main"

Now the application should run as an executable package and as a module.

Running directly, as a module, and as a console script

When you run the script directly with python3 src/simplemdviewer_app.py, under the hood you are first running __main__. If the same script were imported instead of run, it would be running simplemdviewer_app, the name of the module. The if condition is there so main() will only run when the script is run, not every time it is imported by another script:

34
35
if __name__ == "__main__":
    main()

That's it for running scripts directly. On top of that, we use setuptools to specify the package:

30
31
32
[tool.setuptools]
packages = ["simplemdviewer"]
package-dir = {simplemdviewer = "src"}

We have specified a package called simplemdviewer in line 31 whose source code is found in simplemdviewer/src/ in line 32. This is necessary because we don't want to call the application as simplemdviewer_app every time for either module or console script, we want the friendlier name simplemdviewer.

Now, to allow the application to run as a module like in python3 -m simplemdviewer, all we need to do is have a __main__.py to the same folder where the code is:

1
2
3
from . import simplemdviewer_app

simplemdviewer_app.main()

When attempting to run the package simplemdviewer as a module, setuptools will simply look for a __main__.py, which in turn points to the main() function in our simplemdviewer_app.py:

12
13
14
15
def main():
    """Initializes and manages the application execution"""
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

Under the hood of the module, __name__ is actually set to simplemdviewer_app unlike with when running the script directly, but main() is executed anyway.

Now, to create a console script:

27
28
[project.scripts]
simplemdviewer = "simplemdviewer.simplemdviewer_app:main"

In line 28, we specify a project script, namely the entrypoint to run the application. This is functionality provided directly by setuptools. In this case it's a console script because it can run on a terminal; a GUI script would always run without a terminal, but this only matters on Windows. A project script is simply a wrapper that setuptools creates on top of the application so it can be run easily like an executable.

We want that, when attempting to run the command simplemdviewer in the terminal, setuptools searches inside the simplemdviewer package for the module simplemdviewer_app, and runs the function main() directly:

12
13
14
15
def main():
    """Initializes and manages the application execution"""
    app = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()

Doing things this way, we bypass the __main__ check entirely.

This is how the console script is created.

In other words:

MethodToolRuns with
run directly->Python->__main__->main()python3 simplemdviewer_app.py
module->Setuptools->__main__.py->main()python3 -m simplemdviewer
console script->Setuptools->project script->main()simplemdviewer

Running the app

From inside the simplemdviewer/ directory, we can install it in development mode and then run it:

python3 -m pip install --editable .
# As a module
python3 -m simplemdviewer
# As a console script
simplemdviewer

If you have put the required files in the right places, running the application as a module and as a console script should work.

Now that we know it works, let's generate the package for our program.

Make sure that the latest version of build is installed:

python3 -m pip install --upgrade build

From inside the simplemdviewer/ directory, run:

python3 -m build

As soon as the build completes, two archives will be created in the dist/ directory:

  1. The org.kde.simplemdviewer-0.1.tar.gz source archive
  2. The org.kde.simplemdviewer-0.1-py3-none-any.whl package ready for distribution in places such as PyPI

To test the application properly, we can try installing it in a clean virtual environment that has PySide/PyQt:

deactivate
python3 -m venv --system-site-packages clean-env/
source clean-env/bin/activate
python3 -m pip install dist/org.kde.simplemdviewer-0.1-py3-none-any.whl

Run:

# As a module
python3 -m simplemdviewer
# As a console script
simplemdviewer

At this point we can tag and release the source code. Linux distributions will package it and the application will be added to their software repositories.

Well done.