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
« 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
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
14_DH_VAR_RE = re.compile(r"([$][{])([A-Za-z0-9][-_:0-9A-Za-z]*)([}])")
17class AcceptableMigrationIssues:
18 def __init__(self, values: frozenset[str]):
19 self._values = values
21 def __contains__(self, item: str) -> bool:
22 return item in self._values or "ALL" in self._values
25class UnsupportedFeature(RuntimeError):
26 @property
27 def message(self) -> str:
28 return cast("str", self.args[0])
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])
37class ConflictingChange(RuntimeError):
38 @property
39 def message(self) -> str:
40 return cast("str", self.args[0])
43@dataclasses.dataclass(slots=True)
44class MigrationRequest:
45 debian_dir: VirtualPath
46 manifest: HighLevelManifest
47 acceptable_migration_issues: AcceptableMigrationIssues
48 migration_target: DebputyIntegrationMode | None
50 @property
51 def all_packages(self) -> Iterable[BinaryPackage]:
52 return self.manifest.all_packages
54 @property
55 def is_single_binary_package(self) -> bool:
56 return sum(1 for _ in self.all_packages) == 1
58 @property
59 def main_binary(self) -> BinaryPackage:
60 return next(iter(p for p in self.all_packages if p.is_main_package))
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)
76 def warn(self, msg: str) -> None:
77 self.warnings.append(msg)
79 def rename_on_success(self, source: str, dest: str) -> None:
80 self.rename_paths_on_success.append((source, dest))
82 def remove_on_success(self, path: str) -> None:
83 self.remove_paths_on_success.append(path)
85 def require_plugin(self, debputy_plugin: str) -> None:
86 self.required_plugins.append(debputy_plugin)
88 @property
89 def anything_to_do(self) -> bool:
90 return bool(self.total_changes_involved)
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 )
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 )
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)
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
178 return "{{" + key + "}}"
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("${}", "$")
197 def with_extra_substitutions(self, **extra_substitutions: str) -> "Substitution":
198 return self