Levelling up your Python project with GNU Make

Have you ever found yourself running the same commands over and over again to run, test, and install dependencies on your Python project.

If you haven’t heard of make then the answer to that question is probably YES!!!

The following guide should provide some make basics and useful resources for running make on your Python project.

All examples in the guide are run on macOS using a Bash terminal with Python 3.13.

What Is GNU Make?

GNU Make is a widely used build automation tool, which enables users to define sets of commands (known as rules), to make files (known as targets).

Installing GNU Make

Installing make for Linux and Mac should be straightforward via the standard package manager for your Linux OS, or via brew on macOS.

For guidance for Windows see article: How to install and use “make” in Windows?.

Basics

A project using make usually has a file named Makefile at the project root.

Let’s define a basic rule for target foo:

<span># foobar/Makefile </span><span>foo</span><span>:</span>
<span>@</span><span>echo </span>Creating file foo
<span>@</span><span>echo </span>foo file contents <span>></span> foo
<span># foobar/Makefile </span><span>foo</span><span>:</span>
    <span>@</span><span>echo </span>Creating file foo
    <span>@</span><span>echo </span>foo file contents <span>></span> foo
# foobar/Makefile foo: @echo Creating file foo @echo foo file contents > foo

Enter fullscreen mode Exit fullscreen mode

Running make for target foo from the Bash terminal:

<span>$ </span>make foo
Creating file foo
<span>$ </span>make foo
Creating file foo
$ make foo Creating file foo

Enter fullscreen mode Exit fullscreen mode

Executing make foo from the project root runs the rule, creating the target file foo.

Let’s create another rule for bar with a pre-requisite foo:

<span># foobar/Makefile </span><span>.PHONY</span><span>:</span> <span>bar</span>
<span>bar</span><span>:</span> <span>foo</span>
<span>@</span><span>echo </span>bar
<span># foobar/Makefile </span><span>.PHONY</span><span>:</span> <span>bar</span>
<span>bar</span><span>:</span> <span>foo</span>
    <span>@</span><span>echo </span>bar
# foobar/Makefile .PHONY: bar bar: foo @echo bar

Enter fullscreen mode Exit fullscreen mode

<span>$ </span>make bar
bar
<span>$ </span>make bar
bar
$ make bar bar

Enter fullscreen mode Exit fullscreen mode

The line bar: foo states that the file foo is a pre-requisite for bar, and therefore will be created before bar.

However, notice Creating file foo is not printed when running make bar. To demonstrate what’s going on here we can run make target foo again:

<span>$ </span>make foo
make: <span>'foo'</span> is up to date.
<span>$ </span>make foo
make: <span>'foo'</span> is up to date.
$ make foo make: 'foo' is up to date.

Enter fullscreen mode Exit fullscreen mode

foo is not created as the file was already created via make foo.

You may notice .PHONY declaration above bar. This declares the file as a phony target, which is not typically the name of a file, but a rule to be executed when you run make.

Let’s amend target foo and declare it as a phony target:

<span># foobar/Makefile </span><span>.PHONY</span><span>:</span> <span>foo</span>
<span>foo</span><span>:</span>
<span>@</span><span>echo </span>foo
<span># foobar/Makefile </span><span>.PHONY</span><span>:</span> <span>foo</span>
<span>foo</span><span>:</span>
    <span>@</span><span>echo </span>foo
# foobar/Makefile .PHONY: foo foo: @echo foo

Enter fullscreen mode Exit fullscreen mode

<span>$ </span>make bar
foo
bar
<span>$ </span>make bar
foo
bar
$ make bar foo bar

Enter fullscreen mode Exit fullscreen mode

Now when we run make bar, foo is printed to the terminal.

Python Applications

To demonstrate some common applications let’s think about some common user stories which could be defined in a Makefile:

  • As a user I want to run my project
  • As a developer I want to set up the project for development
  • As a developer I want to run the tests locally
  • As a developer I want CI to test across a range of Python versions
  • As a developer I want CI to build the documentation
  • As a developer I want CI to perform lint and formatting checks

User

Provided a basic script main.py which should print “Hello, world!”, we can easily run the script via make:

<span># hello-world/main.py </span><span>def</span> <span>main</span><span>():</span>
<span>return</span> <span>"</span><span>Hello, world!</span><span>"</span>
<span>if</span> <span>__name__</span> <span>==</span> <span>"</span><span>__main__</span><span>"</span><span>:</span>
<span>print</span><span>(</span><span>main</span><span>())</span>
<span># hello-world/main.py </span><span>def</span> <span>main</span><span>():</span>
    <span>return</span> <span>"</span><span>Hello, world!</span><span>"</span>

<span>if</span> <span>__name__</span> <span>==</span> <span>"</span><span>__main__</span><span>"</span><span>:</span>
    <span>print</span><span>(</span><span>main</span><span>())</span>
# hello-world/main.py def main(): return "Hello, world!" if __name__ == "__main__": print(main())

Enter fullscreen mode Exit fullscreen mode

<span># hello-world/Makefile </span><span>.PHONY</span><span>:</span> <span>run</span>
<span>run</span><span>:</span>
python main.py
<span># hello-world/Makefile </span><span>.PHONY</span><span>:</span> <span>run</span>
<span>run</span><span>:</span>
    python main.py
# hello-world/Makefile .PHONY: run run: python main.py

Enter fullscreen mode Exit fullscreen mode

<span>$ </span>make run
Hello, world!
<span>$ </span>make run
Hello, world!
$ make run Hello, world!

Enter fullscreen mode Exit fullscreen mode

As well as for Python scripts, the run target could be useful for any Python application which takes basic args such as a Flask server.

In the example where we have a CLI application where there’s a specific command to run the application, it may be more useful to include a help target to print the CLI helper.

Developer

When developing on a shared / open source project there are often coding standards to follow, and tests to be run before committing code.

Developer Setup

Pre-requisites:

This project makes use of pre-commit to enforce coding standards upon commit (pre-commit hooks), as well as pytest testing framework.

The target make dev, installs the required Python packages for development, and installs the pre-commit hooks:

<span># hello-world/Makefile </span><span>.PHONY</span><span>:</span> <span>dev</span>
<span>dev</span><span>:</span>
python <span>-m</span> pip <span>install</span> <span>-e</span> <span>.</span> pre-commit pytest
pre-commit <span>install</span> <span>--install-hooks</span>
<span># hello-world/Makefile </span><span>.PHONY</span><span>:</span> <span>dev</span>
<span>dev</span><span>:</span>
    python <span>-m</span> pip <span>install</span> <span>-e</span> <span>.</span> pre-commit pytest
    pre-commit <span>install</span> <span>--install-hooks</span>
# hello-world/Makefile .PHONY: dev dev: python -m pip install -e . pre-commit pytest pre-commit install --install-hooks

Enter fullscreen mode Exit fullscreen mode

<span>$ </span>make dev
python <span>-m</span> pip <span>--require-virtualenv</span> <span>install</span> <span>-e</span> <span>.</span> pre-commit pytest
...
Successfully installed main-0.0.0
pre-commit <span>install </span>pre-commit installed at .git/hooks/pre-commit
<span>$ </span>make dev
python <span>-m</span> pip <span>--require-virtualenv</span> <span>install</span> <span>-e</span> <span>.</span> pre-commit pytest
...
Successfully installed main-0.0.0
pre-commit <span>install </span>pre-commit installed at .git/hooks/pre-commit
$ make dev python -m pip --require-virtualenv install -e . pre-commit pytest ... Successfully installed main-0.0.0 pre-commit install pre-commit installed at .git/hooks/pre-commit

Enter fullscreen mode Exit fullscreen mode

The above example also utilizes editable mode which enable us to make changes and test these changes without re-installing the application / package(s) to our local venv.

Testing Locally

Pre-requisites:

  • Run make dev to install pytest
  • Some unit tests in the tests/ directory at the project root
<span># hello-world/Makefile </span><span>.PHONY</span><span>:</span> <span>test</span>
<span>test</span><span>:</span>
pytest tests/
<span># hello-world/Makefile </span><span>.PHONY</span><span>:</span> <span>test</span>
<span>test</span><span>:</span>
    pytest tests/
# hello-world/Makefile .PHONY: test test: pytest tests/

Enter fullscreen mode Exit fullscreen mode

<span>$ </span>make <span>test </span>pytest tests/
...
test_main.py <span>.</span>
<span>$ </span>make <span>test </span>pytest tests/
...
test_main.py <span>.</span>
$ make test pytest tests/ ... test_main.py .

Enter fullscreen mode Exit fullscreen mode

CI Setup

For larger shared / open source projects you may encounter a continuous integration (CI) pipeline, which can run a number of checks on code before it is merged into the main branch.

There are many pre-requisites for setting up CI on a repository which will not be covered in this article.

Setup

<span>$ </span>make install-ci
python <span>-m</span> pip <span>install </span>mypy ruff tox
...
<span>$ </span>make install-ci
python <span>-m</span> pip <span>install </span>mypy ruff tox
...
$ make install-ci python -m pip install mypy ruff tox ...

Enter fullscreen mode Exit fullscreen mode

Lint and Type Checking

Some examples of rules for lint and type checking:

<span>$ </span>make lint
ruff check <span>.</span> <span>--fix</span>
All checks passed!
<span>$ </span>make lint
ruff check <span>.</span> <span>--fix</span>
All checks passed!
$ make lint ruff check . --fix All checks passed!

Enter fullscreen mode Exit fullscreen mode

<span>$ </span>make <span>type </span>mypy main.py
Success: no issues found <span>in </span>1 <span>source </span>file
<span>$ </span>make <span>type </span>mypy main.py
Success: no issues found <span>in </span>1 <span>source </span>file
$ make type mypy main.py Success: no issues found in 1 source file

Enter fullscreen mode Exit fullscreen mode

Testing Across Multiple Python Versions

Pre-requisites:

  • tox config
  • Available Python versions as specified

We may want to run the tests in ci across a range of Python versions with tox:

<span>$ </span>make test-all
tox run-parallel
<span>$ </span>make test-all
tox run-parallel
$ make test-all tox run-parallel

Enter fullscreen mode Exit fullscreen mode

Further Suggestions

Further suggestions for improving your Makefile:

  • Add an environment variable (i.e. PYTEST_ARGS) to pytest command to allow passing in the specific test file / test
  • Measure tests coverage (pytest-cov)
  • Build documentation (sphinx)
  • Publish the project to PyPI (twine)

Conclusion

By adding a Makefile to your Python project, you can streamline so many aspects of development.

Basic tasks like running the project or tests, linting, and packaging your project and more can be standardized with a simple Makefile. This saves time and ensures consistency across your development workflow, as well as reducing the barrier to entry for developers to contribute to your project.

As you grow and share your Python projects, make can be invaluable to maintain consistent and efficient development process.

The examples in this guide and more are available in the following GitHub repository: https://github.com/jmoudev/python-make-examples

Thanks for reading!

原文链接:Levelling up your Python project with GNU Make

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
If we dream, everything is possible.
敢于梦想,一切都将成为可能
评论 抢沙发

请登录后发表评论

    暂无评论内容