Hello! And welcome back! It’s been a lot since I last write some (small) piece of knowledge and today my approach may seem traditional regarding what a dev-blog usually includes. But you know I can’t help myself about sharing some stuff that looks helpful to me, so today I’m bringing the results of my test-and-research.

So I’m going to take advantage of this entry as I really find it helpful to write down a process to make sure I fully understand it. You know what they say, you don’t fully understand some topic until you are able to explain it to your granma.

Getting started

I would definitely say that Python is my language of choice and since all my Python projects are really look-alike (dependencies managed, packaged, necessity-driven, kept them separate from each other, small and cute, …), I really needed some way to quickstart my code using some things that seem familiar to me and doesn’t involve a steep learning curve. And here it is!

But first…

¿Qué es poesía?, dices mientras clavas en mi pupila tu pupila azul. ¿Qué es poesía? ¿Y tú me lo preguntas? Poesía… eres tú.

(Gustavo Adolfo Bécquer)

Moving, isn’t it? Let’s make our Python get moving using Poetry.

Glossary

Let me introduce first to you some style rules I’ll be using all over this quickstart, so we can talk on the same terms:

  • Tools are named like this

  • Code blocks will show up like

    this
    
  • And file names look like this

Requisites

Or things we need to know beforehand. Don’t worry if you don’t consider yourself an expert on this, I do not either. You only need some basic notions since this is going to be as easy and simple as it can be.

  • Python ≥ 3.0
  • Poetry (Python packaging and dependency management made easy)
  • To you, SOLID is more than a state of matter, and you know there’s something called Test Driven Development 1
  • A cup of coffee ☕

Steps

Create poetry project

Create your new project using

poetry new my_project

This command line creates a new directory called my_project on your current one. Inside this directory you will find the following structure:

my_project
├── pyproject.toml
├── README.md
├── my_project
│   └── __init__.py
└── tests
    └── __init__.py
		└── test_my_project.py

Add dependencies

Inside pyproject.toml we find some quite basic configuration lines, including project name, versioning, authors and contributors and some package-building related info. In this file we will add our required dependencies (or we can add them via poetry add -D <dependency>).

💡 -D flag indicates that the dependency must be added only for development

We will add some dependencies to our file using our preferred method and always keeping versions in mind. We will add them under dev-dependencies so Poetry only requires them while we are developing our application

  • pytest: unit testing made simple for Python

  • coverage: unit-testing coverage monitoring tool

💡 We will add coverage using poetry add -D coverage[toml] so we can include coverage options and configuration on our pyproject.toml file using
[tool.coverage.<configuration>] config = "config" ...

We will add this config later

  • flake8: advanced linter for Python

  • flake8-bugbear: flake8 plugin for finding bugs and design problems

💡 For installing flake8 and flake8-bugbear I used bash poetry add -D flake8 flake8-bugbear So both my dependencies are installed at the same time

  • commitizen: define a standard way of committing rules

  • pre-commit: ensure your tests, linters and automations runs before you commit

Our pyproject.toml will look like this:

[tool.poetry]
name = "my_project"
version = "0.1.0"
description = "my_project description"
authors = ["Me <me@memail.com>"]
readme = "README.md"
repository = "https://gitlab.com/my_project"

[tool.poetry.dependencies]
python = "^3.10"

[tool.poetry.dev-dependencies]
pytest = "^7.1.3"
coverage = {extras = ["toml"], version = "^6.4.4"}
flake8 = "^5.0.4"
flake8-bugbear = "^22.9.11"
pre-commit = "^2.20.0"
commitizen= "^2.32"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

Coverage configuration

Now we are going to add coverage configuration on our pyproject.toml adding the following tags

[tool.coverage.run]
omit = [".*", "tests/*"]

[tool.coverage.report]
fail_under = 80

With these lines we are excluding unit-testing coverage for files outside our project files (my_project directory) and instructing coverage to “fail” when coverage is not 80% the least.

Update poetry.lock

With our dependencies and configuration added to pyproject.toml, let’s update our poetry.lock config file by:

poetry update

This updates and installs new dependencies and replace old versions with desired ones.

flake8 linter configuration

To add flake8 custom config I created in my project’s root directory the following .flake8 file, including the following lines

[flake8]
max-line-length = 88
max-complexity = 10
select = C,E,F,W,B,B950
ignore = E203,E501,W503 #this are flake8-bugbear exception/error codes
exclude =
    .git,
    __pycache__,
    *.egg-info,
    .nox,
    .pytest_cache,
    .mypy_cache

pre-commit configuration

We haven’t committed anything yet! Let’s prepare that step. In order to ensure our tests are passing and our commits are following the conventional commit style, we will add the following lines on our .pre-commit-config.yaml

repos:
- repo: https://gitlab.com/pycqa/flake8
  rev: 5.0.4
  hooks:
    - id: flake8
      additional_dependencies: [flake8-bugbear]
- repo: local
  hooks:
    - id: tests
      name: tests
      entry: poetry run pytest
      language: system
      types: [python]
      pass_filenames: false
- repo: https://github.com/commitizen-tools/commitizen
  rev: v2.31.0
  hooks:
    - id: commitizen # Enforce Conventional Commits standard
      name: Conventional commit
      stages: [ commit-msg ]

So, what those all so-called “repos” do? We are going to break them down in pieces:

flake8 repo

This piece here indicates we have to enforce flake8 configurations. We added flake8-bugbear as a flake8 dependency because we want to use flake8-bugbear exceptions and additional error codes.

- repo: https://gitlab.com/pycqa/flake8
  rev: 5.0.4
  hooks:
    - id: flake8
      additional_dependencies: [flake8-bugbear]

local repo for tests

This fragment here ensures our pytest unit-test pass succesfully before we commit+push to our repo.

- repo: local
  hooks:
    - id: tests
      name: tests
      entry: poetry run pytest
      language: system
      types: [python]
      pass_filenames: false

commitizen repo

Finally, here we use commitzen tools for enforcing conventional committing in our commits.

- repo: https://github.com/commitizen-tools/commitizen
  rev: v2.31.0
  hooks:
    - id: commitizen # Enforce Conventional Commits standard
      name: Conventional commit
      stages: [ commit-msg ]

💡 How do I test my pre-commit config? poetry run pre-commit run --all-files

Custom scripts for poetry for running test commands

Usually, this stuff involving aliases and scripting and custom commands is made via Makefile. Unfortunately, the company computer where I work from does not endorse much stuff, so I have to get creative af to look for workarounds everywhere.

Ladies, it’s creation time!

Poetry let us create some custom scripts to call after our run statement.

poetry run <our_script_here_which_does_some_rad_stuff>

Yay!

On the other hand, poetry doesn’t allow us to call cmd scripts but python ones from here.

Nay? I don’t think so.

We are going to add on our project’s root directory a tools.py file including our calls to cmd shell via subprocess dependency

import subprocess

def run_cmd(cmd):
    _c = ["poetry", "run"]
    _c.extend(cmd)

    subprocess.run(cmd)

def run_pytest():
    cmd = ["coverage", "run", "-m", "pytest"]
    run_cmd(cmd)

def run_coverage_report():
    cmd = ["coverage", "report", "-m"]
    run_cmd(cmd)

def test():
    run_pytest()
    run_coverage_report()

TL;DR: our run_cmd() function call does the same2 as scripting the following on our preferred console

poetry run coverage run -m pytest && poetry run coverage report -m

Which runs our unit-tests using pytest under coverage and also shows our coverage report

Wrap up!

So this is it! We have successfully configured our Python project to use TDD, get testing code coverage, use advanced linter rules, enforce (locally, always) conventional commit and having some custom scripts to adapt poetry to our very needs.

Get into your project and happy coding!


1 You don’t really need to know this, but this quickstart sets everything up to use those concepts on your Python application.

2 It’s not the same.

Concatenating 2 instructions using && implies that the second one only runs after the first one only when the first one runs successfully. Here what we are doing is running 2 instructions consecutively but without conditioning one on the other. In our case, it’s more than enough 😄