Skip to content
Merged
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
3 changes: 0 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,3 @@ jobs:
shell: bash
run: |
uv build

- name: Test with python ${{ matrix.python-version }}
run: uv run --frozen pytest
28 changes: 28 additions & 0 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: pytest

on: [pull_request, workflow_dispatch]

jobs:
test-optima:
name: Run python-optima tests
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']

steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: true

- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38
with:
python-version: ${{ matrix.python-version }}

- name: Install the project
run: uv sync --locked --all-extras --dev

- name: Test with python ${{ matrix.python-version }}
run: uv run --frozen pytest
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@
requires = ["uv_build>=0.8.12,<0.10.0"]
build-backend = "uv_build"

[tool.hatch.build.targets.wheel]
packages = ["src/ort"]

[project]
name = "python-ort"
version = "0.3.0"
version = "0.3.1"
description = "A Python Ort model serialization library"
readme = "README.md"
license = "MIT"
Expand Down Expand Up @@ -39,6 +36,7 @@ dev = [
"pycodestyle>=2.14.0",
"pyrefly>=0.40.0",
"pytest>=8.4.2",
"rich>=14.2.0",
"ruff>=0.14.4",
"types-pyyaml>=6.0.12.20250915",
]
Expand All @@ -47,7 +45,8 @@ dev = [
addopts = ["--import-mode=importlib"]
log_cli = true
log_cli_level = "INFO"
pythonpath = "src"
pythonpath = ["src"]
testpaths = ["tests"]

[tool.pylint.messages_control]
disable = [
Expand Down
4 changes: 2 additions & 2 deletions src/ort/models/package_curation_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from .hash import Hash
from .source_code_origin import SourceCodeOrigin
from .vcsinfo import VcsInfo
from .vcsinfo_curation_data import VcsInfoCurationData


class CurationArtifact(BaseModel):
Expand All @@ -28,7 +28,7 @@ class PackageCurationData(BaseModel):
homepage_url: str | None = None
binary_artifact: CurationArtifact | None = None
source_artifact: CurationArtifact | None = None
vcs: VcsInfo | None = None
vcs: VcsInfoCurationData | None = None
is_metadata_only: bool | None = None
is_modified: bool | None = None
declared_license_mapping: dict[str, Any] = Field(default_factory=dict)
Expand Down
37 changes: 37 additions & 0 deletions src/ort/models/vcsinfo_curation_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: 2025 Helio Chissini de Castro <heliocastro@gmail.com>
# SPDX-License-Identifier: MIT

from pydantic import AnyUrl, BaseModel, Field

from .vcstype import VcsType


class VcsInfoCurationData(BaseModel):
"""
Bundles general Version Control System information.

Attributes:
type(VcsType): The type of the VCS, for example Git, GitRepo, Mercurial, etc.
url(AnyUrl): The URL to the VCS repository.
revision(str): The VCS-specific revision (tag, branch, SHA1) that the version of the package maps to.
path(str): The path inside the VCS to take into account.
If the VCS supports checking out only a subdirectory, only this path is checked out.
"""

type: VcsType | None = Field(
default=None,
description="The type of the VCS, for example Git, GitRepo, Mercurial, etc.",
)
url: AnyUrl | None = Field(
default=None,
description="The URL to the VCS repository.",
)
revision: str | None = Field(
default=None,
description="The VCS-specific revision (tag, branch, SHA1) that the version of the package maps to.",
)
path: str | None = Field(
default=None,
description="The path inside the VCS to take into account."
"If the VCS supports checking out only a subdirectory, only this path is checked out.",
)
55 changes: 27 additions & 28 deletions src/ort/models/vcstype.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@

from pydantic import BaseModel, Field, model_validator

# Define known VCS types as constants
GIT = ["Git", "GitHub", "GitLab"]
GIT_REPO = ["GitRepo", "git-repo", "repo"]
MERCURIAL = ["Mercurial", "hg"]
SUBVERSION = ["Subversion", "svn"]

KNOWN_TYPES = GIT + GIT_REPO + MERCURIAL + SUBVERSION


class VcsType(BaseModel):
"""
Expand All @@ -12,35 +20,26 @@ class VcsType(BaseModel):
alias for the string representation.

Attributes:
aliases(list[str]): Primary name and aliases
name(str): Primary name and aliases
"""

aliases: list[str] = Field(default_factory=list, description="Primary name and aliases")

@model_validator(mode="after")
def ensure_non_empty(self):
"""Ensure the aliases list is never empty."""
if not self.aliases:
self.aliases = [""]
return self

def __str__(self):
return self.aliases[0] if self.aliases else ""
name: str = Field(default_factory=str)

@model_validator(mode="before")
@classmethod
def for_name(cls, name: str) -> "VcsType":
"""Lookup known type by name, or create a new instance."""
for t in KNOWN_TYPES:
if any(alias.lower() == name.lower() for alias in t.aliases):
return t
return cls(aliases=[name])


# Define known VCS types as constants
GIT = VcsType(aliases=["Git", "GitHub", "GitLab"])
GIT_REPO = VcsType(aliases=["GitRepo", "git-repo", "repo"])
MERCURIAL = VcsType(aliases=["Mercurial", "hg"])
SUBVERSION = VcsType(aliases=["Subversion", "svn"])
UNKNOWN = VcsType(aliases=[""])

KNOWN_TYPES = [GIT, GIT_REPO, MERCURIAL, SUBVERSION]
def _forName(cls, value):
# Allow direct string input (e.g., "Git" or "gitlab")
if isinstance(value, str):
if any(item.lower() == value.lower() for item in KNOWN_TYPES):
return {"name": value}
else:
# Not a known type → default to empty string
return {"name": ""}
# Allow dict input or existing model
elif isinstance(value, dict):
name = value.get("name", "")
if any(item.lower() == name.lower() for item in KNOWN_TYPES):
return value
else:
return {"name": ""}
return {"name": ""}
89 changes: 89 additions & 0 deletions tests/data/example_curations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
# Example for a complete curation object:
#- id: "Maven:org.hamcrest:hamcrest-core:1.3"
# curations:
# comment: "An explanation why the curation is needed or the reasoning for a license conclusion."
# concluded_license: "Apache-2.0 OR BSD-3-Clause" # Valid SPDX license expression to override the license findings.
# declared_license_mapping:
# "Copyright (C) 2013, Martin Journois": "NONE"
# "BSD": "BSD-3-Clause"
# description: "Curated description."
# homepage_url: "http://example.com"
# binary_artifact:
# url: "http://example.com/binary.zip"
# hash:
# value: "ddce269a1e3d054cae349621c198dd52"
# algorithm: "MD5"
# source_artifact:
# url: "http://example.com/sources.zip"
# hash:
# value: "ddce269a1e3d054cae349621c198dd52"
# algorithm: "MD5"
# vcs:
# type: "Git"
# url: "http://example.com/repo.git"
# revision: "1234abc"
# path: "subdirectory"
# is_metadata_only: true # Whether the package is metadata only.
# is_modified: true # Whether the package is modified compared to the original source.

- id: 'Maven:asm:asm' # No version means the curation will be applied to all versions of the package.
curations:
comment: 'Repository moved to https://gitlab.ow2.org.'
vcs:
type: 'Giot'
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Giot' to 'Git'.

Suggested change
type: 'Giot'
type: 'Git'

Copilot uses AI. Check for mistakes.
url: 'https://gitlab.ow2.org/asm/asm.git'

- id: 'NPM::ast-traverse:0.1.0'
curations:
comment: 'Revision found by comparing the NPM package with the sources from https://github.com/olov/ast-traverse.'
vcs:
revision: 'f864d24ba07cde4b79f16999b1c99bfb240a441e'

- id: 'NPM::ast-traverse:0.1.1'
curations:
comment: 'Revision found by comparing the NPM package with the sources from https://github.com/olov/ast-traverse.'
vcs:
revision: '73f2b3c319af82fd8e490d40dd89a15951069b0d'

- id: 'NPM::ramda:[0.21.0,0.25.0]' # Ivy-style version matchers are supported.
curations:
comment: >-
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
not part of the distributed .tar.gz package, see the `README.md` which says:
"Ramda logo artwork © 2014 J. C. Phillipps. Licensed Creative Commons CC BY-NC-SA 3.0."
concluded_license: 'MIT'

- id: 'Maven:org.jetbrains.kotlin:kotlin-bom'
curations:
comment: 'The package is a Maven BOM file and thus is metadata only.'
is_metadata_only: true

- id: 'PyPI::pyramid-workflow:1.0.0'
curations:
comment: 'The package has an unmappable declared license entry.'
declared_license_mapping:
'BSD-derived (http://www.repoze.org/LICENSE.txt)': 'LicenseRef-scancode-repoze'

- id: 'PyPI::branca'
curations:
comment: 'A copyright statement was used to declare the license.'
declared_license_mapping:
'Copyright (C) 2013, Martin Journois': 'NONE'

- id: 'Maven:androidx.collection:collection:'
curations:
comment: 'Scan the source artifact, because the VCS revision and path are hard to figure out.'
source_code_origins: [ARTIFACT]

- id: 'Maven:androidx.collection:collection:'
curations:
comment: 'Specify the platform for use within policy rules.'
labels:
platform: 'android'

- id: 'NPM:@types:mime-types:2.1.0'
curations:
comment: 'Retrieve the vulnerabilities from Black Duck by the provided origin-id instead of by the purl.'
labels:
black-duck:origin-id: 'npmjs:@types/mime-types/2.1.0'
33 changes: 33 additions & 0 deletions tests/data/example_simple_curation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
- id: "Maven:com.example.app:example:0.0.1"
curations:
comment: "An explanation why the curation is needed or the reasoning for a license conclusion"
purl: "pkg:Maven/com.example.app/example@0.0.1?arch=arm64-v8a#src/main"
authors:
- "Name of one author"
- "Name of another author"
cpe: "cpe:2.3:a:example-org:example-package:0.0.1:*:*:*:*:*:*:*"
concluded_license: "Valid SPDX license expression to override the license findings."
declared_license_mapping:
"license a": "Apache-2.0"
description: "Curated description."
homepage_url: "http://example.com"
binary_artifact:
url: "http://example.com/binary.zip"
hash:
value: "ddce269a1e3d054cae349621c198dd52"
algorithm: "MD5"
source_artifact:
url: "http://example.com/sources.zip"
hash:
value: "ddce269a1e3d054cae349621c198dd52"
algorithm: "MD5"
vcs:
type: "Git"
url: "http://example.com/repo.git"
revision: "1234abc"
path: "subdirectory"
is_metadata_only: true
is_modified: true
source_code_origins: [ARTIFACT, VCS]
labels:
my-key: "my-value"
24 changes: 6 additions & 18 deletions tests/test_ort_repository_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,30 @@
# SPDX-License-Identifier: MIT

from pathlib import Path
from typing import Any

import pytest
import yaml

from ort.models.repository_configuration import (
OrtRepositoryConfiguration,
OrtRepositoryConfigurationIncludes,
OrtRepositoryConfigurationIncludesPath,
PathIncludeReason,
)
from tests.utils.load_yaml_config import load_yaml_config # type: ignore

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


def load_yaml_config(filename) -> Any:
"""
Load a YAML configuration file from the REPO_CONFIG_DIR directory.

Args:
filename (str): The name of the YAML file to load.

Returns:
object: The parsed YAML data as a Python object (usually dict).
"""
with (REPO_CONFIG_DIR / filename).open() as f:
return yaml.safe_load(f)


def test_only_include_valid():
"""
Test that a valid repository configuration with a single path include is loaded correctly.
Verifies that the pattern, reason, and comment fields are present and have expected values,
and that the model objects are instantiated without error and contain the correct data.
"""
config_data = load_yaml_config("only_include.yml")
config_data = load_yaml_config(
filename="only_include.yml",
data_dir=REPO_CONFIG_DIR,
)
includes = config_data.get("includes", {})
if "paths" not in includes:
pytest.fail("Missing 'paths' in includes")
Expand Down Expand Up @@ -80,7 +68,7 @@ def test_only_include_reason_fail():
raises a ValueError when instantiating OrtRepositoryConfigurationIncludesPath.
The test expects failure when 'reason' is not a valid PathIncludeReason enum.
"""
config_data = load_yaml_config("only_include_reason_fail.yml")
config_data = load_yaml_config("only_include_reason_fail.yml", REPO_CONFIG_DIR)
includes = config_data.get("includes", {})
if "paths" not in includes:
pytest.fail("Missing 'paths' in includes")
Expand Down
Loading
Loading