From 4ad541b6f86ee47a0a5cd288430ae70ab7ad0854 Mon Sep 17 00:00:00 2001 From: Jean Abou Samra Date: Sun, 5 Nov 2023 16:16:21 +0100 Subject: [PATCH] Split a pyproject.toml guide out of the "Declaring project metadata" specification Fixes #1349 --- .../creating-and-discovering-plugins.rst | 2 + source/guides/index.rst | 1 + source/guides/writing-pyproject-toml.rst | 383 ++++++++++++++++++ .../declaring-project-metadata.rst | 155 +------ 4 files changed, 392 insertions(+), 149 deletions(-) create mode 100644 source/guides/writing-pyproject-toml.rst diff --git a/source/guides/creating-and-discovering-plugins.rst b/source/guides/creating-and-discovering-plugins.rst index eceebc843..601f2b4a6 100644 --- a/source/guides/creating-and-discovering-plugins.rst +++ b/source/guides/creating-and-discovering-plugins.rst @@ -116,6 +116,8 @@ a list of packages to :func:`setup`'s ``packages`` argument instead of using :doc:`packaging-namespace-packages` documentation and clearly document which approach is preferred for plugins to your project. +.. _plugin-entry-points: + Using package metadata ====================== diff --git a/source/guides/index.rst b/source/guides/index.rst index 10c189a17..c995f8d01 100644 --- a/source/guides/index.rst +++ b/source/guides/index.rst @@ -20,6 +20,7 @@ introduction to packaging, see :doc:`/tutorials/index`. :maxdepth: 1 :caption: Building and Publishing Projects: + writing-pyproject-toml distributing-packages-using-setuptools using-manifest-in single-sourcing-package-version diff --git a/source/guides/writing-pyproject-toml.rst b/source/guides/writing-pyproject-toml.rst new file mode 100644 index 000000000..af5c2fe18 --- /dev/null +++ b/source/guides/writing-pyproject-toml.rst @@ -0,0 +1,383 @@ +.. _writing-pyproject-toml: + +=============================== +Writing your ``pyproject.toml`` +=============================== + +.. TODO: link :term:`build backends` when that's merged + +``pyproject.toml`` is a configuration file used by packaging tools. Most +build backends [#poetry-special]_ allow you to specify your project's +basic metadata, such as the dependencies, your name, etc., in the ``[project]`` +table of your ``pyproject.toml``. + +.. note:: + + You may have heard of ``setup.py`` and ``setup.cfg`` for the setuptools_ + build backend. For new projects, it is recommended to use ``pyproject.toml`` + for basic metadata, and keep ``setup.py`` only if some programmatic configuration + is needed (especially building C extensions). However, putting basic project + metadata in ``setup.py`` or ``setup.cfg`` is still valid. + +.. TODO: link to "is setup.py deprecated?" page when merged + +Static vs. dynamic metadata +=========================== + +Most of the time, you will directly write the value of a field in +``pyproject.toml``. For example: ``requires-python = ">= 3.8"``, or +``version = "1.0"``. + +However, in some cases, it is useful to let your build backend compute +the metadata for you. For example: many build backends can read the +version from a ``__version__`` attribute in your code, or similar. +In such cases, you should mark the field as dynamic using, e.g., + +.. code-block:: toml + + [project] + dynamic = ["version"] + + +When a field is dynamic, it is the build backend's responsibility to +fill it. Consult your build backend's documentation to learn how it +does it. + + +Basic information +================= + +``name`` +-------- + +Put the name of your project on PyPI. This field is required and is the +only field that cannot be marked as dynamic. + +.. code-block:: toml + + [project] + name = "spam" + + +``version`` +----------- + +Put the version of your project. + +.. code-block:: toml + + [project] + version = "2020.0.0" + +This field is required, although it is often marked as dynamic using + +.. code-block:: toml + + [project] + dynamic = ["version"] + + + +Dependencies and requirements +============================= + +``dependencies``/``optional-dependencies`` +------------------------------------------ + +If your project has dependencies, list them like this: + +.. code-block:: toml + + [project] + dependencies = [ + "httpx", + "gidgethub[httpx]>4.0.0", + "django>2.1; os_name != 'nt'", + "django>2.0; os_name == 'nt'", + ] + +See :ref:`Dependency specifiers ` for the full +syntax you can use to constrain versions. + +You may want to make some of your dependencies optional, if they are +only needed for a specific feature of your package. In that case, put +them in ``optional-dependencies``. + +.. code-block:: toml + + [project.optional-dependencies] + gui = ["PyQt5"] + cli = [ + "rich", + "click", + ] + +Each of the keys defines a "packaging extra". In the example above, one +could use, e.g., ``pip install your-project-name[gui]`` to install your +project with GUI support, adding the PyQt5 dependency. + + +``python-requires`` +------------------- + +This lets you declare the minimum version of Python that you support +[#requires-python-upper-bounds]_. + +.. code-block:: toml + + [project] + requires-python = ">= 3.8" + + + +Creating executable scripts +=========================== + +To install a command as part of your package, declare it in the +``[project.scripts]`` table. + +.. code-block:: toml + + [project.scripts] + spam-cli = "spam:main_cli" + +In this example, after installing your project, a ``spam-cli`` command +will be available. Executing this command will do the equivalent of +``from spam import main_cli; main_cli()``. + +On Windows, scripts packaged this way need a terminal, so if you launch +them from within a graphical application, they will make a terminal pop +up. To prevent this from happening, use the ``[project.gui-scripts]`` +table instead of ``[project.scripts]``. + +.. code-block:: toml + + [project.gui-scripts] + spam-gui = "spam:main_gui" + +In that case, launching your script from the command line will give back +control immediately, leaving the script to run in the background. + +The difference between ``[project.scripts]`` and +``[project.gui-scripts]`` is only relevant on Windows. + + + +About your project +================== + +``authors``/``maintainers`` +--------------------------- + +Both of these fields contain lists of people identified by a name and/or +an email address. + +.. code-block:: toml + + [project] + authors = [ + {name = "Pradyun Gedam", email = "pradyun@example.com"}, + {name = "Tzu-Ping Chung", email = "tzu-ping@example.com"}, + {name = "Another person"}, + {email = "different.person@example.com"}, + ] + maintainers = [ + {name = "Brett Cannon", email = "brett@python.org"} + ] + + +``description`` +--------------- + +This should be a one-line description of your project, to show as the +"headline" of your project page on PyPI (`example `_). + +.. code-block:: toml + + [project] + description = "Lovely Spam! Wonderful Spam!" + + +``readme`` +---------- + +This is a longer description of your project, to display on your project +page on PyPI. Typically, your project will have a ``README.md`` or +``README.rst`` file and you just put its file name here. + +.. code-block:: toml + + [project] + readme = "README.md" + +The README's format is auto-detected from the extension: + +- ``README.md`` → Markdown, +- ``README.rst`` → reStructuredText (without Sphinx extensions). + +You can also specify the format explicitly, like this: + +.. code-block:: toml + + [project] + readme = {file = "README.txt", content-type = "text/markdown"} + # or + readme = {file = "README.txt", content-type = "text/x-rst"} + + +``license`` +----------- + +This can take two forms. You can put your license in a file, typically +``LICENSE`` or ``LICENSE.txt``, and link that file here: + +.. code-block:: toml + + [project] + license = {file = "LICENSE"} + +or you can write the name of the license: + + +.. code-block:: toml + + [project] + license = {text = "MIT License"} + + + +``keywords`` +------------ + +This will help PyPI's search box to suggest your project when people +search for these keywords. + +.. code-block:: toml + + [project] + keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] + + +``classifiers`` +--------------- + +A list of PyPI classifiers that apply to your project. Check the +`full list of possibilities `_. + +.. code-block:: toml + + classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python" + ] + + +``urls`` +-------- + +A list of URLs associated with your project, displayed on the left +sidebar of your PyPI project page. If the key contains spaces, don't +forget to quote it. + +.. code-block:: toml + + [project.urls] + Homepage = "https://example.com" + Documentation = "https://readthedocs.org" + Repository = "https://github.com/me/spam.git" + "Bug Tracker" = "https://github.com/me/spam/issues" + Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md" + + +Advanced plugins +================ + +Some packages can be extended through plugins. Examples include Pytest_ +and Pygments_. To create such a plugin, you need to declare it in a subtable +of ``[project.entry-points]`` like this: + +.. code-block:: toml + + [project.entry-points."spam.magical"] + tomatoes = "spam:main_tomatoes" + +See the :ref:`Plugin guide ` for more information. + + + +A full example +============== + +.. code-block:: toml + + [project] + name = "spam" + version = "2020.0.0" + dependencies = [ + "httpx", + "gidgethub[httpx]>4.0.0", + "django>2.1; os_name != 'nt'", + "django>2.0; os_name == 'nt'", + ] + requires-python = ">=3.8" + authors = [ + {name = "Pradyun Gedam", email = "pradyun@example.com"}, + {name = "Tzu-Ping Chung", email = "tzu-ping@example.com"}, + {name = "Another person"}, + {email = "different.person@example.com"}, + ] + maintainers = [ + {name = "Brett Cannon", email = "brett@python.org"} + ] + description = "Lovely Spam! Wonderful Spam!" + readme = "README.rst" + license = {file = "LICENSE.txt"} + keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] + classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python" + ] + + # dynamic = ["version", "description"] + + [project.optional-dependencies] + gui = ["PyQt5"] + cli = [ + "rich", + "click", + ] + + [project.urls] + Homepage = "https://example.com" + Documentation = "https://readthedocs.org" + Repository = "https://github.com/me/spam.git" + "Bug Tracker" = "https://github.com/me/spam/issues" + Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md" + + [project.scripts] + spam-cli = "spam:main_cli" + + [project.gui-scripts] + spam-gui = "spam:main_gui" + + [project.entry-points."spam.magical"] + tomatoes = "spam:main_tomatoes" + + +------------------ + +.. [#poetry-special] At the time of this writing (November 2023), Poetry_ + is a notable exception. It uses its own format for this metadata, in + the ``[tool.poetry]`` table. + +.. [#requires-python-upper-bounds] Think twice before applying an upper bound + like ``requires-python = "<= 3.10"`` here. `This blog post `_ + contains some information regarding possible problems. + +.. _setuptools: https://setuptools.pypa.io +.. _poetry: https://python-poetry.org +.. _pypi-pip: https://pypi.org/project/pip +.. _classifier-list: https://pypi.org/classifiers +.. _requires-python-blog-post: https://iscinumpy.dev/post/bound-version-constraints/#pinning-the-python-version-is-special +.. _pytest: https://pytest.org +.. _pygments: https://pygments.org diff --git a/source/specifications/declaring-project-metadata.rst b/source/specifications/declaring-project-metadata.rst index 4f1d4199d..c7e7e3040 100644 --- a/source/specifications/declaring-project-metadata.rst +++ b/source/specifications/declaring-project-metadata.rst @@ -9,6 +9,12 @@ Declaring project metadata packaging-related tools to consume. It defines the following specification as the canonical source for the format used. +.. warning:: + + This is a **technical, formal specification**. For a gentle, + user-friendly guide to ``pyproject.toml``, see + :ref:`writing-pyproject-toml`. + Specification ============= @@ -75,11 +81,6 @@ The name of the project. Tools SHOULD :ref:`normalize ` this name, as soon as it is read for internal consistency. -.. code-block:: toml - - [project] - name = "spam" - ``version`` ----------- @@ -91,10 +92,6 @@ The version of the project as supported by :pep:`440`. Users SHOULD prefer to specify already-normalized versions. -.. code-block:: toml - - [project] - version = "2020.0.0" ``description`` --------------- @@ -105,10 +102,6 @@ Users SHOULD prefer to specify already-normalized versions. The summary description of the project. -.. code-block:: toml - - [project] - description = "Lovely Spam! Wonderful Spam!" ``readme`` ---------- @@ -148,13 +141,6 @@ alternative content-types which they can transform to a content-type as supported by the :ref:`core metadata `. Otherwise tools MUST raise an error for unsupported content-types. -.. code-block:: toml - - [project] - # A single pyproject.toml file can only have one of the following. - readme = "README.md" - readme = "README.rst" - readme = {file = "README.txt", content-type = "text/markdown"} ``requires-python`` ------------------- @@ -165,10 +151,6 @@ tools MUST raise an error for unsupported content-types. The Python version requirements of the project. -.. code-block:: toml - - [project] - requires-python = ">=3.8" ``license`` ----------- @@ -184,12 +166,6 @@ file's encoding is UTF-8. The ``text`` key has a string value which is the license of the project. These keys are mutually exclusive, so a tool MUST raise an error if the metadata specifies both keys. -.. code-block:: toml - - [project] - # A single pyproject.toml file can only have one of the following. - license = {file = "LICENSE"} - license = {text = "MIT License"} ``authors``/``maintainers`` --------------------------- @@ -232,19 +208,6 @@ follows: as appropriate, with the format ``{name} <{email}>``. 4. Multiple values should be separated by commas. -.. code-block:: toml - - [project] - authors = [ - {name = "Pradyun Gedam", email = "pradyun@example.com"}, - {name = "Tzu-Ping Chung", email = "tzu-ping@example.com"}, - {name = "Another person"}, - {email = "different.person@example.com"}, - ] - maintainers = [ - {name = "Brett Cannon", email = "brett@python.org"} - ] - ``keywords`` ------------ @@ -255,10 +218,6 @@ follows: The keywords for the project. -.. code-block:: toml - - [project] - keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] ``classifiers`` --------------- @@ -269,12 +228,6 @@ The keywords for the project. Trove classifiers which apply to the project. -.. code-block:: toml - - classifiers = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python" - ] ``urls`` -------- @@ -286,13 +239,6 @@ Trove classifiers which apply to the project. A table of URLs where the key is the URL label and the value is the URL itself. -.. code-block:: toml - - [project.urls] - Homepage = "https://example.com" - Documentation = "https://readthedocs.org" - Repository = "https://github.com/me/spam.git" - Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md" Entry points ------------ @@ -323,17 +269,6 @@ Build back-ends MUST raise an error if the metadata defines a be ambiguous in the face of ``[project.scripts]`` and ``[project.gui-scripts]``, respectively. -.. code-block:: toml - - [project.scripts] - spam-cli = "spam:main_cli" - - [project.gui-scripts] - spam-gui = "spam:main_gui" - - [project.entry-points."spam.magical"] - tomatoes = "spam:main_tomatoes" - ``dependencies``/``optional-dependencies`` ------------------------------------------ @@ -361,22 +296,6 @@ in the array thus becomes a corresponding matching :ref:`Provides-Extra ` metadata. -.. code-block:: toml - - [project] - dependencies = [ - "httpx", - "gidgethub[httpx]>4.0.0", - "django>2.1; os_name != 'nt'", - "django>2.0; os_name == 'nt'", - ] - - [project.optional-dependencies] - gui = ["PyQt5"] - cli = [ - "rich", - "click", - ] ``dynamic`` ----------- @@ -415,68 +334,6 @@ provided via tooling later on. the data for it (omitting the data, if determined to be the accurate value, is acceptable). -.. code-block:: toml - - dynamic = ["version", "description", "optional-dependencies"] - - -Example -======= - -.. code-block:: toml - - [project] - name = "spam" - version = "2020.0.0" - description = "Lovely Spam! Wonderful Spam!" - readme = "README.rst" - requires-python = ">=3.8" - license = {file = "LICENSE.txt"} - keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] - authors = [ - {name = "Pradyun Gedam", email = "pradyun@example.com"}, - {name = "Tzu-Ping Chung", email = "tzu-ping@example.com"}, - {name = "Another person"}, - {email = "different.person@example.com"}, - ] - maintainers = [ - {name = "Brett Cannon", email = "brett@python.org"} - ] - classifiers = [ - "Development Status :: 4 - Beta", - "Programming Language :: Python" - ] - - dependencies = [ - "httpx", - "gidgethub[httpx]>4.0.0", - "django>2.1; os_name != 'nt'", - "django>2.0; os_name == 'nt'", - ] - - # dynamic = ["version", "description"] - - [project.optional-dependencies] - gui = ["PyQt5"] - cli = [ - "rich", - "click", - ] - - [project.urls] - Homepage = "https://example.com" - Documentation = "https://readthedocs.org" - Repository = "https://github.com/me/spam.git" - Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md" - - [project.scripts] - spam-cli = "spam:main_cli" - - [project.gui-scripts] - spam-gui = "spam:main_gui" - - [project.entry-points."spam.magical"] - tomatoes = "spam:main_tomatoes" .. _TOML: https://toml.io