You have a script that you run as python pricing/cli.py Ada. You want
users to type pricing Ada after they install your package, and you want
tests to call the code without starting a new process. Both work when the
script's work lives in a main function. Put this in pricing/cli.py:
import argparse
from collections.abc import Sequence
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="pricing")
parser.add_argument("name")
args = parser.parse_args(argv)
print(f"Hello, {args.name}!")
return 0
A test calls main(["Ada"]). A call with no arguments, main(), makes
argparse read the command line instead. The installed command calls
main() with no arguments, which is why argv defaults to None.
Publish the function as a command
Add this table to pyproject.toml:
[project.scripts]
pricing = "pricing.cli:main"
Each line is an entry point, a name in the package's metadata that
points at a function. The part before the colon is the module, and the part
after it is the function. When pip installs the package, it writes a small
pricing program into the environment's scripts folder, bin on Linux and
macOS or Scripts on Windows. That program imports pricing.cli and calls
main(). The folder must be on your shell's PATH. Activating the virtual
environment puts it there.
To support python -m pricing Ada too, add pricing/__main__.py:
from pricing.cli import main
if __name__ == "__main__":
raise SystemExit(main())
python -m pricing runs this file. The guard matters because other code
can import the file as pricing.__main__, and the import alone should not
start the tool. See Python's
__main__ documentation.
Return an exit status
The installed command runs the equivalent of sys.exit(main()). A return
value of 0 or None means success. Return a small nonzero number, such as
1 or 2, for failure.
Do not return a string. sys.exit prints a string to standard error and
exits with status 1, so a return "done" turns a success into a failure.
The -> int annotation lets mypy catch that mistake. See
sys.exit.
List plugins without loading them
An entry point belongs to a group. [project.scripts] fills the
console_scripts group, and you can define your own group for plugins:
[project.entry-points."pricing.backends"]
sqlite = "pricing.adapters.sqlite:SqliteBackend"
Any installed package can add entries to pricing.backends. Your
application reads them with importlib.metadata. This snippet lists the
commands installed in your current environment:
from importlib.metadata import entry_points
for ep in entry_points(group="console_scripts"):
print(ep.name, "->", ep.value)
Listing reads only the metadata. ep.load() imports the module, which runs
its top-level code, and then returns the object.
Check your understanding Why list a group's plugins first and load only the one you need? Show answer Hide answer
Loading a plugin imports its module, which runs its top-level code and imports its dependencies. If the user picks one backend, loading all of them wastes startup time and can fail on a dependency that only an unused plugin needs. Two packages can also register the same name, so decide what happens then instead of letting the second entry silently replace the first.
Find a missing command
Check your understanding
python -m pricing works, but the shell cannot find pricing. What should you check?
Show answer
Hide answer
Check that pyproject.toml declares the script, and that the package is
installed in the environment you think it is. Look for the pricing program
in that environment's scripts folder, then check that the folder is on your
PATH. Also check that python and pip belong to the same environment,
with python -m pip --version.
The module path from python -c "import pricing; print(pricing.__file__)"
shows which code runs, but not whether the package is installed. A checkout
can be importable without an install, and an editable install points back
at the checkout on purpose.
Going further
The entry points specification
also defines gui_scripts. On Windows, a GUI script starts without a
console window, so it has no standard streams unless the program sets them
up. On other platforms, gui_scripts and console_scripts behave the same.