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
« 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
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
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: 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)
53 def warn(self, msg: str) -> None:
54 self.warnings.append(msg)
56 def rename_on_success(self, source: str, dest: str) -> None:
57 self.rename_paths_on_success.append((source, dest))
59 def remove_on_success(self, path: str) -> None:
60 self.remove_paths_on_success.append(path)
62 def require_plugin(self, debputy_plugin: str) -> None:
63 self.required_plugins.append(debputy_plugin)
65 @property
66 def anything_to_do(self) -> bool:
67 return bool(self.total_changes_involved)
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 )
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 )
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)
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
155 return "{{" + key + "}}"
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("${}", "$")
174 def with_extra_substitutions(self, **extra_substitutions: str) -> "Substitution":
175 return self