Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ dependencies = [
"packaging",
"packageurl-python",
"psutil",
"pydantic",
"pydantic>=2.12",
"pypi_simple",
"pyproject_hooks>=1.0.0,!=1.1.0",
"PyYAML",
Expand Down
29 changes: 9 additions & 20 deletions src/fromager/packagesettings/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@

import pydantic
import yaml
from packageurl import PackageURL
from packaging.requirements import Requirement
from packaging.utils import canonicalize_name
from pydantic import Field
from pydantic import AnyUrl, Field
from pydantic_core import core_schema

from ._typedefs import (
MODEL_CONFIG,
BuildDirectory,
EnvVars,
Package,
PurlType,
RawAnnotations,
Template,
UpstreamPurl,
Variant,
VariantChangelog,
)
Expand All @@ -49,7 +50,7 @@ class SbomSettings(pydantic.BaseModel):
supplier: str = "NOASSERTION"
"""SPDX supplier field for the wheel package (e.g. ``Organization: ExampleCo``)"""

namespace: str = "https://spdx.org/spdxdocs"
namespace: AnyUrl = AnyUrl("https://spdx.org/spdxdocs")
"""Base URL for the SPDX documentNamespace"""

creators: list[str] = Field(default_factory=list)
Expand All @@ -58,10 +59,10 @@ class SbomSettings(pydantic.BaseModel):
The fromager tool creator entry is always added automatically.
"""

purl_type: str = "pypi"
purl_type: PurlType = "pypi"
"""Default purl type for all packages (e.g. ``pypi``, ``generic``)"""

repository_url: str | None = None
repository_url: AnyUrl | None = None
"""Default purl ``repository_url`` qualifier for all packages
Comment thread
smoparth marked this conversation as resolved.

When set, this URL is added to every purl as a qualifier
Expand Down Expand Up @@ -89,7 +90,7 @@ class PurlConfig(pydantic.BaseModel):

model_config = MODEL_CONFIG

type: str | None = None
type: PurlType | None = None
"""Override the purl type (e.g. ``generic`` instead of ``pypi``)"""

namespace: str | None = None
Expand All @@ -101,13 +102,13 @@ class PurlConfig(pydantic.BaseModel):
version: str | None = None
"""Override the purl version component (defaults to the resolved version)"""

repository_url: str | None = None
repository_url: AnyUrl | None = None
"""Per-package override for the purl ``repository_url`` qualifier.

Overrides the global ``sbom.repository_url`` setting for this package.
"""

upstream: str | None = None
upstream: UpstreamPurl | None = None
"""Full purl string identifying the upstream source package.

When set, this is used as the upstream identity in the SBOM's
Expand All @@ -118,18 +119,6 @@ class PurlConfig(pydantic.BaseModel):
purl without the ``repository_url`` qualifier.
"""

@pydantic.field_validator("upstream")
@classmethod
def validate_upstream_purl(cls, v: str | None) -> str | None:
"""Validate that upstream is a valid purl string."""
if v is None:
return v
try:
PackageURL.from_string(v)
except ValueError as err:
raise ValueError(f"invalid upstream purl {v!r}") from err
return v


class ResolverDist(pydantic.BaseModel):
"""Packages resolver dist
Expand Down
29 changes: 29 additions & 0 deletions src/fromager/packagesettings/_typedefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
from collections.abc import Mapping

import pydantic
from packageurl import PackageURL
from packaging.utils import NormalizedName, canonicalize_name
from packaging.version import Version
from pydantic import StringConstraints
from pydantic_core import CoreSchema, core_schema

# common settings
Expand All @@ -19,6 +21,8 @@
frozen=True,
# read inline doc strings
use_attribute_docstrings=True,
# preserve URLs as-is without trailing slash normalization
url_preserve_empty_path=True,
)


Expand Down Expand Up @@ -98,6 +102,31 @@ def _validate_envkey(v: typing.Any) -> str:
GlobalChangelog = Mapping[Variant, list[str]]
VariantChangelog = Mapping[PackageVersion, list[str]]


# purl type (e.g. "pypi", "generic", "github")
PurlType = typing.Annotated[
str,
StringConstraints(strip_whitespace=True, to_lower=True, min_length=1),
]


# full purl string identifying an upstream source package
def _validate_upstream_purl(v: str) -> str:
"""Validate that *v* is a well-formed purl string."""
try:
PackageURL.from_string(v)
except ValueError as err:
Comment thread
smoparth marked this conversation as resolved.
raise ValueError(f"invalid upstream purl {v!r}: {err}") from err
return v


UpstreamPurl = typing.Annotated[
str,
StringConstraints(strip_whitespace=True, min_length=1),
pydantic.AfterValidator(_validate_upstream_purl),
]


# Annotations
RawAnnotations = Mapping[str, str]

Expand Down
4 changes: 2 additions & 2 deletions src/fromager/sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def _build_downstream_purl(
qualifiers: dict[str, str] = {}
repo_url = (pc.repository_url if pc else None) or sbom_settings.repository_url
if repo_url:
qualifiers["repository_url"] = repo_url
qualifiers["repository_url"] = str(repo_url)

return PackageURL(
type=purl_type,
Expand Down Expand Up @@ -109,7 +109,7 @@ def generate_sbom(
creators = list(sbom_settings.creators)
creators.append(f"Tool: fromager-{fromager_version}")

namespace = f"{sbom_settings.namespace}/{name}-{version}.spdx.json"
namespace = f"{sbom_settings.namespace!s}/{name}-{version}.spdx.json"

downstream = _build_downstream_purl(
name=name,
Expand Down
27 changes: 27 additions & 0 deletions tests/test_packagesettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
Variant,
substitute_template,
)
from fromager.packagesettings._typedefs import PurlType, UpstreamPurl

TEST_PKG = "test-pkg"
TEST_EMPTY_PKG = "test-empty-pkg"
Expand Down Expand Up @@ -490,6 +491,32 @@ def test_type_builddirectory() -> None:
ta.validate_python("/absolute/path")


def test_type_purl_type() -> None:
"""Verify PurlType normalizes and rejects empty strings."""
ta = pydantic.TypeAdapter(PurlType)
assert ta.validate_python("pypi") == "pypi"
assert ta.validate_python(" Generic ") == "generic"
assert ta.validate_python("GITHUB") == "github"
with pytest.raises(ValueError):
ta.validate_python("")
with pytest.raises(ValueError):
ta.validate_python(" ")


def test_type_upstream_purl() -> None:
"""Verify UpstreamPurl accepts valid purls and rejects invalid strings."""
ta = pydantic.TypeAdapter(UpstreamPurl)
assert ta.validate_python("pkg:pypi/flask@2.0") == "pkg:pypi/flask@2.0"
assert (
ta.validate_python("pkg:github/vllm-project/bart-plugin@v0.2.0")
== "pkg:github/vllm-project/bart-plugin@v0.2.0"
)
with pytest.raises(ValueError):
ta.validate_python("invalid-not-purl")
with pytest.raises(ValueError):
ta.validate_python("")


def test_global_settings(testdata_path: pathlib.Path) -> None:
filename = testdata_path / "context/overrides/settings.yaml"
gs = SettingsFile.from_file(filename)
Expand Down
14 changes: 10 additions & 4 deletions tests/test_sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ def test_generate_sbom_default_purls(tmp_path: pathlib.Path) -> None:

def test_generate_sbom_repository_url_qualifier(tmp_path: pathlib.Path) -> None:
"""Verify global repository_url adds qualifier to downstream but not upstream."""
settings = SbomSettings(repository_url="https://packages.redhat.com")
settings = SbomSettings(
repository_url="https://packages.redhat.com", # type: ignore[arg-type]
)
ctx = make_sbom_ctx(tmp_path, sbom_settings=settings)
doc = sbom.generate_sbom(
ctx=ctx,
Expand All @@ -82,7 +84,7 @@ def test_generate_sbom_custom_settings(tmp_path: pathlib.Path) -> None:
"""Verify custom supplier, namespace, and creators are used."""
settings = SbomSettings(
supplier="Organization: ExampleCo",
namespace="https://www.example.com",
namespace="https://www.example.com", # type: ignore[arg-type]
creators=["Organization: ExampleCo"],
)
ctx = make_sbom_ctx(tmp_path, sbom_settings=settings)
Expand Down Expand Up @@ -132,7 +134,9 @@ def test_generate_sbom_package_repository_url_override(tmp_path: pathlib.Path) -
"""Verify per-package repository_url overrides the global value."""
ctx = make_sbom_ctx(
tmp_path,
sbom_settings=SbomSettings(repository_url="https://packages.redhat.com"),
sbom_settings=SbomSettings(
repository_url="https://packages.redhat.com", # type: ignore[arg-type]
),
package_overrides={
"purl": {"repository_url": "https://mirror.example.com/simple"},
},
Expand All @@ -157,7 +161,9 @@ def test_generate_sbom_upstream_purl_override(tmp_path: pathlib.Path) -> None:
"""Verify upstream purl override for GitHub-sourced packages."""
ctx = make_sbom_ctx(
tmp_path,
sbom_settings=SbomSettings(repository_url="https://packages.redhat.com"),
sbom_settings=SbomSettings(
repository_url="https://packages.redhat.com", # type: ignore[arg-type]
),
package_overrides={
"purl": {"upstream": "pkg:github/vllm-project/bart-plugin@v0.2.0"},
},
Expand Down
Loading