Semantic Versioning In Python With Git Hooks

I write content for AWS, Kubernetes, Python, JavaScript and more. To view all the latest content, be sure to visit my blog and subscribe to my newsletter. Follow me on Twitter.

This is Day 22 of the #100DaysOfPython challenge.

This post will use the pre-commit and commitizen packages to automate our semantic versioning for Python based on our commits.
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
  • Familiarity with Pipenv. See here for my post on Pipenv.
  • Getting started
    We are going to clone and work from the okeeffed/your-first-pip-package-in-python repository and install the required packages.
    $ 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 naming conventions.
    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.10.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
    Originally posted on my blog. To see new posts without delay, read the posts there and subscribe to my newsletter.

    29

    This website collects cookies to deliver better user experience

    Semantic Versioning In Python With Git Hooks