Skip to content

Commit ca389f1

Browse files
authored
feat(curations): Minor fix and tests (heliocastro#12)
2 parents df5a9b8 + fec512a commit ca389f1

12 files changed

Lines changed: 325 additions & 57 deletions

.github/workflows/build.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,3 @@ jobs:
4646
shell: bash
4747
run: |
4848
uv build
49-
50-
- name: Test with python ${{ matrix.python-version }}
51-
run: uv run --frozen pytest

.github/workflows/testing.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: pytest
2+
3+
on: [pull_request, workflow_dispatch]
4+
5+
jobs:
6+
test-optima:
7+
name: Run python-optima tests
8+
runs-on: ubuntu-24.04
9+
strategy:
10+
matrix:
11+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
12+
13+
steps:
14+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
15+
16+
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
17+
with:
18+
enable-cache: true
19+
20+
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install the project
25+
run: uv sync --locked --all-extras --dev
26+
27+
- name: Test with python ${{ matrix.python-version }}
28+
run: uv run --frozen pytest

pyproject.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
requires = ["uv_build>=0.8.12,<0.10.0"]
33
build-backend = "uv_build"
44

5-
[tool.hatch.build.targets.wheel]
6-
packages = ["src/ort"]
7-
85
[project]
96
name = "python-ort"
10-
version = "0.3.0"
7+
version = "0.3.1"
118
description = "A Python Ort model serialization library"
129
readme = "README.md"
1310
license = "MIT"
@@ -39,6 +36,7 @@ dev = [
3936
"pycodestyle>=2.14.0",
4037
"pyrefly>=0.40.0",
4138
"pytest>=8.4.2",
39+
"rich>=14.2.0",
4240
"ruff>=0.14.4",
4341
"types-pyyaml>=6.0.12.20250915",
4442
]
@@ -47,7 +45,8 @@ dev = [
4745
addopts = ["--import-mode=importlib"]
4846
log_cli = true
4947
log_cli_level = "INFO"
50-
pythonpath = "src"
48+
pythonpath = ["src"]
49+
testpaths = ["tests"]
5150

5251
[tool.pylint.messages_control]
5352
disable = [

src/ort/models/package_curation_data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from .hash import Hash
99
from .source_code_origin import SourceCodeOrigin
10-
from .vcsinfo import VcsInfo
10+
from .vcsinfo_curation_data import VcsInfoCurationData
1111

1212

1313
class CurationArtifact(BaseModel):
@@ -28,7 +28,7 @@ class PackageCurationData(BaseModel):
2828
homepage_url: str | None = None
2929
binary_artifact: CurationArtifact | None = None
3030
source_artifact: CurationArtifact | None = None
31-
vcs: VcsInfo | None = None
31+
vcs: VcsInfoCurationData | None = None
3232
is_metadata_only: bool | None = None
3333
is_modified: bool | None = None
3434
declared_license_mapping: dict[str, Any] = Field(default_factory=dict)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# SPDX-FileCopyrightText: 2025 Helio Chissini de Castro <heliocastro@gmail.com>
2+
# SPDX-License-Identifier: MIT
3+
4+
from pydantic import AnyUrl, BaseModel, Field
5+
6+
from .vcstype import VcsType
7+
8+
9+
class VcsInfoCurationData(BaseModel):
10+
"""
11+
Bundles general Version Control System information.
12+
13+
Attributes:
14+
type(VcsType): The type of the VCS, for example Git, GitRepo, Mercurial, etc.
15+
url(AnyUrl): The URL to the VCS repository.
16+
revision(str): The VCS-specific revision (tag, branch, SHA1) that the version of the package maps to.
17+
path(str): The path inside the VCS to take into account.
18+
If the VCS supports checking out only a subdirectory, only this path is checked out.
19+
"""
20+
21+
type: VcsType | None = Field(
22+
default=None,
23+
description="The type of the VCS, for example Git, GitRepo, Mercurial, etc.",
24+
)
25+
url: AnyUrl | None = Field(
26+
default=None,
27+
description="The URL to the VCS repository.",
28+
)
29+
revision: str | None = Field(
30+
default=None,
31+
description="The VCS-specific revision (tag, branch, SHA1) that the version of the package maps to.",
32+
)
33+
path: str | None = Field(
34+
default=None,
35+
description="The path inside the VCS to take into account."
36+
"If the VCS supports checking out only a subdirectory, only this path is checked out.",
37+
)

src/ort/models/vcstype.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33

44
from pydantic import BaseModel, Field, model_validator
55

6+
# Define known VCS types as constants
7+
GIT = ["Git", "GitHub", "GitLab"]
8+
GIT_REPO = ["GitRepo", "git-repo", "repo"]
9+
MERCURIAL = ["Mercurial", "hg"]
10+
SUBVERSION = ["Subversion", "svn"]
11+
12+
KNOWN_TYPES = GIT + GIT_REPO + MERCURIAL + SUBVERSION
13+
614

715
class VcsType(BaseModel):
816
"""
@@ -12,35 +20,26 @@ class VcsType(BaseModel):
1220
alias for the string representation.
1321
1422
Attributes:
15-
aliases(list[str]): Primary name and aliases
23+
name(str): Primary name and aliases
1624
"""
1725

18-
aliases: list[str] = Field(default_factory=list, description="Primary name and aliases")
19-
20-
@model_validator(mode="after")
21-
def ensure_non_empty(self):
22-
"""Ensure the aliases list is never empty."""
23-
if not self.aliases:
24-
self.aliases = [""]
25-
return self
26-
27-
def __str__(self):
28-
return self.aliases[0] if self.aliases else ""
26+
name: str = Field(default_factory=str)
2927

28+
@model_validator(mode="before")
3029
@classmethod
31-
def for_name(cls, name: str) -> "VcsType":
32-
"""Lookup known type by name, or create a new instance."""
33-
for t in KNOWN_TYPES:
34-
if any(alias.lower() == name.lower() for alias in t.aliases):
35-
return t
36-
return cls(aliases=[name])
37-
38-
39-
# Define known VCS types as constants
40-
GIT = VcsType(aliases=["Git", "GitHub", "GitLab"])
41-
GIT_REPO = VcsType(aliases=["GitRepo", "git-repo", "repo"])
42-
MERCURIAL = VcsType(aliases=["Mercurial", "hg"])
43-
SUBVERSION = VcsType(aliases=["Subversion", "svn"])
44-
UNKNOWN = VcsType(aliases=[""])
45-
46-
KNOWN_TYPES = [GIT, GIT_REPO, MERCURIAL, SUBVERSION]
30+
def _forName(cls, value):
31+
# Allow direct string input (e.g., "Git" or "gitlab")
32+
if isinstance(value, str):
33+
if any(item.lower() == value.lower() for item in KNOWN_TYPES):
34+
return {"name": value}
35+
else:
36+
# Not a known type → default to empty string
37+
return {"name": ""}
38+
# Allow dict input or existing model
39+
elif isinstance(value, dict):
40+
name = value.get("name", "")
41+
if any(item.lower() == name.lower() for item in KNOWN_TYPES):
42+
return value
43+
else:
44+
return {"name": ""}
45+
return {"name": ""}

tests/data/example_curations.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
# Example for a complete curation object:
3+
#- id: "Maven:org.hamcrest:hamcrest-core:1.3"
4+
# curations:
5+
# comment: "An explanation why the curation is needed or the reasoning for a license conclusion."
6+
# concluded_license: "Apache-2.0 OR BSD-3-Clause" # Valid SPDX license expression to override the license findings.
7+
# declared_license_mapping:
8+
# "Copyright (C) 2013, Martin Journois": "NONE"
9+
# "BSD": "BSD-3-Clause"
10+
# description: "Curated description."
11+
# homepage_url: "http://example.com"
12+
# binary_artifact:
13+
# url: "http://example.com/binary.zip"
14+
# hash:
15+
# value: "ddce269a1e3d054cae349621c198dd52"
16+
# algorithm: "MD5"
17+
# source_artifact:
18+
# url: "http://example.com/sources.zip"
19+
# hash:
20+
# value: "ddce269a1e3d054cae349621c198dd52"
21+
# algorithm: "MD5"
22+
# vcs:
23+
# type: "Git"
24+
# url: "http://example.com/repo.git"
25+
# revision: "1234abc"
26+
# path: "subdirectory"
27+
# is_metadata_only: true # Whether the package is metadata only.
28+
# is_modified: true # Whether the package is modified compared to the original source.
29+
30+
- id: 'Maven:asm:asm' # No version means the curation will be applied to all versions of the package.
31+
curations:
32+
comment: 'Repository moved to https://gitlab.ow2.org.'
33+
vcs:
34+
type: 'Giot'
35+
url: 'https://gitlab.ow2.org/asm/asm.git'
36+
37+
- id: 'NPM::ast-traverse:0.1.0'
38+
curations:
39+
comment: 'Revision found by comparing the NPM package with the sources from https://github.com/olov/ast-traverse.'
40+
vcs:
41+
revision: 'f864d24ba07cde4b79f16999b1c99bfb240a441e'
42+
43+
- id: 'NPM::ast-traverse:0.1.1'
44+
curations:
45+
comment: 'Revision found by comparing the NPM package with the sources from https://github.com/olov/ast-traverse.'
46+
vcs:
47+
revision: '73f2b3c319af82fd8e490d40dd89a15951069b0d'
48+
49+
- id: 'NPM::ramda:[0.21.0,0.25.0]' # Ivy-style version matchers are supported.
50+
curations:
51+
comment: >-
52+
The package is licensed under MIT per `LICENSE` and `dist/ramda.js`. The project logo is CC-BY-NC-SA-3.0 but it is
53+
not part of the distributed .tar.gz package, see the `README.md` which says:
54+
"Ramda logo artwork © 2014 J. C. Phillipps. Licensed Creative Commons CC BY-NC-SA 3.0."
55+
concluded_license: 'MIT'
56+
57+
- id: 'Maven:org.jetbrains.kotlin:kotlin-bom'
58+
curations:
59+
comment: 'The package is a Maven BOM file and thus is metadata only.'
60+
is_metadata_only: true
61+
62+
- id: 'PyPI::pyramid-workflow:1.0.0'
63+
curations:
64+
comment: 'The package has an unmappable declared license entry.'
65+
declared_license_mapping:
66+
'BSD-derived (http://www.repoze.org/LICENSE.txt)': 'LicenseRef-scancode-repoze'
67+
68+
- id: 'PyPI::branca'
69+
curations:
70+
comment: 'A copyright statement was used to declare the license.'
71+
declared_license_mapping:
72+
'Copyright (C) 2013, Martin Journois': 'NONE'
73+
74+
- id: 'Maven:androidx.collection:collection:'
75+
curations:
76+
comment: 'Scan the source artifact, because the VCS revision and path are hard to figure out.'
77+
source_code_origins: [ARTIFACT]
78+
79+
- id: 'Maven:androidx.collection:collection:'
80+
curations:
81+
comment: 'Specify the platform for use within policy rules.'
82+
labels:
83+
platform: 'android'
84+
85+
- id: 'NPM:@types:mime-types:2.1.0'
86+
curations:
87+
comment: 'Retrieve the vulnerabilities from Black Duck by the provided origin-id instead of by the purl.'
88+
labels:
89+
black-duck:origin-id: 'npmjs:@types/mime-types/2.1.0'
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
- id: "Maven:com.example.app:example:0.0.1"
2+
curations:
3+
comment: "An explanation why the curation is needed or the reasoning for a license conclusion"
4+
purl: "pkg:Maven/com.example.app/example@0.0.1?arch=arm64-v8a#src/main"
5+
authors:
6+
- "Name of one author"
7+
- "Name of another author"
8+
cpe: "cpe:2.3:a:example-org:example-package:0.0.1:*:*:*:*:*:*:*"
9+
concluded_license: "Valid SPDX license expression to override the license findings."
10+
declared_license_mapping:
11+
"license a": "Apache-2.0"
12+
description: "Curated description."
13+
homepage_url: "http://example.com"
14+
binary_artifact:
15+
url: "http://example.com/binary.zip"
16+
hash:
17+
value: "ddce269a1e3d054cae349621c198dd52"
18+
algorithm: "MD5"
19+
source_artifact:
20+
url: "http://example.com/sources.zip"
21+
hash:
22+
value: "ddce269a1e3d054cae349621c198dd52"
23+
algorithm: "MD5"
24+
vcs:
25+
type: "Git"
26+
url: "http://example.com/repo.git"
27+
revision: "1234abc"
28+
path: "subdirectory"
29+
is_metadata_only: true
30+
is_modified: true
31+
source_code_origins: [ARTIFACT, VCS]
32+
labels:
33+
my-key: "my-value"

tests/test_ort_repository_configuration.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,30 @@
22
# SPDX-License-Identifier: MIT
33

44
from pathlib import Path
5-
from typing import Any
65

76
import pytest
8-
import yaml
97

108
from ort.models.repository_configuration import (
119
OrtRepositoryConfiguration,
1210
OrtRepositoryConfigurationIncludes,
1311
OrtRepositoryConfigurationIncludesPath,
1412
PathIncludeReason,
1513
)
14+
from tests.utils.load_yaml_config import load_yaml_config # type: ignore
1615

1716
REPO_CONFIG_DIR = Path(__file__).parent / "data" / "repo_config"
1817

1918

20-
def load_yaml_config(filename) -> Any:
21-
"""
22-
Load a YAML configuration file from the REPO_CONFIG_DIR directory.
23-
24-
Args:
25-
filename (str): The name of the YAML file to load.
26-
27-
Returns:
28-
object: The parsed YAML data as a Python object (usually dict).
29-
"""
30-
with (REPO_CONFIG_DIR / filename).open() as f:
31-
return yaml.safe_load(f)
32-
33-
3419
def test_only_include_valid():
3520
"""
3621
Test that a valid repository configuration with a single path include is loaded correctly.
3722
Verifies that the pattern, reason, and comment fields are present and have expected values,
3823
and that the model objects are instantiated without error and contain the correct data.
3924
"""
40-
config_data = load_yaml_config("only_include.yml")
25+
config_data = load_yaml_config(
26+
filename="only_include.yml",
27+
data_dir=REPO_CONFIG_DIR,
28+
)
4129
includes = config_data.get("includes", {})
4230
if "paths" not in includes:
4331
pytest.fail("Missing 'paths' in includes")
@@ -80,7 +68,7 @@ def test_only_include_reason_fail():
8068
raises a ValueError when instantiating OrtRepositoryConfigurationIncludesPath.
8169
The test expects failure when 'reason' is not a valid PathIncludeReason enum.
8270
"""
83-
config_data = load_yaml_config("only_include_reason_fail.yml")
71+
config_data = load_yaml_config("only_include_reason_fail.yml", REPO_CONFIG_DIR)
8472
includes = config_data.get("includes", {})
8573
if "paths" not in includes:
8674
pytest.fail("Missing 'paths' in includes")

0 commit comments

Comments
 (0)