The entry point must be a function that may be invoked using exactly zero arguments. If you want to pass in arguments from the command line, say you want to invoke it like:
$ pypy a1 a2
You need to read them from sys.argv instead. So your python module should contain this:
def program(arg1, arg2):
print(arg1, arg2)
def main():
import sys
arg1, arg2 = sys.argv[1], sys.argv[2]
program(arg1, arg2)
if __name__ == "__main__":
main()
Alternatively, the main function may instead take an argv argument that defaults to sys.argv if importing sys is desirable at the top level of the module:
def main(argv=sys.argv):
program(argv[1], argv[2])
Running that command as above should print out a1 a2 into the console. Error handling on user input is your own exercise.
The entry point must be a function that may be invoked using exactly zero arguments. If you want to pass in arguments from the command line, say you want to invoke it like:
$ pypy a1 a2
You need to read them from sys.argv instead. So your python module should contain this:
def program(arg1, arg2):
print(arg1, arg2)
def main():
import sys
arg1, arg2 = sys.argv[1], sys.argv[2]
program(arg1, arg2)
if __name__ == "__main__":
main()
Alternatively, the main function may instead take an argv argument that defaults to sys.argv if importing sys is desirable at the top level of the module:
def main(argv=sys.argv):
program(argv[1], argv[2])
Running that command as above should print out a1 a2 into the console. Error handling on user input is your own exercise.
Similar to the answer of metatoaster, instead of using sys.argv you can use argsparse https://docs.python.org/3/library/argparse.html, which makes the passing of argumnets a bit more user-friendly.
import argparse
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('--arg1', help='arg1 help')
parser.add_argument('--arg2', help='arg2 help')
args = parser.parse_args()
print("arg1 {}, arg2 {}".format(args.arg1, args.arg2)
Call it like:
pypy --arg1 1 --arg2 2
python - How to add an entry_point/console script to setup.py that uses arguments - Stack Overflow
argparse - Preferred way to expand a command line script to be used as a library in Python - Software Engineering Stack Exchange
python - Difference between entry_points/console_scripts and scripts in setup.py? - Stack Overflow
plac does not work with console_scripts in setup.py
If there is not a good reason to not do so, I would definitely advocate a spin on option 3. As @jonsharp mentions, breaking up your utility into clean units of functionality is a good way to ensure testability. Even the smallest scripts can eventually morph into a much larger program and making sure that you have an extensible API sooner rather than later will alleviate much headache down the road.
The way I'd approach this is:
- Break up your code into logical methods with clean and clear I/O
- Add unit tests. Having them is never a bad thing.
- Rather than using
if __name__ == '__main__', create amain()(or similar) method containing your entry point - Use setuptool's
setup()function to define the script entry point in your setup.py file.
For example:
from setuptools import setup
setup(
name='mypackage',
version='0.1',
entry_points={
'console_scripts': [ 'myscript = mypackage.mymodule:main' ],
}
)
Now, not only is all of your code (including main()) is easily unit testable, but you can still have your console entry point once you've done a python setup.py install|develop.
Any validation I do using, say, ArgumentParser.add_mutually_exclusive_group will not apply when my tool is run as a library instead of a command line script.
Depending on how your API is designed, you may need to add some extra validation to input parameters, but that should likely be there to prevent unexpected input anyways.
Edit: The only time I would use generally use subprocess is when I'm calling into a non-Python application or another Python script that I don't own or have the time to refactor, but the latter only being as a last resort. Most well-written Python utilities will expose both command line utilities and internal API.
My guess is that you can pursue #3 by way of #2 and that it won't require anything close to a total refactor. Often in these situations, you just need to make a few adjustments at the entry points and (sometimes) exit points.
If needed, wrap your current script in a
main()function.def main(args, stdin = None): if stdin is None: stdin = sys.stdin # full script here if __name__ == '__main__': main(sys.argv[1:])That function should take a list of strings. When you parse command-line options, operate on
args, notsys.argv.Similarly, adjust other parts of your script to avoid direct operations on
sys.stdinand related streams. Instead, use the arguments passed to yourmain()function.
Programatic users (as opposed to command-line users) will pass in a list of strings and any open file handles (or iterables) they want the code to use. Later, if needed, you can make the programatic use more natural by writing a function that knows how to convert typical positional and keyword-style arguments into the list of strings that your option parser needs.
The docs for the (awesome) Click package suggest a few reasons to use entry points instead of scripts, including
- cross-platform compatibility and
- avoiding having the interpreter assign
__name__to__main__, which could cause code to be imported twice (if another module imports your script)
Click is a nice way to implement functions for use as entry_points, btw.
One key difference between these two ways of creating command line executables is that with the setuptools approach (your first example), you have to call a function inside of the script -- in your case this is the func inside of your module. However, in the distutils approach (your second example) you call the script directly (which allows being listed with or without an extension).
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...
There's some design considerations, but I would recommend using a __main__.py for this.
- it allows all command line invocations to share argument parsing logic
- you don't have to touch the scripts, at all
- it is explicit (no do-nothing functions that exist to trigger import)
- it enables refactoring out any other common stateful logic, since __main__.py is never imported when you import your package
- 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.