Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-pyproject-metadata for
openSUSE:Factory checked in at 2026-01-27 16:06:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pyproject-metadata (Old)
and /work/SRC/openSUSE:Factory/.python-pyproject-metadata.new.1928 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyproject-metadata"
Tue Jan 27 16:06:53 2026 rev:6 rq:1329163 version:0.10.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-pyproject-metadata/python-pyproject-metadata.changes
2025-03-16 18:58:38.568610389 +0100
+++
/work/SRC/openSUSE:Factory/.python-pyproject-metadata.new.1928/python-pyproject-metadata.changes
2026-01-27 16:07:02.032805235 +0100
@@ -1,0 +2,17 @@
+Mon Jan 26 08:23:32 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.10.0:
+ * This release adds support for PEP 794 (METADATA 2.5),
+ the new import-names(paces) fields. Support hasn't rolled out
+ in other packages yet, but once it does, you can be ready for
+ it with this release.
+ * As usual, nothing changes if you don't specify the new fields
+ or the new METADATA version.
+ * Add PyPy 3.11 testing
+ * Add Python 3.14 classifier
+ * Use PEP 639 license
+ * Use dependency groups
+ * Enable branch coverage
+ * Enabled most Ruff linting rules on codebase
+
+-------------------------------------------------------------------
Old:
----
pyproject-metadata-0.9.1.tar.gz
New:
----
pyproject-metadata-0.10.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-pyproject-metadata.spec ++++++
--- /var/tmp/diff_new_pack.kbJVkX/_old 2026-01-27 16:07:02.612829696 +0100
+++ /var/tmp/diff_new_pack.kbJVkX/_new 2026-01-27 16:07:02.616829864 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-pyproject-metadata
#
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-pyproject-metadata
-Version: 0.9.1
+Version: 0.10.0
Release: 0
Summary: PEP 621 metadata parsing
License: MIT
++++++ pyproject-metadata-0.9.1.tar.gz -> pyproject-metadata-0.10.0.tar.gz
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/.github/release.yml
new/pyproject-metadata-0.10.0/.github/release.yml
--- old/pyproject-metadata-0.9.1/.github/release.yml 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/.github/release.yml 2025-11-21
16:26:50.000000000 +0100
@@ -1,5 +1,5 @@
changelog:
exclude:
authors:
- - dependabot
- - pre-commit-ci
+ - dependabot[bot]
+ - pre-commit-ci[bot]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/.github/workflows/checks.yml
new/pyproject-metadata-0.10.0/.github/workflows/checks.yml
--- old/pyproject-metadata-0.9.1/.github/workflows/checks.yml 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/.github/workflows/checks.yml 2025-11-21
16:26:50.000000000 +0100
@@ -10,15 +10,14 @@
mypy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
-
- - name: Install nox
- run: pipx install nox
+ - uses: actions/checkout@v5
- name: Setup Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: 3.8
+ - uses: astral-sh/setup-uv@v7
+
- name: Run check for type
- run: nox -s mypy
+ run: uv run noxfile.py -s mypy
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/.github/workflows/release.yml
new/pyproject-metadata-0.10.0/.github/workflows/release.yml
--- old/pyproject-metadata-0.9.1/.github/workflows/release.yml 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/.github/workflows/release.yml 2025-11-21
16:26:50.000000000 +0100
@@ -18,7 +18,7 @@
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: hynek/build-and-inspect-python-package@v2
# Upload to real PyPI on GitHub Releases.
@@ -34,13 +34,13 @@
steps:
- name: Download packages built by build-and-inspect-python-package
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v6
with:
name: Packages
path: dist
- name: Generate artifact attestation for sdist and wheel
- uses:
actions/attest-build-provenance@bd77c077858b8d561b7a36cbe48ef4cc642ca39d #
v2.2.2
+ uses:
actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a #
v3.0.0
with:
subject-path: "dist/pyproject*"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/.github/workflows/tests.yml
new/pyproject-metadata-0.10.0/.github/workflows/tests.yml
--- old/pyproject-metadata-0.9.1/.github/workflows/tests.yml 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/.github/workflows/tests.yml 2025-11-21
16:26:50.000000000 +0100
@@ -28,41 +28,38 @@
- "3.13"
- "3.14"
include:
- - os: macos-13
- python: "3.7"
+ - os: macos-15-intel
+ python: "3.8"
- os: macos-14
python: "3.12"
- os: ubuntu-latest
- python: "pypy-3.10"
+ python: "pypy-3.11"
- os: windows-latest
python: "3.8"
- os: windows-latest
python: "3.11"
- os: windows-latest
python: "3.13"
- - os: ubuntu-22.04
- python: "3.8"
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Set up target Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
- name: Install the latest version of uv
- uses: astral-sh/setup-uv@v5
-
- - name: Limit virtualenv on 3.7
- if: matrix.python == '3.7'
- run: pipx inject --force nox 'virtualenv<20.27.0'
+ uses: astral-sh/setup-uv@v7
- name: Run tests
run: uv run noxfile.py -s test-${{ matrix.python }}
+ - name: Run minimum tests
+ run: uv run noxfile.py -s minimums-${{ matrix.python }}
+
- name: Send coverage report
uses: codecov/codecov-action@v5
env:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/.gitignore
new/pyproject-metadata-0.10.0/.gitignore
--- old/pyproject-metadata-0.9.1/.gitignore 2025-03-10 23:10:43.000000000
+0100
+++ new/pyproject-metadata-0.10.0/.gitignore 2025-11-21 16:26:50.000000000
+0100
@@ -158,3 +158,6 @@
# and can be added to the global gitignore or merged into this file. For a
more nuclear
# option (not recommended) you can uncomment the following to ignore the
entire idea folder.
#.idea/
+
+*pylock.toml
+uv.lock
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/.pre-commit-config.yaml
new/pyproject-metadata-0.10.0/.pre-commit-config.yaml
--- old/pyproject-metadata-0.9.1/.pre-commit-config.yaml 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/.pre-commit-config.yaml 2025-11-21
16:26:50.000000000 +0100
@@ -6,7 +6,7 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: check-ast
- id: check-builtin-literals
@@ -19,9 +19,9 @@
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: "v0.9.10"
+ rev: "v0.14.5"
hooks:
- - id: ruff
+ - id: ruff-check
args: ["--fix", "--show-fixes"]
- id: ruff-format
@@ -33,20 +33,20 @@
- id: rst-inline-touching-normal
- repo: https://github.com/adamchainz/blacken-docs
- rev: 1.19.1
+ rev: 1.20.0
hooks:
- id: blacken-docs
- additional_dependencies: [black==24.*]
+ additional_dependencies: [black==25.*]
- repo: https://github.com/rbubley/mirrors-prettier
- rev: "v3.5.3"
+ rev: "v3.6.2"
hooks:
- id: prettier
types_or: [yaml, markdown, html, css, scss, javascript, json]
args: [--prose-wrap=always]
- repo: https://github.com/henryiii/check-sdist
- rev: "v1.2.0"
+ rev: "v1.3.0"
hooks:
- id: check-sdist
args: [--inject-junk]
@@ -60,17 +60,17 @@
exclude:
^(LICENSE$|src/scikit_build_core/resources/find_python|tests/test_skbuild_settings.py$)
- repo: https://github.com/shellcheck-py/shellcheck-py
- rev: v0.10.0.1
+ rev: v0.11.0.1
hooks:
- id: shellcheck
- repo: https://github.com/henryiii/validate-pyproject-schema-store
- rev: 2025.03.03
+ rev: 2025.11.14
hooks:
- id: validate-pyproject
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.31.3
+ rev: 0.35.0
hooks:
- id: check-dependabot
- id: check-github-workflows
@@ -79,6 +79,6 @@
files: \.schema\.json
- repo: https://github.com/scientific-python/cookie
- rev: 2025.01.22
+ rev: 2025.11.10
hooks:
- id: sp-repo-review
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/.readthedocs.yml
new/pyproject-metadata-0.10.0/.readthedocs.yml
--- old/pyproject-metadata-0.9.1/.readthedocs.yml 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/.readthedocs.yml 2025-11-21
16:26:50.000000000 +0100
@@ -1,12 +1,15 @@
version: 2
+sphinx:
+ configuration: docs/conf.py
+
build:
- os: ubuntu-22.04
+ os: "ubuntu-22.04"
tools:
python: "3.12"
-
-python:
- install:
- - method: pip
- path: .
- extra_requirements: [docs]
+ commands:
+ - asdf plugin add uv
+ - asdf install uv latest
+ - asdf global uv latest
+ - uv run --group docs sphinx-build -T -b html -d docs/_build/doctrees -D
+ language=en docs $READTHEDOCS_OUTPUT/html
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/README.md
new/pyproject-metadata-0.10.0/README.md
--- old/pyproject-metadata-0.9.1/README.md 2025-03-10 23:10:43.000000000
+0100
+++ new/pyproject-metadata-0.10.0/README.md 2025-11-21 16:26:50.000000000
+0100
@@ -47,7 +47,7 @@
)
```
-A backend is also expected to copy entries from `project.licence_files`, which
+A backend is also expected to copy entries from `project.license_files`, which
are paths relative to the project directory, into the `dist-info/licenses`
folder, preserving the original source structure.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/docs/changelog.md
new/pyproject-metadata-0.10.0/docs/changelog.md
--- old/pyproject-metadata-0.9.1/docs/changelog.md 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/docs/changelog.md 2025-11-21
16:26:50.000000000 +0100
@@ -1,9 +1,31 @@
# Changelog
-## Unreleased
+## 0.10.0 (11-21-2025)
+This release adds support for [PEP 794](https://peps.python.org/pep-0794/)
+(METADATA 2.5), the new import-names(paces) fields. Support hasn't rolled out
in
+other packages yet, but once it does, you can be ready for it with this
release.
+As usual, nothing changes if you don't specify the new fields or the new
+METADATA version.
+
+Features:
+
+- Support `import-names(paces)`
- Remove Python 3.7 support
+Fixes:
+
+- Minimum supported version of packaging corrected (now tested)
+
+Internal and CI:
+
+- Add PyPy 3.11 testing
+- Add Python 3.14 classifier
+- Use PEP 639 license
+- Use dependency groups
+- Enable branch coverage
+- Enabled most Ruff linting rules on codebase
+
## 0.9.1 (10-03-2024)
This release fixes form feeds in License files using pre-PEP 639 syntax when
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/noxfile.py
new/pyproject-metadata-0.10.0/noxfile.py
--- old/pyproject-metadata-0.9.1/noxfile.py 2025-03-10 23:10:43.000000000
+0100
+++ new/pyproject-metadata-0.10.0/noxfile.py 2025-11-21 16:26:50.000000000
+0100
@@ -1,3 +1,4 @@
+#!/usr/bin/env -S uv run --script
# SPDX-License-Identifier: MIT
# /// script
@@ -5,17 +6,16 @@
# ///
import argparse
-import os
-import os.path
+from pathlib import Path
import nox
nox.needs_version = ">=2025.2.9"
-nox.options.reuse_existing_virtualenvs = True
nox.options.default_venv_backend = "uv|virtualenv"
-ALL_PYTHONS =
nox.project.python_versions(nox.project.load_toml("pyproject.toml"))
-ALL_PYTHONS += ["3.14", "pypy-3.10"]
+PYPROJECT = nox.project.load_toml("pyproject.toml")
+ALL_PYTHONS = nox.project.python_versions(PYPROJECT)
+ALL_PYTHONS += ["pypy-3.11"]
@nox.session(python="3.8")
@@ -23,9 +23,8 @@
"""
Run a type checker.
"""
- session.install(".", "mypy", "nox", "pytest")
-
- session.run("mypy", "pyproject_metadata", "tests", "noxfile.py")
+ session.install("-e.", "mypy", "nox", "pytest")
+ session.run("mypy")
@nox.session(python=ALL_PYTHONS)
@@ -33,12 +32,11 @@
"""
Run the test suite.
"""
- htmlcov_output = os.path.join(session.virtualenv.location, "htmlcov")
- xmlcov_output = os.path.join(
- session.virtualenv.location, f"coverage-{session.python}.xml"
- )
+ htmlcov_output = Path(session.virtualenv.location) / "htmlcov"
+ xmlcov_output = Path(session.virtualenv.location) /
f"coverage-{session.python}.xml"
- session.install("-e.[test]")
+ test_grp = nox.project.dependency_groups(PYPROJECT, "test")
+ session.install("-e.", *test_grp)
session.run(
"pytest",
@@ -52,12 +50,34 @@
)
[email protected](venv_backend="uv", default=False, python=ALL_PYTHONS)
+def minimums(session: nox.Session) -> None:
+ """
+ Check minimum requirements.
+ """
+ test_grp = nox.project.dependency_groups(PYPROJECT, "test")
+ session.install("-e.", "--resolution=lowest-direct", *test_grp,
silent=False)
+
+ xmlcov_output = (
+ Path(session.virtualenv.location) /
f"coverage-{session.python}-min.xml"
+ )
+
+ session.run(
+ "pytest",
+ "--cov",
+ f"--cov-report=xml:{xmlcov_output}",
+ "--cov-report=term-missing",
+ "--cov-context=test",
+ "tests/",
+ *session.posargs,
+ )
+
+
@nox.session(default=False)
def docs(session: nox.Session) -> None:
"""
Build the docs. Use "--non-interactive" to avoid serving. Pass "-b
linkcheck" to check links.
"""
-
parser = argparse.ArgumentParser()
parser.add_argument(
"-b", dest="builder", default="html", help="Build target (default:
html)"
@@ -66,7 +86,8 @@
serve = args.builder == "html" and session.interactive
extra_installs = ["sphinx-autobuild"] if serve else []
- session.install("-e.[docs]", *extra_installs)
+ docs_grp = nox.project.dependency_groups(PYPROJECT, "docs")
+ session.install("-e.", *docs_grp, *extra_installs)
session.chdir("docs")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/pyproject.toml
new/pyproject-metadata-0.10.0/pyproject.toml
--- old/pyproject-metadata-0.9.1/pyproject.toml 2025-03-10 23:10:43.000000000
+0100
+++ new/pyproject-metadata-0.10.0/pyproject.toml 2025-11-21
16:26:50.000000000 +0100
@@ -1,5 +1,5 @@
[build-system]
-requires = ["flit-core"]
+requires = ["flit-core>=3.11"]
build-backend = "flit_core.buildapi"
[project]
@@ -7,28 +7,32 @@
dynamic = ["version"]
description = "PEP 621 metadata parsing"
readme = "README.md"
-requires-python = ">=3.7"
+requires-python = ">=3.8"
+license = "MIT"
+license-files = ["LICENSE"]
authors = [
{ name = "Filipe Laíns", email = "[email protected]" },
]
classifiers = [
- "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
]
dependencies = [
- "packaging>=19.0",
- "typing_extensions; python_version<'3.8'",
+ "packaging>=23.2",
]
-[project.optional-dependencies]
+[project.urls]
+changelog = "https://pep621.readthedocs.io/en/stable/changelog.html"
+homepage = "https://github.com/pypa/pyproject-metadata"
+
+[dependency-groups]
docs = [
"furo>=2023.9.10",
"sphinx-autodoc-typehints>=1.10.0",
@@ -37,32 +41,25 @@
"myst-parser",
]
test = [
- "pytest-cov[toml]>=2",
- "pytest>=6.2.4",
- 'tomli>=1.0.0;python_version<"3.11"',
- 'exceptiongroup;python_version<"3.11"', # Optional
+ "pytest-cov>=4",
+ "pytest>=7.4; python_version>='3.12'",
+ "pytest>=7; python_version<'3.12'",
+ 'tomli>=1.1;python_version<"3.11"',
+ 'exceptiongroup>=1.0;python_version<"3.11"', # Optional
]
+dev = [{include-group = "test"}]
-[project.urls]
-changelog = "https://pep621.readthedocs.io/en/stable/changelog.html"
-homepage = "https://github.com/pypa/pyproject-metadata"
[tool.flit.sdist]
include = ["LICENSE", "tests/**", "docs/**", ".gitignore"]
-[tool.uv]
-dev-dependencies = ["pyproject-metadata[test]"]
-environments = [
- "python_version >= '3.10'",
-]
-
[tool.pytest.ini_options]
-minversion = "6.0"
+minversion = "7.0"
addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
xfail_strict = true
filterwarnings = ["error"]
-log_cli_level = "info"
+log_level = "INFO"
testpaths = ["tests"]
@@ -70,44 +67,49 @@
strict = true
warn_unreachable = false
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
+files = ["pyproject_metadata", "tests", "noxfile.py"]
+[tool.ruff]
+show-fixes = true
+
[tool.ruff.lint]
-extend-select = [
- "C90", # mccabe
- "B", # flake8-bugbear
- "I", # isort
- "ARG", # flake8-unused-arguments
- "C4", # flake8-comprehensions
- "ICN", # flake8-import-conventions
- "ISC", # flake8-implicit-str-concat
- "EM", # flake8-errmsg
- "G", # flake8-logging-format
- "PGH", # pygrep-hooks
- "PIE", # flake8-pie
- "PL", # pylint
- "PT", # flake8-pytest-style
- "RET", # flake8-return
- "RUF", # Ruff-specific
- "SIM", # flake8-simplify
- "T20", # flake8-print
- "UP", # pyupgrade
- "YTT", # flake8-2020
- "EXE", # flake8-executable
- "NPY", # NumPy specific rules
- "PD", # pandas-vet
-]
+select = ["ALL"]
ignore = [
- "ISC001", # conflicts with formatter
+ "S101", # Asserts are used by mypy and pytest
"PLR09", # Design related (too many X)
"PLR2004", # Magic value in comparison
+ "COM812", # Trailing commas inform the formatter
+ "E501", # Not worried about long lines in strings
+ "D200", # Don't force single line docstring (for now)
+ "D205", # Some of our summaries are more than a line (for now)
+]
+flake8-builtins.ignorelist = ["copyright"]
+pydocstyle.convention = "pep257"
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**" = [
+ "PTH123", # Path.open not needed for simple files
+ "D", # Tests don't need docs
+ "INP001", # Tests don't need an __init__
+ "FBT001", # Bools are fine in test fixutres/params
+ "SLF001", # Tests can access private members
+]
+"docs/**" = [
+ "INP001", # Docs don't need an __init__
+ "ERA001", # Commented out code in conf.py
+ "D",
]
+"noxfile.py" = ["D"]
[tool.ruff.format]
docstring-code-format = true
[tool.coverage]
+run.branch = true
+run.source_pkgs = ["pyproject_metadata"]
html.show_contexts = true
+report.show_missing = true
report.exclude_also = [
"if typing.TYPE_CHECKING:",
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/pyproject_metadata/__init__.py
new/pyproject-metadata-0.10.0/pyproject_metadata/__init__.py
--- old/pyproject-metadata-0.9.1/pyproject_metadata/__init__.py 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/pyproject_metadata/__init__.py
2025-11-21 16:26:50.000000000 +0100
@@ -37,6 +37,8 @@
import email.message
import email.policy
import email.utils
+import itertools
+import keyword
import os
import os.path
import pathlib
@@ -50,7 +52,7 @@
from .pyproject import License, PyProjectReader, Readme
if typing.TYPE_CHECKING:
- from collections.abc import Mapping
+ from collections.abc import Generator, Mapping
from typing import Any
from packaging.requirements import Requirement
@@ -74,7 +76,7 @@
RE_EOL_BYTES = re.compile(rb"[\r\n]+")
-__version__ = "0.9.1"
+__version__ = "0.10.0"
__all__ = [
"ConfigurationError",
@@ -137,7 +139,7 @@
message: email.message.Message
def __setitem__(self, name: str, value: str | None) -> None:
- if not value:
+ if value is None:
return
self.message[name] = value
@@ -186,6 +188,9 @@
max_line_length = 0
def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
+ """
+ Require known headers, and replace newlines with spaces.
+ """
if name.lower() not in constants.KNOWN_METADATA_FIELDS:
msg = f"Unknown field {name!r}"
raise ConfigurationError(msg, key=name)
@@ -196,7 +201,10 @@
if sys.version_info < (3, 12, 4):
# Work around Python bug
https://github.com/python/cpython/issues/117313
def _fold(
- self, name: str, value: Any, refold_binary: bool = False
+ self,
+ name: str,
+ value: Any, # noqa: ANN401
+ refold_binary: bool = False, # noqa: FBT001, FBT002
) -> str: # pragma: no cover
if hasattr(value, "name"):
return value.fold(policy=self) # type: ignore[no-any-return]
@@ -205,7 +213,6 @@
# this is from the library version, and it improperly breaks on
chars like 0x0c, treating
# them as 'form feed' etc.
# we need to ensure that only CR/LF is used as end of line
- # lines = value.splitlines()
# this is a workaround which splits only on CR/LF characters
if isinstance(value, bytes):
@@ -220,11 +227,52 @@
or any(len(x) > maxlen for x in lines[1:])
)
)
- if refold or (refold_binary and
email.policy._has_surrogates(value)): # type: ignore[attr-defined]
+ if refold or (
+ refold_binary and email.policy._has_surrogates(value) # type:
ignore[attr-defined] # noqa: SLF001
+ ):
return self.header_factory(name,
"".join(lines)).fold(policy=self) # type: ignore[arg-type,no-any-return]
return name + ": " + self.linesep.join(lines) + self.linesep #
type: ignore[arg-type]
+def _validate_import_names(
+ names: list[str], key: str, *, errors: ErrorCollector
+) -> Generator[str, None, None]:
+ """
+ Return normalized names for comparisons.
+ """
+ for fullname in names:
+ name, simicolon, private = fullname.partition(";")
+ if simicolon and private.lstrip() != "private":
+ msg = "{key} contains an ending tag other than '; private', got
{value!r}"
+ errors.config_error(msg, key=key, value=fullname)
+ name = name.rstrip()
+
+ for ident in name.split("."):
+ if not ident.isidentifier():
+ msg = "{key} contains {value!r}, which is not a valid
identifier"
+ errors.config_error(msg, key=key, value=fullname)
+
+ elif keyword.iskeyword(ident):
+ msg = "{key} contains a Python keyword, which is not a valid
import name, got {value!r}"
+ errors.config_error(msg, key=key, value=fullname)
+
+ yield name
+
+
+def _validate_dotted_names(names: set[str], *, errors: ErrorCollector) -> None:
+ """
+ Check to make sure every name is accounted for. Takes the union of
de-tagged names.
+ """
+ for name in names:
+ for parent in itertools.accumulate(
+ name.split(".")[:-1], lambda a, b: f"{a}.{b}"
+ ):
+ if parent not in names:
+ msg = "{key} is missing {value!r}, but submodules are present
elsewhere"
+ errors.config_error(msg, key="project.import-namespaces",
value=parent)
+ continue
+
+
class RFC822Message(email.message.EmailMessage):
"""
This is :class:`email.message.EmailMessage` with two small changes: it
defaults to
@@ -233,13 +281,18 @@
"""
def __init__(self) -> None:
+ """
+ Create a new message with RFC822Policy.
+ """
super().__init__(policy=RFC822Policy())
def as_bytes(
- self, unixfrom: bool = False, policy: email.policy.Policy | None = None
+ self,
+ unixfrom: bool = False, # noqa: FBT001, FBT002
+ policy: email.policy.Policy | None = None,
) -> bytes:
"""
- This handles unicode encoding.
+ Will always handle unicode encoding.
"""
return self.as_string(unixfrom, policy=policy).encode("utf-8")
@@ -271,6 +324,8 @@
keywords: list[str] = dataclasses.field(default_factory=list)
scripts: dict[str, str] = dataclasses.field(default_factory=dict)
gui_scripts: dict[str, str] = dataclasses.field(default_factory=dict)
+ import_names: list[str] | None = None
+ import_namespaces: list[str] | None = None
dynamic: list[Dynamic] = dataclasses.field(default_factory=list)
"""
This field is used to track dynamic fields. You can't set a field not in
this list.
@@ -290,6 +345,9 @@
"""
def __post_init__(self) -> None:
+ """
+ Validate the fields on construction.
+ """
self.validate()
@property
@@ -301,6 +359,8 @@
if self.metadata_version is not None:
return self.metadata_version
+ if self.import_names is not None or self.import_namespaces is not None:
+ return "2.5"
if isinstance(self.license, str) or self.license_files is not None:
return "2.4"
if self.dynamic_metadata:
@@ -460,6 +520,12 @@
project.get("gui-scripts", {}), "project.gui-scripts"
)
or {},
+ import_names=pyproject.ensure_list(
+ project.get("import-names", None), "project.import-names"
+ ),
+ import_namespaces=pyproject.ensure_list(
+ project.get("import-namespaces", None),
"project.import-namespaces"
+ ),
dynamic=dynamic,
dynamic_metadata=dynamic_metadata or [],
metadata_version=metadata_version,
@@ -490,9 +556,11 @@
def validate(self, *, warn: bool = True) -> None: # noqa: C901
"""
- Validate metadata for consistency and correctness. Will also produce
- warnings if ``warn`` is given. Respects ``all_errors``. This is called
- when loading a pyproject.toml, and when making metadata. Checks:
+ Validate metadata for consistency and correctness.
+
+ Will also produce warnings if ``warn`` is given. Respects
+ ``all_errors``. This is called when loading a pyproject.toml, and when
+ making metadata. Checks:
- ``metadata_version`` is a known version or None
- ``name`` is a valid project name
@@ -504,6 +572,9 @@
- ``license`` is an SPDX license expression if metadata_version >= 2.4
- ``license_files`` is supported only for metadata_version >= 2.4
- ``project_url`` can't contain keys over 32 characters
+ - ``import-name(paces)s`` is only supported on metadata_version >= 2.5
+ - ``import-name(space)s`` must be valid names, optionally with ``;
private``
+ - ``import-names`` and ``import-namespaces`` cannot overlap.
"""
errors = ErrorCollector(collect_errors=self.all_errors)
@@ -555,14 +626,14 @@
isinstance(self.license, str)
and self.auto_metadata_version in
constants.PRE_SPDX_METADATA_VERSIONS
):
- msg = "Setting {key} to an SPDX license expression is supported
only when emitting metadata version >= 2.4"
+ msg = "Setting {key} to an SPDX license expression is only
supported when emitting metadata version >= 2.4"
errors.config_error(msg, key="project.license")
if (
self.license_files is not None
and self.auto_metadata_version in
constants.PRE_SPDX_METADATA_VERSIONS
):
- msg = "{key} is supported only when emitting metadata version >=
2.4"
+ msg = "{key} is only supported when emitting metadata version >=
2.4"
errors.config_error(msg, key="project.license-files")
for name in self.urls:
@@ -570,6 +641,37 @@
msg = "{key} names cannot be more than 32 characters long"
errors.config_error(msg, key="project.urls", got=name)
+ if (
+ self.import_names is not None
+ and self.auto_metadata_version in
constants.PRE_2_5_METADATA_VERSIONS
+ ):
+ msg = "{key} is only supported when emitting metadata version >=
2.5"
+ errors.config_error(msg, key="project.import-names")
+
+ if (
+ self.import_namespaces is not None
+ and self.auto_metadata_version in
constants.PRE_2_5_METADATA_VERSIONS
+ ):
+ msg = "{key} is only supported when emitting metadata version >=
2.5"
+ errors.config_error(msg, key="project.import-namespaces")
+
+ import_names = set(
+ _validate_import_names(
+ self.import_names or [], "import-names", errors=errors
+ )
+ )
+ import_namespaces = set(
+ _validate_import_names(
+ self.import_namespaces or [], "import-namespaces",
errors=errors
+ )
+ )
+ in_both = import_names & import_namespaces
+ if in_both:
+ msg = "{key} overlaps with 'project.import-namespaces': {in_both}"
+ errors.config_error(msg, key="project.import-names",
in_both=in_both)
+
+ _validate_dotted_names(import_names | import_namespaces, errors=errors)
+
errors.finalize("Metadata validation failed")
def _write_metadata( # noqa: C901
@@ -634,9 +736,16 @@
_build_extra_req(norm_extra, requirement)
)
if self.readme:
- if self.readme.content_type:
- smart_message["Description-Content-Type"] =
self.readme.content_type
+ assert self.readme.content_type # verified earlier
+ smart_message["Description-Content-Type"] =
self.readme.content_type
smart_message.set_payload(self.readme.text)
+ for import_name in self.import_names or []:
+ smart_message["Import-Name"] = import_name
+ for import_namespace in self.import_namespaces or []:
+ smart_message["Import-Namespace"] = import_namespace
+ # Special case for empty import-names
+ if self.import_names is not None and not self.import_names:
+ smart_message["Import-Name"] = ""
# Core Metadata 2.2
if self.auto_metadata_version != "2.1":
for field in self.dynamic_metadata:
@@ -679,7 +788,7 @@
"""
requirement = copy.copy(requirement)
if requirement.marker:
- if "or" in requirement.marker._markers:
+ if "or" in requirement.marker._markers: # noqa: SLF001
requirement.marker = packaging.markers.Marker(
f"({requirement.marker}) and extra == {extra!r}"
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/pyproject_metadata/constants.py
new/pyproject-metadata-0.10.0/pyproject_metadata/constants.py
--- old/pyproject-metadata-0.9.1/pyproject_metadata/constants.py
2025-03-10 23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/pyproject_metadata/constants.py
2025-11-21 16:26:50.000000000 +0100
@@ -24,8 +24,9 @@
return __all__
-KNOWN_METADATA_VERSIONS = {"2.1", "2.2", "2.3", "2.4"}
+KNOWN_METADATA_VERSIONS = {"2.1", "2.2", "2.3", "2.4", "2.5"}
PRE_SPDX_METADATA_VERSIONS = {"2.1", "2.2", "2.3"}
+PRE_2_5_METADATA_VERSIONS = {"2.1", "2.2", "2.3", "2.4"}
PROJECT_TO_METADATA = {
"authors": frozenset(["Author", "Author-Email"]),
@@ -46,6 +47,8 @@
"scripts": frozenset(),
"urls": frozenset(["Project-URL"]),
"version": frozenset(["Version"]),
+ "import-names": frozenset(["Import-Name"]),
+ "import-namespaces": frozenset(["Import-Namespaces"]),
}
KNOWN_TOPLEVEL_FIELDS = {"build-system", "project", "tool",
"dependency-groups"}
@@ -83,6 +86,8 @@
"summary",
"supported-platform", # Not specified via pyproject standards
"version", # Can't be in dynamic
+ "import-name",
+ "import-namespace",
}
KNOWN_MULTIUSE = {
@@ -100,4 +105,6 @@
"requires", # Deprecated
"obsoletes", # Deprecated
"provides", # Deprecated
+ "import-name",
+ "import-namespace",
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/pyproject_metadata/errors.py
new/pyproject-metadata-0.10.0/pyproject_metadata/errors.py
--- old/pyproject-metadata-0.9.1/pyproject_metadata/errors.py 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/pyproject_metadata/errors.py 2025-11-21
16:26:50.000000000 +0100
@@ -28,15 +28,25 @@
class ConfigurationError(Exception):
- """Error in the backend metadata. Has an optional key attribute, which
will be non-None
- if the error is related to a single key in the pyproject.toml file."""
+ """
+ Error in the backend metadata.
+
+ Has an optional key attribute, which will be non-None if the error is
+ related to a single key in the pyproject.toml file.
+ """
- def __init__(self, msg: str, *, key: str | None = None):
+ def __init__(self, msg: str, *, key: str | None = None) -> None:
+ """
+ Create a new error with a key (can be None).
+ """
super().__init__(msg)
self._key = key
@property
def key(self) -> str | None: # pragma: no cover
+ """
+ Return the stored key.
+ """
return self._key
@@ -48,7 +58,7 @@
ExceptionGroup = builtins.ExceptionGroup
else:
- class ExceptionGroup(Exception):
+ class ExceptionGroup(Exception): # noqa: N818
"""A minimal implementation of `ExceptionGroup` from Python 3.11.
Users can replace this with a more complete implementation, such as
from
@@ -61,10 +71,16 @@
exceptions: list[Exception]
def __init__(self, message: str, exceptions: list[Exception]) -> None:
+ """
+ Create a new group with a message and a list of exceptions.
+ """
self.message = message
self.exceptions = exceptions
def __repr__(self) -> str:
+ """
+ Return a repr similar to the stdlib ExceptionGroup.
+ """
return f"{self.__class__.__name__}({self.message!r},
{self.exceptions!r})"
@@ -83,10 +99,10 @@
msg: str,
*,
key: str | None = None,
- got: typing.Any = None,
+ got: object = None,
got_type: type[typing.Any] | None = None,
warn: bool = False,
- **kwargs: typing.Any,
+ **kwargs: object,
) -> None:
"""Raise a configuration error, or add it to the error list."""
msg = msg.format(key=f'"{key}"', **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/pyproject_metadata/project_table.py
new/pyproject-metadata-0.10.0/pyproject_metadata/project_table.py
--- old/pyproject-metadata-0.9.1/pyproject_metadata/project_table.py
2025-03-10 23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/pyproject_metadata/project_table.py
2025-11-21 16:26:50.000000000 +0100
@@ -14,16 +14,12 @@
import typing
from typing import Any, Dict, List, Union
-if sys.version_info < (3, 11):
+if sys.version_info < (3, 11): # pragma: nocover
from typing_extensions import Required
else:
from typing import Required
-if sys.version_info < (3, 8):
- from typing_extensions import Literal, TypedDict
-else:
- from typing import Literal, TypedDict
-
+from typing import Literal, TypedDict
__all__ = [
"BuildSystemTable",
@@ -42,11 +38,19 @@
class ContactTable(TypedDict, total=False):
+ """
+ Can have either name or email.
+ """
+
name: str
email: str
class LicenseTable(TypedDict, total=False):
+ """
+ Can have either text or file. Legacy.
+ """
+
text: str
file: str
@@ -72,6 +76,8 @@
"scripts",
"urls",
"version",
+ "import-names",
+ "import-namespaces",
]
ProjectTable = TypedDict(
@@ -94,6 +100,8 @@
"keywords": List[str],
"scripts": Dict[str, str],
"gui-scripts": Dict[str, str],
+ "import-names": List[str],
+ "import-namespaces": List[str],
"dynamic": List[Dynamic],
},
total=False,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/pyproject_metadata/pyproject.py
new/pyproject-metadata-0.10.0/pyproject_metadata/pyproject.py
--- old/pyproject-metadata-0.9.1/pyproject_metadata/pyproject.py
2025-03-10 23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/pyproject_metadata/pyproject.py
2025-11-21 16:26:50.000000000 +0100
@@ -9,7 +9,6 @@
from __future__ import annotations
import dataclasses
-import pathlib
import re
import typing
@@ -18,6 +17,7 @@
from .errors import ErrorCollector
if typing.TYPE_CHECKING:
+ import pathlib
from collections.abc import Generator, Iterable, Sequence
from packaging.requirements import Requirement
@@ -81,8 +81,10 @@
self.config_error(msg, key=key, got_type=type(value))
return None
- def ensure_list(self, val: list[T], key: str) -> list[T] | None:
+ def ensure_list(self, val: list[T] | None, key: str) -> list[T] | None:
"""Ensure that a value is a list of strings."""
+ if val is None:
+ return None
if not isinstance(val, list):
msg = "Field {key} has an invalid type, expecting a list of
strings"
self.config_error(msg, key=key, got_type=type(val))
@@ -299,7 +301,6 @@
def get_dependencies(self, project: ProjectTable) -> list[Requirement]:
"""Get the dependencies from the project table."""
-
requirement_strings: list[str] | None = None
requirement_strings_raw = project.get("dependencies")
if requirement_strings_raw is not None:
@@ -310,13 +311,13 @@
return []
requirements: list[Requirement] = []
- for req in requirement_strings:
- try:
+ try:
+ for req in requirement_strings:
requirements.append(packaging.requirements.Requirement(req))
- except packaging.requirements.InvalidRequirement as e:
- msg = "Field {key} contains an invalid PEP 508 requirement
string {req!r} ({error!r})"
- self.config_error(msg, key="project.dependencies", req=req,
error=e)
- return []
+ except packaging.requirements.InvalidRequirement as e:
+ msg = "Field {key} contains an invalid PEP 508 requirement string
{req!r} ({error!r})"
+ self.config_error(msg, key="project.dependencies", req=req,
error=e)
+ return []
return requirements
def get_optional_dependencies(
@@ -324,7 +325,6 @@
project: ProjectTable,
) -> dict[str, list[Requirement]]:
"""Get the optional dependencies from the project table."""
-
val = project.get("optional-dependencies")
if not val:
return {}
@@ -376,7 +376,6 @@
def get_entrypoints(self, project: ProjectTable) -> dict[str, dict[str,
str]]:
"""Get the entrypoints from the project table."""
-
val = project.get("entry-points", None)
if val is None:
return {}
@@ -435,7 +434,6 @@
self, project_dir: pathlib.Path, globs: Iterable[str]
) -> Generator[pathlib.Path, None, None]:
"""Given a list of globs, get files that match."""
-
for glob in globs:
if glob.startswith(("..", "/")):
msg = "{glob!r} is an invalid {key} glob: the pattern must
match files within the project directory"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/tests/packages/full-metadata/pyproject.toml
new/pyproject-metadata-0.10.0/tests/packages/full-metadata/pyproject.toml
--- old/pyproject-metadata-0.9.1/tests/packages/full-metadata/pyproject.toml
2025-03-10 23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/tests/packages/full-metadata/pyproject.toml
2025-11-21 16:26:50.000000000 +0100
@@ -10,6 +10,7 @@
{ name = 'Example!' },
]
maintainers = [
+ { name = 'Emailless' },
{ name = 'Other Example', email = '[email protected]' },
]
classifiers = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/tests/packages/metadata-2.5/LICENSE
new/pyproject-metadata-0.10.0/tests/packages/metadata-2.5/LICENSE
--- old/pyproject-metadata-0.9.1/tests/packages/metadata-2.5/LICENSE
1970-01-01 01:00:00.000000000 +0100
+++ new/pyproject-metadata-0.10.0/tests/packages/metadata-2.5/LICENSE
2025-11-21 16:26:50.000000000 +0100
@@ -0,0 +1,20 @@
+Copyright © 2019 Filipe Laíns <[email protected]>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice (including the next
+paragraph) shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/tests/packages/metadata-2.5/README.md
new/pyproject-metadata-0.10.0/tests/packages/metadata-2.5/README.md
--- old/pyproject-metadata-0.9.1/tests/packages/metadata-2.5/README.md
1970-01-01 01:00:00.000000000 +0100
+++ new/pyproject-metadata-0.10.0/tests/packages/metadata-2.5/README.md
2025-11-21 16:26:50.000000000 +0100
@@ -0,0 +1 @@
+some readme 👋
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/tests/packages/metadata-2.5/pyproject.toml
new/pyproject-metadata-0.10.0/tests/packages/metadata-2.5/pyproject.toml
--- old/pyproject-metadata-0.9.1/tests/packages/metadata-2.5/pyproject.toml
1970-01-01 01:00:00.000000000 +0100
+++ new/pyproject-metadata-0.10.0/tests/packages/metadata-2.5/pyproject.toml
2025-11-21 16:26:50.000000000 +0100
@@ -0,0 +1,51 @@
+[project]
+name = 'metadata25'
+version = '3.2.1'
+description = 'A package with all the metadata :)'
+readme = 'README.md'
+license = "MIT"
+license-files = ["LICENSE"]
+keywords = ['trampolim', 'is', 'interesting']
+authors = [
+ { email = '[email protected]' },
+ { name = 'Example!' },
+]
+maintainers = [
+ { name = 'Other Example', email = '[email protected]' },
+]
+classifiers = [
+ 'Development Status :: 4 - Beta',
+ 'Programming Language :: Python',
+]
+
+requires-python = '>=3.8'
+dependencies = [
+ 'dependency1',
+ 'dependency2>1.0.0',
+ 'dependency3[extra]',
+ 'dependency4; os_name != "nt"',
+ 'dependency5[other-extra]>1.0; os_name == "nt"',
+]
+import-names = ["metadata25"]
+
+[project.optional-dependencies]
+test = [
+ 'test_dependency',
+ 'test_dependency[test_extra]',
+ 'test_dependency[test_extra2] > 3.0; os_name == "nt"',
+]
+
+[project.urls]
+homepage = 'example.com'
+documentation = 'readthedocs.org'
+repository = 'github.com/some/repo'
+changelog = 'github.com/some/repo/blob/master/CHANGELOG.rst'
+
+[project.scripts]
+full-metadata = 'full_metadata:main_cli'
+
+[project.gui-scripts]
+full-metadata-gui = 'full_metadata:main_gui'
+
+[project.entry-points.custom]
+full-metadata = 'full_metadata:main_custom'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pyproject-metadata-0.9.1/tests/test_internals.py
new/pyproject-metadata-0.10.0/tests/test_internals.py
--- old/pyproject-metadata-0.9.1/tests/test_internals.py 2025-03-10
23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/tests/test_internals.py 2025-11-21
16:26:50.000000000 +0100
@@ -18,6 +18,6 @@
def test_project_table_all() -> None:
if sys.version_info < (3, 11):
pytest.importorskip("typing_extensions")
- import pyproject_metadata.project_table
+ import pyproject_metadata.project_table # noqa: PLC0415
assert "annotations" not in dir(pyproject_metadata.project_table)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/pyproject-metadata-0.9.1/tests/test_standard_metadata.py
new/pyproject-metadata-0.10.0/tests/test_standard_metadata.py
--- old/pyproject-metadata-0.9.1/tests/test_standard_metadata.py
2025-03-10 23:10:43.000000000 +0100
+++ new/pyproject-metadata-0.10.0/tests/test_standard_metadata.py
2025-11-21 16:26:50.000000000 +0100
@@ -319,6 +319,16 @@
[project]
name = "test"
version = "0.1.0"
+ readme = { file = "pyproject.toml" }
+ """,
+ 'Field "project.readme.content-type" missing',
+ id="Missing content-type for readme file",
+ ),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
description = true
""",
'Field "project.description" has an invalid type, expecting a
string (got bool)',
@@ -780,6 +790,78 @@
"Setting \"project.license\" to an SPDX license expression is not
compatible with 'License ::' classifiers",
id="SPDX license and License trove classifiers",
),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-names = ["is"]
+ """,
+ "\"import-names\" contains a Python keyword, which is not a valid
import name, got 'is'",
+ id="Setting import-names to keyword",
+ ),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-namespaces = ["from"]
+ """,
+ "\"import-namespaces\" contains a Python keyword, which is not a
valid import name, got 'from'",
+ id="Setting import-namespaces to keyword",
+ ),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-names = ["2two"]
+ """,
+ "\"import-names\" contains '2two', which is not a valid
identifier",
+ id="Setting import-names invalid identifier",
+ ),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-namespaces = ["3"]
+ """,
+ "\"import-namespaces\" contains '3', which is not a valid
identifier",
+ id="Setting import-namespaces to invalid identifier",
+ ),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-names = ["one", "two"]
+ import-namespaces = ["one", "three"]
+ """,
+ "\"project.import-names\" overlaps with
'project.import-namespaces': {'one'}",
+ id="Matching entry in import-names and import-namespaces",
+ ),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-names = ["one; private", "two"]
+ import-namespaces = ["one", "three ; private"]
+ """,
+ "\"project.import-names\" overlaps with
'project.import-namespaces': {'one'}",
+ id="Matching entry in import-names and import-namespaces with
private tags",
+ ),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-names = ["one.two"]
+ """,
+ "\"project.import-namespaces\" is missing 'one', but submodules
are present elsewhere",
+ id="Matching entry in import-names and import-namespaces",
+ ),
],
)
def test_load(
@@ -886,6 +968,22 @@
],
id="Four errors including extra keys",
),
+ pytest.param(
+ """
+ [project]
+ name = 'test'
+ version = "0.1.0"
+ import-names = ["test", "other"]
+ import-namespaces = ["other.one.two", "invalid name", "not;
public"]
+ """,
+ [
+ "\"import-namespaces\" contains 'invalid name', which is not a
valid identifier",
+ "\"import-namespaces\" contains an ending tag other than ';
private', got 'not; public'",
+ "\"import-namespaces\" contains a Python keyword, which is not
a valid import name, got 'not; public'",
+ "\"project.import-namespaces\" is missing 'other.one', but
submodules are present elsewhere",
+ ],
+ id="Multiple errors related to names/namespaces",
+ ),
],
)
def test_load_multierror(
@@ -928,7 +1026,7 @@
version = "0.1.0"
license = 'MIT'
""",
- 'Setting "project.license" to an SPDX license expression is
supported only when emitting metadata version >= 2.4',
+ 'Setting "project.license" to an SPDX license expression is only
supported when emitting metadata version >= 2.4',
"2.3",
id="SPDX with metadata_version 2.3",
),
@@ -939,10 +1037,32 @@
version = "0.1.0"
license-files = ['README.md']
""",
- '"project.license-files" is supported only when emitting metadata
version >= 2.4',
+ '"project.license-files" is only supported when emitting metadata
version >= 2.4',
"2.3",
id="license-files with metadata_version 2.3",
),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-names = ['one']
+ """,
+ '"project.import-names" is only supported when emitting metadata
version >= 2.5',
+ "2.4",
+ id="import-names with metadata_version 2.4",
+ ),
+ pytest.param(
+ """
+ [project]
+ name = "test"
+ version = "0.1.0"
+ import-namespaces = ['one']
+ """,
+ '"project.import-namespaces" is only supported when emitting
metadata version >= 2.5',
+ "2.4",
+ id="import-names with metadata_version 2.4",
+ ),
],
)
def test_load_with_metadata_version(
@@ -1021,6 +1141,7 @@
("Example!", None),
]
assert metadata.maintainers == [
+ ("Emailless", None),
("Other Example", "[email protected]"),
]
assert metadata.keywords == ["trampolim", "is", "interesting"]
@@ -1060,6 +1181,25 @@
]
[email protected]("after_rfc", [False, True])
+def test_value_25(after_rfc: bool, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.chdir(DIR / "packages/metadata-2.5")
+ with open("pyproject.toml", "rb") as f:
+ metadata =
pyproject_metadata.StandardMetadata.from_pyproject(tomllib.load(f))
+
+ if after_rfc:
+ metadata.as_rfc822()
+
+ assert metadata.auto_metadata_version == "2.5"
+
+ assert isinstance(metadata.license, str)
+ assert metadata.license == "MIT"
+ assert metadata.license_files == [pathlib.Path("LICENSE")]
+
+ assert metadata.import_names == ["metadata25"]
+ assert metadata.import_namespaces is None
+
+
def test_read_license(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(DIR / "packages/full-metadata2")
with open("pyproject.toml", "rb") as f:
@@ -1117,6 +1257,7 @@
"description_content_type": "text/markdown",
"keywords": ["trampolim", "is", "interesting"],
"license": "some license text",
+ "maintainer": "Emailless",
"maintainer_email": "Other Example <[email protected]>",
"metadata_version": "2.1",
"name": "full_metadata",
@@ -1143,6 +1284,20 @@
}
+def test_readme_text() -> None:
+ pyproject = pyproject_metadata.StandardMetadata.from_pyproject(
+ {
+ "project": {
+ "name": "foo",
+ "version": "1.2.3",
+ "readme": {"text": "onetwothree", "content-type":
"text/plain"},
+ }
+ }
+ )
+ assert pyproject.readme
+ assert pyproject.readme.text == "onetwothree"
+
+
def test_as_rfc822(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(DIR / "packages/full-metadata")
@@ -1157,6 +1312,7 @@
("Keywords", "trampolim,is,interesting"),
("Author", "Example!"),
("Author-Email", "Unknown <[email protected]>"),
+ ("Maintainer", "Emailless"),
("Maintainer-Email", "Other Example <[email protected]>"),
("License", "some license text"),
("Classifier", "Development Status :: 4 - Beta"),
@@ -1183,11 +1339,53 @@
assert core_metadata.get_payload() == "some readme 👋\n"
+def test_rfc822_empty_import_name() -> None:
+ metadata = pyproject_metadata.StandardMetadata.from_pyproject(
+ {"project": {"name": "test", "version": "0.1.0", "import-names": []}}
+ )
+ assert metadata.import_names == []
+ assert metadata.import_namespaces is None
+
+ core_metadata = metadata.as_rfc822()
+ assert core_metadata.items() == [
+ ("Metadata-Version", "2.5"),
+ ("Name", "test"),
+ ("Version", "0.1.0"),
+ ("Import-Name", ""),
+ ]
+
+
+def test_rfc822_full_import_name() -> None:
+ metadata = pyproject_metadata.StandardMetadata.from_pyproject(
+ {
+ "project": {
+ "name": "test",
+ "version": "0.1.0",
+ "import-names": ["one", "two"],
+ "import-namespaces": ["three"],
+ }
+ }
+ )
+ assert metadata.import_names == ["one", "two"]
+ assert metadata.import_namespaces == ["three"]
+
+ core_metadata = metadata.as_rfc822()
+ assert core_metadata.items() == [
+ ("Metadata-Version", "2.5"),
+ ("Name", "test"),
+ ("Version", "0.1.0"),
+ ("Import-Name", "one"),
+ ("Import-Name", "two"),
+ ("Import-Namespace", "three"),
+ ]
+
+
def test_as_json_spdx(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(DIR / "packages/spdx")
with open("pyproject.toml", "rb") as f:
metadata =
pyproject_metadata.StandardMetadata.from_pyproject(tomllib.load(f))
+
core_metadata = metadata.as_json()
assert core_metadata == {
"license_expression": "MIT OR GPL-2.0-or-later OR (FSFUL AND
BSD-2-Clause)",
@@ -1259,11 +1457,12 @@
pre_spdx = (
metadata_version in
pyproject_metadata.constants.PRE_SPDX_METADATA_VERSIONS
)
- with (
+ ctx = (
contextlib.nullcontext()
if pre_spdx
else pytest.warns(pyproject_metadata.errors.ConfigurationWarning)
- ):
+ )
+ with ctx:
metadata = pyproject_metadata.StandardMetadata.from_pyproject(
{
"project": {
@@ -1443,7 +1642,8 @@
def test_as_rfc822_missing_version() -> None:
metadata = pyproject_metadata.StandardMetadata(name="something")
with pytest.raises(
- pyproject_metadata.ConfigurationError, match='Field "project.version"
missing'
+ pyproject_metadata.ConfigurationError,
+ match=re.escape('Field "project.version" missing'),
):
metadata.as_rfc822()
@@ -1451,7 +1651,9 @@
def test_statically_defined_dynamic_field() -> None:
with pytest.raises(
pyproject_metadata.ConfigurationError,
- match='Field "project.version" declared as dynamic in
"project.dynamic" but is defined',
+ match=re.escape(
+ 'Field "project.version" declared as dynamic in "project.dynamic"
but is defined'
+ ),
):
pyproject_metadata.StandardMetadata.from_pyproject(
{
@@ -1470,8 +1672,8 @@
"value",
[
"<3.10",
- ">3.7,<3.11",
- ">3.7,<3.11,!=3.8.4",
+ ">3.8,<3.11",
+ ">3.8,<3.11,!=3.8.4",
"~=3.10,!=3.10.3",
],
)