Skip to content

Commit

Permalink
No need to consider license-files when handling PEP 621 metadata for …
Browse files Browse the repository at this point in the history
…setuptools plugin (#34)

Previously, ini2toml was considering that project.license and project.dynamic were relevant for setting the License-file in PKG-INFO. As clarified in a recent discussion, that is not the case: setuptools can fill License-file even when project.license is static and pyproject.toml' license.file is completely unrelated to setup.cfg' license_files.
  • Loading branch information
abravalheri authored Mar 9, 2022
2 parents 1a57eab + 60c874e commit 459c6d6
Show file tree
Hide file tree
Showing 17 changed files with 100 additions and 141 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
Changelog
=========

Version 0.10
============

* ``setuptools`` plugin:
* Separate the handling of ``license-files`` and PEP 621 metadata, #34
* ``license`` and ``license-files`` are no longer added to ``tool.setuptools.dynamic``.
Instead ``license-files`` is added directly to ``tool.setuptools``, and the ``license`` should be added as ``project.license.text``.

Version 0.9
===========

Expand Down
19 changes: 15 additions & 4 deletions docs/setuptools_pep621.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ proposed by ``ini2toml`` takes the following assumptions:
- ``[options.*]`` sections in ``setup.cfg`` are translated to sub-tables of
``[tool.setuptools]`` in ``pyproject.toml``. For example::

[options.package_data] => [tool.setuptools.package_data]
[options.package_data] => [tool.setuptools.package-data]

- Field and subtables in ``[tool.setuptools]`` have the ``_`` character
replaced by ``-`` in their keys, to follow the conventions set in :pep:`517`
Expand All @@ -41,10 +41,10 @@ proposed by ``ini2toml`` takes the following assumptions:

'file: description.rst' => {file = "description.rst"}

Notice, however, that these directives are not allowed to be used directly
Note, however, that these directives are not allowed to be used directly
under the ``project`` table. Instead, ``ini2toml`` will rely on ``dynamic``,
as explained bellow.
Also note that for some fields (e.g. ``readme`` or ``license``), ``ini2toml``
Also note that for some fields (e.g. ``readme``), ``ini2toml``
might try to automatically convert the directive into values accepted by
:pep:`621` (for complex scenarios ``dynamic`` might still be used).

Expand Down Expand Up @@ -120,8 +120,18 @@ proposed by ``ini2toml`` takes the following assumptions:
:pypi:`setuptools` maintainers decide so. This eventual change is mentioned
by some members of the community as a nice quality of life improvement.

- The ``metadata.license_files`` field in ``setup.cfg`` is not translated to
``project.license.file`` in ``pyproject.toml``, even when a single file is
given. The reason behind this choice is that ``project.license.file`` is
meant to be used in a different way than ``metadata.license_files`` when
generating `core metadata`_ (the first is read and expanded into the
``License`` core metadata field, the second is added as a path - relative to
the project root - as the ``License-file`` core metadata field). This might
change in the future if :pep:`639` is accepted. Meanwhile,
``metadata.license_files`` is translated to ``tool.setuptools.license-files``.

Please notice these conventions are part of a proposal and will probably

Please note these conventions are part of a proposal and will probably
change as soon as a pattern is established by the :pypi:`setuptools` project.
The implementation in ``ini2toml`` is flexible to quickly adapt to these
changes.
Expand All @@ -130,3 +140,4 @@ changes.
.. _TOML: https://toml.io/en/
.. _setuptools own configuration file: https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
.. _entry-points file: https://packaging.python.org/en/latest/specifications/entry-points/
.. _core metadata: https://packaging.python.org/en/latest/specifications/core-metadata/
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ testing =
tomli
pytest
pytest-cov
validate-pyproject>=0.3.2,<2
validate-pyproject>=0.6,<2

typechecking =
typing-extensions; python_version<"3.8"
Expand Down
5 changes: 4 additions & 1 deletion src/ini2toml/drivers/full_toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def _collapse_commented_kv(
len_key = len(_key)
_as_dict = obj.as_dict()

comments = list(obj._all_comments())
comments = list(obj._iter_comments())
len_comments = sum(len(c) for c in comments) + 4
# ^-- extra 4 for ` # `

Expand Down Expand Up @@ -217,6 +217,9 @@ def _convert_irepr_to_toml(irepr: IntermediateRepr, out: T) -> T:
):
child = out.setdefault(parent_key, inline_table())
child[nested_key] = collapsed_value
cmt = list(getattr(value, "_iter_comments", lambda: [])())
if cmt:
child.comment(" ".join(cmt))
continue
else:
nested_key = tuple(rest)
Expand Down
12 changes: 8 additions & 4 deletions src/ini2toml/intermediate_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ def as_commented_list(self) -> "CommentedList[T]":
def __repr__(self):
return f"{self.__class__.__name__}({self.value!r}, {self.comment!r})"

def _iter_comments(self) -> Iterable[str]:
if self.comment:
yield self.comment


class CommentedList(Generic[T], UserList):
def __init__(self, data: Sequence[Commented[List[T]]] = ()):
Expand All @@ -235,6 +239,9 @@ def insert_line(self, i, values: Iterable[T], comment: Optional[str] = None):
if values or comment:
self.insert(i, Commented(values, comment))

def _iter_comments(self: Iterable[Commented]) -> Iterable[str]:
return chain.from_iterable(entry._iter_comments() for entry in self)


class CommentedKV(Generic[T], UserList):
def __init__(self, data: Sequence[Commented[List[KV[T]]]] = ()):
Expand All @@ -261,10 +268,7 @@ def as_dict(self) -> dict:
out[k] = v
return out

def _all_comments(self) -> Iterable[str]:
for entry in self:
if entry.has_comment():
yield entry.comment
_iter_comments = CommentedList._iter_comments

def to_ir(self) -> IntermediateRepr:
""":class:`CommentedKV` are usually intended to represent INI options, while
Expand Down
86 changes: 23 additions & 63 deletions src/ini2toml/plugins/setuptools_pep621.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,7 @@
import warnings
from functools import partial, reduce
from itertools import chain, zip_longest
from typing import (
Any,
Dict,
List,
Mapping,
Optional,
Sequence,
Set,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from typing import Any, Dict, List, Mapping, Sequence, Set, Tuple, Type, TypeVar, Union

try:
from packaging.requirements import Requirement
Expand Down Expand Up @@ -86,8 +73,6 @@
"bdist_wheel",
*getattr(distutils_commands, "__all__", []),
)
DEFAULT_LICENSE_FILES = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")
# defaults from the `wheel` package


def activate(translator: Translator):
Expand Down Expand Up @@ -152,8 +137,8 @@ def processing_rules(self) -> ProcessingRules:
# `merge_and_rename_long_description_and_content_type`
# ---
("metadata", "license-files"): split_list_comma,
# => NOTICE: in PEP 621, it should be a single file
# further processed via `handle_license_and_files`
# => NOTICE: not standard for now, needs PEP 639
# further processed via `remove_metadata_not_in_pep621`
# ---
("metadata", "url"): split_url,
("metadata", "download-url"): split_url,
Expand Down Expand Up @@ -341,50 +326,20 @@ def merge_and_rename_long_description_and_content_type(self, doc: R) -> R:
metadata.rename("long-description", "readme")
return doc

def handle_license_and_files(self, doc: R) -> R:
"""In :pep:`621` we have a single field for license, which might have a single
value (file path) or a dict-like structure::
license-files => license.file
license => license.text
def handle_license(self, doc: R) -> R:
"""In :pep:`621` we have a single field for license, which is not compatible
with setuptools ``license-files``.
This field is meant to fill the ``License`` core metadata as a plain license
text (not a path to a file). Even when the ``project.license.file`` table field
is given, the idea is that the file should be expanded into text.
We also have to be aware that :pep:`621` accepts a single file, so the option of
combining multiple files as presented in ``setup.cfg`` have to be handled via
``dynamic``.
This will likely change once :pep:`639` is accepted.
Meanwhile we have to translate ``license-files`` into a setuptools specific
configuration.
"""
metadata: IR = doc["metadata"]
files: Optional[CommentedList[str]] = metadata.get("license-files")
# Setuptools automatically includes license files if not present
# so let's make it dynamic
files_as_list = (files and files.as_list()) or list(DEFAULT_LICENSE_FILES)
text = metadata.get("license")

# PEP 621 specifies a single "file". If there is more, we need to use "dynamic"
if files_as_list and (
len(files_as_list) > 1
or any(char in files_as_list[0] for char in "*?[") # glob pattern
or text # PEP 621 forbids both license and license-files at the same time
):
metadata.setdefault("dynamic", []).append("license")
dynamic = doc.setdefault("options.dynamic", IR())
if text:
dynamic.append("license", text)
dynamic.append("license-files", files_as_list)
# 'file' and 'text' are mutually exclusive in PEP 621
metadata.pop("license", None)
metadata.pop("license-files", None)
return doc

if files_as_list:
files = cast(CommentedList[str], files)
license = IR(file=Commented(files_as_list[0], files[0].comment))
elif text:
license = IR(text=metadata["license"])
else:
return doc

fields = ("license-files", "license")
metadata.replace_first_remove_others(fields, "license", license)
if "license" in metadata:
metadata.rename("license", ("license", "text"))
return doc

def move_and_split_entrypoints(self, doc: R) -> R:
Expand Down Expand Up @@ -438,9 +393,14 @@ def remove_metadata_not_in_pep621(self, doc: R) -> R:
""":pep:`621` does not cover all project metadata in ``setup.cfg "metadata"``
section. That is left as "tool" specific configuration.
"""
specific = ["platforms", "provides", "obsoletes"]
metadata, options = doc["metadata"], doc["options"]
options.update({k: metadata.pop(k) for k in specific if k in metadata})
# TODO: PEP 621 does not specify an equivalent for 'License-file' metadata,
# but once PEP 639 is approved this will change
metadata = doc.get("metadata", IR())
non_standard = ("platforms", "provides", "obsoletes", "license-files")
specific = [k for k in non_standard if k in metadata]
if specific:
options = doc.setdefault("options", IR())
options.update({k: metadata.pop(k) for k in specific})
return doc

def rename_script_files(self, doc: R) -> R:
Expand Down Expand Up @@ -665,7 +625,7 @@ def pep621_transform(self, doc: R) -> R:
self.merge_and_rename_urls,
self.merge_authors_maintainers_and_emails,
self.merge_and_rename_long_description_and_content_type,
self.handle_license_and_files,
self.handle_license,
self.move_and_split_entrypoints,
self.move_options_missing_in_pep621,
self.remove_metadata_not_in_pep621,
Expand Down
5 changes: 2 additions & 3 deletions tests/examples/django/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ name = "Django"
authors = [{name = "Django Software Foundation", email = "[email protected]"}]
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
readme = "README.rst"
license = {text = "BSD-3-Clause"}
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Environment :: Web Environment",
Expand All @@ -25,14 +26,14 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dynamic = ["license", "version"]
requires-python = ">=3.8"
dependencies = [
"asgiref >= 3.3.2",
'backports.zoneinfo; python_version<"3.9"',
"sqlparse >= 0.2.2",
"tzdata; sys_platform == 'win32'",
]
dynamic = ["version"]

[project.urls]
Homepage = "https://www.djangoproject.com/"
Expand All @@ -57,8 +58,6 @@ zip-safe = false
find = {namespaces = false}

[tool.setuptools.dynamic]
license = "BSD-3-Clause"
license-files = ["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"]
version = {attr = "django.__version__"}

[tool.distutils.bdist_rpm]
Expand Down
5 changes: 2 additions & 3 deletions tests/examples/flask/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "Flask"
license = {text = "BSD-3-Clause"}
authors = [{name = "Armin Ronacher", email = "[email protected]"}]
maintainers = [{name = "Pallets", email = "[email protected]"}]
description = "A simple framework for building complex web applications."
Expand All @@ -20,8 +21,8 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
"Topic :: Software Development :: Libraries :: Application Frameworks",
]
dynamic = ["license", "version"]
requires-python = ">= 3.6"
dynamic = ["version"]

[project.urls]
Homepage = "https://palletsprojects.com/p/flask"
Expand Down Expand Up @@ -50,8 +51,6 @@ where = ["src"]
namespaces = false

[tool.setuptools.dynamic]
license = "BSD-3-Clause"
license-files = ["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"]
version = {attr = "flask.__version__"}

[tool.pytest.ini_options]
Expand Down
8 changes: 3 additions & 5 deletions tests/examples/pandas/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ build-backend = "setuptools.build_meta"
name = "pandas"
description = "Powerful data structures for data analysis, time series, and statistics"
authors = [{name = "The Pandas Development Team", email = "[email protected]"}]
license = {text = "BSD-3-Clause"}
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
Expand All @@ -20,13 +21,13 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Topic :: Scientific/Engineering",
]
dynamic = ["license", "version"]
requires-python = ">=3.8"
dependencies = [
"numpy>=1.18.5",
"python-dateutil>=2.8.1",
"pytz>=2020.1",
]
dynamic = ["version"]

[project.readme]
file = "README.md"
Expand All @@ -52,6 +53,7 @@ test = [
include-package-data = true
zip-safe = false
platforms = ["any"]
license-files = ["LICENSE"]

[tool.setuptools.package-data]
"*" = ["templates/*", "_libs/**/*.dll"]
Expand All @@ -63,10 +65,6 @@ include = ["pandas", "pandas.*"]
# resulting files.
namespaces = false

[tool.setuptools.dynamic]
license = "BSD-3-Clause"
license-files = ["LICENSE"]

[tool.distutils.build_ext]
inplace = true

Expand Down
7 changes: 2 additions & 5 deletions tests/examples/pluggy/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pluggy"
description = "plugin and hook calling mechanisms for python"
license = {text = "MIT"}
authors = [{name = "Holger Krekel", email = "[email protected]"}]
classifiers = [
"Development Status :: 6 - Mature",
Expand All @@ -30,9 +31,9 @@ classifiers = [
"Programming Language :: Python :: 3.10",
]
urls = {Homepage = "https://github.com/pytest-dev/pluggy"}
dynamic = ["license", "version"]
requires-python = ">=3.6"
dependencies = ['importlib-metadata>=0.12;python_version<"3.8"']
dynamic = ["version"]

[project.readme]
file = "README.rst"
Expand All @@ -54,9 +55,5 @@ package-dir = {"" = "src"}
platforms = ["unix", "linux", "osx", "win32"]
include-package-data = false

[tool.setuptools.dynamic]
license = "MIT"
license-files = ["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"]

[tool.devpi.upload]
formats = "sdist.tgz,bdist_wheel"
Loading

0 comments on commit 459c6d6

Please sign in to comment.