Coverage for src/debputy/manifest_parser/mapper_code.py: 58%

61 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-01-16 17:20 +0000

1import dataclasses 

2from typing import ( 

3 TypeVar, 

4 Optional, 

5 TYPE_CHECKING, 

6) 

7from collections.abc import Callable, Sequence, Mapping 

8 

9from debputy.manifest_parser.exceptions import ManifestTypeException 

10from debputy.packages import BinaryPackage 

11from debputy.util import assume_not_none 

12 

13if TYPE_CHECKING: 

14 from debputy.manifest_parser.util import AttributePath 

15 from debputy.manifest_parser.parser_data import ParserContextData 

16 

17S = TypeVar("S") 

18T = TypeVar("T") 

19 

20 

21@dataclasses.dataclass(slots=True, frozen=True) 

22class PackageSelectorRule: 

23 valid_values: frozenset[str] 

24 impl: Callable[[BinaryPackage, str], bool] 

25 

26 

27def _pkg_selector_arch(package: BinaryPackage, selection_value: str) -> bool: 

28 if selection_value == "all": 

29 return package.is_arch_all 

30 return not package.is_arch_all 

31 

32 

33PACKAGE_SELECTORS: Mapping[str, PackageSelectorRule] = { 

34 "arch": PackageSelectorRule( 

35 frozenset(["all", "any"]), 

36 _pkg_selector_arch, 

37 ), 

38 "package-type": PackageSelectorRule( 

39 frozenset(["deb", "udeb"]), 

40 lambda p, v: p.package_type == v, 

41 ), 

42} 

43 

44 

45def type_mapper_str2package( 

46 raw_package_name: str, 

47 ap: "AttributePath", 

48 opc: Optional["ParserContextData"], 

49) -> BinaryPackage: 

50 pc = assume_not_none(opc) 

51 if "{{" in raw_package_name: 

52 resolved_package_name = pc.substitution.substitute(raw_package_name, ap.path) 

53 else: 

54 resolved_package_name = raw_package_name 

55 

56 package_name_in_message = raw_package_name 

57 if resolved_package_name != raw_package_name: 

58 package_name_in_message = f'"{resolved_package_name}" ["{raw_package_name}"]' 

59 

60 if not pc.is_known_package(resolved_package_name): 60 ↛ 61line 60 didn't jump to line 61 because the condition on line 60 was never true

61 package_names = ", ".join(pc.binary_packages) 

62 raise ManifestTypeException( 

63 f'The value {package_name_in_message} (from "{ap.path}") does not reference a package declared in' 

64 f" debian/control. Valid package names are: {package_names}" 

65 ) 

66 package_data = pc.binary_package_data(resolved_package_name) 

67 if package_data.is_auto_generated_package: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true

68 package_names = ", ".join(pc.binary_packages) 

69 raise ManifestTypeException( 

70 f'The package name {package_name_in_message} (from "{ap.path}") references an auto-generated package.' 

71 " However, auto-generated packages are now permitted here. Valid package names are:" 

72 f" {package_names}" 

73 ) 

74 return package_data.binary_package 

75 

76 

77@dataclasses.dataclass(slots=True, frozen=True) 

78class PackageSelector: 

79 matched_binary_packages: Sequence[BinaryPackage] 

80 

81 @classmethod 

82 def parse( 

83 cls, 

84 raw_value: str, 

85 attribute_path: "AttributePath", 

86 parser_context: "ParserContextData", 

87 ) -> "PackageSelector": 

88 pc = assume_not_none(parser_context) 

89 if ":" in raw_value: 

90 key, value = raw_value.split(":", maxsplit=1) 

91 selector = PACKAGE_SELECTORS.get(key) 

92 if selector is None: 

93 package_names = ", ".join(pc.binary_packages) 

94 valid_selectors = ", ".join( 

95 f"{k}:<value>" for k in sorted(PACKAGE_SELECTORS) 

96 ) 

97 raise ManifestTypeException( 

98 f'Unknown package selector "{raw_value}" (from "{attribute_path.path}").' 

99 f" Valid values here are: {package_names}, OR one of {valid_selectors}" 

100 ) 

101 

102 if value not in selector.valid_values: 

103 valid_values = ", ".join(sorted(selector.valid_values)) 

104 raise ManifestTypeException( 

105 f'The selection criteria "{key}" does not accept "{value}" (from "{attribute_path.path}").' 

106 f" Valid values here are: {valid_values}" 

107 ) 

108 

109 matches = tuple( 

110 p 

111 for p in parser_context.binary_packages.values() 

112 if selector.impl(p, value) 

113 ) 

114 return PackageSelector(matches) 

115 package = type_mapper_str2package(raw_value, attribute_path, parser_context) 

116 return PackageSelector((package,)) 

117 

118 

119def wrap_into_list( 

120 x: T, 

121 _ap: "AttributePath", 

122 _pc: Optional["ParserContextData"], 

123) -> list[T]: 

124 return [x] 

125 

126 

127def normalize_into_list( 

128 x: T | list[T], 

129 _ap: "AttributePath", 

130 _pc: Optional["ParserContextData"], 

131) -> list[T]: 

132 return x if isinstance(x, list) else [x] 

133 

134 

135def map_each_element( 

136 mapper: Callable[[S, "AttributePath", Optional["ParserContextData"]], T], 

137) -> Callable[[list[S], "AttributePath", Optional["ParserContextData"]], list[T]]: 

138 def _generated_mapper( 

139 xs: list[S], 

140 ap: "AttributePath", 

141 pc: Optional["ParserContextData"], 

142 ) -> list[T]: 

143 return [mapper(s, ap[i], pc) for i, s in enumerate(xs)] 

144 

145 return _generated_mapper