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
66 changes: 49 additions & 17 deletions gen/generate-bindings
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
#! /bin/sh
#
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
#
# Generate the per-version bindings and the bindings package __init__.py.

set -e

# SPDX versions to generate
SPDX_VERSIONS="3.0.1"

mkdir -p "gen"

echo "# Import all versions" > __init__.py

get_context_url() {
echo "https://spdx.org/rdf/$1/spdx-context.jsonld"
}

# Generate the per-version bindings with shacl2code.
# Each version ends up in its own subpackage (e.g. "v3_0_1").
for v in $SPDX_VERSIONS; do
MODNAME="v$(echo "$v" | sed 's/[^a-zA-Z0-9_]/_/g')"

Expand All @@ -35,19 +36,50 @@ for v in $SPDX_VERSIONS; do
python \
--output "$MODNAME"
fi

echo "from . import $MODNAME" >> __init__.py
done

MODNAME=""
CONTEXT_URL=""
# Generate the bindings package __init__.py.
{
echo '# SPDX-FileType: SOURCE'
echo '# SPDX-License-Identifier: Apache-2.0'
echo '#'
echo '# Generated by generate-bindings at build time. DO NOT EDIT.'
echo '"""SPDX model bindings, one submodule per version.'
echo
echo 'Versions are not imported here, so importing one does not load the others.'
echo '"""'
echo
echo '# JSON-LD @context URL -> version submodule name, for lazy lookup by load().'
echo '_CONTEXT_TABLE = {'
for v in $SPDX_VERSIONS; do
MODNAME="v$(echo "$v" | sed 's/[^a-zA-Z0-9_]/_/g')"
CONTEXT_URL="$(get_context_url "$v")"
echo " \"$CONTEXT_URL\": \"$MODNAME\","
done
echo '}'
} > __init__.py

echo >> __init__.py
echo "# Generate context table" >> __init__.py
echo "_CONTEXT_TABLE = {" >> __init__.py
for v in $SPDX_VERSIONS; do
MODNAME="v$(echo "$v" | sed 's/[^a-zA-Z0-9_]/_/g')"
CONTEXT_URL="$(get_context_url "$v")"
echo " '$CONTEXT_URL': $MODNAME" >> __init__.py
done
echo "}" >> __init__.py
# Generate _reexport.pyi: lets type checkers resolve spdx_python_model.vX types
# while the runtime loads versions lazily (imported only under TYPE_CHECKING).
{
echo '# SPDX-FileType: SOURCE'
echo '# SPDX-License-Identifier: Apache-2.0'
echo '#'
echo '# Generated by generate-bindings at build time. DO NOT EDIT.'
echo '"""Type-checking-only re-exports so ``spdx_python_model.vX`` types resolve.'
echo
echo 'Imported under TYPE_CHECKING only; the runtime loads versions lazily via the'
echo 'package __getattr__. __all__ marks the names as re-exports for --no-implicit-reexport.'
echo '"""'
for v in $SPDX_VERSIONS; do
MODNAME="v$(echo "$v" | sed 's/[^a-zA-Z0-9_]/_/g')"
echo "from . import $MODNAME"
done
echo
echo '__all__ = ['
for v in $SPDX_VERSIONS; do
MODNAME="v$(echo "$v" | sed 's/[^a-zA-Z0-9_]/_/g')"
echo " \"$MODNAME\","
done
echo ']'
} > _reexport.pyi
31 changes: 23 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ license = "Apache-2.0"

[project.optional-dependencies]
dev = [
"mypy >= 1.19.1",
"pylint >= 3.3.9",
"pytest >= 7.4",
]

Expand All @@ -48,14 +50,6 @@ requires = [
]
build-backend = "hatchling.build"

[tool.hatch.version]
path = "src/spdx_python_model/version.py"

[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]

[[tool.hatch.build.hooks.build-scripts.scripts]]
out_dir = "src/spdx_python_model/bindings"
work_dir = "gen"
Expand All @@ -67,3 +61,24 @@ artifacts = [
"*.py",
"*.pyi",
]

[tool.hatch.version]
path = "src/spdx_python_model/version.py"

[tool.mypy]
disallow_any_decorated = true
disallow_any_unimported = true
extra_checks = true
python_version = "3.9"
strict = true
strict_bytes = true
warn_unreachable = true

[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]

[tool.pylint.main]
# The generated binding fails style checks. Ignore it.
ignore-paths = ['^src/spdx_python_model/bindings/v3_0_1/.*$']
60 changes: 43 additions & 17 deletions src/spdx_python_model/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
#
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
#
"""SPDX 3 model."""

from .bindings import *
from .version import VERSION
import importlib
import json
from pathlib import Path
from types import ModuleType
from typing import TYPE_CHECKING, Any, List, Tuple

from .bindings import _CONTEXT_TABLE
from .version import VERSION
from .version import VERSION as __version__

from pathlib import Path
import json
if TYPE_CHECKING:
# Generated re-exports to give type checkers version types.
# No imported during runtime.
from .bindings._reexport import * # noqa: F403

__all__ = ["LoadError", "VERSION", "__version__", "load", "load_data"]

# Version submodule names accepted by __getattr__ for top-level import.
_VERSION_MODULES = frozenset(_CONTEXT_TABLE.values())


class LoadError(Exception):
pass
"""Raised when a SPDX document cannot be loaded."""


def __getattr__(name: str) -> ModuleType:
"""Lazily import a version's bindings on first top-level access (PEP 562)."""
Comment thread
bact marked this conversation as resolved.
if name in _VERSION_MODULES:
return importlib.import_module(f"{__name__}.bindings.{name}")
Comment thread
bact marked this conversation as resolved.
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Comment thread
bact marked this conversation as resolved.


def __dir__() -> List[str]:
return sorted(set(globals()) | _VERSION_MODULES)


def load_data(data):
def load_data(data: Any) -> Tuple[ModuleType, Any]:
"""
Automatically load a SPDX 3 JSON document with the correct model based on
its context
Expand All @@ -32,26 +56,28 @@ def load_data(data):
if not isinstance(data, dict):
raise TypeError("Data must be a dictionary")

if "@context" not in data:
context = data.get("@context")
if not context:
raise LoadError("No @context in data")

context_url = None

if isinstance(data["@context"], str):
context_url = data["@context"]
elif isinstance(data["@context"], list):
for item in data["@context"]:
if isinstance(context, str):
context_url = context
elif isinstance(context, list):
for item in context:
if isinstance(item, str):
context_url = item
break

if not context_url:
raise LoadError("No valid @context URL string found in data")

if context_url not in _CONTEXT_TABLE:
raise LoadError(f"Unknown context URL '{context}'")
module_name = _CONTEXT_TABLE.get(context_url)
if module_name is None:
raise LoadError(f"Unknown context URL '{context_url}'")

model = _CONTEXT_TABLE[context_url]
model = importlib.import_module(f"{__name__}.bindings.{module_name}")

d = model.JSONLDDeserializer()
objset = model.SHACLObjectSet()
Expand All @@ -61,12 +87,12 @@ def load_data(data):
return model, objset


def load(path: Path):
def load(path: Path) -> Tuple[ModuleType, Any]:
"""
Automatically load a SPDX 3 JSON document with the correct model based on
its context

:param data: The path to the SPDX 3 JSON file
:param path: The path to the SPDX 3 JSON file

:returns: A tuple that contains the model and the decoded SHACLObjectSet

Expand Down
3 changes: 2 additions & 1 deletion src/spdx_python_model/version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0
#
"""Package version information."""

VERSION = "0.0.5"
24 changes: 23 additions & 1 deletion tests/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,28 @@ def test_direct_create():


def test_alias():
from spdx_python_model import v3_0_1 as spdx
# The eager path under spdx_python_model.bindings stays available too.
from spdx_python_model.bindings import v3_0_1 as spdx

p = spdx.Person()


def test_lazy_import():
"""Importing the package must not eagerly load any version bindings."""
import importlib
import sys

for mod in list(sys.modules):
if mod.startswith("spdx_python_model"):
del sys.modules[mod]

importlib.import_module("spdx_python_model")
assert not any(
m.startswith("spdx_python_model.bindings.v") for m in sys.modules
), "no version bindings should be loaded on 'import spdx_python_model'"

# Accessing a version triggers a lazy import of just that version.
import spdx_python_model

assert spdx_python_model.v3_0_1 is not None
assert "spdx_python_model.bindings.v3_0_1" in sys.modules
1 change: 0 additions & 1 deletion tests/test_load.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: Apache-2.0

import importlib
import re
from pathlib import Path

Expand Down