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

60 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-07 09:27 +0000

1import dataclasses 

2from typing import ( 

3 TypeVar, 

4 Optional, 

5 Union, 

6 List, 

7 Callable, 

8 TYPE_CHECKING, 

9 Sequence, 

10 Mapping, 

11 FrozenSet, 

12) 

13 

14from debputy.manifest_parser.exceptions import ManifestTypeException 

15from debputy.packages import BinaryPackage 

16from debputy.util import assume_not_none 

17 

18if TYPE_CHECKING: 

19 from debputy.manifest_parser.util import AttributePath 

20 from debputy.manifest_parser.parser_data import ParserContextData 

21 

22S = TypeVar("S") 

23T = TypeVar("T") 

24 

25 

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

27class PackageSelectorRule: 

28 valid_values: FrozenSet[str] 

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

30 

31 

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

33 if selection_value == "all": 

34 return package.is_arch_all 

35 return not package.is_arch_all 

36 

37 

38PACKAGE_SELECTORS: Mapping[str, PackageSelectorRule] = { 

39 "arch": PackageSelectorRule( 

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

41 _pkg_selector_arch, 

42 ), 

43 "package-type": PackageSelectorRule( 

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

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

46 ), 

47} 

48 

49 

50def type_mapper_str2package( 

51 raw_package_name: str, 

52 ap: "AttributePath", 

53 opc: Optional["ParserContextData"], 

54) -> BinaryPackage: 

55 pc = assume_not_none(opc) 

56 if "{{" in raw_package_name: 

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

58 else: 

59 resolved_package_name = raw_package_name 

60 

61 package_name_in_message = raw_package_name 

62 if resolved_package_name != raw_package_name: 

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

64 

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

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

67 raise ManifestTypeException( 

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

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

70 ) 

71 package_data = pc.binary_package_data(resolved_package_name) 

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

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

74 raise ManifestTypeException( 

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

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

77 f" {package_names}" 

78 ) 

79 return package_data.binary_package 

80 

81 

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

83class PackageSelector: 

84 matched_binary_packages: Sequence[BinaryPackage] 

85 

86 @classmethod 

87 def parse( 

88 cls, 

89 raw_value: str, 

90 attribute_path: "AttributePath", 

91 parser_context: "ParserContextData", 

92 ) -> "PackageSelector": 

93 pc = assume_not_none(parser_context) 

94 if ":" in raw_value: 

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

96 selector = PACKAGE_SELECTORS.get(key) 

97 if selector is None: 

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

99 valid_selectors = ", ".join( 

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

101 ) 

102 raise ManifestTypeException( 

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

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

105 ) 

106 

107 if value not in selector.valid_values: 

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

109 raise ManifestTypeException( 

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

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

112 ) 

113 

114 matches = tuple( 

115 p 

116 for p in parser_context.binary_packages.values() 

117 if selector.impl(p, value) 

118 ) 

119 return PackageSelector(matches) 

120 package = type_mapper_str2package(raw_value, attribute_path, parser_context) 

121 return PackageSelector((package,)) 

122 

123 

124def wrap_into_list( 

125 x: T, 

126 _ap: "AttributePath", 

127 _pc: Optional["ParserContextData"], 

128) -> List[T]: 

129 return [x] 

130 

131 

132def normalize_into_list( 

133 x: Union[T, List[T]], 

134 _ap: "AttributePath", 

135 _pc: Optional["ParserContextData"], 

136) -> List[T]: 

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

138 

139 

140def map_each_element( 

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

142) -> Callable[[List[S], "AttributePath", Optional["ParserContextData"]], List[T]]: 

143 def _generated_mapper( 

144 xs: List[S], 

145 ap: "AttributePath", 

146 pc: Optional["ParserContextData"], 

147 ) -> List[T]: 

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

149 

150 return _generated_mapper