<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Dan Yeaw's Blog</title><link>https://dan.yeaw.me/</link><description>Open source, mobility, and me.</description><atom:link href="https://dan.yeaw.me/rss.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2025 &lt;a href="mailto:dan@yeaw.me"&gt;Dan Yeaw&lt;/a&gt; </copyright><lastBuildDate>Fri, 28 Nov 2025 21:50:02 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>GNOME GitLab CLI</title><link>https://dan.yeaw.me/posts/gnome-gitlab-cli/</link><dc:creator>Dan Yeaw</dc:creator><description>&lt;p&gt;Git authentication has evolved quite a bit since 2005 when SSH keys were the
go-to method for connecting to remote repositories.&lt;/p&gt;
&lt;p&gt;Throughout the 2010s, GitHub made SSH the standard while HTTPS quietly gained
traction since it played nicer with corporate firewalls. When GitHub
eventually required personal access tokens instead of passwords, tools like
Git Credential Manager emerged to handle the credential storage, making HTTPS
authentication more practical.&lt;/p&gt;
&lt;p&gt;In 2020 when GitHub released their CLI tool (&lt;code&gt;gh&lt;/code&gt;),
which uses OAuth for authentication and suddenly made getting started way
easier—no more fumbling through SSH key generation tutorials for newcomers.&lt;/p&gt;
&lt;p&gt;I used to have SSH keys installed on my Yubikey, but they were always a hassle
to set up and use. These days authenticating with &lt;code&gt;gh&lt;/code&gt; is definitely the way
to go.&lt;/p&gt;
&lt;p&gt;I also do quite a lot of open source contribution to GNOME's GitLab instance,
which seems to have completely missed the change to HTTPS that GitHub went
through. GitLab does have a CLI tool as well called &lt;code&gt;glab&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Clement Sam started &lt;code&gt;glab&lt;/code&gt; as an open source project, and then GitLab adopted
it in 2022. By default it wants to connect to gitlab.com using SSH, here's 
how you can set it up using GNOME GitLab and HTTPS.&lt;/p&gt;
&lt;p&gt;First &lt;a href="https://gitlab.gnome.org/-/user_settings/personal_access_tokens?scopes=api%2Cwrite_repository"&gt;create a new personal access token&lt;/a&gt;
with &lt;code&gt;write_repository&lt;/code&gt; and &lt;code&gt;api&lt;/code&gt; scopes.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;glab&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;host&lt;span class="w"&gt; &lt;/span&gt;gitlab.gnome.org
glab&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;git_protocol&lt;span class="w"&gt; &lt;/span&gt;https
glab&lt;span class="w"&gt; &lt;/span&gt;auth&lt;span class="w"&gt; &lt;/span&gt;login&lt;span class="w"&gt; &lt;/span&gt;--use-keyring&lt;span class="w"&gt; &lt;/span&gt;--hostname&lt;span class="w"&gt; &lt;/span&gt;gitlab.gnome.org
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Authenticate with your token that you just created. That's it-enjoy
contributing to GNOME!&lt;/p&gt;</description><category>GNOME</category><guid>https://dan.yeaw.me/posts/gnome-gitlab-cli/</guid><pubDate>Fri, 28 Nov 2025 20:54:51 GMT</pubDate></item><item><title>A Big Job Change</title><link>https://dan.yeaw.me/posts/a-big-job-change/</link><dc:creator>Dan Yeaw</dc:creator><description>&lt;p&gt;I recently changed jobs, and now I am an Engineering Manager for OSS at Anaconda!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Anaconda Logo" src="https://dan.yeaw.me/images/Anaconda_Logo.png"&gt;&lt;/p&gt;
&lt;p&gt;This is my second major career pivot, and I thought I would share why I decided to make the change. Even though being a
Submarine Officer, a Functional Safety Lead, and working on OSS are very different, they share a common thread of
leadership and deeply technical engineering. I’m excited to bring these skills into my new role.&lt;/p&gt;
&lt;h3&gt;Goodbye to Ford&lt;/h3&gt;
&lt;p&gt;I spent the last 11 years leading Functional Safety at Ford. It was incredibly rewarding to grow from an individual
contributor to a manager and eventually to leading a global team dedicated to ensuring Ford vehicles were safe.&lt;/p&gt;
&lt;p&gt;While this role let me support Functional Safety Engineers across the company, I started to miss getting hands-on with
technical contributions since most of my time was focused on leading the team.&lt;/p&gt;
&lt;p&gt;Looking back, there are a couple of things I wish had been different at Ford:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A strong bias for external talent with executive leadership&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Too much bureaucracy, especially in approval processes&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Having a good mix of new talent join an organization is so important because fresh ideas and perspectives can make a big
difference. However, in Ford’s software engineering areas, about 90% of the executive leadership roles were filled by
people from outside the company. While I wasn’t aiming for higher leadership roles, this clear preference for external
hires made it feel like developing and retaining internal talent wasn’t a priority. As you might expect, it took new
leaders a while to adapt, and there was a lot of turnover.&lt;/p&gt;
&lt;p&gt;On top of that, the approval process for things like hiring and travel was overly complicated. Simple approvals could
take months with no feedback. This culture of control slowed everything down. Delegating authority—like giving managers
a budget and headcount to work with and holding them accountable—would have made things so much smoother and faster.&lt;/p&gt;
&lt;p&gt;The thing I’ll miss most about Ford is the people. I loved collaborating with all the Functional Safety Engineers and
everyone else I worked with. I wish them all the best in the future!&lt;/p&gt;
&lt;h3&gt;Hello Anaconda&lt;/h3&gt;
&lt;p&gt;I am now an &lt;strong&gt;Engineering Manager for Open Source Software&lt;/strong&gt; at &lt;a href="https://anaconda.com"&gt;Anaconda&lt;/a&gt;, where I lead a team of
engineers working on amazing projects like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://beeware.org"&gt;BeeWare&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pyscript.net"&gt;PyScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jupyter.org"&gt;Jupyter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fsspec/filesystem_spec"&gt;fsspec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/intake/intake"&gt;Intake&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...and more!&lt;/p&gt;
&lt;p&gt;Over the last seven years, I’ve been contributing to open source projects, especially in Python. Getting the chance to
lead a team that does this full-time feels like a dream come true.&lt;/p&gt;
&lt;p&gt;One of the things I’m most excited about with these projects is how they help make programming more accessible. BeeWare,
for example, makes it possible to create apps for mobile and desktop, and PyScript lets you write Python directly in
your web browser. Both tools are fantastic for helping anyone pick up Python, build something useful, and share it with
others. Meanwhile, Jupyter and fsspec are key tools for data science, making it easier to analyze diverse datasets and
integrate different data sources.&lt;/p&gt;
&lt;p&gt;I’m thrilled to have the opportunity to strengthen the open-source communities around these projects, encourage
healthier collaboration, and create more value for Python users by connecting these tools with the broader PyData
ecosystem.&lt;/p&gt;</description><category>Anaconda</category><category>Leadership</category><category>Python</category><guid>https://dan.yeaw.me/posts/a-big-job-change/</guid><pubDate>Sat, 21 Dec 2024 20:05:04 GMT</pubDate></item><item><title>openSUSE with Passwordless U2F Login</title><link>https://dan.yeaw.me/posts/opensuse-with-passwordless-u2f-login/</link><dc:creator>Dan Yeaw</dc:creator><description>&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/opensuse-yubikey.png" alt="openSUSE Geeko with Yubikey" style="max-height:400px"&gt;&lt;/p&gt;
&lt;p&gt;I have a &lt;a href="https://www.yubico.com/products/yubikey-5-overview/"&gt;Yubikey 5 NFC&lt;/a&gt;
that I use for 2-Factor Authentication (2FA) on websites that support it and for
storing my GPG keys.&lt;/p&gt;
&lt;p&gt;I recently got a new laptop, and I quickly got tired of trying to remember a
long new password. I have also gotten use to using things like Windows Hello at
work, where a pin or fingerprint can be used to Log in.&lt;/p&gt;
&lt;p&gt;Most of the articles I found about setting up U2F in Linux were using Ubuntu,
and since I am using openSUSE and some files are in different places, this post
documents that process. Although Universal 2nd Factor (U2F) on the Yubikey can
be used to add 2FA to make your Linux laptop more secure, my focus is on a
passwordless login so that I don't need to enter a long password at all to log
in to my Linux account.&lt;/p&gt;
&lt;p&gt;WARNING: An erroneous PAM configuration may lock you completely out of your
systems or prevent you from gaining root privileges. Before getting started,
open a terminal and su to root. Before closing the terminal, test your
configuration thoroughly.&lt;/p&gt;
&lt;h3&gt;Installing the Required Software&lt;/h3&gt;
&lt;p&gt;The openSUSE repository includes a package called &lt;code&gt;pam_u2f&lt;/code&gt;. This package adds U2F
support for Pluggable Authentication Modules (PAM). PAM provides the libraries
for Linux that allow configuration of authentication of users. In this case we
want to authenticate using a U2F module, so we install it by opening a terminal
and typing:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;zypper&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pam_u2f
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Associating the U2F Key With Your Account&lt;/h3&gt;
&lt;p&gt;The U2F PAM module needs to make use of an authentication file that associates
the user name that will login with the Yubikey token. Open a terminal and insert
your Yubikey.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;~/.config/Yubico
$&lt;span class="w"&gt; &lt;/span&gt;pamu2fcfg&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;whoami&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;~/.config/Yubico/u2f_keys
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When your device begins flashing, touch the metal contact to confirm the association.&lt;/p&gt;
&lt;p&gt;For increased security, we'll next move the u2f_keys file to an area where
you'll need sudo permission to edit the file.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/etc/Yubico
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt;  &lt;/span&gt;~/.config/Yubico/u2f_keys&lt;span class="w"&gt; &lt;/span&gt;/etc/Yubico/u2f_keys
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;chown&lt;span class="w"&gt; &lt;/span&gt;root.root&lt;span class="w"&gt; &lt;/span&gt;/etc/Yubico/u2f_keys
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Edit the PAM Configuration&lt;/h3&gt;
&lt;p&gt;Once the u2f_keys file is moved to a safer location the PAM file will need to be
modified so that u2f PAM module can find the u2f_keys file. For these PAM
configurations, openSUSE creates configuration files, which end in "pc", 
generated by the &lt;code&gt;pam-config&lt;/code&gt; utility, and then symbolically links the PAM
configuration files to these. Unfortunately, we can't use pam-config to
configure the pam_u2f module, so we'll need to manually edit the config. To do
this, we will only need to make changes to the main PAM authorization
configuration called &lt;code&gt;common-auth&lt;/code&gt;, so first we need to remove the symbolic link
and then copy the configuration so that we can edit it.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/pam.d
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;common-auth
$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;common-auth-pc&lt;span class="w"&gt; &lt;/span&gt;common-auth
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now edit the configuration file that you created. I would normally use vim to
make a quick edit to a file, but I will use nano instead in case you aren't
familiar with vim commands:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;nano&lt;span class="w"&gt; &lt;/span&gt;common-auth
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Scroll to the bottom or hit Alt+/, there should be three lines that aren't commented out:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;auth    required        pam_env.so
auth    optional        pam_gnome_keyring.so
auth    required        pam_unix.so     try_first_pass
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After the pam_gnome_keyring line, add a new line so that your file looks like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;auth    required        pam_env.so      
auth    optional        pam_gnome_keyring.so
auth    sufficient      pam_u2f.so      authfile=/etc/Yubico/u2f_keys cue
auth    required        pam_unix.so     try_first_pass
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's discuss what this did:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;auth adds a new definition for login authorization&lt;/li&gt;
&lt;li&gt;sufficient allows a login with Yubikey only, but isn't required to login if
  the user's password is entered&lt;/li&gt;
&lt;li&gt;pam_u2f.so is the PAM U2F module&lt;/li&gt;
&lt;li&gt;authfile sets the authorization file that we created earlier&lt;/li&gt;
&lt;li&gt;cue creates a prompt to remind you to touch your Yubikey&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you are done adding the configuration line, save the file by pressing
Ctrl+x and then hit enter.&lt;/p&gt;
&lt;h3&gt;Test Logging In&lt;/h3&gt;
&lt;p&gt;Before you close your su terminal, make sure that logging in works using U2F. Using
a new terminal, try to login:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;su&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;whoami&lt;span class="k"&gt;)&lt;/span&gt;
Password:&lt;span class="w"&gt; &lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Instead of entering your password, hit enter. You should see a prompt to touch
your device:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Please&lt;span class="w"&gt; &lt;/span&gt;touch&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;device.
$
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If that is successful then congrats, you should now be able to restart your
computer and login using your Yubikey!&lt;/p&gt;
&lt;h3&gt;Troubleshooting - Enable Debug Mode&lt;/h3&gt;
&lt;p&gt;If you are unable to login and are unsure why, you can enable debugging on the
Yubico PAM module. First open a terminal, then execute:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;touch&lt;span class="w"&gt; &lt;/span&gt;/var/log/pam_u2f.log
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Edit the /etc/pam.d/common-auth file again and add &lt;code&gt;debug debug_file=/var/log/pam_u2f.log&lt;/code&gt;
to the end of the line that you added earlier. Save the file and now each
login attempt will be saved in the /var/log/pam_u2f.log file.&lt;/p&gt;
&lt;h3&gt;Unlock GNOME Keyring&lt;/h3&gt;
&lt;p&gt;If you are using GNOME, even though you successfully logged in with your Yubikey,
GNOME will still ask you to unlock your login keyring with your login password.
This defeats the purpose of setting up your Yubikey to login in the first place.
There is a project called
&lt;a href="https://git.recolic.net/root/gnome-keyring-yubikey-unlock"&gt;gnome-keyring-yubikey-unlock&lt;/a&gt;
that solves this by encrypting the keyring-name : password pair with GnuPG and
save it as secret-file. Then on starting GNOME, a script will automatically run
that calls GnuPG to decrypt the secret file, and pipe use the password to unlock
your keyring.&lt;/p&gt;
&lt;p&gt;To build and install it openSUSE, run the following commands: &lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;zypper&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;libgnome-keyring-devel&lt;span class="w"&gt; &lt;/span&gt;git
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://git.recolic.net/root/gnome-keyring-yubikey-unlock&lt;span class="w"&gt; &lt;/span&gt;--recursive
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gnome-keyring-yubikey-unlock/src
$&lt;span class="w"&gt; &lt;/span&gt;make
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;..
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next we need to get your public key id:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;gpg&lt;span class="w"&gt; &lt;/span&gt;--list-keys
/home/dan/.gnupg/pubring.kbx
----------------------------
pub&lt;span class="w"&gt;   &lt;/span&gt;rsa4096&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2020&lt;/span&gt;-12-22&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;SC&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;30EE9BFEC3FD0B37F9088DBE42239C515C9B9841
uid&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;ultimate&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Dan&lt;span class="w"&gt; &lt;/span&gt;Yeaw&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;dan@yeaw.me&amp;gt;
sub&lt;span class="w"&gt;   &lt;/span&gt;rsa4096&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2021&lt;/span&gt;-11-10&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;A&lt;span class="o"&gt;]&lt;/span&gt;
sub&lt;span class="w"&gt;   &lt;/span&gt;rsa4096&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2021&lt;/span&gt;-11-10&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;E&lt;span class="o"&gt;]&lt;/span&gt;
sub&lt;span class="w"&gt;   &lt;/span&gt;rsa4096&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2021&lt;/span&gt;-11-10&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;S&lt;span class="o"&gt;]&lt;/span&gt;
sub&lt;span class="w"&gt;   &lt;/span&gt;rsa4096&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2020&lt;/span&gt;-12-22&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;E&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The hexadecimal id that starts with 30EE9BF is my public gpg key id. Next we are going
to create the encrypted keyring password pair. Replace YOUR_PUBLIC_GPG_KEY with your
public gpg key id from the last step and replace YOUR_LOGIN_PASSWORD with the password
for your user account.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;./create_secret_file.sh&lt;span class="w"&gt; &lt;/span&gt;~/.gnupg/gnome_keyring_yubikey_secret&lt;span class="w"&gt; &lt;/span&gt;YOUR_PUBLIC_GPG_KEY
&amp;gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;Please&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;keyring_name&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;password&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;following&lt;span class="w"&gt; &lt;/span&gt;format:

keyring1:password1
keyring2:password2

login:12345678

&amp;gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;When&lt;span class="w"&gt; &lt;/span&gt;you&lt;span class="w"&gt; &lt;/span&gt;are&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;use&lt;span class="w"&gt; &lt;/span&gt;Ctrl-D&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;end.
login:YOUR_LOGIN_PASSWORD
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next we want to change the permissions of the file, so that only your user can read
and write to the file:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/.gnupg/gnome_keyring_yubikey_secret
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, create an autostart entry so that the script loads when you login to GNOME:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;nano&lt;span class="w"&gt; &lt;/span&gt;~/.config/autostart/net.recolic.gnome-keyring-yubikey-unlock.desktop
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Add the following to the file, replacing YOUR_USER with your username:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;[&lt;/span&gt;Desktop&lt;span class="w"&gt; &lt;/span&gt;Entry&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Application
&lt;span class="nv"&gt;Exec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/YOUR_USER/Projects/gnome-keyring-yubikey-unlock/unlock_keyrings.sh&lt;span class="w"&gt; &lt;/span&gt;/home/YOUR_USER/.gnupg/gnome_keyring_yubikey_secret
&lt;span class="nv"&gt;Hidden&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
X-GNOME-Autostart-enabled&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GNOME&lt;span class="w"&gt; &lt;/span&gt;Keyring&lt;span class="w"&gt; &lt;/span&gt;Yubikey&lt;span class="w"&gt; &lt;/span&gt;Unlock
&lt;span class="nv"&gt;Comment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Unlocks&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;GNOME&lt;span class="w"&gt; &lt;/span&gt;Login&lt;span class="w"&gt; &lt;/span&gt;Keyring&lt;span class="w"&gt; &lt;/span&gt;without&lt;span class="w"&gt; &lt;/span&gt;password
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hit Control+x and then enter to save the file. Restart your computer, and you
should now be able to login and run openSUSE without manually entering your
password.&lt;/p&gt;</description><category>openSUSE</category><category>Yubikey</category><guid>https://dan.yeaw.me/posts/opensuse-with-passwordless-u2f-login/</guid><pubDate>Sat, 23 Jan 2021 21:26:49 GMT</pubDate></item><item><title>GitHub Actions: Automate Your Python Development Workflow</title><link>https://dan.yeaw.me/posts/github-actions-automate-your-python-development-workflow/</link><dc:creator>Dan Yeaw</dc:creator><description>&lt;p&gt;At GitHub Universe 2018, GitHub launched GitHub Actions in beta. Later in
August 2019, GitHub announced the expansion of GitHub Actions to include
Continuous Integration / Continuous Delivery (CI/CD). At Universe 2019,
GitHub
&lt;a href="https://github.blog/2019-11-13-universe-day-one/#github-actions"&gt;announced&lt;/a&gt;
that Actions are out of beta and generally available. I spent the last few
days, while I was taking some vacation during Thanksgiving, to explore GitHub
Actions for automation of Python projects.&lt;/p&gt;
&lt;p&gt;With my involvement in the &lt;a href="https://gaphor.org"&gt;Gaphor&lt;/a&gt; project, we have a GUI
application to &lt;a href="https://github.com/gaphor/gaphor"&gt;maintain&lt;/a&gt;, as well as two
libraries, a diagramming widget called
&lt;a href="https://github.com/gaphor/gaphas"&gt;Gaphas&lt;/a&gt;, and we more recently took over
maintenance of a library that enables multidispatch and events called
&lt;a href="https://github.com/gaphor/generic"&gt;Generic&lt;/a&gt;. It is important to have an
efficient programming workflow to maintain these projects, so we can spend more
of our open source volunteer time focusing on implementing new features and
other enjoyable parts of programming, and less time doing manual and boring
project maintenance.&lt;/p&gt;
&lt;p&gt;In this blog post, I am going to give an overview of what CI/CD is, my previous
experience with other CI/CD systems, how to test and deploy Python applications
and libraries using GitHub Actions, and finally highlight some other Actions
that can be used to automate other parts of your Python workflow.&lt;/p&gt;
&lt;h3&gt;Overview of CI/CD&lt;/h3&gt;
&lt;p&gt;Continuous Integration (CI) is the practice of frequently integrating changes to
code with the existing code repository.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/continuous-integration.svg" alt="Continuous Integration" style="max-height:200px"&gt;&lt;/p&gt;
&lt;p&gt;Continuous Delivery / Delivery (CD) then extends CI by making sure the software checked in
to the master branch is always in a state to be delivered to users, and
automates the deployment process.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/continuous-delivery-deployment.svg" alt="Continuous Delivery / Deployment" style="max-height:300px"&gt;&lt;/p&gt;
&lt;p&gt;For open source projects on GitHub or GitLab,
the workflow often looks like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The latest development is on the mainline branch called master.&lt;/li&gt;
&lt;li&gt;Contributors create their own copy of the project, called a fork, and then
clone their fork to their local computer and setup a development environment.&lt;/li&gt;
&lt;li&gt;Contributors create a local branch on their computer for a change they want
to make, add tests for their changes, and make the changes.&lt;/li&gt;
&lt;li&gt;Once all the unit tests pass locally, they commit the changes and push them
to the new branch on their fork.&lt;/li&gt;
&lt;li&gt;They open a Pull Request to the original repo.&lt;/li&gt;
&lt;li&gt;The Pull Request kicks off a build on the CI system automatically, runs
formatting and other lint checks, and runs all the tests.&lt;/li&gt;
&lt;li&gt;Once all the tests pass, and the maintainers of the project are good with
the updates, they merge the changes back to the master branch.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Either in a fixed release cadence, or occasionally, the maintainers then add a
version tag to master, and kickoff the CD system to package and release a new
version to users.&lt;/p&gt;
&lt;h3&gt;My Experience with other CI/CD Systems&lt;/h3&gt;
&lt;p&gt;Since most open source projects didn't want the overhead of maintaining their
own local CI server using software like Jenkins, the use of cloud-based or
hosted CI services became very popular over the last 7 years. The most
frequently used of these was Travis CI with Circle CI a close second. Although
both of these services introduced initial support for Windows over the last
year, the majority of users are running tests on Linux and macOS only. It is
common for projects using Travis or Circle to use another service called
AppVeyor if they need to test on Windows.&lt;/p&gt;
&lt;p&gt;I think the popularity of Travis CI and the other similar services is based on
how easy they were to get going with. You would login to the service with your
GitHub account, tell the service to test one of your projects, add a YAML
formatted file to your repository using one of the examples, and push to the
software repository (repo) to trigger your first build. Although these services
are still hugely popular, 2019 was the year that they started to lose some of
their momentum. In January 2019, a company called Idera bought Travis CI. In
February Travis CI then
&lt;a href="https://twitter.com/alicegoldfuss/status/1098604563664420865"&gt;laid-off&lt;/a&gt; a lot
of their senior engineers and technical staff.&lt;/p&gt;
&lt;p&gt;The 800-pound gorilla entered the space in 2018, when Microsoft bought GitHub
in June and then rebranded their Visual Studio Team Services ecosystem and
launched Azure Pipelines as a CI service in September. Like most of the popular
services, it was free for open source projects. The notable features of this
service was that it launched supporting Linux, macOS, and Windows, and it
allowed for 10 parallel jobs. Although the other services offer parallel
builds, on some platforms they are limited for open source projects, and I
would often be waiting for a server called an "agent" to be available with
Travis CI. Following the lay-offs at Travis CI, I was ready to explore other
services to use, and Azure Pipelines was the new hot CI system.&lt;/p&gt;
&lt;p&gt;In March 2019, I was getting ready to launch version 1.0.0 of Gaphor after
spending a couple of years helping to update it to Python 3 and PyGObject. We
had been using Travis CI, and we were lacking the ability to test and package
the app on all three major platforms. I used this as an opportunity to learn
Azure Pipelines with the goal of being able to fill this gap we had in our
workflow.&lt;/p&gt;
&lt;p&gt;My takeaways from this experience is that Azure Pipelines is lacking much of
the ease of use as compared to Travis CI, but has other huge advantages
including build speed and the flexibility and power to create complex
cross-platform workflows. Developing a complex workflow on any of these CI
systems is challenging because the feedback you receive takes a long time to
get back to you. In order to create a workflow, I normally:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a branch of the project I am working on&lt;/li&gt;
&lt;li&gt;Develop a YAML configuration based on the documentation and examples available&lt;/li&gt;
&lt;li&gt;Push to the branch, to kickoff the CI build&lt;/li&gt;
&lt;li&gt;Realize that something didn't work as expected after 10 minutes of waiting
for the build to run&lt;/li&gt;
&lt;li&gt;Go back to step 2 and repeat, over and over again&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;One of my other main takeaways was that the documentation was often lacking
good examples of complex workflows, and was not very clear on how to use each
step. This drove even more trial and error, which requires a lot of patience as
you are working on a solution. After a lot of effort, I was able to complete a
configuration that tested Gaphor on Linux, macOS, and Windows. I also was able
to partially get the CD to work by setting up Pipelines to add the built dmg
file for macOS to a draft release when I push a new version tag. A couple of
weeks ago, I was also able build and upload Python Wheel and source
distribution, along with the Windows binaries built in
&lt;a href="https://www.msys2.org"&gt;MSYS2&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Despite the challenges getting there, the result was very good! Azure Pipelines
is screaming fast, about twice as fast as Travis CI was for my complex
workflows (25 minutes to 12 minutes). The tight integration that allows testing
on all three major platforms was also just what I was looking for.&lt;/p&gt;
&lt;h3&gt;How to Test a Python Library using GitHub Actions&lt;/h3&gt;
&lt;p&gt;With all the background out of the way, now enters GitHub Actions. Although I
was very pleased with how Azure Pipelines performs, I thought it would be nice
to have something that could better mix the ease of use of Travis CI with the
power Azure Pipelines provides. I hadn't made use of any Actions before
trying to replace both Travis and Pipelines on the three Gaphor projects that
I mentioned at the beginning of the post.&lt;/p&gt;
&lt;p&gt;I started first with the libraries, in order to give GitHub Actions a try with
some of the more straightforward workflows before jumping in to converting Gaphor
itself. Both Gaphas and Generic were using Travis CI. The workflow was pretty
standard for a Python package:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run lint using &lt;a href="https://pre-commit.com"&gt;pre-commit&lt;/a&gt; to run
&lt;a href="https://black.rtd.io"&gt;Black&lt;/a&gt; over the code base&lt;/li&gt;
&lt;li&gt;Use a matrix build to test the library using Python 2.7, 3.6, 3.7, and 3.8&lt;/li&gt;
&lt;li&gt;Upload coverage information&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To get started with GitHub Actions on a project, go to the Actions tab on the
main repo:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/github-actions-tab.png" alt="GitHub Actions Tab" style="max-height:60px"&gt;&lt;/p&gt;
&lt;p&gt;Based on your project being made up of mostly Python, GitHub will suggest three
different workflows that you can use as templates to create your own:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Python application - test on a single Python version&lt;/li&gt;
&lt;li&gt;Python package - test on multiple Python versions&lt;/li&gt;
&lt;li&gt;Publish Python Package - publish a package to PyPI using Twine&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Below is the workflow I had in mind:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/library-workflow.svg" alt="Library Workflow" style="max-height:200px"&gt;&lt;/p&gt;
&lt;p&gt;I want to start with a lint job that is run, and once that has successfully
completed, I want to start parallel jobs using the multiple versions of Python
that my library supports.&lt;/p&gt;
&lt;p&gt;For these libraries, the 2nd workflow was the closest for what I was looking
for, since I wanted to test on multiple versions of Python. I selected the
&lt;code&gt;Set up this workflow&lt;/code&gt; option. GitHub then creates a new draft YAML file based
on the template that you selected, and places it in the &lt;code&gt;.github/workflows&lt;/code&gt;
directory in your repo. At the top of the screen you can also change the name
of the YAML file from &lt;code&gt;pythonpackage.yml&lt;/code&gt; to any filename you choose. I called
mine &lt;code&gt;build.yml&lt;/code&gt;, since calling this type of workflow a build is the
nomenclature I am familiar with.&lt;/p&gt;
&lt;p&gt;As a side note, the online editor that GitHub has implemented for creating
Actions is quite good. It includes full autocomplete (toggled with Ctrl+Space),
and it actively highlights errors in your YAML file to ensure the correct
syntax for each workflow. These type of error checks are priceless due to the
long feedback loop, and I actually recommend using the online editor at this
point over what VSCode or Pycharm provide.&lt;/p&gt;
&lt;h4&gt;Execute on Events&lt;/h4&gt;
&lt;p&gt;The top of each workflow file are two keywords: &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;on&lt;/code&gt;. The &lt;code&gt;name&lt;/code&gt; sets
what will be displayed in the Actions tab for the workflow you are creating. If
you don't define a name, then the name of the YAML file will be shown as the
Action is running. The &lt;code&gt;on&lt;/code&gt; keyword defines what will cause the workflow to be
started. The template uses a value of &lt;code&gt;push&lt;/code&gt;, which means
that the workflow will be kicked off when you push to any branch in the
repo. Here is an example of how I set these settings for my libraries:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build&lt;/span&gt;
&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;master&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Instead of running this workflow on any push event, I wanted a build to happen
during two conditions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Any Pull Request&lt;/li&gt;
&lt;li&gt;Any push to the master branch&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can see how that was configured above. Being able to start a workflow on
any type of event in GitHub is extremely powerful, and it one of the advantages
of the tight integration that GitHub Actions has.&lt;/p&gt;
&lt;h4&gt;Lint Job&lt;/h4&gt;
&lt;p&gt;The next section of the YAML file is called &lt;code&gt;jobs&lt;/code&gt;, this is where each main
block of the workflow will be defined as a job. The jobs will then be further
broken down in to steps, and multiple commands can be executed in each step.
Each job that you define is given a name. In the template, the job is named
&lt;code&gt;build&lt;/code&gt;, but there isn't any special significance of this name. They also are
running a lint step for each version of Python being tested against. I decided
that I wanted to run lint once as a separate job, and then once that is
complete, all the testing can be kicked off in parallel.&lt;/p&gt;
&lt;p&gt;In order to add lint as a separate job, I created a new job called &lt;code&gt;lint&lt;/code&gt;
nested within the &lt;code&gt;jobs&lt;/code&gt; keyword. Below is an example of my lint job:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Setup Python&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'3.x'&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Dependencies&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;pip install pre-commit&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;pre-commit install-hooks&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Lint with pre-commit&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;pre-commit run --all-files&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next comes the &lt;code&gt;runs-on&lt;/code&gt; keyword which defines which platform GitHub Actions
will run this job on, and in this case I am running on linting on the latest
available version of Ubuntu. The &lt;code&gt;steps&lt;/code&gt; keyword is where most of the workflow
content will be, since it defines each step that will be taken as it is run.
Each step optionally gets a name, and then either defines an Action to use, or a
command to run.&lt;/p&gt;
&lt;p&gt;Let's start with the Actions first, since they are the first two steps in my
lint job. The keyword for an Action is &lt;code&gt;uses&lt;/code&gt;, and the value is the action repo
name and the version. I think of Actions as a library, a reusable step that I
can use in my CI/CD pipeline without having to reinvent the wheel. GitHub
developed these first two Actions that I am making use of, but you will see
later that you can make use of any Actions posted by other users, and even
create your own using the Actions SDK and some TypeScript. I am now convinced
that this is the "secret sauce" of GitHub Actions, and will be what makes this
service truly special. I will discuss more about this later.&lt;/p&gt;
&lt;p&gt;The first two Actions I am using clones a copy of the code I am testing from my
repo, and sets up Python. Actions often use the &lt;code&gt;with&lt;/code&gt; keyword for the
configuration options, and in this case I am telling the &lt;code&gt;setup-python&lt;/code&gt; action
to use a newer version from Python 3.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Setup Python&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'3.x'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The last two steps of the linting job are using the &lt;code&gt;run&lt;/code&gt; keyword. Here I am
defining commands to execute that aren't covered by an Action. As I mentioned
earlier, I am using pre-commit to run Black over the project and check the code
formatting is correct. I have this broken up in to two steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install Dependencies - installs pre-commit, and the pre-commit hook
environments&lt;/li&gt;
&lt;li&gt;Lint with pre-commit - runs Black against all the files in the repo&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the &lt;em&gt;Install Dependencies&lt;/em&gt; step, I am also using the pipe operator, "|",
which signifies that I am giving multiple commands, and I am separating each
one on a new line. We now should have a complete lint job for a Python library,
if you haven't already, now would be a good time to commit and push your
changes to a branch, and check the lint job passes for your repo.&lt;/p&gt;
&lt;h4&gt;Test Job&lt;/h4&gt;
&lt;p&gt;For the test job, I created another job called &lt;code&gt;test&lt;/code&gt;, and it also uses
the &lt;code&gt;ubuntu-latest&lt;/code&gt; platform for the job. I did use one new keyword here called
&lt;code&gt;needs&lt;/code&gt;. This defines that this job should only be started once the lint job has
finished successfully. If I didn't include this, then the lint job and all the
other test jobs would all be started in parallel.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;lint&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next up I used another new keyword called &lt;code&gt;strategy&lt;/code&gt;. A strategy creates a build
matrix for your jobs. A build matrix is a set of different configurations of the
virtual environment used for the job. For example, you can run a job against
multiple operating systems, tool version, or in this case against different
versions of Python. This prevents repetitiveness because otherwise you would
need to copy and paste the same steps over and over again for different versions
of Python. Finally, the template we are using also had a max-parallel keyword
which limits the number of parallel jobs that can run simultaneously. I am only
using four versions of Python, and I don't have any reason to limit the number
of parallel jobs, so I removed this line for my YAML file.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;2.7&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;3.6&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;3.7&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;3.8&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now on to the steps of the job. My first two steps, checkout the sources and
setup Python, are the same two steps as I had above in the lint job. There is
one difference, and that is that I am using the &lt;code&gt;${{ matrix.python-version }}&lt;/code&gt;
syntax in the setup Python step. I use the {{ }} syntax to define an
expression. I am using a special kind of expression called a context, which is
a way to access information about a workflow run, the virtual environment,
jobs, steps, and in this case the Python version information from the matrix
parameters that I configured earlier. Finally, I use the $ symbol in front of
the context expression to tell Actions to expand the expression in to its
value. If version 3.8 of Python is currently running from the matrix, then &lt;code&gt;${{
matrix.python-version }}&lt;/code&gt; is replaced by &lt;code&gt;3.8&lt;/code&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Set up Python ${{ matrix.python-version }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v1&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ matrix.python-version }}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Since I am testing a GTK diagramming library, I need to also install some
Ubuntu dependencies. I use the &lt;code&gt;&amp;gt;&lt;/code&gt; symbol as YAML syntax to ignore the newlines
in my run value, this allows me to execute a really long command while keeping
my standard line length in my .yml file. &lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Ubuntu Dependencies&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;sudo apt-get update -q &amp;amp;&amp;amp; sudo apt-get install&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;--no-install-recommends -y xvfb python3-dev python3-gi&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;python3-gi-cairo gir1.2-gtk-3.0 libgirepository1.0-dev libcairo2-dev&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For my projects, I love using Poetry for managing my Python dependencies.
See my other article on &lt;a href="https://dan.yeaw.me/posts/python-packaging-with-poetry-and-briefcase/"&gt;Python Packaging with Poetry and
Briefcase&lt;/a&gt; for more
information on how to make use of Poetry for your projects. I am using a custom
Action that &lt;a href="https://github.com/dschep"&gt;Daniel Schep&lt;/a&gt; created that installs
Poetry. Although installing Poetry manually is pretty straightforward, I really
like being able to make use of these building blocks that others have created.
Although you should always use a Python virtual environment while you are
working on a local development environment, they aren't really needed since
the environment created for CI/CD is already isolated and won't be reused. This
would be a nice improvement to the &lt;code&gt;install-poetry-action&lt;/code&gt;, so that the
creation of virtualenvs are turned off by default.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Poetry&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;dschep/install-poetry-action@v1.2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.0.0b3&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Turn off Virtualenvs&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;poetry config virtualenvs.create false&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next we have Poetry install the dependencies using the &lt;code&gt;poetry.lock&lt;/code&gt; file using
the &lt;code&gt;poetry install&lt;/code&gt; command. Then we are to the key step of the job, which is
to run all the tests using Pytest. I preface the &lt;code&gt;pytest&lt;/code&gt; command with
&lt;code&gt;xvfb-run&lt;/code&gt; because this is a GUI library, and many of the tests would fail
because there is no display server, like X or Wayland, running on the CI runner.
The X virtual framebuffer (Xvfb) display server is used to perform all the
graphical operations in memory without showing any screen output.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Python Dependencies&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;poetry install&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Test with Pytest&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;xvfb-run pytest&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The final step of the test phase is to upload the code coverage information. We
are using &lt;a href="https://codeclimate.com/oss/"&gt;Code Climate&lt;/a&gt; for analyzing coverage,
because it also integrates a nice maintainability score based on things like
code smells and duplication it detects. I find this to be a good tool to help
us focus our refactoring and other maintenance efforts.
&lt;a href="https://coveralls.io"&gt;Coveralls&lt;/a&gt; and &lt;a href="https://codecov.io"&gt;Codecov&lt;/a&gt; are good
options that I have used as well. In order for the code coverage information to
be recorded while Pytest is running, I am using the
&lt;a href="https://pytest-cov.rtd.io"&gt;pytest-cov&lt;/a&gt; Pytest plugin.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Code Climate Coverage Action&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;paambaati/codeclimate-action@v2.3.0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;CC_TEST_REPORTER_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;195e9f83022747c8eefa3ec9510dd730081ef111acd99c98ea0efed7f632ff8a&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;coverageCommand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;coverage xml&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;CD Workflow - Upload to PyPI&lt;/h4&gt;
&lt;p&gt;I am using a second workflow for my app, and this workflow would actually be
more in place for a library, so I'll cover it here. The Python Package Index
(PyPI) is normally how we share libraries across Python projects, and it is
where they are installed from when you run &lt;code&gt;pip install&lt;/code&gt;. Once I am ready to
release a new version of my library, I want the CD pipeline to upload it to
PyPI automatically.&lt;/p&gt;
&lt;p&gt;If you recall from earlier, the third GitHub Action Python workflow template was
called Publish Python Package. This template is close to what I needed for my
use case, except I am using Poetry to build and upload instead of using
&lt;code&gt;setup.py&lt;/code&gt; to build and Twine to upload. I also used a slightly different event
trigger.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;published&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This sets my workflow to execute when I fully publish the GitHub release. The
Publish Python Package template used the event &lt;code&gt;created&lt;/code&gt; instead. However, it
makes more sense to me to publish the new version, and then upload it to PyPI,
instead of uploading to PyPI and then publishing it. Once a version is uploaded
to PyPI it can't be reuploaded, and new version has to be created to upload
again. In other words, doing the most permanent step last is my preference.&lt;/p&gt;
&lt;p&gt;The rest of the workflow, until we get to the last step, should look very
similar to the test workflow:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Set up Python&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'3.x'&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Poetry&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;dschep/install-poetry-action@v1.2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1.0.0b3&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Dependencies&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;poetry install&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build and publish&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;poetry build&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;poetry publish -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The final step in the workflow uses the &lt;code&gt;poetry publish&lt;/code&gt; command to upload the
Wheel and sdist to PyPI. I defined the &lt;code&gt;secrets.PYPI_USERNAME&lt;/code&gt; and
&lt;code&gt;secrets.PYPI_PASSWORD&lt;/code&gt; context expressions by going to the repository
settings, then selecting Secrets, and defining two new encrypted environmental
variables that are only exposed to this workflow. If a contributor created a
Pull Request from a fork of this repo, the secrets would not be passed to any
of workflows started from the Pull Request. These secrets, passed via the &lt;code&gt;-u&lt;/code&gt;
and &lt;code&gt;-p&lt;/code&gt; options of the &lt;code&gt;publish&lt;/code&gt; command, are used to authenticate with the
PyPI servers.&lt;/p&gt;
&lt;p&gt;At this point, we are done with our configuration to test and release a
library. Commit and push your changes to your branch, and ensure all the steps
pass successfully. This is what the output will look like on the Actions tab in
GitHub:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/github-actions-output.png" alt="GitHub Actions Output" style="max-height:60px"&gt;&lt;/p&gt;
&lt;p&gt;I have posted the final version of my complete GitHub Actions workflows for a
Python library on the &lt;a href="https://github.com/gaphor/gaphas/tree/master/.github/workflows"&gt;Gaphas
repo&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;How to Test and Deploy a Python Application using GitHub Actions&lt;/h3&gt;
&lt;p&gt;My use case for testing a cross-platform Python Application is slightly
different from the previous one we looked at for a library. For the library, it
was really important we tested on all the supported versions of Python. For an
application, I package the application for the platform it is running on with
the version of Python that I want the app to use, normally the latest stable
release of Python. So instead of testing with multiple versions of Python, it
becomes much more important to ensure that the tests pass on all the platforms
that the application will run on, and then package and deploy the app for each
platform.&lt;/p&gt;
&lt;p&gt;Below are the two pipelines I would like to create, one for CI and one for CD.
Although you could combine these in to a single pipeline, I like that GitHub
Actions allows so much flexibility in being able to define any GitHub event to
start a workflow. This tight integration is definitely a huge bonus here, and it
allows you to make each workflow a little more atomic and understandable. I
named my two workflows &lt;code&gt;build.yml&lt;/code&gt; for the CI portion, and &lt;code&gt;release.yml&lt;/code&gt; for the
CD portion.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/app-workflow.svg" alt="App Workflow" style="max-height:300px"&gt;&lt;/p&gt;
&lt;h4&gt;Caching Python Dependencies&lt;/h4&gt;
&lt;p&gt;Although the lint phase is the same between a library and an application, I am
going to add in one more optional cache step that I didn't include earlier for
simplification:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Use Python Dependency Cache&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/cache@v1.0.3&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;~/.cache/pip&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ runner.os }}-pip-${{ hashFiles('**/poetry.lock') }}&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;restore-keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ runner.os }}-pip-&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It is a good practice to use a cache to store information that doesn't often
change in your builds, like Python dependencies. It can help speed up the build
process and lessen the load on the PyPI servers. While setting this up, I also
learned from the &lt;a href="https://docs.travis-ci.com/user/caching/#things-not-to-cache"&gt;Travis CI
documentation&lt;/a&gt;
that you should not cache large files that are quick to install, but are slow
to download like Ubuntu packages and docker images. These files take as long to
download from the cache as they do from the original source. This explains why
the cache action doesn't have any examples on caching these types of files.&lt;/p&gt;
&lt;p&gt;The caches work by checking if a cached archive exists at the beginning of the
workflow. If it exists, it downloads it and unpacks it to the path location.
At the end of the workflow, the action checks if the cache previously existed,
if not, this is called a cache miss, and it creates a new archive and uploads it
to remote storage.&lt;/p&gt;
&lt;p&gt;A few configurations to notice, the &lt;code&gt;path&lt;/code&gt; is operating system dependent because
pip stores its cache in different locations. My configuration above is for
Ubuntu, but you would need to use &lt;code&gt;~\AppData\Local\pip\Cache&lt;/code&gt; for Windows and
&lt;code&gt;~/Library/Caches/pip&lt;/code&gt; for macOS. The &lt;code&gt;key&lt;/code&gt; is used to determine if the correct
cache exists for restoring and saving to. Since I am using Poetry for
dependency management, I am taking the hash of the &lt;code&gt;poetry.lock&lt;/code&gt; file and
adding it to end of a key which contains the context expression for the
operating system that the job is running on, &lt;code&gt;runner.os&lt;/code&gt;, and pip. This will
look like
&lt;code&gt;Windows-pip-45f8427e5cd3738684a3ca8d009c0ef6de81aa1226afbe5be9216ba645c66e8a&lt;/code&gt;,
where the end is a long hash. This way if my project dependencies change, my
&lt;code&gt;poetry.lock&lt;/code&gt; will be updated, and a new cache will be created instead of
restoring from the old cache. If you aren't using Poetry, you could also use
your &lt;code&gt;requirements.txt&lt;/code&gt; or &lt;code&gt;Pipfile.lock&lt;/code&gt; for the same purpose.&lt;/p&gt;
&lt;p&gt;As we mentioned earlier, if the &lt;code&gt;key&lt;/code&gt; doesn't match an existing cache, it's
called a cache miss. The final configuration option called &lt;code&gt;restore-keys&lt;/code&gt; is
optional, and it provides an ordered list of keys to use for restoring the
cache. It does this by sequentially searching for any caches that partially
match in the restore-keys list. If a key partially matches, the action
downloads and unpacks the archive for use, until the new cache is uploaded at
the end of the workflow.&lt;/p&gt;
&lt;h4&gt;Test Job&lt;/h4&gt;
&lt;p&gt;Ideally, it would be great to use a build matrix to test across platforms. This
way you could have similar build steps for each platform without repeating
yourself. This would look something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ matrix.os }}&lt;/span&gt;
&lt;span class="nt"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;os&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;windows-latest&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;macOS-latest&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Ubuntu Dependencies&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;matrix.os == 'ubuntu-latest'&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;sudo apt-get update -q &amp;amp;&amp;amp; sudo apt-get install&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;--no-install-recommends -y xvfb python3-dev python3-gi&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="no"&gt;python3-gi-cairo gir1.2-gtk-3.0 libgirepository1.0-dev libcairo2-dev&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Brew Dependencies&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;matrix.os == 'macOS-latest'&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;brew install gobject-introspection gtk+3 adwaita-icon-theme&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice the &lt;code&gt;if&lt;/code&gt; keyword tests which operating system is currently being used in
order to modify the commands for each platform. As I mentioned earlier, the GTK
app I am working on, requires &lt;a href="https://www.msys2.org"&gt;MSYS2&lt;/a&gt; in order to test
and package it for Windows. Since MSYS2 is a niche platform, most of the steps
are unique and require manually setting paths and executing shell scripts. At
some point maybe we can get some of these unique parts better wrapped in an
action, so that when we abstract up to the steps, they can be more common
across platforms. Right now, using a matrix for each operating system in my
case wasn't easier than just creating three separate jobs, one for each
platform.&lt;/p&gt;
&lt;p&gt;If you are interested in a more complex matrix setup, Jeff Triplett
&lt;a href="https://twitter.com/webology/status/1201887760413528065?s=20"&gt;posted&lt;/a&gt; his
configuration for running five different Django versions against five different
Python versions.&lt;/p&gt;
&lt;p&gt;The implementation of the three test jobs is similar to the library test job
that we looked at earlier.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;test-linux&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;lint&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;span class="nt"&gt;test-macos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;lint&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;macOS-latest&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;span class="nt"&gt;test-windows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;needs:lint&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;runs-on&lt;/span&gt;&lt;span class="p p-Indicator"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;windows-latest&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The other steps to install the dependencies, setup caching, and test with Pytest
were identical.&lt;/p&gt;
&lt;h4&gt;CD Workflow - Release the App Installers&lt;/h4&gt;
&lt;p&gt;Now that we have gone through the CI workflow for a Python application, on to
the CD portion. This workflow is using different event triggers:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Release&lt;/span&gt;

&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;created&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;edited&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;GitHub has a Release tab that is built in to each repo. The deployment workflow
here is started if I create or modify a release. You can define multiple events
that will start the workflow by adding them as a comma separated list. When I
want to release a new version of Gaphor:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I update the version number in the &lt;code&gt;pyproject.toml&lt;/code&gt;, commit the change, add
a version tag, and finally push the commit and the tag.&lt;/li&gt;
&lt;li&gt;Once the tests pass, I edit a previously drafted release to point the tag to
the tag of the release.&lt;/li&gt;
&lt;li&gt;The release workflow automatically builds and uploads the Python Wheel and
sdist, the macOS dmg, and the Windows installer.&lt;/li&gt;
&lt;li&gt;Once I am ready, I click on the GitHub option to Publish release.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In order to achieve this workflow, first we create a job for Windows and macOS:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;upload-windows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;windows-latest&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;span class="nt"&gt;upload-macos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;macOS-latest&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The next steps to checkout the source, setup Python, install dependencies,
install poetry, turn off virtualenvs, use the cache, and have poetry install the
Python dependencies are the exact same as the application Test Job above.&lt;/p&gt;
&lt;p&gt;Next we build the wheel and sdist, which is a single command when using Poetry:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build Wheel and sdist&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;poetry build&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Our packaging for Windows is using custom shell scripts that run
&lt;a href="https://www.pyinstaller.org"&gt;PyInstaller&lt;/a&gt; to package up the app, libraries, and
Python, and makensis to create a Windows installer. We are also using a custom
shell script to package the app for macOS. Once I execute the scripts to
package the app, I then upload the release assets to GitHub:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Upload Assets&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;AButler/upload-release-assets@v2.0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'macos-dmg/*dmg;dist/*;win-installer/*.exe'&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;repo-token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here I am using Andrew Butler's
&lt;a href="https://github.com/AButler/upload-release-assets"&gt;upload-release-assets&lt;/a&gt;
action. GitHub also has an action to perform this called
&lt;a href="https://github.com/actions/upload-release-asset"&gt;upload-release-asset&lt;/a&gt;, but
at the time of writing this, it didn't support uploading multiple files using
wildcard characters, called glob patterns. &lt;code&gt;secrets.GITHUB_TOKEN&lt;/code&gt; is another
context expression to get the access token to allow Actions permissions to
access the project repository, in this case to upload the release assets to a
drafted release.&lt;/p&gt;
&lt;p&gt;The final version of my complete GitHub Actions workflows for the
cross-platform app are posted on the &lt;a href="https://github.com/gaphor/gaphor/tree/master/.github/workflows"&gt;Gaphor
repo&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Future Improvements to My Workflow&lt;/h3&gt;
&lt;p&gt;I think there is still some opportunity to simplify the workflows that I have
created through updates to existing actions or creating new actions. As I
mentioned earlier, it would be nice to have things at a maturity level so that
no custom environment variable, paths, or shell scripts need to be run. Instead,
we would be building workflows with actions as building blocks. I wasn't
expecting this before I started working with GitHub Actions, but I am sold that
this would be immensely powerful.&lt;/p&gt;
&lt;p&gt;Since GitHub recently released CI/CD for Actions, many of the GitHub provided
actions could use a little polish still. Most of the things that I thought of
for improvements, already had been recognized by others with Issues opened for
Feature requests. If we give it a little time, I am sure these will be improved
soon.&lt;/p&gt;
&lt;p&gt;I also said that one of my goals was to release to the three major platforms,
but if you were paying attention in the last section, I only mentioned Windows
and macOS. We are currently packaging our app using Flatpak for Linux and it is
distributed through &lt;a href="https://flathub.org"&gt;FlatHub&lt;/a&gt;. FlatHub does have an
automatic build system, but it requires manifest files stored in a special
separate FlatHub repo for the app. I also contributed to the &lt;a href="https://github.com/flatpak/flatpak-builder-tools"&gt;Flatpak Builder
Tools&lt;/a&gt; in order to
automatically generate the needed manifest from the &lt;code&gt;poetry.lock&lt;/code&gt; file. This
works good, but it would be nice in the future to have the CD workflow for my
app, kickoff updates to the FlatHub repo.&lt;/p&gt;
&lt;h3&gt;Bonus - Other Great Actions&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/marketplace/actions/debugging-with-tmate"&gt;Debugging with
tmate&lt;/a&gt; - tmate is
a terminal sharing app built on top of tmux. This great action allows you to
pause a workflow in the middle of executing the steps, and then ssh in to the
host runner and debug your configuration. I was getting a Python segmentation
fault while running my tests, and this action proved to be extremely useful.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/marketplace/actions/release-drafter"&gt;Release Drafter&lt;/a&gt; - In
my app CD workflow, I showed that I am executing it when I create or edit a
release. The release drafter action drafts my next release with release notes
based on the Pull Requests that are merged. I then only have to edit the
release to add the tag I want to release with, and all of my release assets
automatically get uploaded. The &lt;a href="https://github.com/marketplace/actions/pr-labeler"&gt;PR
Labeler&lt;/a&gt; action goes along
with this well to label your Pull Requests based on branch name patterns like
&lt;code&gt;feature/*&lt;/code&gt;.&lt;/p&gt;</description><category>CI/CD</category><category>GitHub</category><category>programming</category><category>Python</category><guid>https://dan.yeaw.me/posts/github-actions-automate-your-python-development-workflow/</guid><pubDate>Thu, 05 Dec 2019 02:22:00 GMT</pubDate></item><item><title>How to Rock Python Packaging with Poetry and Briefcase</title><link>https://dan.yeaw.me/posts/python-packaging-with-poetry-and-briefcase/</link><dc:creator>Dan Yeaw</dc:creator><description>&lt;p&gt;NOTE: Briefcase now automatically creates a new project with a &lt;code&gt;pyproject.toml&lt;/code&gt;
file by running &lt;code&gt;briefcase new&lt;/code&gt;. I would recommend following the &lt;a href="https://docs.beeware.org/en/latest/tutorial/tutorial-1.html"&gt;BeeWare
Tutorial&lt;/a&gt; to setup
a new project if you want to use Briefcase for packaging it.&lt;/p&gt;
&lt;p&gt;As part of modernizing &lt;a href="https://github.com/gaphor/gaphas"&gt;Gaphas&lt;/a&gt;, the
diagramming widget for Python, I took another look at what the best practices
are for packaging and releasing a new version of a Python library or
application. There are new configuration formats and tools to make packaging
and distributing your Python code much easier.&lt;/p&gt;
&lt;h2&gt;A Short Background on Packaging&lt;/h2&gt;
&lt;p&gt;There are two main use cases for packaging:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Packaging a Library - software that other programs will make use of.&lt;/li&gt;
&lt;li&gt;Packaging an Application - software that a user will make use of.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This may not be a completely accurate definition because software does not
always fit cleanly in to one of these bins, but these use cases will help to
keep focus on what exactly we are trying achieve with the packaging.&lt;/p&gt;
&lt;h4&gt;The Library&lt;/h4&gt;
&lt;p&gt;The goal for packaging a library is to place it on the Python Packaging Index
(PyPI), so other projects can &lt;code&gt;pip install&lt;/code&gt; it. In order to distribute a
library, the standard format is the Wheel. It allows for providing a built&lt;/p&gt;
&lt;p&gt;distribution of files and metadata so that pip only needs to extract files out
of the distribution and move them to the correct location on the target system
for the package to be installed. In other words, nothing needs to be built and
re-compiled.&lt;/p&gt;
&lt;p&gt;Previously if you wanted to
achieve this, it was common to have four configuration files:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;setup.py - The setup script for building, distributing and installing
  modules using the Distutils.&lt;/li&gt;
&lt;li&gt;requirements.txt - Allow easy install of requirements using &lt;code&gt;pip install -r&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;setup.cfg - The setup configuration file&lt;/li&gt;
&lt;li&gt;MANIFEST.in - The manifest template, directs sdist how to generate
  a manifest&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;The Application&lt;/h4&gt;
&lt;p&gt;The goal for packaging an application is get it in the formats where you can
distribute it on the different platforms for easy installation by your users.
For Windows this is often an exe or msi. For macOS this is an app. For Linux
this is a deb, flatpak, appimage, or snap. There is a whole host of tools to
do this like:
&lt;a href="https://py2exe.org"&gt;py2exe&lt;/a&gt;, &lt;a href="https://py2app.readthedocs.io/"&gt;py2app&lt;/a&gt;,
&lt;a href="https://cx-freeze.readthedocs.io/"&gt;cx_Freeze&lt;/a&gt;,
&lt;a href="https://pyinstaller.readthedocs.io/"&gt;PyInstaller&lt;/a&gt;, and
&lt;a href="https://github.com/jaredks/rumps"&gt;rumps&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;pyproject.toml&lt;/h4&gt;
&lt;p&gt;On the packaging front, in May of 2016,
&lt;a href="https://www.python.org/dev/peps/pep-0518/"&gt;PEP 518&lt;/a&gt; was created. The PEP does
a good job of describing all of the shortcoming of the setup script method to
specify build requirements. The PEP also specified a new configuration format
call pyproject.toml. If you aren't familiar with TOML, it is human-usable and is
more simple than YAML.&lt;/p&gt;
&lt;p&gt;The pyproject.toml replaced those four configuration files above using two main
sections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;[build-system]&lt;/code&gt; - The build-system table contains the minimum requirements
  for the build system to execute.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[tool]&lt;/code&gt; - The tool table is where different tools can have users specify
  configuration data.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;The Tools&lt;/h4&gt;
&lt;p&gt;Making use of this new configuration format, a tool called
&lt;a href="https://github.com/takluyver/flit"&gt;flit&lt;/a&gt; has been around since 2015 as a
simple way to put Python Libraries on PyPI.&lt;/p&gt;
&lt;p&gt;In 2017,
&lt;a href="https://github.com/pypa/pipenv"&gt;Pipenv&lt;/a&gt; was created to solve pain points about
managing virtualenvs and dependencies for Python Applications by using a new
Pipfile to manage dependencies. The other major enhancement was the use of a
lock file. While a Wheel is the important output for a Library, for an
Application, the lock file becomes the important thing created for the project.
The lock file contains the exact version of every dependency so that it can be
repeatably rebuilt.&lt;/p&gt;
&lt;p&gt;In 2018, a new project called &lt;a href="https://github.com/sdispater/poetry"&gt;Poetry&lt;/a&gt;
combined some of the ideas from flit and Pipenv to create a new tool that aims
to further simplify and improve packaging. Like flit, Poetry makes use of the
pyproject.toml to manage configuration all in one place. Like Pipenv, Poetry
uses a lock file (poetry.lock) and will automatically create a virtualenv
if one does not already exist. It also has other advantages like exhaustive
dependency resolution that we will explore more thoroughly below.&lt;/p&gt;
&lt;p&gt;For Application distribution, I am going to focus on a single tool called
&lt;a href="https://briefcase.readthedocs.io/"&gt;Briefcase&lt;/a&gt; which along with the other set
of &lt;a href="https://pybee.org"&gt;BeeWare&lt;/a&gt; tools and libraries allows for you to
distribute your program as a native application to Windows, Linux, macOS, iOS,
Android, and the web.&lt;/p&gt;
&lt;h2&gt;Tutorial&lt;/h2&gt;
&lt;p&gt;With the background information out of the way, lets work through how you can
create a new Python project from scratch, and then package and distribute it.&lt;/p&gt;
&lt;h4&gt;Initial Tool Installation&lt;/h4&gt;
&lt;p&gt;To do that, I am going to introduce one more tool (the last one I promise!)
called &lt;a href="https://cookiecutter.readthedocs.io/"&gt;cookiecutter&lt;/a&gt;. Cookiecutter
provides Python project templates, so that you can quickly get up to speed
creating a project that can be packaged and distributed without creating a
bunch of files and boilerplate manually.&lt;/p&gt;
&lt;p&gt;To install cookiecutter, depending on your setup and operating system, from a
virtualenv you can run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;cookiecutter
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next we are going to install Poetry. The recommended way is to run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-sSL&lt;span class="w"&gt; &lt;/span&gt;https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;TestPyPI Account Sign-Up&lt;/h4&gt;
&lt;p&gt;As part of this tutorial we will be publishing packages. If you don't already
have an account, please register for an account on
&lt;a href="https://test.pypi.org/account/register/"&gt;TestPyPI&lt;/a&gt;. TestPyPI allows you to try
distribution tools and processes without affecting the real PyPI.&lt;/p&gt;
&lt;h4&gt;Create Your Project&lt;/h4&gt;
&lt;p&gt;To create the Python project, we are going to use the Briefcase template, so
run &lt;code&gt;cookiecutter&lt;/code&gt; on this template:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;cookiecutter&lt;span class="w"&gt; &lt;/span&gt;https://github.com/pybee/briefcase-template
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Cookiecutter will ask you for information about the project like the name,
description, and software licence. Once this is finished, add any additional
code to your project, or just keep it as is for this demo.&lt;/p&gt;
&lt;p&gt;Change your directory to the app name you gave (I called mine dantestapp), and
initialize git:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dantestapp
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;init
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Create a pyproject.toml Configuration&lt;/h4&gt;
&lt;p&gt;Poetry comes equipped to create a pyproject.toml file for your project, which
makes it easy to add it to an existing or new project. To initiliaze the
configuration run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;init
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The command guides you through creating your pyproject.toml config. It
automatically pulls in the configuration values from the briefcase-template
that we created earlier so using the default values by hitting enter after the
first six questions will be fine. This is what it provided for an output:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Package&lt;span class="w"&gt; &lt;/span&gt;name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;dantestapp&lt;span class="o"&gt;]&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;
Version&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.1.0&lt;span class="o"&gt;]&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;
Description&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;
Author&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;Dan&lt;span class="w"&gt; &lt;/span&gt;Yeaw&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;dan@yeaw.me&amp;gt;,&lt;span class="w"&gt; &lt;/span&gt;n&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;skip&lt;span class="o"&gt;]&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;
License&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;MIT
Compatible&lt;span class="w"&gt; &lt;/span&gt;Python&lt;span class="w"&gt; &lt;/span&gt;versions&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;^3.7&lt;span class="o"&gt;]&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h5&gt;Define Dependencies&lt;/h5&gt;
&lt;p&gt;The configuration generator then asks for you to define your dependencies:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Would&lt;span class="w"&gt; &lt;/span&gt;you&lt;span class="w"&gt; &lt;/span&gt;like&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;define&lt;span class="w"&gt; &lt;/span&gt;your&lt;span class="w"&gt; &lt;/span&gt;dependencies&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;require&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;interactively?&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;yes/no&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;yes&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hit enter for yes.&lt;/p&gt;
&lt;p&gt;For the next prompt &lt;code&gt;Search for package:&lt;/code&gt; enter in briefcase. We are setting
briefcase as a dependency for our project to run.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Enter&lt;span class="w"&gt; &lt;/span&gt;package&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# to add, or the complete package name if it is not listed: &lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;briefcase
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;django-briefcase
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Type 0 to select the first option. and hit enter to select the latest version.
You now need to repeat this process to also add Toga as a dependency. Toga is
the native cross-platform GUI toolkit. Once you are done, hit enter again to
complete searching for other dependencies.&lt;/p&gt;
&lt;h5&gt;Define Development Dependencies&lt;/h5&gt;
&lt;p&gt;At the next prompt the config generator is now asking us to define our development
dependencies:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Would&lt;span class="w"&gt; &lt;/span&gt;you&lt;span class="w"&gt; &lt;/span&gt;like&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;define&lt;span class="w"&gt; &lt;/span&gt;your&lt;span class="w"&gt; &lt;/span&gt;dev&lt;span class="w"&gt; &lt;/span&gt;dependencies&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;require-dev&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;interactively&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;yes/no&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;yes&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hit enter to select the default value which is yes.&lt;/p&gt;
&lt;p&gt;We are going to make &lt;a href="https://pytest.org"&gt;pytest&lt;/a&gt; a development dependency for
the project.&lt;/p&gt;
&lt;p&gt;At the prompt &lt;code&gt;Search for package:&lt;/code&gt; enter in pytest.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;Found&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;packages&lt;span class="w"&gt; &lt;/span&gt;matching&lt;span class="w"&gt; &lt;/span&gt;pytest

Enter&lt;span class="w"&gt; &lt;/span&gt;package&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# to add, or the complete package name if it is not listed: &lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You will get a long list of pytest packages. Type 0 to select the first option.
and hit enter to select the latest version. Then hit enter again to complete
searching for other development dependencies.&lt;/p&gt;
&lt;h5&gt;Complete the Configuration&lt;/h5&gt;
&lt;p&gt;The final step of the configuration generator summaries the configuration that it created.
Notice that first three sections are tool tables for Poetry, and the final one is
the build-system table.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;[&lt;/span&gt;tool.poetry&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dantestapp"&lt;/span&gt;
&lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="nv"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;authors&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Dan Yeaw &amp;lt;dan@yeaw.me&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;license&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MIT"&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;tool.poetry.dependencies&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^3.7"&lt;/span&gt;
&lt;span class="nv"&gt;briefcase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.2.8"&lt;/span&gt;
&lt;span class="nv"&gt;toga&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.2.15"&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;tool.poetry.dev-dependencies&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;pytest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.0"&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;build-system&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;requires&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"poetry&amp;gt;=0.12"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
build-backend&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"poetry.masonry.api"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The dependencies use a "caret requirement", like &lt;code&gt;python = "^3.7"&lt;/code&gt;. This makes
use of &lt;a href="https://semver.org/"&gt;semantic versioning&lt;/a&gt;. So in this example if Python 3.8
is released, then it will automatically update to this version. But, it won't
update to 4.0 automatically, since that is a major version change. If we put in
our configuration &lt;code&gt;"^3.7.2"&lt;/code&gt;, then it would automatically update to
3.7.3 which it is released, but not 3.8, since that is a new minor version.&lt;/p&gt;
&lt;p&gt;There are also "tilde requirements" that are more restrictive. So if you enter `python = "~3.7"
it will only allow update to the next patch level, like from 3.7.2 to 3.7.3. The combination
of caret and tilde requirements allows you to get updates to your dependencies when they are released, but
puts you in control to ensure that incompatible changes won't break your app. Nice!&lt;/p&gt;
&lt;p&gt;The final prompt asks: &lt;code&gt;Do you confim generation? (yes/no) [yes]&lt;/code&gt;. Go ahead 
and hit enter to confirm. Congrats, you have generated a pyproject.toml
configuration!&lt;/p&gt;
&lt;h4&gt;Install Dependencies&lt;/h4&gt;
&lt;p&gt;OK, the hard work is over, we have created our project and finished the configuration.
Now it is time to see how Poetry and Briefcase really shines.&lt;/p&gt;
&lt;p&gt;To install the dependencies that you defined in the pyproject.toml, just run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;install
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Poetry includes an exhaustive dependency resolver, so it will now resolve all of the
dependencies it needs to install Briefcase, Toga, and pytest. It will also create
a &lt;code&gt;poetry.lock&lt;/code&gt; file which ensures that anyone using your program would get the
exact same set of dependencies that you used and tested with.&lt;/p&gt;
&lt;p&gt;Notice that we also did not create or specify a virtual environment. Poetry automatically
creates one prior to installing packages, if one isn't already activated. If you
would like to see which packages are installed and which virtual environment
Poetry is using you can run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;show&lt;span class="w"&gt; &lt;/span&gt;-v
or
$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;--list
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Bundle and Run your Application for Platform Distribution&lt;/h4&gt;
&lt;p&gt;For a Python Application, you want to bundle the application and all of its
dependencies into a single package so that it can easily be installed on a users
platform without the user manually install Python and other modules.&lt;/p&gt;
&lt;p&gt;Briefcase allows you to package and run your app using your platform:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;(&lt;/span&gt;Windows&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;setup.py&lt;span class="w"&gt; &lt;/span&gt;windows&lt;span class="w"&gt; &lt;/span&gt;-s
&lt;span class="o"&gt;(&lt;/span&gt;macOS&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;setup.py&lt;span class="w"&gt; &lt;/span&gt;macos&lt;span class="w"&gt; &lt;/span&gt;-s
&lt;span class="o"&gt;(&lt;/span&gt;Linux&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;setup.py&lt;span class="w"&gt; &lt;/span&gt;linux&lt;span class="w"&gt; &lt;/span&gt;-s
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Your app will launch, will just be a blank window at this point.&lt;/p&gt;
&lt;p&gt;Also notice that it creates a folder with the platform name that you used
above. Inside this folder, Briefcase has packaged your app for distribution on
your platform. Briefcase also has distribution options for android, ios, and
django.&lt;/p&gt;
&lt;h4&gt;Build your Library for Distribution on PyPI&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;build

Building&lt;span class="w"&gt; &lt;/span&gt;dantestapp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.1.0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;Building&lt;span class="w"&gt; &lt;/span&gt;sdist
&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;Built&lt;span class="w"&gt; &lt;/span&gt;dantestapp-0.1.0.tar.gz

&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;Building&lt;span class="w"&gt; &lt;/span&gt;wheel
&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;Built&lt;span class="w"&gt; &lt;/span&gt;dantestapp-0.1.0-py3-none-any.whl
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The source distribution (sdist) and wheel are now in a new &lt;code&gt;dist&lt;/code&gt; folder.&lt;/p&gt;
&lt;h4&gt;Publish your Library to PyPI&lt;/h4&gt;
&lt;p&gt;First we are going to add the TestPyPI repository to Poetry, so that it knows
where to publish to. The default location is to the real PyPI.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;repositories.test-pypi&lt;span class="w"&gt; &lt;/span&gt;https://test.pypi.org/legacy/
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now simply run:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;publish&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;test-pypi
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;-r&lt;/code&gt; argument tells Poetry to use the repository that we configured. Poetry
then will ask for your username and password. Congrats! Your package is now
available to be viewed at https://test.pypi.org/project/your-project-name/ and
can be pip installed with &lt;code&gt;pip install -i https://test.pypi.org/simple/
your-project-name&lt;/code&gt;.&lt;/p&gt;</description><category>BeeWare</category><category>Briefcase</category><category>packaging</category><category>Poetry</category><category>Python</category><guid>https://dan.yeaw.me/posts/python-packaging-with-poetry-and-briefcase/</guid><pubDate>Thu, 03 Jan 2019 04:01:00 GMT</pubDate></item><item><title>5 Steps to Build Python Native GUI Widgets for BeeWare</title><link>https://dan.yeaw.me/posts/gui-widget-for-beeware/</link><dc:creator>Dan Yeaw</dc:creator><description>&lt;p&gt;Part of my work at Ford Motor Company is to use Model-Based Systems Engineering
through languages like SysML to help design safety in to complex automated
and electrified technologies. In my free time I took over maintaining a UML
tool called &lt;a href="https://github.com/gaphor/gaphor"&gt;Gaphor&lt;/a&gt; with the aim of
eventually turning it in to a simple SysML tool for beginners. I'm sure I'll
be writing about this much more in the future.&lt;/p&gt;
&lt;p&gt;Eventually I got really frustrated with the current set of GUI toolkits that
are available for Python. I want the ability to write an app once and have it
look and feel great on all of my devices, but instead I was dealing with
toolkits that are wrapped or introspected around C or C++ libraries, or
visually look like a blast from past. They made me feel like I was going
against the grain of Python instead of writing great Pythonic code.&lt;/p&gt;
&lt;p&gt;If you haven't heard of BeeWare yet, it is a set of software libraries for
cross-platform native app development from a single Python codebase and tools
to simplify app deployment. When I say cross-platform and native, I mean truly
that. The project aims to build, deploy, and run apps for Windows, Linux, macOS, Android,
iPhone, and the web. It is native because it is actually that platform's native
GUI widgets, not a theme, icon pack, or webpage wrapper.&lt;/p&gt;
&lt;p&gt;A little over a year ago, I started to contribute to the BeeWare project. I
needed a canvas drawing widget for the app I am working on, I saw that this was
not supported by BeeWare, so I went ahead and created it. Based on my
experience, this blog post details how I would create a new widget from
scratch, now that I have done it before, with the hope that it helps you
implement your own widget as well.&lt;/p&gt;
&lt;p&gt;If you are new to BeeWare, I recommend to start out with the
&lt;a href="https://briefcase.readthedocs.io/en/latest/tutorial/index.html"&gt;Briefcase&lt;/a&gt; and
&lt;a href="https://toga.readthedocs.io/en/latest/tutorial/index.html"&gt;Toga&lt;/a&gt; Tutorials, and
then the &lt;a href="https://pybee.org/contributing/how/first-time/"&gt;First-time Contributor's Guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="BeeWare Logo with Brutus the Bee and text" src="https://dan.yeaw.me/images/beeware-logo.png"&gt;&lt;/p&gt;
&lt;p&gt;The current status of the BeeWare project, at the time of writing this, is that
it is a solid proof of concept. Creating a simple app on macOS, Linux, or iOS
is definitely possible. In fact there is an app called Travel Tips on Apple's
App Store that was created by Russell Keith-Magee as a demonstration. Support
for some of the other platforms like Windows and Android is lagging behind
some, so except some very rough edges.&lt;/p&gt;
&lt;p&gt;This alpha status may not be so exciting for you if you are just trying to
build an app, but I think it is &lt;strong&gt;very&lt;/strong&gt; exciting for those that want to
contribute to an open source project. Although there are many ways to get
involved, users keep asking how they can build a GUI widget that isn't yet
supported. I think this is a great way to make a significant contribution.&lt;/p&gt;
&lt;p&gt;A GUI widget forms the controls and logic that a user interacts with when using
a GUI. The BeeWare project uses a GUI widget toolkit called Toga, and below is
a view of what some of the widgets look like in Linux.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Example of Toga Widgets in a demo app" src="https://dan.yeaw.me/images/toga-widgets.png"&gt;  &lt;/p&gt;
&lt;p&gt;There are button, table, tree, and icon widgets in the example. Since I
contributed a canvas drawing widget, I will be using that for the example of
how you could contribute your own widget to the project.&lt;/p&gt;
&lt;p&gt;There are three internal layers that make up every widget:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;Interface&lt;/strong&gt; layer&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Implementation&lt;/strong&gt; layer&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Native&lt;/strong&gt; layer  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/toga-blackbox.svg" alt="Toga Blackbox" style="max-height:200px"&gt;&lt;/p&gt;
&lt;p&gt;As the input to Toga, the Interface layer provides the public API for the GUI
application that you are building. This is the code you will type to build your
app using Toga.&lt;/p&gt;
&lt;p&gt;As the output of Toga, the Native layer connects the Toga_impl's to the Native
Platform. For C language based platforms, Toga directly calls the native
widgets. For example with Gtk+ on Linux, the Toga_gtk directly calls the Gtk+
widgets through PyGObject. For other platforms, more intermediate work may be
required through a bridge or transpiler:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS and iOS, the Rubicon-ObjC project provides a bridge between Objective-C and Python.&lt;/li&gt;
&lt;li&gt;Web, Batavia provides a javascript implementation of the Python virtual machine. &lt;/li&gt;
&lt;li&gt;Android, VOC is a transpiler that converts Python in to Java bytecode.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/toga-whitebox.svg" alt="Toga Whitebox" style="max-height:150px"&gt;&lt;/p&gt;
&lt;p&gt;The Interface layer calls public methods that are in the Toga_core portion of
the project and this is where this Interface layer API is defined. Toga_core
also provides any abstract functionality that is independent of the platform
that Toga is running on, like setting up and running the app itself.&lt;/p&gt;
&lt;p&gt;The Implementation layer connects Toga_core to the Toga_impl component.&lt;/p&gt;
&lt;p&gt;A couple of other terms you should know about are &lt;code&gt;impl&lt;/code&gt; and &lt;code&gt;interface&lt;/code&gt;.
1. From Toga_core, &lt;code&gt;self.impl&lt;/code&gt; is used to go across the interface layer to
Toga_impl.
2. From Toga_impl, &lt;code&gt;self.interface&lt;/code&gt; is used to go across the interface layer
back to Toga_core.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/toga-impl-interface.svg" alt="More Terms" style="max-height:170px"&gt;&lt;/p&gt;
&lt;p&gt;Toga uses the Factory Method design pattern in order to improve testability.
This pattern creates objects using a factory method instead of directly
calling a constructor. In Toga, this factory method is in Toga_core and it is
used to instantiate a platform backend as the Toga_impl like Toga_ios, Toga_cocoa
or Toga_gtk. The factory method automatically selects the correct backend based
on the &lt;code&gt;sys.platform&lt;/code&gt; of the platform it is running on.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/factory-pattern.svg" alt="Factory Method" style="max-height:250px"&gt;&lt;/p&gt;
&lt;p&gt;Toga_dummy is also a type of Toga_impl backend, and it is used for smoke testing
without a specific platform to find simple failures. When tests are initialized,
Toga_dummy is passed in as the factory. This allows the tests and the creation
of objects to be separated which improves maintainability and makes the test
code easier to read.&lt;/p&gt;
&lt;p&gt;I know there is a lot there, but understanding the software architecture of
Toga together with the surrounding projects and interfaces will be key to
implementing your own widget. With that background information out of the way,
lets not delay any further, and jump in to building a widget.&lt;/p&gt;
&lt;h2&gt;Step 0&lt;/h2&gt;
&lt;h3&gt;Pick your development platform&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Normally pick the platform that you are most familiar with&lt;/li&gt;
&lt;li&gt;macOS and Gtk+ are the most developed :thumbsup:&lt;/li&gt;
&lt;li&gt;Is this a mobile only widget (camera, GPS, etc)?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This seems somewhat obvious, since the platform you select will most likely be
based on the laptop or other device you are using right now. But do consider
this. Most of my experience developing widgets are on Gtk+ and Cocoa so this is
where I am coming from. Implementing widgets on other platforms is definitely
needed as well, but it may be an additional challenge due to those platforms
not as well developed with Toga yet. These other platforms may be more
challenging, but they are also the areas where the BeeWare project needs the
most help, so if you have some experience with them or feel especially brave,
definitely go for it.&lt;/p&gt;
&lt;h2&gt;Step 1&lt;/h2&gt;
&lt;h3&gt;Research your widget&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Abstraction requires knowledge of specific examples&lt;/li&gt;
&lt;li&gt;Create use cases or user stories&lt;/li&gt;
&lt;li&gt;Get feedback&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since Toga is an abstraction of native GUI toolkits, understanding the APIs for
these platforms is extremely important in order to develop a well abstracted API
for Toga. In other words, these native platforms provide the inspiration and
constraints on implementing your own widget.&lt;/p&gt;
&lt;p&gt;As an example, of how you would conduct this research, this is how you would
draw a rectangle on a Canvas on different platforms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tkinter&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_rectangle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;wxpython&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;wx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Panel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EVT_PAINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OnPaint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;OnPaint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PaintDC&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetBrush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Brush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Colour&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;dc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DrawRectangle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;HTML canvas&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"myCanvas"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2d"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rgb(200, 0, 0)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Gtk+&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;drawingarea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Gtk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DrawingArea&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;drawingarea&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"draw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;da&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_source_rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rectangle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The other thing to understand is how a user will use this widget to build their
own app. I like to create a quick Use Case diagram to flush this out, but you
could also use User Stories or similar methods.&lt;/p&gt;
&lt;p&gt;For the case of the Canvas widget, I came up with three main use cases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A simple drawing app, where a user adds shapes, colors, and text to the
   screen.&lt;/li&gt;
&lt;li&gt;A vectoring drawing app, where a user draws lines and shapes, and then needs
   the ability to edit the nodes of the lines.&lt;/li&gt;
&lt;li&gt;A platformer game, where there is a lot of objects draw on the screen,
   including the hero. The hero needs its own drawing context so that they can
   run, jump, and move around without unintentionally modifying the rest of the
   objects.  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src="https://dan.yeaw.me/images/usecases.svg" alt="Use Cases" style="max-height:300px"&gt;&lt;/p&gt;
&lt;p&gt;The last part of Step 1 is to get feedback. I recommend creating a GitHub Issue
or Draft Pull Request at this point and start to discuss the design of your
widget abstraction with others and continue that discussion as you design your
python API in step 2.&lt;/p&gt;
&lt;h2&gt;Step 2&lt;/h2&gt;
&lt;h3&gt;Write Docs&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Write your API documentation first&lt;/li&gt;
&lt;li&gt;The API provides the set of clearly defined methods of communication (layers) between the software components&lt;/li&gt;
&lt;li&gt;Documentation Driven Development&lt;/li&gt;
&lt;li&gt;This is iterative with Step 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With your Use Cases from Step 1, start your docs by explaining what your widget
is and what it is used for. When looking at the Canvas widgets from my research,
I noticed that the current drawing widgets were very procedural, you have to
create your canvas drawing using many steps. For example, you have to first set
the color to draw with, then draw an object, and then fill in that object.&lt;/p&gt;
&lt;p&gt;Python has the context manager and the "with" statement, and making use of this
for a canvas allows the user to better break up the draw operations with some
nesting. It also allows for automatically starting or closing drawing of a
closed path for the user. This is an example of the types of things that you
can take advantage of in an API that was designed for Python. It is easy to try
to copy the API that you are familiar with, but I think you can really make
your widget stand out by taking a step back and looking at how you can make an
API that users will really enjoy using.&lt;/p&gt;
&lt;p&gt;Here is an example of writing the initial explanation and widget API for the
canvas widget:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;The canvas is used for creating a blank widget that you can
draw on.

&lt;span class="gu"&gt;Usage&lt;/span&gt;
&lt;span class="gu"&gt;--&lt;/span&gt;

Simple usage to draw a colored rectangle on the screen using
the arc drawing object:

import toga
canvas = toga.Canvas(style=Pack(flex=1))
with canvas.fill(color=rgb(200, 0, 0)) as fill:
    fill.rect(10, 10, 100, 100)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once that is complete, now might be a good time to ask for feedback to see if
you have missed any use cases or if others have any ideas of how to improve the
public API of the widget. One way to collect feedback would be to submit an
issue or a "work in progress" pull request to the &lt;a href="https://github.com/pybee/Toga"&gt;Toga
project&lt;/a&gt;, or ask for feedback on the &lt;a href="https://gitter.im/pybee/general"&gt;Gitter
channel&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, start to work out the structure of your Toga_core code based on your API.
I recommend creating the class and method definitions and add the docstrings to
outline what each portion of the software does and what arguments and return
values it provides. This is part of the overall documentation that will be
generated by Sphinx for your widget, and creating this before writing your code
will provide the next level of API documentation.&lt;/p&gt;
&lt;p&gt;Here is an example of how that structure and docstrings would look for a canvas
widget:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Create new canvas.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        id (str):  An identifier for this widget.&lt;/span&gt;
&lt;span class="sd"&gt;        style (:obj:`Style`): An optional style object. &lt;/span&gt;
&lt;span class="sd"&gt;        factory (:obj:`module`): A python module that is&lt;/span&gt;
&lt;span class="sd"&gt;            capable to return a implementation of this class.&lt;/span&gt;

&lt;span class="sd"&gt;     """&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Constructs and returns a :class:`Rect &amp;lt;Rect&amp;gt;`.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        x (float): x coordinate for the rectangle.&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;    """&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Step 3&lt;/h2&gt;
&lt;h3&gt;Implement your Toga_core widget using TDD&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Write a test for each function of the widget outlined in the API from Step 3&lt;/li&gt;
&lt;li&gt;Check that the tests fail&lt;/li&gt;
&lt;li&gt;Specify the implementation layer API&lt;/li&gt;
&lt;li&gt;Write the core code for the widget to call the implementation layer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Test Driven Development is a test-first technique to write your tests prior to
or in parallel with writing the software. I am being opinionated here,
because you don't have to write your code using this process. But, I think
this will really help you think about what you want from the code as you
implement these different API layers.&lt;/p&gt;
&lt;p&gt;Toga_core has a "tests" folder, and this is where you need to create your tests
for the widget. Sometimes it can be challenging to know what tests to write, but
in the previous step you already outlined what the use cases and scenarios are for
using your widget, and the API to make use of the widget. Break this up in to atomic
tests to check that you can successfully create the widget, and then make use of and
modify the widget using all of the outlined scenarios.&lt;/p&gt;
&lt;p&gt;Here is a test to check that the widget is created. The &lt;code&gt;canvas._impl.interface&lt;/code&gt;
is testing the call to the Toga_impl component ("_impl") and then back to the
Toga_core component ("interface"). In other words we are testing that the canvas object
is the same object as we go to the Implementation layer to the Toga_impl and
then back across the Implementation layer to the Toga_core. The object should be
equal as long as it was created successfully. The second line of the test
&lt;code&gt;assertActionPerformed&lt;/code&gt; is using the dummy backend to test that the canvas was
created, and I'll discuss that more in Step 4 below.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_widget_created&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_impl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertActionPerformed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"create Canvas"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Further along in my test creation I also wanted to check that the user could modify
a widget that was already created. So I created a test that modifies the coordinates
and size of a rectangle. &lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_rect_modify&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;rect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;redraw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertActionPerformedWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once you are done creating your tests and make sure that they are failing as
expected, it is time to move on to filling in all of those Toga_core classes
and objects that you left blank in the previous step.&lt;/p&gt;
&lt;p&gt;Toga provides a base Widget class that all widgets derive from. It defines the
interface for core functionality for children, styling, layout and ownership by
specific App and Window. Below our class Canvas is derived from Widget and is
initialized:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As part of the class initialization, Toga also uses the factory method to
determine the correct Toga_impl platform, and then connect it from the
Toga_core to Toga_impl&lt;code&gt;self._impl&lt;/code&gt; and back the other way using
&lt;code&gt;interface=self&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;        &lt;span class="c1"&gt;# Create a platform specific implementation of Canvas&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_impl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, we fill in our methods to call the creation of the rectangle on the
Toga_impl component using the Implementation layer:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_impl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Step 4&lt;/h2&gt;
&lt;h3&gt;Implement the Toga_impl widget on the dummy backend&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Dummy is for automatic testing without a native platform&lt;/li&gt;
&lt;li&gt;Code the implementation layer API endpoint, create a method for each call of the API&lt;/li&gt;
&lt;li&gt;Check that all tests now pass&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When your widget is integrated with Toga, we want unit tests to run with the
test suite automatically during continuous integration. It may be difficult
during these tests to start up every platform and check that your widget is
working correctly, so there is a Toga_impl called dummy that doesn't require a
platform at all. This allows for smoke testing to make sure that the widget
correctly calling the Implementation layer API.&lt;/p&gt;
&lt;p&gt;Now go ahead and implement the Toga_impl widget on the dummy backend. There
needs to be methods for each call from the Toga_core to the Toga_impl. Below we
check that the Canvas create and rect method actions were invoked through
Implementation layer API calls.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"create Canvas"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;"rect"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You now should be able to run and pass all the tests that you created in Step 3.&lt;/p&gt;
&lt;h2&gt;Step 5&lt;/h2&gt;
&lt;h3&gt;Implement the Toga_impl widget on your platform backend&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Copy toga_dummy and create a new endpoint for the platform you chose in Step 1&lt;/li&gt;
&lt;li&gt;Make use of the native interface API for this widget on your platform&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If after your research in Step 1, you aren't feeling confident in how the widget
should work on your platform, now would be a good time to take a break to go
practice. Build a simple canvas drawing app for your platform using the native
widgets. Once you have done that, now is the time to create the Toga_impl for
your platform that calls those native widgets on your platform.&lt;/p&gt;
&lt;p&gt;In my example, Gtk+ uses an event callback to do native drawing. So I create a
Gtk.DrawingArea to draw on when my Canvas widget is created, and then I connect
that drawing callback to the gtk_draw_callback function which then calls a method
in Toga_core through the Implementation layer:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Canvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;native&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Gtk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DrawingArea&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;native&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;native&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"draw"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gtk_draw_callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;gtk_draw_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gtk_context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draw_context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gtk_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Some platforms like Android or Cocoa will require transpiling or bridging to
the native platform calls since those platforms using a different programming
language. This may require the creation of extra native objects to, for
example, reserve memory on those platforms. Here is an example of what this
extra TogaCanvas class would like with the Cocoa platform:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TogaCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NSView&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@objc_method&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;drawRect_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NSRect&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NSGraphicsContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graphicsPort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally create each method for your native implementation. Below we create an
implementation of the rectangle creation that calls Gtk+'s cairo drawing:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draw_context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;draw_context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rectangle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Iterate&lt;/h2&gt;
&lt;h4&gt;Iterate through steps 1-5 to complete your widget implementation&lt;/h4&gt;
&lt;p&gt;In the examples, we created a Canvas and a rectangle drawing operation on that
canvas. Now it is time to iterate back through all the steps and implement all
the other drawing operations that a Canvas needs like the other shapes, lines,
and text. Once you finish this, you should now have a complete widget!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Toga Tutorial 4 for a Canvas Widget" src="https://dan.yeaw.me/images/tutorial-4.png"&gt;&lt;/p&gt;
&lt;p&gt;Tada! You did it, Submit a PR!&lt;/p&gt;
&lt;p&gt;I would be interested in how it goes for you, drop me a line with your
experience creating a widget.&lt;/p&gt;
&lt;p&gt;2018-11-10: Minor editorial updates.&lt;/p&gt;
&lt;p&gt;2019-04-27: Split Toga Architecture diagram up to make it more clear.&lt;/p&gt;
&lt;p&gt;2019-05-02: Improve description about research in Step 1. Add description of
&lt;code&gt;impl&lt;/code&gt; and &lt;code&gt;interface&lt;/code&gt; in architecture section.&lt;/p&gt;</description><category>BeeWare</category><category>GUI</category><category>programming</category><category>Python</category><category>Toga</category><category>widget</category><guid>https://dan.yeaw.me/posts/gui-widget-for-beeware/</guid><pubDate>Sun, 04 Nov 2018 01:36:00 GMT</pubDate></item></channel></rss>