debputy.manifest_parser.mapper_code

src/debputy/manifest_parser/mapper_code.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import dataclasses
from typing import (
    TypeVar,
    Optional,
    Union,
    List,
    TYPE_CHECKING,
    FrozenSet,
)
from collections.abc import Callable, Sequence, Mapping

from debputy.manifest_parser.exceptions import ManifestTypeException
from debputy.packages import BinaryPackage
from debputy.util import assume_not_none

if TYPE_CHECKING:
    from debputy.manifest_parser.util import AttributePath
    from debputy.manifest_parser.parser_data import ParserContextData

S = TypeVar("S")
T = TypeVar("T")


@dataclasses.dataclass(slots=True, frozen=True)
class PackageSelectorRule:
    valid_values: frozenset[str]
    impl: Callable[[BinaryPackage, str], bool]


def _pkg_selector_arch(package: BinaryPackage, selection_value: str) -> bool:
    if selection_value == "all":
        return package.is_arch_all
    return not package.is_arch_all


PACKAGE_SELECTORS: Mapping[str, PackageSelectorRule] = {
    "arch": PackageSelectorRule(
        frozenset(["all", "any"]),
        _pkg_selector_arch,
    ),
    "package-type": PackageSelectorRule(
        frozenset(["deb", "udeb"]),
        lambda p, v: p.package_type == v,
    ),
}


def type_mapper_str2package(
    raw_package_name: str,
    ap: "AttributePath",
    opc: Optional["ParserContextData"],
) -> BinaryPackage:
    pc = assume_not_none(opc)
    if "{{" in raw_package_name:
        resolved_package_name = pc.substitution.substitute(raw_package_name, ap.path)
    else:
        resolved_package_name = raw_package_name

    package_name_in_message = raw_package_name
    if resolved_package_name != raw_package_name:
        package_name_in_message = f'"{resolved_package_name}" ["{raw_package_name}"]'

    if not pc.is_known_package(resolved_package_name):
        package_names = ", ".join(pc.binary_packages)
        raise ManifestTypeException(
            f'The value {package_name_in_message} (from "{ap.path}") does not reference a package declared in'
            f" debian/control. Valid package names are: {package_names}"
        )
    package_data = pc.binary_package_data(resolved_package_name)
    if package_data.is_auto_generated_package:
        package_names = ", ".join(pc.binary_packages)
        raise ManifestTypeException(
            f'The package name {package_name_in_message} (from "{ap.path}") references an auto-generated package.'
            " However, auto-generated packages are now permitted here. Valid package names are:"
            f" {package_names}"
        )
    return package_data.binary_package


@dataclasses.dataclass(slots=True, frozen=True)
class PackageSelector:
    matched_binary_packages: Sequence[BinaryPackage]

    @classmethod
    def parse(
        cls,
        raw_value: str,
        attribute_path: "AttributePath",
        parser_context: "ParserContextData",
    ) -> "PackageSelector":
        pc = assume_not_none(parser_context)
        if ":" in raw_value:
            key, value = raw_value.split(":", maxsplit=1)
            selector = PACKAGE_SELECTORS.get(key)
            if selector is None:
                package_names = ", ".join(pc.binary_packages)
                valid_selectors = ", ".join(
                    f"{k}:<value>" for k in sorted(PACKAGE_SELECTORS)
                )
                raise ManifestTypeException(
                    f'Unknown package selector "{raw_value}" (from "{attribute_path.path}").'
                    f" Valid values here are: {package_names}, OR one of {valid_selectors}"
                )

            if value not in selector.valid_values:
                valid_values = ", ".join(sorted(selector.valid_values))
                raise ManifestTypeException(
                    f'The selection criteria "{key}" does not accept "{value}" (from "{attribute_path.path}").'
                    f" Valid values here are: {valid_values}"
                )

            matches = tuple(
                p
                for p in parser_context.binary_packages.values()
                if selector.impl(p, value)
            )
            return PackageSelector(matches)
        package = type_mapper_str2package(raw_value, attribute_path, parser_context)
        return PackageSelector((package,))


def wrap_into_list(
    x: T,
    _ap: "AttributePath",
    _pc: Optional["ParserContextData"],
) -> list[T]:
    return [x]


def normalize_into_list(
    x: T | list[T],
    _ap: "AttributePath",
    _pc: Optional["ParserContextData"],
) -> list[T]:
    return x if isinstance(x, list) else [x]


def map_each_element(
    mapper: Callable[[S, "AttributePath", Optional["ParserContextData"]], T],
) -> Callable[[list[S], "AttributePath", Optional["ParserContextData"]], list[T]]:
    def _generated_mapper(
        xs: list[S],
        ap: "AttributePath",
        pc: Optional["ParserContextData"],
    ) -> list[T]:
        return [mapper(s, ap[i], pc) for i, s in enumerate(xs)]

    return _generated_mapper