pythonpython-venv

How to let myprogram.py use venv without setting any envs beforehand?


I'm a newbie to python(3), but not to programming in general.

I'd like to distribute a git repo with myprogram consisting of these files:

requirements.txt
myprogram.py
lib/modulea.py
lib/moduleb.py

My question is: What is the best-practice and least surprising way to let users run myprogram.py using the dependencies in requirements.txt? So that after git clone, and some idiomatic installation command(s), ./myprogram.py or /some/path/to/myprogram.py "just works" without having to first set magical venv or python3 environment variables?

I want to be able to run it using the #! shebang so that /path/to/myprogram.py and double-clicking it from the file manager GUI does the correct thing.

I already know I can create a wrapper.sh or make a clever shebang line. But I'm looking for the best-practice approach, since I'm new to python.

More details

I'm guessing that users would

git clone $url workdir
cd workdir
python3 -m venv .
./bin/pip install -r requirements.txt

And from now on this uses the modules from requirements.txt:

./myprogram.py

If I knew that the project directory was always /home/peter/workdir, I could start the myprogram.py with:

#!/home/peter/workdir/bin/python3

but I'd like to avoid hard-coding the project directory in myprogram.py.

This also seems to work in my tiny demo, but clearly this is brittle and not best-practice, but it illustrates what I'm trying to do:

#!/usr/bin/env python3
import os
import sys
print(os.path.join(os.path.dirname(__file__), 'lib', 'python3.10', 'site-packages'))

I'm sure I could come up with some home-grown shebang line that works, but what is the idiomatic way to do this in python3?

Again: After pip install, I absolutely refuse to have to to set any environment variables or call any setup code in future shells before running myprogram.py. (Unless that strongly conflicts with "idiomatic", which I hope isn't the case)...


Solution

  • Expanding @sinoroc's comment into an answer:

    I've looked at https://packaging.python.org/en/latest/tutorials/packaging-projects/ and also at "entrypoints", and this is the smallest example I can think of. Create an empty directory with these two files:

    pyproject.toml:

    [build-system]
    requires = ["hatchling"]
    build-backend = "hatchling.build"
    
    [project]
    name = "example_module_pmorch"
    version = "0.0.1"
    # Just a bogus dependency for illustration
    dependencies = [
      "tqdm"
    ]
    
    [project.scripts]
    runme = "example_module_pmorch:cli_main"
    

    src/example_module_pmorch/__init__.py:

    def cli_main():
        print("I'm the entrypoint")
    

    Now if I run this:

    $ python3 -m venv .
    
    # Adding -e during development is optional 
    $ ./bin/pip install .
    

    Then ./bin/runme does the right thing and prints I'm the entrypoint.

    And during development:

    PYTHONPATH=./src:$PYTHONPATH \
      ./bin/python -c 'import similar_images similar_images.cli_main()'