diff --git a/packer/src/sarasacw_omrf_packer/metadata.py b/packer/src/sarasacw_omrf_packer/metadata.py index 7a5bed6..549ebb4 100644 --- a/packer/src/sarasacw_omrf_packer/metadata.py +++ b/packer/src/sarasacw_omrf_packer/metadata.py @@ -18,7 +18,14 @@ from re import Pattern, compile from typing import Any, Callable from semver import Version from . import utils -from .utils import Triple, VERSION, dict_chain_get, dict_typed_get, dict_typed_get_required +from .utils import ( + Triple, + VERSION, + dict_chain_get, + dict_typed_get, + dict_typed_get_required, + list_typed_iter, +) @dataclass(frozen=True) @@ -104,15 +111,14 @@ class MetadataCMake: def from_dict(d: dict[str, Any]) -> "MetadataCMake": """Build a :class:`MetadataCMake` from its raw TOML table.""" raw_dependencies = dict_typed_get(d, "dependencies", list) - if raw_dependencies is not None: - dependencies_list: list[MetadataCMakeDependency] = [] - for i, item in enumerate(raw_dependencies): - if not isinstance(item, dict): - raise TypeError(f"dependencies[{i}] must be a table") - dependencies_list.append(MetadataCMakeDependency.from_dict(item)) - dependencies = tuple(dependencies_list) - else: - dependencies = None + dependencies = ( + tuple( + MetadataCMakeDependency.from_dict(item) + for item in list_typed_iter(raw_dependencies, dict) + ) + if raw_dependencies is not None + else None + ) return MetadataCMake( namespace_name=dict_typed_get(d, "namespace_name", str), @@ -157,15 +163,11 @@ class MetadataPkgConfig: def from_dict(d: dict[str, Any]) -> "MetadataPkgConfig": """Build a :class:`MetadataPkgConfig` from its raw TOML table.""" raw_requires = dict_typed_get(d, "requires", list) - if raw_requires is not None: - requires_list: list[str] = [] - for i, item in enumerate(raw_requires): - if not isinstance(item, str): - raise TypeError(f"requires[{i}] must be a string") - requires_list.append(item) - requires = tuple(requires_list) - else: - requires = None + requires = ( + tuple(list_typed_iter(raw_requires, str)) + if raw_requires is not None + else None + ) return MetadataPkgConfig( id=dict_typed_get(d, "id", str), @@ -210,13 +212,12 @@ class Metadata: else: min_version = None - raw_headers = dict_typed_get_required(d, "headers", list) - headers_list: list[MetadataHeader] = [] - for i, item in enumerate(raw_headers): - if not isinstance(item, dict): - raise TypeError(f"headers[{i}] must be a table") - headers_list.append(MetadataHeader.from_dict(item)) - headers = tuple(headers_list) + headers = tuple( + MetadataHeader.from_dict(item) + for item in list_typed_iter( + dict_typed_get_required(d, "headers", list), dict + ) + ) raw_cmake = dict_typed_get(d, "cmake", dict) cmake = MetadataCMake.from_dict(raw_cmake) if raw_cmake is not None else None @@ -260,6 +261,7 @@ _SUBPROCESS_TIMEOUT_SEC: int = 10 _HOST_TRIPLE_PATTERN: Pattern = compile(r"(?m)^host:\s*(\S+)$") """The pattern for match host triple in ``rustc -vV``. The group 1 is the triple result.""" + class MetadataExtractor: """Access layer over ``cargo metadata`` and the OMRF metadata table. @@ -313,8 +315,7 @@ class MetadataExtractor: raise RuntimeError("fail to fetch host triple: timed out") if proc.returncode != 0: raise RuntimeError( - "fail to fetch host triple: " - + stderr.decode("utf-8", errors="ignore") + "fail to fetch host triple: " + stderr.decode("utf-8", errors="ignore") ) m = _HOST_TRIPLE_PATTERN.search(stdout.decode("utf-8", errors="strict")) if m is None: @@ -367,11 +368,8 @@ class MetadataExtractor: :returns: The matching package item. :raises RuntimeError: if no matching package is found. """ - packages: list[Any] = dict_typed_get_required(cargo_metadata, "packages", list) - for i, package in enumerate(packages): - if not isinstance(package, dict): - raise TypeError(f"packages[{i}] must be a table") - + packages = dict_typed_get_required(cargo_metadata, "packages", list) + for package in list_typed_iter(packages, dict): raw_manifest_path = dict_typed_get_required(package, "manifest_path", str) manifest_path = Path(raw_manifest_path) if manifest_path == cargo_toml_path: @@ -393,11 +391,8 @@ class MetadataExtractor: # build the path to lib.rs for comparing librs = cargo_toml_path.parent / "src" / "lib.rs" # start checking - targets: list[Any] = dict_typed_get_required(cargo_package, "targets", list) - for i, target in enumerate(targets): - if not isinstance(target, dict): - raise TypeError(f"targets[{i}] must be a table") - + targets = dict_typed_get_required(cargo_package, "targets", list) + for target in list_typed_iter(targets, dict): raw_src_path = dict_typed_get_required(target, "src_path", str) src_path = Path(raw_src_path) if src_path == librs: diff --git a/packer/src/sarasacw_omrf_packer/utils.py b/packer/src/sarasacw_omrf_packer/utils.py index f73a115..c5d5f70 100644 --- a/packer/src/sarasacw_omrf_packer/utils.py +++ b/packer/src/sarasacw_omrf_packer/utils.py @@ -7,7 +7,7 @@ validation and template-directory resolution. from dataclasses import dataclass from pathlib import Path from re import Pattern, compile -from typing import Any, overload +from typing import Any, Iterator, overload from semver import Version VERSION: Version = Version(1, 0, 0) @@ -106,6 +106,20 @@ def dict_chain_get(d: dict[str, Any], *args: str) -> dict[str, Any]: return d +def list_typed_iter[T](lst: list[Any], ty: type[T]) -> Iterator[T]: + """Yield each element of ``lst`` after checking it is an instance of ``ty``. + + :param lst: The list to iterate. + :param ty: Expected type of every element. + :returns: An iterator over the validated elements, typed ``T``. + :raises TypeError: if an element is not an instance of ``ty``. + """ + for i, item in enumerate(lst): + if not isinstance(item, ty): + raise TypeError(f"item {i} of the given list is not a {ty.__name__}") + yield item + + _NAME_PATTERN: Pattern = compile(r"[a-zA-Z0-9_+-]+")