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

88 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +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 OutputStylingBase 

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: OutputStylingBase 

43 successful_manifest_changes: int = 0 

44 already_present: int = 0 

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

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

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

48 default_factory=list 

49 ) 

50 assumed_compat: Optional[int] = None 

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

52 

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

54 self.warnings.append(msg) 

55 

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

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

58 

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

60 self.remove_paths_on_success.append(path) 

61 

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

63 self.required_plugins.append(debputy_plugin) 

64 

65 @property 

66 def anything_to_do(self) -> bool: 

67 return bool(self.total_changes_involved) 

68 

69 @property 

70 def performed_changes(self) -> int: 

71 return ( 

72 self.successful_manifest_changes 

73 + len(self.remove_paths_on_success) 

74 + len(self.rename_paths_on_success) 

75 ) 

76 

77 @property 

78 def total_changes_involved(self) -> int: 

79 return ( 

80 self.successful_manifest_changes 

81 + len(self.warnings) 

82 + len(self.remove_paths_on_success) 

83 + len(self.rename_paths_on_success) 

84 ) 

85 

86 

87class DHMigrationSubstitution(Substitution): 

88 def __init__( 

89 self, 

90 dpkg_arch_table: DpkgArchitectureBuildProcessValuesTable, 

91 acceptable_migration_issues: AcceptableMigrationIssues, 

92 feature_migration: FeatureMigration, 

93 mutable_manifest: MutableYAMLManifest, 

94 ) -> None: 

95 self._acceptable_migration_issues = acceptable_migration_issues 

96 self._dpkg_arch_table = dpkg_arch_table 

97 self._feature_migration = feature_migration 

98 self._mutable_manifest = mutable_manifest 

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

100 one2one = [ 

101 "DEB_SOURCE", 

102 "DEB_VERSION", 

103 "DEB_VERSION_EPOCH_UPSTREAM", 

104 "DEB_VERSION_UPSTREAM_REVISION", 

105 "DEB_VERSION_UPSTREAM", 

106 "SOURCE_DATE_EPOCH", 

107 ] 

108 self._builtin_substs = { 

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

110 "Space": " ", 

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

112 "Dollar": "${}", 

113 } 

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

115 

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

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

118 return self._builtin_substs[key] 

119 if key in self._dpkg_arch_table: 

120 return "{{" + key + "}}" 

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

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

123 raise UnsupportedFeature( 

124 "Use of environment based substitution variable {{" 

125 + key 

126 + "}} is not" 

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

128 ["dh-subst-env"], 

129 ) 

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

131 raise UnsupportedFeature( 

132 "Unknown substitution variable {{" 

133 + key 

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

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

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

137 ) 

138 manifest_definitions = self._mutable_manifest.manifest_definitions( 

139 create_if_absent=False 

140 ) 

141 manifest_variables = manifest_definitions.manifest_variables( 

142 create_if_absent=False 

143 ) 

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

145 manifest_definitions.create_definition_if_missing() 

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

147 self._feature_migration.warn( 

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

149 + key 

150 + "}} from" 

151 + f" {definition_source}" 

152 ) 

153 self._feature_migration.successful_manifest_changes += 1 

154 

155 return "{{" + key + "}}" 

156 

157 def substitute( 

158 self, 

159 value: str, 

160 definition_source: str, 

161 /, 

162 escape_glob_characters: bool = False, 

163 ) -> str: 

164 if "${" not in value: 

165 return value 

166 replacement = self._apply_substitution( 

167 _DH_VAR_RE, 

168 value, 

169 definition_source, 

170 escape_glob_characters=escape_glob_characters, 

171 ) 

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

173 

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

175 return self