diff --git a/source/conf.py b/source/conf.py index cd459ffb2..c777550ce 100644 --- a/source/conf.py +++ b/source/conf.py @@ -132,6 +132,7 @@ # Ignore while StackOverflow is blocking GitHub CI. Ref: # https://github.com/pypa/packaging.python.org/pull/1474 "https://stackoverflow.com/*", + "https://pyscaffold.org/*", ] linkcheck_retries = 5 # Ignore anchors for links to GitHub project pages -- GitHub adds anchors from diff --git a/source/discussions/src-layout-vs-flat-layout.rst b/source/discussions/src-layout-vs-flat-layout.rst index bfa405729..c38968345 100644 --- a/source/discussions/src-layout-vs-flat-layout.rst +++ b/source/discussions/src-layout-vs-flat-layout.rst @@ -79,3 +79,27 @@ layout and the flat layout: ``tox.ini``) and packaging/tooling configuration files (eg: ``setup.py``, ``noxfile.py``) on the import path. This would make certain imports work in editable installations but not regular installations. + +.. _running-cli-from-source-src-layout: + +Running a command-line interface from source with src-layout +============================================================ + +Due to the firstly mentioned specialty of the src layout, a command-line +interface can not be run directly from the :term:`source tree `, +but requires installation of the package in +:doc:`Development Mode ` +for testing purposes. Since this can be unpractical in some situations, +a workaround could be to prepend the package folder to Python's +:py:data:`sys.path` when called via its :file:`__main__.py` file: + +.. code-block:: python + + import os + import sys + + if not __package__: + # Make CLI runnable from source tree with + # python src/package + package_source_path = os.path.dirname(os.path.dirname(__file__)) + sys.path.insert(0, package_source_path) diff --git a/source/guides/analyzing-pypi-package-downloads.rst b/source/guides/analyzing-pypi-package-downloads.rst index 62efea7ab..5fecc99c1 100644 --- a/source/guides/analyzing-pypi-package-downloads.rst +++ b/source/guides/analyzing-pypi-package-downloads.rst @@ -1,3 +1,5 @@ +.. _analyzing-pypi-package-downloads: + ================================ Analyzing PyPI package downloads ================================ diff --git a/source/guides/creating-command-line-tools.rst b/source/guides/creating-command-line-tools.rst new file mode 100644 index 000000000..9f040ce7d --- /dev/null +++ b/source/guides/creating-command-line-tools.rst @@ -0,0 +1,192 @@ +.. _creating-command-line-tools: + +========================================= +Creating and packaging command-line tools +========================================= + +This guide will walk you through creating and packaging a standalone command-line application +that can be installed with :ref:`pipx`, a tool creating and managing :term:`Python Virtual Environments ` +and exposing the executable scripts of packages (and available manual pages) for use on the command-line. + +Creating the package +==================== + +First of all, create a source tree for the :term:`project `. For the sake of an example, we'll +build a simple tool outputting a greeting (a string) for a person based on arguments given on the command-line. + +.. todo:: Advise on the optimal structure of a Python package in another guide or discussion and link to it here. + +This project will adhere to :ref:`src-layout ` and in the end be alike this file tree, +with the top-level folder and package name ``greetings``: + +:: + + . + ├── pyproject.toml + └── src + └── greetings + ├── cli.py + ├── greet.py + ├── __init__.py + └── __main__.py + +The actual code responsible for the tool's functionality will be stored in the file :file:`greet.py`, +named after the main module: + +.. code-block:: python + + import typer + from typing_extensions import Annotated + + + def greet( + name: Annotated[str, typer.Argument(help="The (last, if --gender is given) name of the person to greet")] = "", + gender: Annotated[str, typer.Option(help="The gender of the person to greet")] = "", + knight: Annotated[bool, typer.Option(help="Whether the person is a knight")] = False, + count: Annotated[int, typer.Option(help="Number of times to greet the person")] = 1 + ): + greeting = "Greetings, dear " + masculine = gender == "masculine" + feminine = gender == "feminine" + if gender or knight: + salutation = "" + if knight: + salutation = "Sir " + elif masculine: + salutation = "Mr. " + elif feminine: + salutation = "Ms. " + greeting += salutation + if name: + greeting += f"{name}!" + else: + pronoun = "her" if feminine else "his" if masculine or knight else "its" + greeting += f"what's-{pronoun}-name" + else: + if name: + greeting += f"{name}!" + elif not gender: + greeting += "friend!" + for i in range(0, count): + print(greeting) + +The above function receives several keyword arguments that determine how the greeting to output is constructed. +Now, construct the command-line interface to provision it with the same, which is done +in :file:`cli.py`: + +.. code-block:: python + + import typer + + from .hello import greet + + + app = typer.Typer() + app.command()(greet) + + + if __name__ == "__main__": + app() + +The command-line interface is built with typer_, an easy-to-use CLI parser based on Python type hints. It provides +auto-completion and nicely styled command-line help out of the box. Another option would be :py:mod:`argparse`, +a command-line parser which is included in Python's standard library. It is sufficient for most needs, but requires +a lot of code, usually in ``cli.py``, to function properly. Alternatively, docopt_ makes it possible to create CLI +interfaces based solely on docstrings; advanced users are encouraged to make use of click_ (on which ``typer`` is based). + +Now, add an empty :file:`__init__.py` file, to define the project as a regular :term:`import package `. + +The file :file:`__main__.py` marks the main entry point for the application when running it via :mod:`runpy` +(i.e. ``python -m greetings``, which works immediately with flat layout, but requires installation of the package with src layout), +so initizalize the command-line interface here: + +.. code-block:: python + + if __name__ == "__main__": + from greetings.cli import app + app() + +.. note:: + + In order to enable calling the command-line interface directly from the :term:`source tree `, + i.e. as ``python src/greetings``, a certain hack could be placed in this file; read more at + :ref:`running-cli-from-source-src-layout`. + + +``pyproject.toml`` +------------------ + +The project's :term:`metadata ` is placed in :term:`pyproject.toml`. The :term:`pyproject metadata keys ` and the ``[build-system]`` table may be filled in as described in :ref:`writing-pyproject-toml`, adding a dependency +on ``typer`` (this tutorial uses version *0.12.3*). + +For the project to be recognised as a command-line tool, additionally a ``console_scripts`` :ref:`entry point ` (see :ref:`console_scripts`) needs to be added as a :term:`subkey `: + +.. code-block:: toml + + [project.scripts] + greet = "greetings.cli:app" + +Now, the project's source tree is ready to be transformed into a :term:`distribution package `, +which makes it installable. + + +Installing the package with ``pipx`` +==================================== + +After installing ``pipx`` as described in :ref:`installing-stand-alone-command-line-tools`, install your project: + +.. code-block:: console + + $ cd path/to/greetings/ + $ pipx install . + +This will expose the executable script we defined as an entry point and make the command ``greet`` available. +Let's test it: + +.. code-block:: console + + $ greet --knight Lancelot + Greetings, dear Sir Lancelot! + $ greet --gender feminine Parks + Greetings, dear Ms. Parks! + $ greet --gender masculine + Greetings, dear Mr. what's-his-name! + +Since this example uses ``typer``, you could now also get an overview of the program's usage by calling it with +the ``--help`` option, or configure completions via the ``--install-completion`` option. + +To just run the program without installing it permanently, use ``pipx run``, which will create a temporary +(but cached) virtual environment for it: + +.. code-block:: console + + $ pipx run --spec . greet --knight + +This syntax is a bit unpractical, however; as the name of the entry point we defined above does not match the package name, +we need to state explicitly which executable script to run (even though there is only on in existence). + +There is, however, a more practical solution to this problem, in the form of an entry point specific to ``pipx run``. +The same can be defined as follows in :file:`pyproject.toml`: + +.. code-block:: toml + + [project.entry-points."pipx.run"] + greetings = "greetings.cli:app" + + +Thanks to this entry point (which *must* match the package name), ``pipx`` will pick up the executable script as the +default one and run it, which makes this command possible: + +.. code-block:: console + + $ pipx run . --knight + +Conclusion +========== + +You know by now how to package a command-line application written in Python. A further step could be to distribute you package, +meaning uploading it to a :term:`package index `, most commonly :term:`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 ` on how your package is received! + +.. _click: https://click.palletsprojects.com/ +.. _docopt: https://docopt.readthedocs.io/en/latest/ +.. _typer: https://typer.tiangolo.com/ diff --git a/source/guides/dropping-older-python-versions.rst b/source/guides/dropping-older-python-versions.rst index c0c2b4434..267d7b923 100644 --- a/source/guides/dropping-older-python-versions.rst +++ b/source/guides/dropping-older-python-versions.rst @@ -4,34 +4,27 @@ Dropping support for older Python versions ========================================== -Dropping support for older Python versions is supported by the standard :ref:`core-metadata` 1.2 specification via a "Requires-Python" attribute. +The ability to drop support for older Python versions is enabled by the standard :ref:`core-metadata` 1.2 specification via the :ref:`"Requires-Python" ` attribute. -Metadata 1.2+ clients, such as Pip 9.0+, will adhere to this specification by matching the current Python runtime and comparing it with the required version +Metadata 1.2+ installers, such as Pip, will adhere to this specification by matching the current Python runtime and comparing it with the required version in the package metadata. If they do not match, it will attempt to install the last package distribution that supported that Python runtime. -This mechanism can be used to drop support for older Python versions, by amending the "Requires-Python" attribute in the package metadata. - -This guide is specifically for users of :ref:`setuptools`, other packaging tools such as ``flit`` may offer similar functionality but users will need to consult relevant documentation. +This mechanism can be used to drop support for older Python versions, by amending the ``Requires-Python`` attribute in the package metadata. Requirements ------------ -This workflow requires that: - -1. The publisher is using the latest version of :ref:`setuptools`, -2. The latest version of :ref:`twine` is used to upload the package, -3. The user installing the package has at least Pip 9.0, or a client that supports the Metadata 1.2 specification. +This workflow requires that the user installing the package uses Pip [#]_, or another installer that supports the Metadata 1.2 specification. Dealing with the universal wheels --------------------------------- -Traditionally, projects providing Python code that is semantically +Traditionally, :ref:`setuptools` projects providing Python code that is semantically compatible with both Python 2 and Python 3, produce :term:`wheels ` that have a ``py2.py3`` tag in their names. When dropping support for Python 2, it is important not to forget to change this tag to just ``py3``. It is often configured within :file:`setup.cfg` under -the ``[bdist_wheel]`` section by setting ``universal = 1`` if they -use setuptools. +the ``[bdist_wheel]`` section by setting ``universal = 1``. If you use this method, either remove this option or section, or explicitly set ``universal`` to ``0``: @@ -43,69 +36,69 @@ explicitly set ``universal`` to ``0``: [bdist_wheel] universal = 0 # Make the generated wheels have "py3" tag -.. tip:: +.. hint:: - Since it is possible to override the :file:`setup.cfg` settings via - CLI flags, make sure that your scripts don't have ``--universal`` in - your package creation scripts. + Regarding :ref:`deprecated ` direct ``setup.py`` invocations, + passing the ``--universal`` flag on the command line could override this setting. Defining the Python version required ------------------------------------ -1. Download the newest version of Setuptools -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Ensure that before you generate source distributions or binary distributions, you update Setuptools and install twine. +1. Install twine +~~~~~~~~~~~~~~~~ +Ensure that you have twine available at its latest version. Steps: .. tab:: Unix/macOS .. code-block:: bash - python3 -m pip install --upgrade setuptools twine + python3 -m pip install --upgrade twine .. tab:: Windows .. code-block:: bat - py -m pip install --upgrade setuptools twine - -``setuptools`` version should be above 24.0.0. + py -m pip install --upgrade twine 2. Specify the version ranges for supported Python distributions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can specify version ranges and exclusion rules, such as at least Python 3. Or, Python 2.7, 3.4 and beyond. +Set the version ranges declaring which Python distributions are supported +within your project's :file:`pyproject.toml`. The :ref:`requires-python` configuration field +corresponds to the :ref:`Requires-Python ` core metadata field: -Examples: +.. code-block:: toml -.. code-block:: text + [build-system] + ... - Requires-Python: ">=3" - Requires-Python: ">2.7,!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [project] + requires-python = ">= 3.8" # At least Python 3.8 -The way to set those values is within the call to ``setup`` within your -:file:`setup.py` script. This will insert the ``Requires-Python`` -metadata values based on the argument you provide in ``python_requires``. +You can specify version ranges and exclusion rules (complying with the :ref:`version-specifiers` specification), +such as at least Python 3.9. Or, at least Python 3.7 and beyond, skipping the 3.7.0 and 3.7.1 point releases: -.. code-block:: python +.. code-block:: toml - from setuptools import setup + requires-python = ">= 3.9" + requires-python = ">= 3.7, != 3.7.0, != 3.7.1" - setup( - # Your setup arguments - python_requires='>=2.7', # Your supported Python ranges - ) +If using the :ref:`setuptools` build backend, consult the `dependency-management`_ documentation for more options. + +.. caution:: + Avoid adding upper bounds to the version ranges, e. g. ``">= 3.8, < 3.10"``. Doing so can cause different errors + and version conflicts. See the `discourse-discussion`_ for more information. 3. Validating the Metadata before publishing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Within a Python source package (the zip or the tar-gz file you download) is a text file called PKG-INFO. -This file is generated by :ref:`distutils` or :ref:`setuptools` when it generates the source package. -The file contains a set of keys and values, the list of keys is part of the PyPa standard metadata format. +This file is generated by the :term:`build backend ` when it generates the source package. +The file contains a set of keys and values, the list of keys is part of the PyPA standard metadata format. You can see the contents of the generated file like this: @@ -115,24 +108,31 @@ You can see the contents of the generated file like this: Validate that the following is in place, before publishing the package: -- If you have upgraded correctly, the Metadata-Version value should be 1.2 or higher. -- The Requires-Python field is set and matches your specification in setup.py. +- If you have upgraded correctly, the ``Metadata-Version`` value should be 1.2 or higher. +- The ``Requires-Python`` field is set and matches your specification in the configuration file. -4. Using Twine to publish +4. Publishing the package ~~~~~~~~~~~~~~~~~~~~~~~~~ -Twine has a number of advantages, apart from being faster it is now the supported method for publishing packages. - -Make sure you are using the newest version of Twine, at least 1.9. +Proceed as suggested in :ref:`Uploading your Project to PyPI`. -Dropping a Python release +Dropping a Python version ------------------------- -Once you have published a package with the Requires-Python metadata, you can then make a further update removing that Python runtime from support. +In principle, at least metadata support for Python versions should be kept as long as possible, because +once that has been dropped, people still depending on a version will be forced to downgrade. +If however supporting a specific version becomes a blocker for a new feature or other issues occur, the metadata +``Requires-Python`` should be amended. Of course this also depends on whether the project needs to be stable and +well-covered for a wider range of users. + +Each version compatibility change should have its own release. + +.. tip:: -It must be done in this order for the automated fallback to work. + When dropping a Python version, it might also be rewarding to upgrade the project's code syntax generally, apart from updating the versions used in visible places (like the testing environment). Tools like pyupgrade_ or `ruff `_ can automate some of this work. -For example, you published the Requires-Python: ">=2.7" as version 1.0.0 of your package. +.. _discourse-discussion: https://discuss.python.org/t/requires-python-upper-limits/12663 +.. _pyupgrade: https://pypi.org/project/pyupgrade/ +.. _dependency-management: https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#python-requirement -If you were then to update the version string to ">=3.5", and publish a new version 2.0.0 of your package, any users running Pip 9.0+ from version 2.7 will -have version 1.0.0 of the package installed, and any >=3.5 users will receive version 2.0.0. +.. [#] Support for the Metadata 1.2 specification has been added in Pip 9.0. diff --git a/source/guides/installing-stand-alone-command-line-tools.rst b/source/guides/installing-stand-alone-command-line-tools.rst index 8578a3b28..c078fd1e4 100644 --- a/source/guides/installing-stand-alone-command-line-tools.rst +++ b/source/guides/installing-stand-alone-command-line-tools.rst @@ -1,3 +1,5 @@ +.. _installing-stand-alone-command-line-tools: + Installing stand alone command line tools ========================================= diff --git a/source/guides/installing-using-linux-tools.rst b/source/guides/installing-using-linux-tools.rst index f0914f8dc..56647f3e9 100644 --- a/source/guides/installing-using-linux-tools.rst +++ b/source/guides/installing-using-linux-tools.rst @@ -51,7 +51,7 @@ To install pip and wheel for the system Python, there are two options: 1. Enable the `EPEL repository `_ using `these instructions - `__. + `__. On EPEL 7, you can install pip and wheel like so: .. code-block:: bash diff --git a/source/guides/section-build-and-publish.rst b/source/guides/section-build-and-publish.rst index 2af29d763..8e0c9ab3b 100644 --- a/source/guides/section-build-and-publish.rst +++ b/source/guides/section-build-and-publish.rst @@ -11,6 +11,7 @@ Building and Publishing dropping-older-python-versions packaging-binary-extensions packaging-namespace-packages + creating-command-line-tools creating-and-discovering-plugins using-testpypi making-a-pypi-friendly-readme diff --git a/source/specifications/direct-url-data-structure.rst b/source/specifications/direct-url-data-structure.rst index 231198ee8..6a4e8fe01 100644 --- a/source/specifications/direct-url-data-structure.rst +++ b/source/specifications/direct-url-data-structure.rst @@ -31,6 +31,9 @@ Depending on what ``url`` refers to, the second field MUST be one of ``vcs_info` local directory). These info fields have a (possibly empty) subdictionary as value, with the possible keys defined below. +Security Considerations +----------------------- + When persisted, ``url`` MUST be stripped of any sensitive authentication information, for security reasons. @@ -44,7 +47,7 @@ expression: Additionally, the user:password section of the URL MAY be a well-known, non security sensitive string. A typical example is ``git`` -in the case of an URL such as ``ssh://git@gitlab.com/user/repo``. +in the case of a URL such as ``ssh://git@gitlab.com/user/repo``. VCS URLs -------- diff --git a/source/specifications/externally-managed-environments.rst b/source/specifications/externally-managed-environments.rst index 2944eb3da..65fc14a62 100644 --- a/source/specifications/externally-managed-environments.rst +++ b/source/specifications/externally-managed-environments.rst @@ -205,7 +205,7 @@ virtual environment to install packages. Software distributors who have a non-Python-specific package manager that manages libraries in the ``sys.path`` of their Python package -should, in general, ship a ``EXTERNALLY-MANAGED`` file in their +should, in general, ship an ``EXTERNALLY-MANAGED`` file in their standard library directory. For instance, Debian may ship a file in ``/usr/lib/python3.9/EXTERNALLY-MANAGED`` consisting of something like diff --git a/source/specifications/simple-repository-api.rst b/source/specifications/simple-repository-api.rst index b9f87ce7b..0d65a58aa 100644 --- a/source/specifications/simple-repository-api.rst +++ b/source/specifications/simple-repository-api.rst @@ -360,7 +360,7 @@ spec: * All JSON responses will *always* be a JSON object rather than an array or other type. -* While JSON doesn't natively support an URL type, any value that represents an +* While JSON doesn't natively support a URL type, any value that represents an URL in this API may be either absolute or relative as long as they point to the correct location. If relative, they are relative to the current URL as if it were HTML. @@ -616,7 +616,7 @@ likely just be treated the same as a ``406 Not Acceptable`` error. This spec **does** require that if the meta version ``latest`` is being used, the server **MUST** respond with the content type for the actual version that is contained in the response -(i.e. A ``Accept: application/vnd.pypi.simple.latest+json`` request that returns +(i.e. an ``Accept: application/vnd.pypi.simple.latest+json`` request that returns a ``v1.x`` response should have a ``Content-Type`` of ``application/vnd.pypi.simple.v1+json``). @@ -725,7 +725,7 @@ may *optionally* be used instead. URL Parameter ^^^^^^^^^^^^^ -Servers that implement the Simple API may choose to support an URL parameter named +Servers that implement the Simple API may choose to support a URL parameter named ``format`` to allow the clients to request a specific version of the URL. The value of the ``format`` parameter should be **one** of the valid content types. diff --git a/source/specifications/version-specifiers.rst b/source/specifications/version-specifiers.rst index cde0bc49a..a5ba36498 100644 --- a/source/specifications/version-specifiers.rst +++ b/source/specifications/version-specifiers.rst @@ -1178,7 +1178,7 @@ more information on ``file://`` URLs on Windows see Summary of differences from pkg_resources.parse_version ======================================================= -* Note: this comparison is to ``pkg_resourses.parse_version`` as it existed at +* Note: this comparison is to ``pkg_resources.parse_version`` as it existed at the time :pep:`440` was written. After the PEP was accepted, setuptools 6.0 and later versions adopted the behaviour described here.