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

61 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +0000

1import dataclasses 

2from typing import ( 

3 TypeVar, 

4 Optional, 

5 Union, 

6 List, 

7 TYPE_CHECKING, 

8 FrozenSet, 

9) 

10from collections.abc import Callable, Sequence, Mapping 

11 

12from debputy.manifest_parser.exceptions import ManifestTypeException 

13from debputy.packages import BinaryPackage 

14from debputy.util import assume_not_none 

15 

16if TYPE_CHECKING: 

17 from debputy.manifest_parser.util import AttributePath 

18 from debputy.manifest_parser.parser_data import ParserContextData 

19 

20S = TypeVar("S") 

21T = TypeVar("T") 

22 

23 

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

25class PackageSelectorRule: 

26 valid_values: frozenset[str] 

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

28 

29 

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

31 if selection_value == "all": 

32 return package.is_arch_all 

33 return not package.is_arch_all 

34 

35 

36PACKAGE_SELECTORS: Mapping[str, PackageSelectorRule] = { 

37 "arch": PackageSelectorRule( 

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

39 _pkg_selector_arch, 

40 ), 

41 "package-type": PackageSelectorRule( 

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

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

44 ), 

45} 

46 

47 

48def type_mapper_str2package( 

49 raw_package_name: str, 

50 ap: "AttributePath", 

51 opc: Optional["ParserContextData"], 

52) -> BinaryPackage: 

53 pc = assume_not_none(opc) 

54 if "{{" in raw_package_name: 

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

56 else: 

57 resolved_package_name = raw_package_name 

58 

59 package_name_in_message = raw_package_name 

60 if resolved_package_name != raw_package_name: 

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

62 

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

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

65 raise ManifestTypeException( 

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

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

68 ) 

69 package_data = pc.binary_package_data(resolved_package_name) 

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

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

72 raise ManifestTypeException( 

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

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

75 f" {package_names}" 

76 ) 

77 return package_data.binary_package 

78 

79 

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

81class PackageSelector: 

82 matched_binary_packages: Sequence[BinaryPackage] 

83 

84 @classmethod 

85 def parse( 

86 cls, 

87 raw_value: str, 

88 attribute_path: "AttributePath", 

89 parser_context: "ParserContextData", 

90 ) -> "PackageSelector": 

91 pc = assume_not_none(parser_context) 

92 if ":" in raw_value: 

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

94 selector = PACKAGE_SELECTORS.get(key) 

95 if selector is None: 

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

97 valid_selectors = ", ".join( 

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

99 ) 

100 raise ManifestTypeException( 

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

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

103 ) 

104 

105 if value not in selector.valid_values: 

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

107 raise ManifestTypeException( 

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

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

110 ) 

111 

112 matches = tuple( 

113 p 

114 for p in parser_context.binary_packages.values() 

115 if selector.impl(p, value) 

116 ) 

117 return PackageSelector(matches) 

118 package = type_mapper_str2package(raw_value, attribute_path, parser_context) 

119 return PackageSelector((package,)) 

120 

121 

122def wrap_into_list( 

123 x: T, 

124 _ap: "AttributePath", 

125 _pc: Optional["ParserContextData"], 

126) -> list[T]: 

127 return [x] 

128 

129 

130def normalize_into_list( 

131 x: T | list[T], 

132 _ap: "AttributePath", 

133 _pc: Optional["ParserContextData"], 

134) -> list[T]: 

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

136 

137 

138def map_each_element( 

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

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

141 def _generated_mapper( 

142 xs: list[S], 

143 ap: "AttributePath", 

144 pc: Optional["ParserContextData"], 

145 ) -> list[T]: 

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

147 

148 return _generated_mapper