Coverage for src/debputy/dh_migration/models.py: 84%

106 statements  

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

1import dataclasses 

2import re 

3from collections.abc import Sequence, Iterable 

4from typing import cast 

5 

6from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable 

7from debputy.commands.debputy_cmd.output import IOBasedOutputStyling 

8from debputy.highlevel_manifest import MutableYAMLManifest, HighLevelManifest 

9from debputy.packages import BinaryPackage 

10from debputy.plugin.api import VirtualPath 

11from debputy.plugin.api.spec import DebputyIntegrationMode 

12from debputy.substitution import Substitution 

13 

14_DH_VAR_RE = re.compile(r"([$][{])([A-Za-z0-9][-_:0-9A-Za-z]*)([}])") 

15 

16 

17class AcceptableMigrationIssues: 

18 def __init__(self, values: frozenset[str]): 

19 self._values = values 

20 

21 def __contains__(self, item: str) -> bool: 

22 return item in self._values or "ALL" in self._values 

23 

24 

25class UnsupportedFeature(RuntimeError): 

26 @property 

27 def message(self) -> str: 

28 return cast("str", self.args[0]) 

29 

30 @property 

31 def issue_keys(self) -> Sequence[str] | None: 

32 if len(self.args) < 2: 

33 return None 

34 return cast("Sequence[str]", self.args[1]) 

35 

36 

37class ConflictingChange(RuntimeError): 

38 @property 

39 def message(self) -> str: 

40 return cast("str", self.args[0]) 

41 

42 

43@dataclasses.dataclass(slots=True) 

44class MigrationRequest: 

45 debian_dir: VirtualPath 

46 manifest: HighLevelManifest 

47 acceptable_migration_issues: AcceptableMigrationIssues 

48 migration_target: DebputyIntegrationMode | None 

49 

50 @property 

51 def all_packages(self) -> Iterable[BinaryPackage]: 

52 return self.manifest.all_packages 

53 

54 @property 

55 def is_single_binary_package(self) -> bool: 

56 return sum(1 for _ in self.all_packages) == 1 

57 

58 @property 

59 def main_binary(self) -> BinaryPackage: 

60 return next(iter(p for p in self.all_packages if p.is_main_package)) 

61 

62 

63@dataclasses.dataclass(slots=True) 

64class FeatureMigration: 

65 tagline: str 

66 fo: IOBasedOutputStyling 

67 successful_manifest_changes: int = 0 

68 warnings: list[str] = dataclasses.field(default_factory=list) 

69 remove_paths_on_success: list[str] = dataclasses.field(default_factory=list) 

70 rename_paths_on_success: list[tuple[str, str]] = dataclasses.field( 

71 default_factory=list 

72 ) 

73 assumed_compat: int | None = None 

74 required_plugins: list[str] = dataclasses.field(default_factory=list) 

75 

76 def warn(self, msg: str) -> None: 

77 self.warnings.append(msg) 

78 

79 def rename_on_success(self, source: str, dest: str) -> None: 

80 self.rename_paths_on_success.append((source, dest)) 

81 

82 def remove_on_success(self, path: str) -> None: 

83 self.remove_paths_on_success.append(path) 

84 

85 def require_plugin(self, debputy_plugin: str) -> None: 

86 self.required_plugins.append(debputy_plugin) 

87 

88 @property 

89 def anything_to_do(self) -> bool: 

90 return bool(self.total_changes_involved) 

91 

92 @property 

93 def performed_changes(self) -> int: 

94 return ( 

95 self.successful_manifest_changes 

96 + len(self.remove_paths_on_success) 

97 + len(self.rename_paths_on_success) 

98 ) 

99 

100 @property 

101 def total_changes_involved(self) -> int: 

102 return ( 

103 self.successful_manifest_changes 

104 + len(self.warnings) 

105 + len(self.remove_paths_on_success) 

106 + len(self.rename_paths_on_success) 

107 ) 

108 

109 

110class DHMigrationSubstitution(Substitution): 

111 def __init__( 

112 self, 

113 dpkg_arch_table: DpkgArchitectureBuildProcessValuesTable, 

114 acceptable_migration_issues: AcceptableMigrationIssues, 

115 feature_migration: FeatureMigration, 

116 mutable_manifest: MutableYAMLManifest, 

117 ) -> None: 

118 self._acceptable_migration_issues = acceptable_migration_issues 

119 self._dpkg_arch_table = dpkg_arch_table 

120 self._feature_migration = feature_migration 

121 self._mutable_manifest = mutable_manifest 

122 # TODO: load 1:1 variables from the real subst instance (less stuff to keep in sync) 

123 one2one = [ 

124 "DEB_SOURCE", 

125 "DEB_VERSION", 

126 "DEB_VERSION_EPOCH_UPSTREAM", 

127 "DEB_VERSION_UPSTREAM_REVISION", 

128 "DEB_VERSION_UPSTREAM", 

129 "SOURCE_DATE_EPOCH", 

130 ] 

131 self._builtin_substs = { 

132 "Tab": "{{token:TAB}}", 

133 "Space": " ", 

134 "Newline": "{{token:NEWLINE}}", 

135 "Dollar": "${}", 

136 } 

137 self._builtin_substs.update((x, "{{" + x + "}}") for x in one2one) 

138 

139 def _replacement(self, key: str, definition_source: str) -> str: 

140 if key in self._builtin_substs: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true

141 return self._builtin_substs[key] 

142 if key in self._dpkg_arch_table: 

143 return "{{" + key + "}}" 

144 if key.startswith("env:"): 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true

145 if "dh-subst-env" not in self._acceptable_migration_issues: 

146 raise UnsupportedFeature( 

147 "Use of environment based substitution variable {{" 

148 + key 

149 + "}} is not" 

150 f" supported in debputy. The variable was spotted at {definition_source}", 

151 ["dh-subst-env"], 

152 ) 

153 elif "dh-subst-unknown-variable" not in self._acceptable_migration_issues: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true

154 raise UnsupportedFeature( 

155 "Unknown substitution variable {{" 

156 + key 

157 + "}}, which does not have a known" 

158 f" counter part in debputy. The variable was spotted at {definition_source}", 

159 ["dh-subst-unknown-variable"], 

160 ) 

161 manifest_definitions = self._mutable_manifest.manifest_definitions( 

162 create_if_absent=False 

163 ) 

164 manifest_variables = manifest_definitions.manifest_variables( 

165 create_if_absent=False 

166 ) 

167 if key not in manifest_variables.variables: 167 ↛ 178line 167 didn't jump to line 178 because the condition on line 167 was always true

168 manifest_definitions.create_definition_if_missing() 

169 manifest_variables[key] = "TODO: Provide variable value for " + key 

170 self._feature_migration.warn( 

171 "TODO: MANUAL MIGRATION of unresolved substitution variable {{" 

172 + key 

173 + "}} from" 

174 + f" {definition_source}" 

175 ) 

176 self._feature_migration.successful_manifest_changes += 1 

177 

178 return "{{" + key + "}}" 

179 

180 def substitute( 

181 self, 

182 value: str, 

183 definition_source: str, 

184 /, 

185 escape_glob_characters: bool = False, 

186 ) -> str: 

187 if "${" not in value: 

188 return value 

189 replacement = self._apply_substitution( 

190 _DH_VAR_RE, 

191 value, 

192 definition_source, 

193 escape_glob_characters=escape_glob_characters, 

194 ) 

195 return replacement.replace("${}", "$") 

196 

197 def with_extra_substitutions(self, **extra_substitutions: str) -> "Substitution": 

198 return self