Python Setuptools: quick way to add scripts without "main" function as "console_scripts" entry points
Asked Answered
B

4

7

My request seems unorthodox, but I would like to quickly package an old repository, consisting mostly of python executable scripts.

The problem is that those scripts were not designed as modules, so some of them execute code directly at the module top-level, and some other have the if __name__=='__main__' part.

How would you distribute those scripts using setuptools, without too much rewrite?

  • I know I could just put them under the scripts option of setup(), but it's not advised, and also it doesn't allow me to rename them.
  • I would like to skip defining a main() function in all those scripts, also because some scripts call weird recursive functions with side effects on global variables, so I'm a bit afraid of breaking stuff.
  • When I try providing only the module name as console_scripts (e.g "myscript=mypkg.myscript" instead of "myscript=mypkg.myscript:main"), it logically complains after installation that a module is not callable.

Is there a way to create scripts from modules? At least when they have a if __name__=='__main__'?

Binkley answered 4/1, 2020 at 15:8 Comment(1)
define "package". if you only want something executable, then there is pyinstaller which takes a python script and bundles it with dependencies and python interpreterKenwood
M
1

There's some design considerations, but I would recommend using a __main__.py for this.

  1. it allows all command line invocations to share argument parsing logic
  2. you don't have to touch the scripts, at all
  3. it is explicit (no do-nothing functions that exist to trigger import)
  4. it enables refactoring out any other common stateful logic, since __main__.py is never imported when you import your package
  5. convention. People expect to find command line invocations in __main__.py.

__main__.py

from pathlib import Path
from runpy import run_path

pkg_dir = Path(__file__).resolve().parent

def execute_script():
    script_pth = pkg_dir / "local path to script"
    run_path(str(script_pth), run_name="__main__")

Then, you can set your_package.__main__:execute_script as a console script in setup.py/pyproject.toml. You can obviously have as many scripts as you like this way.

Malady answered 22/1 at 23:12 Comment(0)
B
3

I just realised part of the answer:

in the case where the module executes everything at the top-level, i.e. on import, it's therefore ok to define a dummy "no-op" main function, like so:

# Content of mypkg/myscript.py

print("myscript being executed!")

def main():
    pass  # Do nothing!

This solution will still force me to add this line to the existing scripts, but I think it's a quick but cautious solution.

No solution if the code is under a if __name__=='__main__' though...

Binkley answered 4/1, 2020 at 17:40 Comment(0)
M
1

There's some design considerations, but I would recommend using a __main__.py for this.

  1. it allows all command line invocations to share argument parsing logic
  2. you don't have to touch the scripts, at all
  3. it is explicit (no do-nothing functions that exist to trigger import)
  4. it enables refactoring out any other common stateful logic, since __main__.py is never imported when you import your package
  5. convention. People expect to find command line invocations in __main__.py.

__main__.py

from pathlib import Path
from runpy import run_path

pkg_dir = Path(__file__).resolve().parent

def execute_script():
    script_pth = pkg_dir / "local path to script"
    run_path(str(script_pth), run_name="__main__")

Then, you can set your_package.__main__:execute_script as a console script in setup.py/pyproject.toml. You can obviously have as many scripts as you like this way.

Malady answered 22/1 at 23:12 Comment(0)
K
0

i had the same problem when packaging chromium depot_tools
where i used some bash code
to transform the python scripts to python modules

you cant use scripts because imports would fail
so you must use console_scripts
so you need a main function in every script

replace

if __name__ == '__main__':
    # main body

with

def main():
    # main body

if __name__ == '__main__':
    main()

and add the script as some_script=some_module.some_script:main

note: some main functions require arguments, usually argv

replace

def main(argv):
  options = parse_argv(argv)
  # ...

with

def main_argv(argv):
  options = parse_argv(argv)
  # ...

def main():
  import sys
  return main_argv(sys.argv)

add empty __init__.py files to every folder

replace file-imports with relative imports

-import utils
+from . import utils

-from utils import some_tool
+from .utils import some_tool

module files cannot have kebab-case.py names
you rename (or symlink) them to snake_case.py names

some scripts will try to write tempfiles to their basedir
which can cause [Errno 30] Read-only file system errors
so you will have to patch the tempfile path

all in all, this is a solvable problem
and it would be nice to have a migration tool
similar to the 2to3 tool in python3.11
(${python311}/bin/2to3 -w -n --no-diffs .)
to automate at least the simple transforms

Kenwood answered 1/12, 2023 at 15:50 Comment(0)
R
-2

You can use the following codes.

def main():
    pass # or do something

if __name__ == "__main__":
    main()
Reinhard answered 28/4, 2021 at 9:54 Comment(1)
I mentioned in bullet point 2 that I would like to avoid defining a main function if possible.Binkley

© 2022 - 2024 — McMap. All rights reserved.