Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: pypa/flit
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 3.10.1
Choose a base ref
...
head repository: pypa/flit
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 3.11.0
Choose a head ref

Commits on Nov 21, 2024

  1. Add initial support for license-files

    cdce8p committed Nov 21, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    lukekarrys Luke Karrys
    Copy the full SHA
    2704faf View commit details

Commits on Nov 22, 2024

  1. Update default_license_files_globs

    Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
    cdce8p and AA-Turner committed Nov 22, 2024
    Copy the full SHA
    7fbf608 View commit details

Commits on Jan 5, 2025

  1. Add tox posargs

    cdce8p committed Jan 5, 2025
    Copy the full SHA
    fe26f81 View commit details
  2. Move some tests

    cdce8p committed Jan 5, 2025
    Copy the full SHA
    f28fd3e View commit details

Commits on Jan 20, 2025

  1. Add support for license expressions

    cdce8p authored and takluyver committed Jan 20, 2025
    Copy the full SHA
    640b6ad View commit details
  2. Copy the full SHA
    6d60848 View commit details
  3. Allow unrecognised license IDs with FLIT_ALLOW_INVALID

    takluyver committed Jan 20, 2025
    Copy the full SHA
    06faa0b View commit details
  4. Move Python 3.7 tests to Ubuntu 22.04

    takluyver committed Jan 20, 2025
    Copy the full SHA
    9e5bb2c View commit details
  5. Merge pull request #713 from pypa/ci-py37-ubuntu-22.04

    Move Python 3.7 tests to Ubuntu 22.04
    takluyver authored Jan 20, 2025
    Copy the full SHA
    432042e View commit details

Commits on Feb 7, 2025

  1. Code review

    cdce8p committed Feb 7, 2025
    Copy the full SHA
    3d7895a View commit details
  2. Fix path passed to read_pep621_metadata in test

    Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
    takluyver and cdce8p authored Feb 7, 2025
    Copy the full SHA
    c62ba7d View commit details
  3. Fix the path passed to read_pep621_metadata in more tests

    Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
    takluyver and cdce8p authored Feb 7, 2025
    Copy the full SHA
    e08f30f View commit details
  4. Merge pull request #705 from cdce8p/license-files

    Add initial support for license-files
    takluyver authored Feb 7, 2025
    Copy the full SHA
    dc30bf8 View commit details
  5. Merge pull request #706 from cdce8p/tox-posargs

    Add tox posargs
    takluyver authored Feb 7, 2025
    Copy the full SHA
    c13d414 View commit details

Commits on Feb 8, 2025

  1. Merge branch 'main' into license-expression

    takluyver authored Feb 8, 2025
    Copy the full SHA
    f08bb9f View commit details
  2. Copy the full SHA
    345bf1c View commit details
  3. Add back project.license.file to license files

    takluyver committed Feb 8, 2025
    Copy the full SHA
    0d0d06b View commit details
  4. Merge pull request #712 from pypa/license-expression

    Support simple SPDX license expressions (PEP 639)
    takluyver authored Feb 8, 2025
    Copy the full SHA
    2818f41 View commit details
  5. Copy the full SHA
    97fbff2 View commit details
  6. Grammar fix in error message

    takluyver committed Feb 8, 2025
    Copy the full SHA
    54b7aab View commit details
  7. Use new-style license metadata for Flit itself

    takluyver committed Feb 8, 2025
    Copy the full SHA
    001ba9b View commit details
  8. Require flit_core >= 3.11 for building flit itself

    takluyver committed Feb 8, 2025
    Copy the full SHA
    b5a0122 View commit details
  9. Update pyproject example in docs with license expression

    cdce8p committed Feb 8, 2025
    Copy the full SHA
    e29cefc View commit details
  10. Merge pull request #717 from cdce8p/update-doc-license

    Update pyproject example in docs with license expression
    takluyver authored Feb 8, 2025
    Copy the full SHA
    17f9142 View commit details

Commits on Feb 9, 2025

  1. Copy the full SHA
    dd49ea6 View commit details
  2. Simplify some code using pathlib read_text & write_text methods

    takluyver committed Feb 9, 2025
    Copy the full SHA
    1ddd569 View commit details
  3. Fix failing test for flit init

    takluyver committed Feb 9, 2025
    Copy the full SHA
    30bf6b6 View commit details

Commits on Feb 12, 2025

  1. Merge pull request #719 from pypa/init-license-expr

    Create new style license expression & license-files list in flit init
    takluyver authored Feb 12, 2025
    Copy the full SHA
    48d9a3c View commit details
  2. Merge pull request #715 from pypa/metadata-2.4

    Package metadata version 2.4
    takluyver authored Feb 12, 2025
    Copy the full SHA
    e7c4239 View commit details

Commits on Feb 18, 2025

  1. Copy the full SHA
    d2137f3 View commit details
  2. Bump version: 3.10.1 → 3.11.0

    takluyver committed Feb 18, 2025
    Copy the full SHA
    389c23c View commit details

Commits on Feb 19, 2025

  1. Merge pull request #722 from pypa/changelog-3.11

    Prepare to release 3.11
    takluyver authored Feb 19, 2025
    Copy the full SHA
    5f8c75f View commit details
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 3.10.1
current_version = 3.11.0
commit = True
tag = False

28 changes: 27 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -21,7 +21,33 @@ jobs:
strategy:
matrix:
platform: ["ubuntu-latest", "windows-latest"]
python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" , "3.12", "3.13" ]
python-version: [ "3.8", "3.9", "3.10", "3.11" , "3.12", "3.13" ]
steps:
- uses: actions/checkout@v4

- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions codecov
- name: Run tests
run: tox

- name: Codecov upload
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: codecov

test-py37:
runs-on: "ubuntu-22.04"
strategy:
matrix:
python-version: [ "3.7", ]
steps:
- uses: actions/checkout@v4

2 changes: 1 addition & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@
# built documents.
#
# The short X.Y version.
version = '3.10.1'
version = '3.11.0'
# The full version, including alpha/beta/rc tags.
release = version #+ '.1'

16 changes: 16 additions & 0 deletions doc/history.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
Release history
===============

Version 3.11
------------

- Support for SPDX license expressions and multiple license files, as detailed
in :pep:`639`::

license = "BSD-3-Clause"
license-files = ["LICENSE"]

For now, only a single license identifier is allowed. More complex expressions
describing multiple licenses & expressions may be supported in a future
version.
- The `metadata format <https://packaging.python.org/en/latest/specifications/core-metadata/>`_
in produced packages is now version 2.4, to support the expanded license
information.

Version 3.10.1
--------------

14 changes: 9 additions & 5 deletions doc/pyproject_toml.rst
Original file line number Diff line number Diff line change
@@ -20,13 +20,15 @@ defined by PEP 517. For any new project using Flit, it will look like this:
.. code-block:: toml
[build-system]
requires = ["flit_core >=3.2,<4"]
requires = ["flit_core >=3.11,<4"]
build-backend = "flit_core.buildapi"
Version constraints:

- For now, all packages should specify ``<4``, so they won't be impacted by
changes in the next major version.
- ``license-files`` and license expressions in the ``license`` field require
``flit_core >=3.11``.
- :ref:`pyproject_toml_project` requires ``flit_core >=3.2``
- :ref:`pyproject_old_metadata` requires ``flit_core >=2,<4``
- The older :doc:`flit.ini file <flit_ini>` requires ``flit_core <3``.
@@ -59,9 +61,7 @@ A simple ``[project]`` table might look like this:
{name = "Thomas Kluyver", email = "thomas@kluyver.me.uk"},
]
readme = "README.rst"
classifiers = [
"License :: OSI Approved :: MIT License",
]
license = "MIT"
requires-python = ">=3.5"
dynamic = ["version", "description"]
@@ -96,8 +96,12 @@ requires-python
A version specifier for the versions of Python this requires, e.g. ``~=3.3`` or
``>=3.3,<4``, which are equivalents.
license
A table with either a ``file`` key (a relative path to a license file) or a
A valid SPDX `license expression <https://peps.python.org/pep-0639/#term-license-expression>`_
or a table with either a ``file`` key (a relative path to a license file) or a
``text`` key (the license text).
license-files
A list of glob patterns for license files to include.
Defaults to ``['COPYING*', 'LICEN[CS]E*']``.
authors
A list of tables with ``name`` and ``email`` keys (both optional) describing
the authors of the project.
2 changes: 1 addition & 1 deletion flit/__init__.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
from .config import ConfigError
from .log import enable_colourful_output

__version__ = '3.10.1'
__version__ = '3.11.0'

log = logging.getLogger(__name__)

25 changes: 11 additions & 14 deletions flit/init.py
Original file line number Diff line number Diff line change
@@ -49,10 +49,10 @@ def store_defaults(d):
('skip', "Skip - choose a license later"),
]

license_names_to_classifiers = {
'mit': 'License :: OSI Approved :: MIT License',
'gpl3': 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'apache': 'License :: OSI Approved :: Apache Software License'
license_names_to_spdx = {
'mit': 'MIT',
'apache': 'Apache-2.0',
'gpl3': 'GPL-3.0-or-later',
}

license_templates_dir = Path(__file__).parent / 'license_templates'
@@ -119,11 +119,11 @@ def write_license(self, name, author):
if (self.directory / 'LICENSE').exists():
return
year = date.today().year
with (license_templates_dir / name).open(encoding='utf-8') as f:
license_text = f.read()
license_text = (license_templates_dir / name).read_text('utf-8')

with (self.directory / 'LICENSE').open('w', encoding='utf-8') as f:
f.write(license_text.format(year=year, author=author))
(self.directory / 'LICENSE').write_text(
license_text.format(year=year, author=author), encoding='utf-8'
)

def find_readme(self):
allowed = ("readme.md","readme.rst","readme.txt")
@@ -213,9 +213,7 @@ def initialise(self):
else:
authors_list = "[]"

classifiers = []
if license != 'skip':
classifiers = [license_names_to_classifiers[license]]
self.write_license(license, author)

with (self.directory / 'pyproject.toml').open('w', encoding='utf-8') as f:
@@ -225,9 +223,8 @@ def initialise(self):
if readme:
f.write(tomli_w.dumps({'readme': readme}))
if license != 'skip':
f.write('license = {file = "LICENSE"}\n')
if classifiers:
f.write(f"classifiers = {json.dumps(classifiers)}\n")
f.write(tomli_w.dumps({'license': license_names_to_spdx[license]}))
f.write(f"license-files = {json.dumps(['LICENSE'])}\n")
f.write('dynamic = ["version", "description"]\n')
if home_page:
f.write("\n" + tomli_w.dumps({
@@ -239,7 +236,7 @@ def initialise(self):

TEMPLATE = """\
[build-system]
requires = ["flit_core >=3.2,<4"]
requires = ["flit_core >=3.11,<4"]
build-backend = "flit_core.buildapi"
[project]
2 changes: 1 addition & 1 deletion flit_core/flit_core/__init__.py
Original file line number Diff line number Diff line change
@@ -4,4 +4,4 @@
All the convenient development features live in the main 'flit' package.
"""

__version__ = '3.10.1'
__version__ = '3.11.0'
651 changes: 651 additions & 0 deletions flit_core/flit_core/_spdx_data.py

Large diffs are not rendered by default.

16 changes: 14 additions & 2 deletions flit_core/flit_core/common.py
Original file line number Diff line number Diff line change
@@ -336,6 +336,7 @@ class Metadata(object):
maintainer = None
maintainer_email = None
license = None
license_expression = None
description = None
keywords = None
download_url = None
@@ -354,9 +355,10 @@ class Metadata(object):
obsoletes_dist = ()
requires_external = ()
provides_extra = ()
license_files = ()
dynamic = ()

metadata_version = "2.3"
metadata_version = "2.4"

def __init__(self, data):
data = data.copy()
@@ -398,7 +400,6 @@ def write_metadata_file(self, fp):
optional_fields = [
'Summary',
'Home-page',
'License',
'Keywords',
'Author',
'Author-email',
@@ -422,9 +423,20 @@ def write_metadata_file(self, fp):
value = '\n '.join(value.splitlines())
fp.write(u"{}: {}\n".format(field, value))


license_expr = getattr(self, self._normalise_field_name("License-Expression"))
license = getattr(self, self._normalise_field_name("License"))
if license_expr:
fp.write(u'License-Expression: {}\n'.format(license_expr))
elif license: # Deprecated, superseded by License-Expression
fp.write(u'License: {}\n'.format(license))

for clsfr in self.classifiers:
fp.write(u'Classifier: {}\n'.format(clsfr))

for file in self.license_files:
fp.write(u'License-File: {}\n'.format(file))

for req in self.requires_dist:
normalised_req = self._normalise_requires_dist(req)
fp.write(u'Requires-Dist: {}\n'.format(normalised_req))
167 changes: 148 additions & 19 deletions flit_core/flit_core/config.py
Original file line number Diff line number Diff line change
@@ -60,6 +60,7 @@ class ConfigError(ValueError):
'readme',
'requires-python',
'license',
'license-files',
'authors',
'maintainers',
'keywords',
@@ -73,6 +74,9 @@ class ConfigError(ValueError):
'dynamic',
}

default_license_files_globs = ['COPYING*', 'LICEN[CS]E*']
license_files_allowed_chars = re.compile(r'^[\w\-\.\/\*\?\[\]]+$')


def read_flit_config(path):
"""Read and check the `pyproject.toml` file with data about the package.
@@ -427,6 +431,15 @@ def _prep_metadata(md_sect, path):
# For internal use, record the main requirements as a '.none' extra.
res.reqs_by_extra['.none'] = reqs_noextra

if path:
license_files = sorted(
_license_files_from_globs(
path.parent, default_license_files_globs, warn_no_files=False
)
)
res.referenced_files.extend(license_files)
md_dict['license_files'] = license_files

return res

def _expand_requires_extra(re):
@@ -439,12 +452,57 @@ def _expand_requires_extra(re):
yield '{} ; extra == "{}"'.format(req, extra)


def _license_files_from_globs(project_dir: Path, globs, warn_no_files = True):
license_files = set()
for pattern in globs:
if isabs_ish(pattern):
raise ConfigError(
"Invalid glob pattern for [project.license-files]: '{}'. "
"Pattern must not start with '/'.".format(pattern)
)
if ".." in pattern:
raise ConfigError(
"Invalid glob pattern for [project.license-files]: '{}'. "
"Pattern must not contain '..'".format(pattern)
)
if license_files_allowed_chars.match(pattern) is None:
raise ConfigError(
"Invalid glob pattern for [project.license-files]: '{}'. "
"Pattern contains invalid characters. "
"https://packaging.python.org/en/latest/specifications/pyproject-toml/#license-files"
)
try:
files = [
str(file.relative_to(project_dir)).replace(osp.sep, "/")
for file in project_dir.glob(pattern)
if file.is_file()
]
except ValueError as ex:
raise ConfigError(
"Invalid glob pattern for [project.license-files]: '{}'. {}".format(pattern, ex.args[0])
)

if not files and warn_no_files:
raise ConfigError(
"No files found for [project.license-files]: '{}' pattern".format(pattern)
)
license_files.update(files)
return license_files

def _check_type(d, field_name, cls):
if not isinstance(d[field_name], cls):
raise ConfigError(
"{} field should be {}, not {}".format(field_name, cls, type(d[field_name]))
)

def _check_types(d, field_name, cls_list) -> None:
if not isinstance(d[field_name], cls_list):
raise ConfigError(
"{} field should be {}, not {}".format(
field_name, ' or '.join(map(str, cls_list)), type(d[field_name])
)
)

def _check_list_of_str(d, field_name):
if not isinstance(d[field_name], list) or not all(
isinstance(e, str) for e in d[field_name]
@@ -525,31 +583,59 @@ def read_pep621_metadata(proj, path) -> LoadedConfig:
if 'requires-python' in proj:
md_dict['requires_python'] = proj['requires-python']

license_files = set()
if 'license' in proj:
_check_type(proj, 'license', dict)
license_tbl = proj['license']
unrec_keys = set(license_tbl.keys()) - {'text', 'file'}
if unrec_keys:
raise ConfigError(
"Unrecognised keys in [project.license]: {}".format(unrec_keys)
)
_check_types(proj, 'license', (str, dict))
if isinstance(proj['license'], str):
md_dict['license_expression'] = normalize_license_expr(proj['license'])
else:
license_tbl = proj['license']
unrec_keys = set(license_tbl.keys()) - {'text', 'file'}
if unrec_keys:
raise ConfigError(
"Unrecognised keys in [project.license]: {}".format(unrec_keys)
)

# TODO: Do something with license info.
# The 'License' field in packaging metadata is a brief description of
# a license, not the full text or a file path. PEP 639 will improve on
# how licenses are recorded.
if 'file' in license_tbl:
if 'text' in license_tbl:
# The 'License' field in packaging metadata is a brief description of
# a license, not the full text or a file path.
if 'file' in license_tbl:
if 'text' in license_tbl:
raise ConfigError(
"[project.license] should specify file or text, not both"
)
license_f = license_tbl['file']
if isabs_ish(license_f):
raise ConfigError(
f"License file path ({license_f}) cannot be an absolute path"
)
if not (path.parent / license_f).is_file():
raise ConfigError(f"License file {license_f} does not exist")
license_files.add(license_tbl['file'])
elif 'text' in license_tbl:
pass
else:
raise ConfigError(
"[project.license] should specify file or text, not both"
"file or text field required in [project.license] table"
)
lc.referenced_files.append(license_tbl['file'])
elif 'text' in license_tbl:
pass
else:

if 'license-files' in proj:
_check_type(proj, 'license-files', list)
globs = proj['license-files']
license_files = _license_files_from_globs(path.parent, globs)
if isinstance(proj.get('license'), dict):
raise ConfigError(
"file or text field required in [project.license] table"
"license-files cannot be used with a license table, "
"use 'project.license' with a license expression instead"
)
else:
license_files.update(
_license_files_from_globs(
path.parent, default_license_files_globs, warn_no_files=False
)
)
license_files_sorted = sorted(license_files)
lc.referenced_files.extend(license_files_sorted)
md_dict['license_files'] = license_files_sorted

if 'authors' in proj:
_check_type(proj, 'authors', list)
@@ -565,6 +651,16 @@ def read_pep621_metadata(proj, path) -> LoadedConfig:

if 'classifiers' in proj:
_check_list_of_str(proj, 'classifiers')
classifiers = proj['classifiers']
license_expr = md_dict.get('license_expression', None)
if license_expr:
for cl in classifiers:
if not cl.startswith('License :: '):
continue
raise ConfigError(
"License classifiers are deprecated in favor of the license expression. "
"Remove the '{}' classifier".format(cl)
)
md_dict['classifiers'] = proj['classifiers']

if 'urls' in proj:
@@ -718,3 +814,36 @@ def isabs_ish(path):
absolute paths, we also want to reject these odd halfway paths.
"""
return os.path.isabs(path) or path.startswith(('/', '\\'))


def normalize_license_expr(s: str):
"""Validate & normalise an SPDX license expression
For now this only handles simple expressions (referring to 1 license)
"""
from ._spdx_data import licenses
ls = s.lower()
if ls.startswith('licenseref-'):
ref = s.partition('-')[2]
if re.match(r'([a-zA-Z0-9\-.])+$', ref):
# Normalise case of LicenseRef, leave the rest alone
return "LicenseRef-" + ref
raise ConfigError(
"LicenseRef- license expression can only contain ASCII letters "
"& digits, - and ."
)

or_later = s.endswith('+')
if or_later:
ls = ls[:-1]

try:
info = licenses[ls]
except KeyError:
if os.environ.get('FLIT_ALLOW_INVALID'):
log.warning("Invalid license ID {!r} allowed by FLIT_ALLOW_INVALID"
.format(s))
return s
raise ConfigError(f"{s!r} is not a recognised SPDX license ID")

return info['id'] + ('+' if or_later else '')
6 changes: 2 additions & 4 deletions flit_core/flit_core/wheel.py
Original file line number Diff line number Diff line change
@@ -183,10 +183,8 @@ def write_metadata(self):
with self._write_to_zip(self.dist_info + '/entry_points.txt') as f:
common.write_entry_points(self.entrypoints, f)

for base in ('COPYING', 'LICENSE'):
for path in sorted(self.directory.glob(base + '*')):
if path.is_file():
self._add_file(path, '%s/%s' % (self.dist_info, path.name))
for file in self.metadata.license_files:
self._add_file(self.directory / file, '%s/licenses/%s' % (self.dist_info, file))

with self._write_to_zip(self.dist_info + '/WHEEL') as f:
_write_wheel_file(f, supports_py2=self.metadata.supports_py2)
4 changes: 2 additions & 2 deletions flit_core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -12,9 +12,9 @@ description = "Distribution-building parts of Flit. See flit package for more in
dependencies = []
requires-python = '>=3.6'
readme = "README.rst"
license = {file = "LICENSE"}
license = "BSD-3-Clause"
license-files = ["LICENSE*", "flit_core/vendor/**/LICENSE*"]
classifiers = [
"License :: OSI Approved :: BSD License",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dynamic = ["version"]
1 change: 1 addition & 0 deletions flit_core/tests_core/samples/pep621_license_files/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file should be added to wheels
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Readme
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file should be added to wheels
3 changes: 3 additions & 0 deletions flit_core/tests_core/samples/pep621_license_files/module1a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Example module"""

__version__ = '0.1'
39 changes: 39 additions & 0 deletions flit_core/tests_core/samples/pep621_license_files/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"

[project]
name = "module1"
authors = [
{name = "Sir Röbin", email = "robin@camelot.uk"}
]
maintainers = [
{name = "Sir Galahad"}
]
readme = "README.rst"
license-files = ["**/LICENSE*"]
requires-python = ">=3.7"
dependencies = [
"requests >= 2.18",
"docutils",
]
keywords = ["example", "test"]
dynamic = [
"version",
"description",
]

[project.optional-dependencies]
test = [
"pytest",
"mock; python_version<'3.6'"
]

[project.urls]
homepage = "http://github.com/sirrobin/module1"

[project.entry-points.flit_test_example]
foo = "module1:main"

[tool.flit.module]
name = "module1a"
24 changes: 24 additions & 0 deletions flit_core/tests_core/test_common.py
Original file line number Diff line number Diff line change
@@ -205,3 +205,27 @@ def test_metadata_2_3_provides_extra(provides_extra, expected_result):
msg = email.parser.Parser(policy=email.policy.compat32).parse(sio)
assert msg['Provides-Extra'] == expected_result
assert not msg.defects

@pytest.mark.parametrize(
('value', 'expected_license', 'expected_license_expression'),
[
({'license': 'MIT'}, 'MIT', None),
({'license_expression': 'MIT'}, None, 'MIT'),
({'license_expression': 'Apache-2.0'}, None, 'Apache-2.0'),
],
)
def test_metadata_license(value, expected_license, expected_license_expression):
d = {
'name': 'foo',
'version': '1.0',
**value,
}
md = Metadata(d)
sio = StringIO()
md.write_metadata_file(sio)
sio.seek(0)

msg = email.parser.Parser(policy=email.policy.compat32).parse(sio)
assert msg.get('License') == expected_license
assert msg.get('License-Expression') == expected_license_expression
assert not msg.defects
88 changes: 86 additions & 2 deletions flit_core/tests_core/test_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import sys
from pathlib import Path
import pytest

@@ -139,6 +140,33 @@ def test_bad_include_paths(path, err_match):
({'license': {'fromage': 2}}, '[Uu]nrecognised'),
({'license': {'file': 'LICENSE', 'text': 'xyz'}}, 'both'),
({'license': {}}, 'required'),
({'license': 1}, "license field should be <class 'str'> or <class 'dict'>, not <class 'int'>"),
# ({'license': "MIT License"}, "Invalid license expression: 'MIT License'"), # TODO
(
{'license': 'MIT', 'classifiers': ['License :: OSI Approved :: MIT License']},
"License classifiers are deprecated in favor of the license expression",
),
({'license-files': 1}, r"\blist\b"),
({'license-files': ["/LICENSE"]}, r"'/LICENSE'.+must not start with '/'"),
({'license-files': ["../LICENSE"]}, r"'../LICENSE'.+must not contain '..'"),
({'license-files': ["NOT_FOUND"]}, r"No files found.+'NOT_FOUND'"),
({'license-files': ["(LICENSE | LICENCE)"]}, "Pattern contains invalid characters"),
pytest.param(
{'license-files': ["**LICENSE"]}, r"'\*\*LICENSE'.+Invalid pattern",
marks=[pytest.mark.skipif(
sys.version_info >= (3, 13), reason="Pattern is valid for 3.13+"
)]
),
pytest.param(
{'license-files': ["./"]}, r"'./'.+Unacceptable pattern",
marks=[pytest.mark.skipif(
sys.version_info < (3, 13), reason="Pattern started to raise ValueError in 3.13"
)]
),
(
{'license': {'file': 'LICENSE'}, 'license-files': ["LICENSE"]},
"license-files cannot be used with a license table",
),
({'keywords': 'foo'}, 'list'),
({'keywords': ['foo', 7]}, 'strings'),
({'entry-points': {'foo': 'module1:main'}}, 'entry-point.*tables'),
@@ -161,7 +189,7 @@ def test_bad_pep621_info(proj_bad, err_match):
proj = {'name': 'module1', 'version': '1.0', 'description': 'x'}
proj.update(proj_bad)
with pytest.raises(config.ConfigError, match=err_match):
config.read_pep621_metadata(proj, samples_dir / 'pep621')
config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')

@pytest.mark.parametrize(('readme', 'err_match'), [
({'file': 'README.rst'}, 'required'),
@@ -177,4 +205,60 @@ def test_bad_pep621_readme(readme, err_match):
'name': 'module1', 'version': '1.0', 'description': 'x', 'readme': readme
}
with pytest.raises(config.ConfigError, match=err_match):
config.read_pep621_metadata(proj, samples_dir / 'pep621')
config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


@pytest.mark.parametrize(('value', 'license_expression'), [
# Accept and normalize valid SPDX expressions for 'license = ...'
("mit", "MIT"),
("apache-2.0", "Apache-2.0"),
("APACHE-2.0+", "Apache-2.0+"),
# TODO: compound expressions
#("mit and (apache-2.0 or bsd-2-clause)", "MIT AND (Apache-2.0 OR BSD-2-Clause)"),
# LicenseRef expressions: only the LicenseRef is normalised
("LiceNseref-Public-DoMain", "LicenseRef-Public-DoMain"),
])
def test_license_expr(value, license_expression):
proj = {
'name': 'module1', 'version': '1.0', 'description': 'x', 'license': value
}
info = config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')
assert 'license' not in info.metadata
assert info.metadata['license_expression'] == license_expression

def test_license_expr_error():
proj = {
'name': 'module1', 'version': '1.0', 'description': 'x',
'license': 'LicenseRef-foo_bar', # Underscore not allowed
}
with pytest.raises(config.ConfigError, match="can only contain"):
config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')

proj['license'] = "BSD-33-Clause" # Not a real license
with pytest.raises(config.ConfigError, match="recognised"):
config.read_pep621_metadata(proj, samples_dir / 'pep621' / 'pyproject.toml')


def test_license_file_defaults_with_old_metadata():
metadata = {'module': 'mymod', 'author': ''}
info = config._prep_metadata(metadata, samples_dir / 'pep621_license_files' / 'pyproject.toml')
assert info.metadata['license_files'] == ["LICENSE"]


@pytest.mark.parametrize(('proj_license_files', 'files'), [
({}, ["LICENSE"]), # Only match default patterns
({'license-files': []}, []),
({'license-files': ["LICENSE"]}, ["LICENSE"]),
({'license-files': ["LICENSE*"]}, ["LICENSE"]),
({'license-files': ["LICEN[CS]E*"]}, ["LICENSE"]),
({'license-files': ["**/LICENSE*"]}, ["LICENSE", "module/vendor/LICENSE_VENDOR"]),
({'license-files': ["module/vendor/LICENSE*"]}, ["module/vendor/LICENSE_VENDOR"]),
({'license-files': ["LICENSE", "module/**/LICENSE*"]}, ["LICENSE", "module/vendor/LICENSE_VENDOR"]),
# Add project.license.file + match default patterns
({'license': {'file': 'module/vendor/LICENSE_VENDOR'}}, ["LICENSE", "module/vendor/LICENSE_VENDOR"]),
])
def test_pep621_license_files(proj_license_files, files):
proj = {'name': 'module1', 'version': '1.0', 'description': 'x'}
proj.update(proj_license_files)
info = config.read_pep621_metadata(proj, samples_dir / 'pep621_license_files' / 'pyproject.toml')
assert info.metadata['license_files'] == files
8 changes: 8 additions & 0 deletions flit_core/tests_core/test_wheel.py
Original file line number Diff line number Diff line change
@@ -45,3 +45,11 @@ def test_data_dir(tmp_path):
assert_isfile(info.file)
with ZipFile(info.file, 'r') as zf:
assert 'module1-0.1.data/data/share/man/man1/foo.1' in zf.namelist()


def test_license_files(tmp_path):
info = make_wheel_in(samples_dir / 'pep621_license_files' / 'pyproject.toml', tmp_path)
assert_isfile(info.file)
with ZipFile(info.file, 'r') as zf:
assert 'module1-0.1.dist-info/licenses/LICENSE' in zf.namelist()
assert 'module1-0.1.dist-info/licenses/module/vendor/LICENSE_VENDOR' in zf.namelist()
24 changes: 24 additions & 0 deletions prepare_license_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Call with path to SPDX license-list-data repo, cloned from:
# https://github.com/spdx/license-list-data

import json
import pprint
import sys
from pathlib import Path

list_data_repo = Path(sys.argv[1])
with (list_data_repo / 'json' / 'licenses.json').open('rb') as f:
licenses_json = json.load(f)

condensed = {
l['licenseId'].lower() : {'id': l['licenseId']}
for l in licenses_json['licenses']
if not l['isDeprecatedLicenseId']
}

with Path('flit_core', 'flit_core', '_spdx_data.py').open('w') as f:
f.write("# This file is generated from SPDX license data; don't edit it manually.\n\n")

f.write("licenses = \\\n")
pprint.pprint(condensed, f)

8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["flit_core >=3.10.1,<4"]
requires = ["flit_core >=3.11,<4"]
build-backend = "flit_core.buildapi"

[project]
@@ -8,17 +8,17 @@ authors = [
{name = "Thomas Kluyver", email = "thomas@kluyver.me.uk"},
]
dependencies = [
"flit_core >=3.10.1",
"flit_core >=3.11.0",
"requests",
"docutils",
"tomli-w",
"pip",
]
requires-python = ">=3.8"
readme = "README.rst"
license = {file = "LICENSE"}
license = "BSD-3-Clause"
license-files = ["LICENSE"]
classifiers = ["Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Topic :: Software Development :: Libraries :: Python Modules",
]
3 changes: 2 additions & 1 deletion tests/test_init.py
Original file line number Diff line number Diff line change
@@ -113,7 +113,8 @@ def test_init():
data = tomllib.load(f)
assert data['project']['authors'][0]['email'] == "test@example.com"
license = Path(td) / 'LICENSE'
assert data['project']['license']['file'] == 'LICENSE'
assert data['project']['license'] == 'MIT'
assert data['project']['license-files'] == ['LICENSE']
assert_isfile(license)
with license.open() as f:
license_text = f.read()
2 changes: 1 addition & 1 deletion tests/test_wheel.py
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ def test_wheel_src_module(copy_sample):
with unpack(whl_file) as unpacked:
assert_isfile(Path(unpacked, 'module3.py'))
assert_isdir(Path(unpacked, 'module3-0.1.dist-info'))
assert_isfile(Path(unpacked, 'module3-0.1.dist-info', 'LICENSE'))
assert_isfile(Path(unpacked, 'module3-0.1.dist-info', 'licenses', 'LICENSE'))

def test_editable_wheel_src_module(copy_sample):
td = copy_sample('module3')
6 changes: 3 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -28,17 +28,17 @@ setenv =
PYTHONPATH = flit_core

commands =
python -m pytest --cov=flit --cov=flit_core/flit_core
python -m pytest --cov=flit --cov=flit_core/flit_core {posargs}

# Python 3.6: only test flit_core
[testenv:py36]
commands =
python -m pytest --cov=flit_core/flit_core flit_core
python -m pytest --cov=flit_core/flit_core flit_core {posargs}

# Python 3.7: only test flit_core
[testenv:py37]
commands =
python -m pytest --cov=flit_core/flit_core flit_core
python -m pytest --cov=flit_core/flit_core flit_core {posargs}

[testenv:bootstrap]
skip_install = true