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
« 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
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
10_DH_VAR_RE = re.compile(r"([$][{])([A-Za-z0-9][-_:0-9A-Za-z]*)([}])")
13class AcceptableMigrationIssues:
14 def __init__(self, values: FrozenSet[str]):
15 self._values = values
17 def __contains__(self, item: str) -> bool:
18 return item in self._values or "ALL" in self._values
21class UnsupportedFeature(RuntimeError):
22 @property
23 def message(self) -> str:
24 return cast("str", self.args[0])
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])
33class ConflictingChange(RuntimeError):
34 @property
35 def message(self) -> str:
36 return cast("str", self.args[0])
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)
52 def warn(self, msg: str) -> None:
53 self.warnings.append(msg)
55 def rename_on_success(self, source: str, dest: str) -> None:
56 self.rename_paths_on_success.append((source, dest))
58 def remove_on_success(self, path: str) -> None:
59 self.remove_paths_on_success.append(path)
61 def require_plugin(self, debputy_plugin: str) -> None:
62 self.required_plugins.append(debputy_plugin)
64 @property
65 def anything_to_do(self) -> bool:
66 return bool(self.total_changes_involved)
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 )
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 )
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)
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
154 return "{{" + key + "}}"
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("${}", "$")
173 def with_extra_substitutions(self, **extra_substitutions: str) -> "Substitution":
174 return self