Adventures in Python Packaging and AI
Several years ago, I wrote a short Python script that outputs a list of the second Tuesday’s of each month. The second Tuesday of the month being Microsoft “Patch Tuesday”. The purpose of the script was just to provide a simple and quick way to determine when new OS patches would become available.
That script had remained largely untouched since I wrote it back in 2020.
More on this later...
Python CLI Tools
I’ve written many Python scripts over the years for my day job as a Linux Sysadmin, as well as quite a few that I just enjoy writing and using on my own like my, not yet awesome but some day will be, weather app.
But most of those scripts have always been purpose built and usually run on just one or two dedicated machines for years and years.
I’ve always been fascinated by cli tools like Ansible, that are packaged and distributable to almost any computer.
My little programs are no where near as large and complex as Ansible but I want to implement a command line interface with sub commands and options in a similar way that these types of tools do.
Enter python click
In my search for how to implement a simple cli tool I came across Click. Which recommended using Setuptools to package your scripts for others to use.
And so down the rabbit hole I went...
All my books are useless
I started scanning through the dozen or so Python books I have laying around and none of them covered packaging and distribution of Python code.
I’m sure setting up a Python project in such a way that it can be packaged and distributed is trivial to people who work in that space everyday but from the perspective of an outsider it is just dark magic.
For anyone who writes Python programming books please consider adding a chapter on packaging.
The internet (AKA Land of Confusion)
Since my books were of little to no help I started doing what all good IT people do and searched the interwebs for a good simple solution. This was not entirely unhelpful as it did help me figure out what I should be looking for, however, as is often the case with the internet the results of the search were overwhelming.
At first I didn’t find much information at all - mostly just unanswered questions that were several years old from other people who wanted to know how to distribute Python programs. The questions that were answered, typically say something like “Use x tool”. Sometimes with a link to the tool, sometimes not, never with an explanation of HOW to use the suggested tool.
After learning some of the vocabulary and refining my search I discovered that there are a multitude of ways to package Python scripts - as well as a multitude of opinions on “The Best Way™”.
Narrowing it down?
After learning some of the common vocabulary around packaging I was able to determine the most common tools people use to distribute Python code seem to be:
-
- I found this one by scanning the project repo for a cli note taking application I like called jrnl
-
- I think this is the most common approach? Spoiler alert this is the one I went with. Though I’m curious about learning to use Poetry.
-
- I saw this listed as a recommendation in the Python packaging user guide.
The next problem - me
Since my earlier research with Click had recommended setuptools, and the setuptools web site seemed to have an air of “officiality” to it. I decided to start there.
Once this part of the mystery was solved, the next issue was with my ability to understand the documentation.
There were simply too many options for file names, and project structure. Not to mention terminology that I’m still not really comfortable with.
- Like, what the fuck is
__init__.py
? - Should I use
pyproject.toml
,setup.cfg
,setup.py
and which options are minimally required for each one? - There are plenty of code snippets but why no complete examples?
- Or are they complete examples and I just didn’t know it?
Copilot
Back to the start of this post. I have a script that will find and display “Patch Tuesday” for every month in a given year, and I wanted to package that script.
It didn’t go well...
After repeated and frustrating failed attempts to set up the proper directory structure and include the just right information in a pyproject.toml
(chosen only because it was the first tab in the examples). I decided to try GitHub Copilot that is built into VSCode. Copilot chosen because it’s free, and because I had heard it was easy to integrate into VSCode.
The prompt
I gave Copilot this prompt:
What changes would I need to make to this project so that it would be distributable with pip
The results were far better than I expected.
Response
Copilots response was nearly perfect. I should note that this didn’t work exactly as the response said, but it was close enough and gave me just enough information to fill the gaps in my knowledge.
To make your project distributable with
pip
, you need to create asetup.py
file and ensure your project follows a standard directory structure. Here are the steps:Project Structure: Organize your project directory as follows:
get_patch_tuesday/ ├── get_patch_tuesday/ │ ├── __init__.py │ ├── patch_tues.py ├── setup.py ├── README.md ├── LICENSECreate
__init__.py
: Create an empty__init__.py
file inside theget_patch_tuesday
directory to make it a package.Create
setup.py
: Create asetup.py
file in the root directory with the following content:# filepath: /Users/luke/projects/get_patch_tuesday/setup.py from setuptools import setup, find_packages setup( name="get_patch_tuesday", version="0.1", packages=find_packages(), entry_points={ 'console_scripts': [ 'get_patch_tuesday=get_patch_tuesday.patch_tues:main', ], }, install_requires=[ # List any dependencies here ], author="Your Name", author_email="your.email@example.com", description="A script to get Patch Tuesday for a given year.", long_description=open('README.md').read(), long_description_content_type='text/markdown', url="https://github.com/yourusername/get_patch_tuesday", classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires='>=3.6', )Modify patch_tues.py: Add a
main
function to patch_tues.py and call it if the script is executed directly:# filepath: /Users/luke/projects/get_patch_tuesday/get_patch_tuesday/patch_tues.py #!/usr/bin/env python3 import calendar import datetime import argparse def main(): # get the current year now = datetime.datetime.now().year parser = argparse.ArgumentParser( description="Get patch tuesday for a given year. Current year by default." ) parser.add_argument( "year", nargs="?", help="The year, example 2020", default=now, type=int ) args = parser.parse_args() for month in range(1, 13): cal = calendar.monthcalendar(args.year, month) # Second Tuesday will be in the second or third week of the month week2 = cal[1] week3 = cal[2] # Check if Tuesday is between 8 and 14. If so Second tuesday is in week 2. Else it's week 3 if 8 <= week2[calendar.TUESDAY] <= 14: patchday = week2[calendar.TUESDAY] else: patchday = week3[calendar.TUESDAY] print(calendar.month_name[month], patchday, args.year) if __name__ == "__main__": main()Create
README.md
: Create aREADME.md
file with a description of your project.Create LICENSE: Add a license file to your project (e.g., MIT License).
Install and Test Locally: Run the following command to install your package locally and test it:
pip install -e .Publish to PyPI: Follow the instructions on PyPI to publish your package.
By following these steps, you will have a project that can be distributed and installed using
pip
.
One of the key things I was missing was a “main” function that needs to be called like this for some reason, that I’m still reading about.
if __name__ == "__main__":
main()
Copilot had some pathing errors and for some reason renamed a file... but after a slightly less frustrating troubleshooting session I was able to get this script into a state where it was installable with pip!
I guess AI isn’t completely useless
Judging by the AI on my iPhone that can’t even create a proper note Copilot seems to have some useful features. One thing I was surprised about was the level of detail in the explanation. I didn’t paste the rest of the back and forth I had with the chatbot because it was long.
I asked it to explain the reasoning behind examples which made the whole thing more approachable for the next time I tackle this problem.
Most of the trouble I ran into probably stems from my lack of formal education in programming.
I just pick things up here and there as I go. As I said earlier I’m sure this is entirely trivial to people who’ve been doing this for years and learned it day one in school. But simple things like this can trip people up for weeks, and I guess had I bit the bullet and asked AI for help earlier I would’ve saved myself a good bit of frustration.