2021-11-06

How to Use Semantic Versioning for Shared Django Apps

How to Use Semantic Versioning for Shared Django Apps

When you are building websites with Django, there is no need to track their software versions. You can just have a stable branch in Git repository and update the production environment whenever it makes sense. However, when you create a Django app or package shared among various websites and maybe with multiple people, it is vital to keep track of the package versions.

The Benefits

Versioning allows you to identify a specific state of your package and has these benefits:

  • Developers can be aware of which package version works with their websites together flawlessly.
  • You can track which versions had which bugs or certain features when communicating with open-source communities or technical support.
  • In the documentation, you can clearly see which version of the software it is referring to.
  • When fixing bugs of a particular version, developers of the versioned package have a narrower scope of code to check at version control commits.
  • Just from the version number, it's clear if the upgrade will only fix the bugs or if it requires more attention to make your software compatible.
  • When talking to other developers about a particular package, you can clearly state what you are talking about (yes, developers talk about software versions from time to time 😅).

Semantic Versioning

The most popular versioning scheme is called semantic versioning. The version consists of three numbers separated by dots. Django framework is using it too. For example, the latest stable version at the time of writing is 3.2.9. In semantic versioning, the first number is the major version, the second is the minor version, and the third is a patch version: MAJOR.MINOR.PATCH.

  1. MAJOR version introduces backward incompatible features.
  2. MINOR version adds functionality that is backward compatible.
  3. PATCH version is for backward-compatible bug fixes.

There can also be some versioning outliers for alpha, beta, release candidates rc1, rc2, etc., but we will not cover them in this post.

By convention, the package's version is saved in its myapp/__init__.py file as __version__ variable, for example:

__version__ = "0.2.4"

In addition, it is saved in setup.py, README.md, CHANGELOG.md, and probably some more files.

Changelog

When you develop a Django app or another Python package that you will share with other developers, it is also recommended to have a changelog. Usually, it's a markdown-based document with the list of features, bugs, and deprecations that were worked on between specific versions of your package.

A good changelog starts with the "Unreleased" section and is followed by sections for each version in reverse order. In each of those sections, there are lists of changes grouped into categories:

  • Added
  • Changed
  • Deprecated
  • Removed
  • Fixed
  • Security

This could be the starting template for CHANGELOG.md:

Changelog
=========

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


[Unreleased]
------------


<!--
### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security
-->

As the new version is released, it will replace the Unreleased section while creating a new Unreleased section above it. For example, this:

[Unreleased]
------------

### Added

- Logging the cookie consent choices in the database because of the legal requirements.

### Fixed

- Disabling the buttons while saving the Cookie Choices so that they are not triggered more than once with slow Internet connections.

would become this:

[Unreleased]
------------

[v0.2.0] - 2021-10-27
------------------

### Added

- Logging the cookie consent choices in the database because of the legal requirements.

### Fixed

- Disabling the buttons while saving the Cookie Choices so that they are not triggered more than once with slow Internet connections.

To keep track of the versions manually would be pretty tedious work, with the likelihood of forgetting one or more files or mismatching the version numbers. Gladly, there is a utility tool to do that for you, which is called bump2version.

Using bump2version

Installation

Install the bump2version utility to your virtual environment the standard way with pip:

(env)$ pip install bump2version

Preparation

In your package's __init__.py file, set the version to 0.0.0:

__version__ = "0.0.0"

Set the version to 0.0.0 in all other files, where the version needs to be mentioned, for example, README.md and setup.py.

Then create setup.cfg with the following content:

[bumpversion]
current_version = 0.0.0
commit = True
tag = True

[bumpversion:file:setup.py]
search = version="{current_version}"
replace = version="{new_version}"

[bumpversion:file:myapp/__init__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"

[bumpversion:file:README.md]
search = django_myapp-{current_version}-py2.py3-none-any.whl
replace = django_myapp-{new_version}-py2.py3-none-any.whl

[bumpversion:file:CHANGELOG.md]
search = 
    [Unreleased]
    ------------
replace = 
    [Unreleased]
    ------------
    
    [v{new_version}] - {utcnow:%%Y-%%m-%%d}
    ------------------

[bdist_wheel]
universal = 1

Replace "django_myapp" and "myapp" with your Django app there. If you have more files, mentioning the package version, make sure to have analogous version replacements in the setup.cfg.

Commit and push your changes to the Git repository.

Usage

Every time you want to create a new version, type this in the shell:

$ bump2version patch

or

$ bump2version minor

or

$ bump2version major

followed by the command to build the package:

$ python3 setup.py sdist bdist_wheel

As mentioned before, patch is for the bug fixes, minor is for backward-compatible changes, and major is for backward-incompatible changes.

The bump2version command will use the configuration at setup.cfg and will do these things:

  • It will increment the current version number depending on the parameter passed to it.
  • It will replace the old version with the new one in the setup.py, myapp/__init__.py, and README.md.
  • It will take care of correct versioning in the CHANGELOG.md.
  • Then, it will commit the changes and create a tag according to the pattern vMAJOR.MINOR.PATCH there.

Some further details

There are two things to note there.

First, in Markdown, there are two ways of setting the headings:

Changelog
=========

[Unreleased]
------------

is identical to

# Changelog

## [Unreleased]

Usually, I would use the second format for Markdown, but the replacement function in setup.cfg treats lines with the # symbol as comments, and escaping doesn't work either.

For example, this wouldn't work:

[bumpversion:file:CHANGELOG.md]
search = 
    ## [Unreleased]
replace = 
    ## [Unreleased]
    
    ## [v{new_version}] - {utcnow:%%Y-%%m-%%d}

If anybody knows or finds a simple solution with the shorter Markdown variation, let me know.

Second, instead of using setup.cfg, you can use .bumpversion.cfg, but that file is hidden, so I recommend sticking with setup.cfg.

Final words

You can see this type of semantic versioning configuration in the paid Django GDPR Cookie Consent app I recently published on Gumroad.

Happy programming!


Cover photo by Jacob Campbell

No comments:

Post a Comment