Skip to content

Commit

Permalink
feat: support PEP 770
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
  • Loading branch information
henryiii committed Feb 14, 2025
1 parent 175b1ec commit abeaf39
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 6 deletions.
17 changes: 16 additions & 1 deletion pyproject_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ class StandardMetadata:
description: str | None = None
license: License | str | None = None
license_files: list[pathlib.Path] | None = None
sbom_files: list[pathlib.Path] | None = None
readme: Readme | None = None
requires_python: packaging.specifiers.SpecifierSet | None = None
dependencies: list[Requirement] = dataclasses.field(default_factory=list)
Expand Down Expand Up @@ -262,7 +263,8 @@ def auto_metadata_version(self) -> str:
"""
if self.metadata_version is not None:
return self.metadata_version

if self.sbom_files 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:
Expand Down Expand Up @@ -393,6 +395,7 @@ def from_pyproject( # noqa: C901
description=description,
license=pyproject.get_license(project, project_dir),
license_files=pyproject.get_license_files(project, project_dir),
sbom_files=pyproject.get_sbom_files(project, project_dir),
readme=pyproject.get_readme(project, project_dir),
requires_python=requires_python,
dependencies=pyproject.get_dependencies(project),
Expand Down Expand Up @@ -465,6 +468,7 @@ def validate(self, *, warn: bool = True) -> None: # noqa: C901
- License classifiers deprecated for metadata_version >= 2.4 (warning)
- ``license`` is an SPDX license expression if metadata_version >= 2.4
- ``license_files`` is supported only for metadata_version >= 2.4
- ``sbom-files`` is supported only for metadata_version >= 2.5
- ``project_url`` can't contain keys over 32 characters
"""
errors = ErrorCollector(collect_errors=self.all_errors)
Expand Down Expand Up @@ -527,6 +531,13 @@ def validate(self, *, warn: bool = True) -> None: # noqa: C901
msg = "{key} is supported only when emitting metadata version >= 2.4"
errors.config_error(msg, key="project.license-files")

if (
self.sbom_files is not None
and self.auto_metadata_version in constants.PRE_SBOM_METADATA_VERSIONS
):
msg = "{key} is supported only when emitting metadata version >= 2.5"
errors.config_error(msg, key="project.sbom-files")

Check warning on line 539 in pyproject_metadata/__init__.py

View check run for this annotation

Codecov / codecov/patch

pyproject_metadata/__init__.py#L538-L539

Added lines #L538 - L539 were not covered by tests

for name in self.urls:
if len(name) > 32:
msg = "{key} names cannot be more than 32 characters long"
Expand Down Expand Up @@ -577,6 +588,10 @@ def _write_metadata( # noqa: C901
):
smart_message["License-File"] = os.fspath(self.license.file.as_posix())

if self.sbom_files is not None:
for sbom_file in sorted(set(self.sbom_files)):
smart_message["Sbom-File"] = os.fspath(sbom_file.as_posix())

for classifier in self.classifiers:
smart_message["Classifier"] = classifier
# skip 'Provides-Dist'
Expand Down
7 changes: 6 additions & 1 deletion pyproject_metadata/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"KNOWN_MULTIUSE",
"KNOWN_PROJECT_FIELDS",
"KNOWN_TOPLEVEL_FIELDS",
"PRE_SBOM_METADATA_VERSIONS",
"PRE_SPDX_METADATA_VERSIONS",
"PROJECT_TO_METADATA",
]
Expand All @@ -24,8 +25,9 @@ def __dir__() -> list[str]:
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_SBOM_METADATA_VERSIONS = {"2.1", "2.2", "2.3", "2.4"}

PROJECT_TO_METADATA = {
"authors": frozenset(["Author", "Author-Email"]),
Expand All @@ -38,6 +40,7 @@ def __dir__() -> list[str]:
"keywords": frozenset(["Keywords"]),
"license": frozenset(["License", "License-Expression"]),
"license-files": frozenset(["License-File"]),
"sbom-files": frozenset(["Sbom-File"]),
"maintainers": frozenset(["Maintainer", "Maintainer-Email"]),
"name": frozenset(["Name"]),
"optional-dependencies": frozenset(["Provides-Extra", "Requires-Dist"]),
Expand Down Expand Up @@ -65,6 +68,7 @@ def __dir__() -> list[str]:
"license",
"license-expression",
"license-file",
"sbom-file",
"maintainer",
"maintainer-email",
"metadata-version",
Expand All @@ -91,6 +95,7 @@ def __dir__() -> list[str]:
"provides-extra",
"supported-platform",
"license-file",
"sbom-file",
"classifier",
"requires-dist",
"requires-external",
Expand Down
3 changes: 3 additions & 0 deletions pyproject_metadata/project_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ class LicenseTable(TypedDict, total=False):
"gui-scripts",
"keywords",
"license",
"license-files",
"maintainers",
"optional-dependencies",
"readme",
"requires-python",
"sbom-files",
"scripts",
"urls",
"version",
Expand All @@ -78,6 +80,7 @@ class LicenseTable(TypedDict, total=False):
"description": str,
"license": Union[LicenseTable, str],
"license-files": List[str],
"sbom-files": List[str],
"readme": Union[str, ReadmeTable],
"requires-python": str,
"dependencies": List[str],
Expand Down
31 changes: 27 additions & 4 deletions pyproject_metadata/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,30 @@ def get_license_files(
if self.ensure_list(license_files, "project.license-files") is None:
return None

return list(self._get_files_from_globs(project_dir, license_files))
return list(
self._get_files_from_globs(
project_dir, license_files, "project.license-files"
)
)

def get_sbom_files(
self, project: ProjectTable, project_dir: pathlib.Path
) -> list[pathlib.Path] | None:
"""Get the sbom-files list of files from the project table.
Returns None if an error occurred (including invalid globs, etc) or if
not present.
"""

sbom_files = project.get("sbom-files")
if sbom_files is None:
return None
if self.ensure_list(sbom_files, "project.sbom-files") is None:
return None

Check warning on line 238 in pyproject_metadata/pyproject.py

View check run for this annotation

Codecov / codecov/patch

pyproject_metadata/pyproject.py#L238

Added line #L238 was not covered by tests

return list(
self._get_files_from_globs(project_dir, sbom_files, "project.sbom-files")
)

def get_readme( # noqa: C901
self, project: ProjectTable, project_dir: pathlib.Path
Expand Down Expand Up @@ -432,19 +455,19 @@ def get_dynamic(self, project: ProjectTable) -> list[Dynamic]:
return dynamic

def _get_files_from_globs(
self, project_dir: pathlib.Path, globs: Iterable[str]
self, project_dir: pathlib.Path, globs: Iterable[str], key: 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"
self.config_error(msg, key="project.license-files", glob=glob)
self.config_error(msg, key=key, glob=glob)
break
files = [f for f in project_dir.glob(glob) if f.is_file()]
if not files:
msg = "Every pattern in {key} must match at least one file: {glob!r} did not match any"
self.config_error(msg, key="project.license-files", glob=glob)
self.config_error(msg, key=key, glob=glob)
break
for f in files:
yield f.relative_to(project_dir)
Empty file added tests/packages/sbom/bom.json
Empty file.
4 changes: 4 additions & 0 deletions tests/packages/sbom/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[project]
name = "test-sbom"
version = "0.1.0"
sbom-files = ["bom.json", "sboms/*"]
Empty file.
34 changes: 34 additions & 0 deletions tests/test_standard_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1281,6 +1281,40 @@ def test_license_file_24(
assert "License-File: LICENSE.txt" in message


def test_as_json_sbom(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(DIR / "packages/sbom")

with open("pyproject.toml", "rb") as f:
metadata = pyproject_metadata.StandardMetadata.from_pyproject(tomllib.load(f))
core_metadata = metadata.as_json()
assert core_metadata == {
"sbom_file": [
"bom.json",
"sboms/newbom.json",
],
"metadata_version": "2.5",
"name": "test-sbom",
"version": "0.1.0",
}


def test_as_rfc822_sbom(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(DIR / "packages/sbom")

with open("pyproject.toml", "rb") as f:
metadata = pyproject_metadata.StandardMetadata.from_pyproject(tomllib.load(f))
core_metadata = metadata.as_rfc822()
assert core_metadata.items() == [
("Metadata-Version", "2.5"),
("Name", "test-sbom"),
("Version", "0.1.0"),
("Sbom-File", "bom.json"),
("Sbom-File", "sboms/newbom.json"),
]

assert core_metadata.get_payload() is None


def test_as_rfc822_dynamic(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(DIR / "packages/dynamic-description")

Expand Down

0 comments on commit abeaf39

Please sign in to comment.