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

87 statements  

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

1import dataclasses 

2import re 

3from typing import Sequence, Optional, FrozenSet, Tuple, List, cast 

4 

5from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable 

6from debputy.commands.debputy_cmd.output import IOBasedOutputStyling 

7from debputy.highlevel_manifest import MutableYAMLManifest 

8from debputy.substitution import Substitution 

9 

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

11 

12 

13class AcceptableMigrationIssues: 

14 def __init__(self, values: FrozenSet[str]): 

15 self._values = values 

16 

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

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

19 

20 

21class UnsupportedFeature(RuntimeError): 

22 @property 

23 def message(self) -> str: 

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

25 

26 @property 

27 def issue_keys(self) -> Optional[Sequence[str]]: 

28 if len(self.args) < 2: 

29 return None 

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

31 

32 

33class ConflictingChange(RuntimeError): 

34 @property 

35 def message(self) -> str: 

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

37 

38 

39@dataclasses.dataclass(slots=True) 

40class FeatureMigration: 

41 tagline: str 

42 fo: IOBasedOutputStyling 

43 successful_manifest_changes: int = 0 

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

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

46 rename_paths_on_success: List[Tuple[str, str]] = dataclasses.field( 

47 default_factory=list 

48 ) 

49 assumed_compat: Optional[int] = None 

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

51 

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

53 self.warnings.append(msg) 

54 

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

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

57 

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

59 self.remove_paths_on_success.append(path) 

60 

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

62 self.required_plugins.append(debputy_plugin) 

63 

64 @property 

65 def anything_to_do(self) -> bool: 

66 return bool(self.total_changes_involved) 

67 

68 @property 

69 def performed_changes(self) -> int: 

70 return ( 

71 self.successful_manifest_changes 

72 + len(self.remove_paths_on_success) 

73 + len(self.rename_paths_on_success) 

74 ) 

75 

76 @property 

77 def total_changes_involved(self) -> int: 

78 return ( 

79 self.successful_manifest_changes 

80 + len(self.warnings) 

81 + len(self.remove_paths_on_success) 

82 + len(self.rename_paths_on_success) 

83 ) 

84 

85 

86class DHMigrationSubstitution(Substitution): 

87 def __init__( 

88 self, 

89 dpkg_arch_table: DpkgArchitectureBuildProcessValuesTable, 

90 acceptable_migration_issues: AcceptableMigrationIssues, 

91 feature_migration: FeatureMigration, 

92 mutable_manifest: MutableYAMLManifest, 

93 ) -> None: 

94 self._acceptable_migration_issues = acceptable_migration_issues 

95 self._dpkg_arch_table = dpkg_arch_table 

96 self._feature_migration = feature_migration 

97 self._mutable_manifest = mutable_manifest 

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

99 one2one = [ 

100 "DEB_SOURCE", 

101 "DEB_VERSION", 

102 "DEB_VERSION_EPOCH_UPSTREAM", 

103 "DEB_VERSION_UPSTREAM_REVISION", 

104 "DEB_VERSION_UPSTREAM", 

105 "SOURCE_DATE_EPOCH", 

106 ] 

107 self._builtin_substs = { 

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

109 "Space": " ", 

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

111 "Dollar": "${}", 

112 } 

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

114 

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

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

117 return self._builtin_substs[key] 

118 if key in self._dpkg_arch_table: 

119 return "{{" + key + "}}" 

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

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

122 raise UnsupportedFeature( 

123 "Use of environment based substitution variable {{" 

124 + key 

125 + "}} is not" 

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

127 ["dh-subst-env"], 

128 ) 

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

130 raise UnsupportedFeature( 

131 "Unknown substitution variable {{" 

132 + key 

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

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

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

136 ) 

137 manifest_definitions = self._mutable_manifest.manifest_definitions( 

138 create_if_absent=False 

139 ) 

140 manifest_variables = manifest_definitions.manifest_variables( 

141 create_if_absent=False 

142 ) 

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

144 manifest_definitions.create_definition_if_missing() 

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

146 self._feature_migration.warn( 

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

148 + key 

149 + "}} from" 

150 + f" {definition_source}" 

151 ) 

152 self._feature_migration.successful_manifest_changes += 1 

153 

154 return "{{" + key + "}}" 

155 

156 def substitute( 

157 self, 

158 value: str, 

159 definition_source: str, 

160 /, 

161 escape_glob_characters: bool = False, 

162 ) -> str: 

163 if "${" not in value: 

164 return value 

165 replacement = self._apply_substitution( 

166 _DH_VAR_RE, 

167 value, 

168 definition_source, 

169 escape_glob_characters=escape_glob_characters, 

170 ) 

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

172 

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

174 return self