Skip to content

Commit 7b0a451

Browse files
authored
Merge branch 'main' into patch-1
2 parents 8891fa5 + caa2007 commit 7b0a451

12 files changed

+282
-60
lines changed

source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
# Ignore while StackOverflow is blocking GitHub CI. Ref:
133133
# https://github.com/pypa/packaging.python.org/pull/1474
134134
"https://stackoverflow.com/*",
135+
"https://pyscaffold.org/*",
135136
]
136137
linkcheck_retries = 5
137138
# Ignore anchors for links to GitHub project pages -- GitHub adds anchors from

source/discussions/src-layout-vs-flat-layout.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,27 @@ layout and the flat layout:
7979
``tox.ini``) and packaging/tooling configuration files (eg: ``setup.py``,
8080
``noxfile.py``) on the import path. This would make certain imports work
8181
in editable installations but not regular installations.
82+
83+
.. _running-cli-from-source-src-layout:
84+
85+
Running a command-line interface from source with src-layout
86+
============================================================
87+
88+
Due to the firstly mentioned specialty of the src layout, a command-line
89+
interface can not be run directly from the :term:`source tree <Project Source Tree>`,
90+
but requires installation of the package in
91+
:doc:`Development Mode <setuptools:userguide/development_mode>`
92+
for testing purposes. Since this can be unpractical in some situations,
93+
a workaround could be to prepend the package folder to Python's
94+
:py:data:`sys.path` when called via its :file:`__main__.py` file:
95+
96+
.. code-block:: python
97+
98+
import os
99+
import sys
100+
101+
if not __package__:
102+
# Make CLI runnable from source tree with
103+
# python src/package
104+
package_source_path = os.path.dirname(os.path.dirname(__file__))
105+
sys.path.insert(0, package_source_path)

source/guides/analyzing-pypi-package-downloads.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _analyzing-pypi-package-downloads:
2+
13
================================
24
Analyzing PyPI package downloads
35
================================
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
.. _creating-command-line-tools:
2+
3+
=========================================
4+
Creating and packaging command-line tools
5+
=========================================
6+
7+
This guide will walk you through creating and packaging a standalone command-line application
8+
that can be installed with :ref:`pipx`, a tool creating and managing :term:`Python Virtual Environments <Virtual Environment>`
9+
and exposing the executable scripts of packages (and available manual pages) for use on the command-line.
10+
11+
Creating the package
12+
====================
13+
14+
First of all, create a source tree for the :term:`project <Project>`. For the sake of an example, we'll
15+
build a simple tool outputting a greeting (a string) for a person based on arguments given on the command-line.
16+
17+
.. todo:: Advise on the optimal structure of a Python package in another guide or discussion and link to it here.
18+
19+
This project will adhere to :ref:`src-layout <src-layout-vs-flat-layout>` and in the end be alike this file tree,
20+
with the top-level folder and package name ``greetings``:
21+
22+
::
23+
24+
.
25+
├── pyproject.toml
26+
└── src
27+
└── greetings
28+
├── cli.py
29+
├── greet.py
30+
├── __init__.py
31+
└── __main__.py
32+
33+
The actual code responsible for the tool's functionality will be stored in the file :file:`greet.py`,
34+
named after the main module:
35+
36+
.. code-block:: python
37+
38+
import typer
39+
from typing_extensions import Annotated
40+
41+
42+
def greet(
43+
name: Annotated[str, typer.Argument(help="The (last, if --gender is given) name of the person to greet")] = "",
44+
gender: Annotated[str, typer.Option(help="The gender of the person to greet")] = "",
45+
knight: Annotated[bool, typer.Option(help="Whether the person is a knight")] = False,
46+
count: Annotated[int, typer.Option(help="Number of times to greet the person")] = 1
47+
):
48+
greeting = "Greetings, dear "
49+
masculine = gender == "masculine"
50+
feminine = gender == "feminine"
51+
if gender or knight:
52+
salutation = ""
53+
if knight:
54+
salutation = "Sir "
55+
elif masculine:
56+
salutation = "Mr. "
57+
elif feminine:
58+
salutation = "Ms. "
59+
greeting += salutation
60+
if name:
61+
greeting += f"{name}!"
62+
else:
63+
pronoun = "her" if feminine else "his" if masculine or knight else "its"
64+
greeting += f"what's-{pronoun}-name"
65+
else:
66+
if name:
67+
greeting += f"{name}!"
68+
elif not gender:
69+
greeting += "friend!"
70+
for i in range(0, count):
71+
print(greeting)
72+
73+
The above function receives several keyword arguments that determine how the greeting to output is constructed.
74+
Now, construct the command-line interface to provision it with the same, which is done
75+
in :file:`cli.py`:
76+
77+
.. code-block:: python
78+
79+
import typer
80+
81+
from .hello import greet
82+
83+
84+
app = typer.Typer()
85+
app.command()(greet)
86+
87+
88+
if __name__ == "__main__":
89+
app()
90+
91+
The command-line interface is built with typer_, an easy-to-use CLI parser based on Python type hints. It provides
92+
auto-completion and nicely styled command-line help out of the box. Another option would be :py:mod:`argparse`,
93+
a command-line parser which is included in Python's standard library. It is sufficient for most needs, but requires
94+
a lot of code, usually in ``cli.py``, to function properly. Alternatively, docopt_ makes it possible to create CLI
95+
interfaces based solely on docstrings; advanced users are encouraged to make use of click_ (on which ``typer`` is based).
96+
97+
Now, add an empty :file:`__init__.py` file, to define the project as a regular :term:`import package <Import Package>`.
98+
99+
The file :file:`__main__.py` marks the main entry point for the application when running it via :mod:`runpy`
100+
(i.e. ``python -m greetings``, which works immediately with flat layout, but requires installation of the package with src layout),
101+
so initizalize the command-line interface here:
102+
103+
.. code-block:: python
104+
105+
if __name__ == "__main__":
106+
from greetings.cli import app
107+
app()
108+
109+
.. note::
110+
111+
In order to enable calling the command-line interface directly from the :term:`source tree <Project Source Tree>`,
112+
i.e. as ``python src/greetings``, a certain hack could be placed in this file; read more at
113+
:ref:`running-cli-from-source-src-layout`.
114+
115+
116+
``pyproject.toml``
117+
------------------
118+
119+
The project's :term:`metadata <Pyproject Metadata>` is placed in :term:`pyproject.toml`. The :term:`pyproject metadata keys <Pyproject Metadata Key>` and the ``[build-system]`` table may be filled in as described in :ref:`writing-pyproject-toml`, adding a dependency
120+
on ``typer`` (this tutorial uses version *0.12.3*).
121+
122+
For the project to be recognised as a command-line tool, additionally a ``console_scripts`` :ref:`entry point <entry-points>` (see :ref:`console_scripts`) needs to be added as a :term:`subkey <Pyproject Metadata Subkey>`:
123+
124+
.. code-block:: toml
125+
126+
[project.scripts]
127+
greet = "greetings.cli:app"
128+
129+
Now, the project's source tree is ready to be transformed into a :term:`distribution package <Distribution Package>`,
130+
which makes it installable.
131+
132+
133+
Installing the package with ``pipx``
134+
====================================
135+
136+
After installing ``pipx`` as described in :ref:`installing-stand-alone-command-line-tools`, install your project:
137+
138+
.. code-block:: console
139+
140+
$ cd path/to/greetings/
141+
$ pipx install .
142+
143+
This will expose the executable script we defined as an entry point and make the command ``greet`` available.
144+
Let's test it:
145+
146+
.. code-block:: console
147+
148+
$ greet --knight Lancelot
149+
Greetings, dear Sir Lancelot!
150+
$ greet --gender feminine Parks
151+
Greetings, dear Ms. Parks!
152+
$ greet --gender masculine
153+
Greetings, dear Mr. what's-his-name!
154+
155+
Since this example uses ``typer``, you could now also get an overview of the program's usage by calling it with
156+
the ``--help`` option, or configure completions via the ``--install-completion`` option.
157+
158+
To just run the program without installing it permanently, use ``pipx run``, which will create a temporary
159+
(but cached) virtual environment for it:
160+
161+
.. code-block:: console
162+
163+
$ pipx run --spec . greet --knight
164+
165+
This syntax is a bit unpractical, however; as the name of the entry point we defined above does not match the package name,
166+
we need to state explicitly which executable script to run (even though there is only on in existence).
167+
168+
There is, however, a more practical solution to this problem, in the form of an entry point specific to ``pipx run``.
169+
The same can be defined as follows in :file:`pyproject.toml`:
170+
171+
.. code-block:: toml
172+
173+
[project.entry-points."pipx.run"]
174+
greetings = "greetings.cli:app"
175+
176+
177+
Thanks to this entry point (which *must* match the package name), ``pipx`` will pick up the executable script as the
178+
default one and run it, which makes this command possible:
179+
180+
.. code-block:: console
181+
182+
$ pipx run . --knight
183+
184+
Conclusion
185+
==========
186+
187+
You know by now how to package a command-line application written in Python. A further step could be to distribute you package,
188+
meaning uploading it to a :term:`package index <Package Index>`, most commonly :term:`PyPI <Python Package Index (PyPI)>`. To do that, follow the instructions at :ref:`Packaging your project`. And once you're done, don't forget to :ref:`do some research <analyzing-pypi-package-downloads>` on how your package is received!
189+
190+
.. _click: https://click.palletsprojects.com/
191+
.. _docopt: https://docopt.readthedocs.io/en/latest/
192+
.. _typer: https://typer.tiangolo.com/

0 commit comments

Comments
 (0)