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
💡 We will add coverage using
poetry add -D coverage[toml]
so we can include coverage options and configuration on ourpyproject.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 😄