Skip to content

Commit 459c6d6

Browse files
authored
No need to consider license-files when handling PEP 621 metadata for 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.
2 parents 1a57eab + 60c874e commit 459c6d6

File tree

17 files changed

+100
-141
lines changed

17 files changed

+100
-141
lines changed

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
Changelog
33
=========
44

5+
Version 0.10
6+
============
7+
8+
* ``setuptools`` plugin:
9+
* Separate the handling of ``license-files`` and PEP 621 metadata, #34
10+
* ``license`` and ``license-files`` are no longer added to ``tool.setuptools.dynamic``.
11+
Instead ``license-files`` is added directly to ``tool.setuptools``, and the ``license`` should be added as ``project.license.text``.
12+
513
Version 0.9
614
===========
715

docs/setuptools_pep621.rst

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ proposed by ``ini2toml`` takes the following assumptions:
3030
- ``[options.*]`` sections in ``setup.cfg`` are translated to sub-tables of
3131
``[tool.setuptools]`` in ``pyproject.toml``. For example::
3232

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

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

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

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

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

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

124-
Please notice these conventions are part of a proposal and will probably
133+
134+
Please note these conventions are part of a proposal and will probably
125135
change as soon as a pattern is established by the :pypi:`setuptools` project.
126136
The implementation in ``ini2toml`` is flexible to quickly adapt to these
127137
changes.
@@ -130,3 +140,4 @@ changes.
130140
.. _TOML: https://toml.io/en/
131141
.. _setuptools own configuration file: https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
132142
.. _entry-points file: https://packaging.python.org/en/latest/specifications/entry-points/
143+
.. _core metadata: https://packaging.python.org/en/latest/specifications/core-metadata/

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ testing =
8181
tomli
8282
pytest
8383
pytest-cov
84-
validate-pyproject>=0.3.2,<2
84+
validate-pyproject>=0.6,<2
8585

8686
typechecking =
8787
typing-extensions; python_version<"3.8"

src/ini2toml/drivers/full_toml.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def _collapse_commented_kv(
105105
len_key = len(_key)
106106
_as_dict = obj.as_dict()
107107

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

@@ -217,6 +217,9 @@ def _convert_irepr_to_toml(irepr: IntermediateRepr, out: T) -> T:
217217
):
218218
child = out.setdefault(parent_key, inline_table())
219219
child[nested_key] = collapsed_value
220+
cmt = list(getattr(value, "_iter_comments", lambda: [])())
221+
if cmt:
222+
child.comment(" ".join(cmt))
220223
continue
221224
else:
222225
nested_key = tuple(rest)

src/ini2toml/intermediate_repr.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ def as_commented_list(self) -> "CommentedList[T]":
217217
def __repr__(self):
218218
return f"{self.__class__.__name__}({self.value!r}, {self.comment!r})"
219219

220+
def _iter_comments(self) -> Iterable[str]:
221+
if self.comment:
222+
yield self.comment
223+
220224

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

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

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

264-
def _all_comments(self) -> Iterable[str]:
265-
for entry in self:
266-
if entry.has_comment():
267-
yield entry.comment
271+
_iter_comments = CommentedList._iter_comments
268272

269273
def to_ir(self) -> IntermediateRepr:
270274
""":class:`CommentedKV` are usually intended to represent INI options, while

src/ini2toml/plugins/setuptools_pep621.py

Lines changed: 23 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,7 @@
33
import warnings
44
from functools import partial, reduce
55
from itertools import chain, zip_longest
6-
from typing import (
7-
Any,
8-
Dict,
9-
List,
10-
Mapping,
11-
Optional,
12-
Sequence,
13-
Set,
14-
Tuple,
15-
Type,
16-
TypeVar,
17-
Union,
18-
cast,
19-
)
6+
from typing import Any, Dict, List, Mapping, Sequence, Set, Tuple, Type, TypeVar, Union
207

218
try:
229
from packaging.requirements import Requirement
@@ -86,8 +73,6 @@
8673
"bdist_wheel",
8774
*getattr(distutils_commands, "__all__", []),
8875
)
89-
DEFAULT_LICENSE_FILES = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")
90-
# defaults from the `wheel` package
9176

9277

9378
def activate(translator: Translator):
@@ -152,8 +137,8 @@ def processing_rules(self) -> ProcessingRules:
152137
# `merge_and_rename_long_description_and_content_type`
153138
# ---
154139
("metadata", "license-files"): split_list_comma,
155-
# => NOTICE: in PEP 621, it should be a single file
156-
# further processed via `handle_license_and_files`
140+
# => NOTICE: not standard for now, needs PEP 639
141+
# further processed via `remove_metadata_not_in_pep621`
157142
# ---
158143
("metadata", "url"): split_url,
159144
("metadata", "download-url"): split_url,
@@ -341,50 +326,20 @@ def merge_and_rename_long_description_and_content_type(self, doc: R) -> R:
341326
metadata.rename("long-description", "readme")
342327
return doc
343328

344-
def handle_license_and_files(self, doc: R) -> R:
345-
"""In :pep:`621` we have a single field for license, which might have a single
346-
value (file path) or a dict-like structure::
347-
348-
license-files => license.file
349-
license => license.text
329+
def handle_license(self, doc: R) -> R:
330+
"""In :pep:`621` we have a single field for license, which is not compatible
331+
with setuptools ``license-files``.
332+
This field is meant to fill the ``License`` core metadata as a plain license
333+
text (not a path to a file). Even when the ``project.license.file`` table field
334+
is given, the idea is that the file should be expanded into text.
350335
351-
We also have to be aware that :pep:`621` accepts a single file, so the option of
352-
combining multiple files as presented in ``setup.cfg`` have to be handled via
353-
``dynamic``.
336+
This will likely change once :pep:`639` is accepted.
337+
Meanwhile we have to translate ``license-files`` into a setuptools specific
338+
configuration.
354339
"""
355340
metadata: IR = doc["metadata"]
356-
files: Optional[CommentedList[str]] = metadata.get("license-files")
357-
# Setuptools automatically includes license files if not present
358-
# so let's make it dynamic
359-
files_as_list = (files and files.as_list()) or list(DEFAULT_LICENSE_FILES)
360-
text = metadata.get("license")
361-
362-
# PEP 621 specifies a single "file". If there is more, we need to use "dynamic"
363-
if files_as_list and (
364-
len(files_as_list) > 1
365-
or any(char in files_as_list[0] for char in "*?[") # glob pattern
366-
or text # PEP 621 forbids both license and license-files at the same time
367-
):
368-
metadata.setdefault("dynamic", []).append("license")
369-
dynamic = doc.setdefault("options.dynamic", IR())
370-
if text:
371-
dynamic.append("license", text)
372-
dynamic.append("license-files", files_as_list)
373-
# 'file' and 'text' are mutually exclusive in PEP 621
374-
metadata.pop("license", None)
375-
metadata.pop("license-files", None)
376-
return doc
377-
378-
if files_as_list:
379-
files = cast(CommentedList[str], files)
380-
license = IR(file=Commented(files_as_list[0], files[0].comment))
381-
elif text:
382-
license = IR(text=metadata["license"])
383-
else:
384-
return doc
385-
386-
fields = ("license-files", "license")
387-
metadata.replace_first_remove_others(fields, "license", license)
341+
if "license" in metadata:
342+
metadata.rename("license", ("license", "text"))
388343
return doc
389344

390345
def move_and_split_entrypoints(self, doc: R) -> R:
@@ -438,9 +393,14 @@ def remove_metadata_not_in_pep621(self, doc: R) -> R:
438393
""":pep:`621` does not cover all project metadata in ``setup.cfg "metadata"``
439394
section. That is left as "tool" specific configuration.
440395
"""
441-
specific = ["platforms", "provides", "obsoletes"]
442-
metadata, options = doc["metadata"], doc["options"]
443-
options.update({k: metadata.pop(k) for k in specific if k in metadata})
396+
# TODO: PEP 621 does not specify an equivalent for 'License-file' metadata,
397+
# but once PEP 639 is approved this will change
398+
metadata = doc.get("metadata", IR())
399+
non_standard = ("platforms", "provides", "obsoletes", "license-files")
400+
specific = [k for k in non_standard if k in metadata]
401+
if specific:
402+
options = doc.setdefault("options", IR())
403+
options.update({k: metadata.pop(k) for k in specific})
444404
return doc
445405

446406
def rename_script_files(self, doc: R) -> R:
@@ -665,7 +625,7 @@ def pep621_transform(self, doc: R) -> R:
665625
self.merge_and_rename_urls,
666626
self.merge_authors_maintainers_and_emails,
667627
self.merge_and_rename_long_description_and_content_type,
668-
self.handle_license_and_files,
628+
self.handle_license,
669629
self.move_and_split_entrypoints,
670630
self.move_options_missing_in_pep621,
671631
self.remove_metadata_not_in_pep621,

tests/examples/django/pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ name = "Django"
77
authors = [{name = "Django Software Foundation", email = "[email protected]"}]
88
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
99
readme = "README.rst"
10+
license = {text = "BSD-3-Clause"}
1011
classifiers = [
1112
"Development Status :: 2 - Pre-Alpha",
1213
"Environment :: Web Environment",
@@ -25,14 +26,14 @@ classifiers = [
2526
"Topic :: Software Development :: Libraries :: Application Frameworks",
2627
"Topic :: Software Development :: Libraries :: Python Modules",
2728
]
28-
dynamic = ["license", "version"]
2929
requires-python = ">=3.8"
3030
dependencies = [
3131
"asgiref >= 3.3.2",
3232
'backports.zoneinfo; python_version<"3.9"',
3333
"sqlparse >= 0.2.2",
3434
"tzdata; sys_platform == 'win32'",
3535
]
36+
dynamic = ["version"]
3637

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

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

6463
[tool.distutils.bdist_rpm]

tests/examples/flask/pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "Flask"
7+
license = {text = "BSD-3-Clause"}
78
authors = [{name = "Armin Ronacher", email = "[email protected]"}]
89
maintainers = [{name = "Pallets", email = "[email protected]"}]
910
description = "A simple framework for building complex web applications."
@@ -20,8 +21,8 @@ classifiers = [
2021
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
2122
"Topic :: Software Development :: Libraries :: Application Frameworks",
2223
]
23-
dynamic = ["license", "version"]
2424
requires-python = ">= 3.6"
25+
dynamic = ["version"]
2526

2627
[project.urls]
2728
Homepage = "https://palletsprojects.com/p/flask"
@@ -50,8 +51,6 @@ where = ["src"]
5051
namespaces = false
5152

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

5756
[tool.pytest.ini_options]

tests/examples/pandas/pyproject.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ build-backend = "setuptools.build_meta"
66
name = "pandas"
77
description = "Powerful data structures for data analysis, time series, and statistics"
88
authors = [{name = "The Pandas Development Team", email = "[email protected]"}]
9+
license = {text = "BSD-3-Clause"}
910
classifiers = [
1011
"Development Status :: 5 - Production/Stable",
1112
"Environment :: Console",
@@ -20,13 +21,13 @@ classifiers = [
2021
"Programming Language :: Python :: 3.9",
2122
"Topic :: Scientific/Engineering",
2223
]
23-
dynamic = ["license", "version"]
2424
requires-python = ">=3.8"
2525
dependencies = [
2626
"numpy>=1.18.5",
2727
"python-dateutil>=2.8.1",
2828
"pytz>=2020.1",
2929
]
30+
dynamic = ["version"]
3031

3132
[project.readme]
3233
file = "README.md"
@@ -52,6 +53,7 @@ test = [
5253
include-package-data = true
5354
zip-safe = false
5455
platforms = ["any"]
56+
license-files = ["LICENSE"]
5557

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

66-
[tool.setuptools.dynamic]
67-
license = "BSD-3-Clause"
68-
license-files = ["LICENSE"]
69-
7068
[tool.distutils.build_ext]
7169
inplace = true
7270

tests/examples/pluggy/pyproject.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ build-backend = "setuptools.build_meta"
88
[project]
99
name = "pluggy"
1010
description = "plugin and hook calling mechanisms for python"
11+
license = {text = "MIT"}
1112
authors = [{name = "Holger Krekel", email = "[email protected]"}]
1213
classifiers = [
1314
"Development Status :: 6 - Mature",
@@ -30,9 +31,9 @@ classifiers = [
3031
"Programming Language :: Python :: 3.10",
3132
]
3233
urls = {Homepage = "https://github.com/pytest-dev/pluggy"}
33-
dynamic = ["license", "version"]
3434
requires-python = ">=3.6"
3535
dependencies = ['importlib-metadata>=0.12;python_version<"3.8"']
36+
dynamic = ["version"]
3637

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

57-
[tool.setuptools.dynamic]
58-
license = "MIT"
59-
license-files = ["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"]
60-
6158
[tool.devpi.upload]
6259
formats = "sdist.tgz,bdist_wheel"

0 commit comments

Comments
 (0)