How to package a Python project to be run in the console

I wrote a project in Python which uses multiple scripts (modules). The main script calls on the other modules and reads some files from a folder in order to perform its function.

I am trying to package it for distribution. I wish to make it a runable program from the command line, meaning, once the user downloads it and installs it, they can call the program like this:

$ python my_program -i arg1 -o arg2

or similar.

The tutorials I find online wrap the project in the form of a package that you can import.

>> import my_program
>> my_program.run_stuff(arg1, arg2)

That's not what I want.

Upvotes: 1

Views: 2276

Answers (1)

john-hen
john-hen

Reputation: 4856

Since you wrote "or similar", let's say you want to call the program like so:

$ my_program -i arg1 -o arg2

This is even shorter. It's also how we call ubiquitous Python tools like pip. And there is an established procedure to define an "entry point" (as it's known) for any Python package.

All Python packaging tools allow that. We could use the classic Setuptools if we wanted to — that's what Pip does. Or use Poetry, a more modern alternative. But it's often easiest to set up with Flit.

In the simplest case, your package my_program only contains an __init__.py file that defines a function:

def main():
    print('Running my program...')

That function would typically act on the command-line arguments in sys.argv. The function doesn't have to be called main, it could be any name, and it could also be in any other module of the package.

We can then define the entry point for the console script in the project's meta data. Flit reads that from a configuration file named pyproject.toml in the root folder. So the repository now looks like this:

.
├── my_program
│   └── __init__.py
└── pyproject.toml

Using the most recent standard for the meta data, PEP 621, pyproject.toml would contain:

[project]
name = 'my_program'
version = '1.0.0'
description = 'Can be run in the console from anywhere.'

[project.scripts]
my_program = 'my_program:main'

[build-system]
requires = ['flit_core>=3.2,<4']
build-backend = 'flit_core.buildapi'

In the [project.scripts] section we have mapped the console command my_program to the main function in the top-level name space of the my_program package. Again, it could be any other function elsewhere in the package too.

Now we package the project:

$ flit build --format wheel
Copying package file(s) from my_program              I-flit_core.wheel
Writing metadata files                               I-flit_core.wheel
Writing the record of files                          I-flit_core.wheel
Built wheel: dist\my_program-1.0.0-py2.py3-none-any.whl  I-flit_core.wheel

This puts the packaged "wheel" in a folder named dist. We could upload that .whl file to PyPI for distribution, or install it with Pip right away:

$ pip install dist/my_program-1.0.0-py2.py3-none-any.whl
Processing .\dist\my_program-1.0.0-py2.py3-none-any.whl
Installing collected packages: my-program
Successfully installed my-program-1.0.0

Now we can run the program like any other console application:

$ my_program
Running my program...

What Pip did there for us is, it created a small launcher for our package right next to its very own launcher. Just like it did for Flit too. On Windows, for example, there is now a my_program.exe inside Python's Scripts folder, right next to pip.exe and flit.exe.

Upvotes: 6

Related Questions