Transcription
A high-quality CI pipeline boosts your code quality, reduces code review strain, and solidifies best practices across your team and organization. I'll show you how to set up a sleek, modern CI pipeline for Python with the latest tools: linting, formatting, type consistency, and more. Let's go!
Here is a completely empty project that we will be building out. First, let's create an empty project with UV in it. We will use the `--package` flag to set up a build backend that we will need later, and an empty lock file with `uv lock`. This lock file makes sure that everyone installs the same dependencies and transitive dependencies with the exact same versions everywhere.
If you modify the `.toml` file directly, which is certainly not advised, the lock file is out of sync because it is not updated. We can verify if the lock file is in sync with the `.toml` using the `uv lock` command and adding the `--locked` flag. Let's clean up the `.toml` file again so the lock file isn't in sync. This is the first check that we will add to our CI pipeline.
Let's create a GitHub Actions file at `.github/workflows/code-quality.yml`. Now, since I want my code checks to run in parallel, I'm going to abstract away the installation of UV to a composite action. This allows me to refer to the action as part of a step within a job. Nice!
Secondly, we can add a linter to make sure that everyone uses the same code quality standards. Linters can check if your code has docstrings, type hints, doesn't contain any secret values, it can check cyclomatic complexity, and much, much more. There are quite a few linters, but the most complete and fastest is Ruff. To check if the project is up to par, we can run `uvx ruff check`. This will report all lines that violate the rule, and we can add it to CI. We can also set a dependency on the completion of the lock file check.
Complimentary to linting, we have formatting. This checks how your code is visually structured, such as sorting of imports, double quotes or single quotes, line length, backslashes or parentheses, etc. With `uvx ruff format`, we can completely reformat our code to be fully PEP compliant. As of recording this video, `format` does not take into account import formatting. However, the `--fix` flag of the `ruff check` command can help us there. Ruff makes sure that all the imports are sorted correctly. If you want to check if all files are formatted correctly, we can run `uvx ruff format --check`. So that is the next step that we will add to our CI pipeline.
Next up is type checking. Since Python is a dynamic language and type hints are well, hints, it is entirely possible to write good type hints for a function but still ignore them. For example, I can write a function that hints at a certain return type but instead returns something completely else. My linter complains, which is very good, but it will compile fine. Ruff unfortunately does not catch these, so we need to have something else to make sure that the types are what you document them to be. We can use Pyright. To get Pyright up and running, we can add that as a dev dependency to our UV project, and now we can run `uv run pyright`. And we see that Pyright indeed complains that something is going on with a return type. Let's fix this so our CI doesn't complain, and of course, add this to our code quality pipeline.
So far, we've only checked the validity and the structure of syntax. Next up is actual implementation. Unit tests are the first and easiest way to check the validity of your implementation. Let's add a simple test case for the function that we've just created. To run our unit tests, we can add Pytest to our dev dependencies, and to run a unit test, we can do `uv run pytest --test-folder`. And I like to add the `-v` for verbose flag to have a bit more structured output.
Additionally to just running unit tests, we can also track the code coverage. For this, we can add `pytest-cov` to our dev dependencies and rerun our test command with the `--cov` flag. It is also possible to generate an XML file from your coverage report. This XML file we will need later to upload it to codecov.io. First, let's add this to our CI pipeline. As a bonus, it is also optional to add a `--durations` flag, which prints out the timing for each test.
Lastly, it is nice to check if our project can actually be built into a wheel. As we speak, the latest version of UV sets up a build backend if you use the `--package` flag, which is what we did. That means we can just run `uv build`, which creates a wheel file for us in this folder, and let's add that one to CI as well.
The CI pipeline is nearly complete. I just want to show you how to set up the uploading of the code coverage file to codecov.io. If you navigate to app.codecov.io, you can log in with your GitHub account. After you've logged in and selected your GitHub project, you can go to Configuration, General, and here's the repository token that we can use to link GitHub to codecov.io. So, copy this token, go to your GitHub repository, navigate to Settings, Secrets and variables, Actions, and then you can add your token as an environment secret here.
After you've done that, the only thing that is left to do is add a small step in our CI pipeline that uploads the code coverage report to codecov.io. And with this, our CI is completed. If you then navigate back to your GitHub repository, navigate to Actions, and one of the commits, you can see a CI pipeline that does exactly what we've told the YAML to do. And that's it! With this CI pipeline, you've set up a reliable process to make sure that your code is clean, consistent, and tested at every step. By automating these checks, you're freeing up time for deeper code reviews. With tools like Ruff, Pyright, and Pytest, you're well on your way to set up a seamless CI pipeline. Thanks for watching, and feel free to subscribe if you found this video helpful. If you have any more questions, let me know in the comments, and I'll do my best to answer them. Happy coding!