Semantic Versioning In Python With Git Hooks
Published: Aug 10, 2021
Last updated: Aug 10, 2021
This is Day 22 of the #100DaysOfPython challenge.
This post will use the pre-commit
commitizen
The post will build off the work done in the post "Your First Pip Package With Python"
The final code can be found here
Prerequisites
Getting started
We are going to clone and work from the okeeffed/your-first-pip-package-in-python
$ git clone https://github.com/okeeffed/your-first-pip-package-in-python semantic-versioning-in-python-with-git-hooks $ cd semantic-versioning-in-python-with-git-hooks # Init the virtual environment $ pipenv install --dev pre-commit Commitizen toml # Create some required files $ touch .pre-commit-config.yaml
At this stage, we are ready to configure our pre-commit hook.
Pre-commit configuration
We need to add the following to .pre-commit-config.yaml
:
--- repos: - repo: https://github.com/commitizen-tools/commitizen rev: master hooks: - id: commitizen stages: [commit-msg]
Once done, we can setup our Commitizen
configuration.
Setting up Commitizen
We can set this up by running pipenv run cz init
:
$ pipenv run cz init ? Please choose a supported config file: (default: pyproject.toml) pyproject.toml ? Please choose a cz (commit rule): (default: cz_conventional_commits) cz_conventional_commits No Existing Tag. Set tag to v0.0.1 ? Please enter the correct version format: (default: "$version") ? Do you want to install pre-commit hook? Yes commitizen already in pre-commit config commitizen pre-commit hook is now installed in your '.git' You can bump the version and create changelog running: cz bump --changelog The configuration are all set.
We are now at a stage where our commits can affect our versioning! A file is created for us pyproject.toml
:
[tool] [tool.commitizen] name = "cz_conventional_commits" version = "0.0.1" tag_format = "$version"
This will contain the version information that is maintained by Commitizen for us.
Automated semantic versioning at work
First of all, we will need to create a tag for version 0.0.1
:
$ git tag -a 0.0.1 -m "Init version"
Our version will change based on the naming that we give our commits. The installed Git hook will enforce that our commits follow the Conventional Commits
Let's see this in action. First, let's add a new function to the file demo_pip_math/math.py
:
def divide(x: int, y: int) -> float: """divide one number by another Args: x (int): first number in the division y (int): second number in the division Returns: int: division of x and y """ return x / y
We want to add and commit this code. Let's see what happens when we set an invalid version:
$ git add demo_pip_math/math.py $ git commit -m "added new feature division" commitizen check.........................................................Failed - hook id: commitizen - exit code: 14 commit validation: failed! please enter a commit message in the commitizen format. commit "": "not a valid commit name" pattern: (build|ci|docs|feat|fix|perf|refactor|style|test|chore|revert|bump)(\(\S+\))?!?:(\s.*)
This tells use that we did not match the expected pattern.
Given that we are adding a new feature, let's try with the feat:
prefix:
$ git commit -m "feat: added new divide functionality" commitizen check.........................................................Passed [main 333d291] feat: added new divide functionality 1 file changed, 13 insertions(+)
We've passed!
Now that we've made a change, we can use Commitizen
to help us update our version and create a Changelog and update the version:
$ pipenv run cz bump bump: version 0.0.1 → 0.1.0 tag to create: 0.1.0 increment detected: MINOR Done!
If we now check pyproject.toml
, we can see that change reflected for us:
[tool] [tool.commitizen] name = "cz_conventional_commits" version = "0.1.0" tag_format = "$version"
Creating the Changelog
To create a Changelog, run pipenv run cz changelog
:
$ pipenv run cz changelog # ... no output
This will create a CHANGELOG.md
file in our project root directory, now with the following information:
## 0.1.0 (2021-08-10) ### Feat - added new divide functionality ## 0.0.1 (2021-08-10) ### Feat - add in division function - init commit
Success!
Keeping setup.py in sync
Finally, we want to make sure that our setup.py
is in sync with what is reflected in pyproject.toml
.
We can do this with the toml
package.
Inside of setup.py
, change the file to be the following:
import setuptools import toml from os.path import join, dirname, abspath pyproject_path = join(dirname(abspath("__file__")), '../pyproject.toml') file = open(pyproject_path, "r") toml_str = file.read() parsed_toml = toml.loads(toml_str) with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="demo_pip_math", version=parsed_toml['tool']['commitizen']['version'], author="Dennis O'Keeffe", author_email="hello@dennisokeeffe.com", description="Demo your first Pip package.", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/okeeffed/your-first-pip-package-in-python", packages=setuptools.find_packages(), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires='>=3.6', keywords='pip-demo math', project_urls={ 'Homepage': 'https://github.com/okeeffed/your-first-pip-package-in-python', }, )
We are reading in the TOML and finding the reference to the current version.
We can check that this still works as expected with a build
script that was already added in the repo:
$ pipenv run build running sdist running egg_info creating demo_pip_math.egg-info writing demo_pip_math.egg-info/PKG-INFO writing dependency_links to demo_pip_math.egg-info/dependency_links.txt writing top-level names to demo_pip_math.egg-info/top_level.txt writing manifest file 'demo_pip_math.egg-info/SOURCES.txt' reading manifest file 'demo_pip_math.egg-info/SOURCES.txt' reading manifest template 'MANIFEST.in' adding license file 'LICENSE' writing manifest file 'demo_pip_math.egg-info/SOURCES.txt' running check creating demo_pip_math-0.1.0 creating demo_pip_math-0.1.0/demo_pip_math creating demo_pip_math-0.1.0/demo_pip_math.egg-info creating demo_pip_math-0.1.0/tests copying files to demo_pip_math-0.1.0... copying LICENSE -> demo_pip_math-0.1.0 copying MANIFEST.in -> demo_pip_math-0.1.0 copying README.md -> demo_pip_math-0.1.0 copying pyproject.toml -> demo_pip_math-0.1.0 copying setup.py -> demo_pip_math-0.1.0 copying demo_pip_math/__init__.py -> demo_pip_math-0.1.0/demo_pip_math copying demo_pip_math/math.py -> demo_pip_math-0.1.0/demo_pip_math copying demo_pip_math.egg-info/PKG-INFO -> demo_pip_math-0.1.0/demo_pip_math.egg-info copying demo_pip_math.egg-info/SOURCES.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info copying demo_pip_math.egg-info/dependency_links.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info copying demo_pip_math.egg-info/top_level.txt -> demo_pip_math-0.1.0/demo_pip_math.egg-info copying tests/__init__.py -> demo_pip_math-0.1.0/tests copying tests/test_math.py -> demo_pip_math-0.1.0/tests Writing demo_pip_math-0.1.0/setup.cfg creating dist Creating tar archive removing 'demo_pip_math-0.1.0' (and everything under it) running bdist_wheel running build running build_py creating build creating build/lib creating build/lib/demo_pip_math copying demo_pip_math/__init__.py -> build/lib/demo_pip_math copying demo_pip_math/math.py -> build/lib/demo_pip_math creating build/lib/tests copying tests/__init__.py -> build/lib/tests copying tests/test_math.py -> build/lib/tests warning: build_py: byte-compiling is disabled, skipping. installing to build/bdist.macosx-11-x86_64/wheel running install running install_lib creating build/bdist.macosx-11-x86_64 creating build/bdist.macosx-11-x86_64/wheel creating build/bdist.macosx-11-x86_64/wheel/demo_pip_math copying build/lib/demo_pip_math/__init__.py -> build/bdist.macosx-11-x86_64/wheel/demo_pip_math copying build/lib/demo_pip_math/math.py -> build/bdist.macosx-11-x86_64/wheel/demo_pip_math creating build/bdist.macosx-11-x86_64/wheel/tests copying build/lib/tests/__init__.py -> build/bdist.macosx-11-x86_64/wheel/tests copying build/lib/tests/test_math.py -> build/bdist.macosx-11-x86_64/wheel/tests warning: install_lib: byte-compiling is disabled, skipping. running install_egg_info Copying demo_pip_math.egg-info to build/bdist.macosx-11-x86_64/wheel/demo_pip_math-0.1.0-py3.9.egg-info running install_scripts adding license file "LICENSE" (matched pattern "LICEN[CS]E*") creating build/bdist.macosx-11-x86_64/wheel/demo_pip_math-0.1.0.dist-info/WHEEL creating 'dist/demo_pip_math-0.1.0-py3-none-any.whl' and adding 'build/bdist.macosx-11-x86_64/wheel' to it adding 'demo_pip_math/__init__.py' adding 'demo_pip_math/math.py' adding 'tests/__init__.py' adding 'tests/test_math.py' adding 'demo_pip_math-0.1.0.dist-info/LICENSE' adding 'demo_pip_math-0.1.0.dist-info/METADATA' adding 'demo_pip_math-0.1.0.dist-info/WHEEL' adding 'demo_pip_math-0.1.0.dist-info/top_level.txt' adding 'demo_pip_math-0.1.0.dist-info/RECORD' removing build/bdist.macosx-11-x86_64/wheel
We can see that version 0.1.0
is used in this build.
Summary
Today's post demonstrated how to use the commitizen
, pre-commit
and toml
packages to help automate our versioning process based on Conventional Commits.
This helps larger teams keep their packages semantically correct with little effort added to their work.
Resources and further reading
Photo credit: snapsbyclark
Dennis O'Keeffe
Melbourne, Australia
1,200+ PEOPLE ALREADY JOINED ❤️️
Get fresh posts + news direct to your inbox.
No spam. We only send you relevant content.
Semantic Versioning In Python With Git Hooks
Introduction