Coverage for src/debputy/maintscript_snippet.py: 74%

92 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-05-11 16:06 +0000

1import dataclasses 

2from typing import Literal, Self 

3from collections.abc import Sequence, Iterable 

4 

5from debputy.manifest_parser.tagging_types import DebputyDispatchableType 

6from debputy.manifest_parser.util import AttributePath 

7 

8STD_CONTROL_SCRIPTS = frozenset( 

9 { 

10 "preinst", 

11 "prerm", 

12 "postinst", 

13 "postrm", 

14 } 

15) 

16UDEB_CONTROL_SCRIPTS = frozenset( 

17 { 

18 "postinst", 

19 "menutest", 

20 "isinstallable", 

21 } 

22) 

23ALL_CONTROL_SCRIPTS = STD_CONTROL_SCRIPTS | UDEB_CONTROL_SCRIPTS | {"config"} 

24 

25 

26@dataclasses.dataclass(slots=True, frozen=True) 

27class MaintscriptSnippet: 

28 definition_source: str 

29 snippet: str 

30 snippet_order: Literal["service"] | None = None 

31 uses_debconf: bool = False 

32 

33 def script_content(self) -> str: 

34 lines = [ 

35 f"# Snippet source: {self.definition_source}\n", 

36 self.snippet, 

37 ] 

38 if not self.snippet.endswith("\n"): 38 ↛ 39line 38 didn't jump to line 39 because the condition on line 38 was never true

39 lines.append("\n") 

40 return "".join(lines) 

41 

42 

43class MaintscriptSnippetContainer: 

44 def __init__(self) -> None: 

45 self._generic_snippets: list[MaintscriptSnippet] = [] 

46 self._snippets_by_order: dict[Literal["service"], list[MaintscriptSnippet]] = {} 

47 

48 def copy(self) -> "MaintscriptSnippetContainer": 

49 instance = self.__class__() 

50 instance._generic_snippets = self._generic_snippets.copy() 

51 instance._snippets_by_order = self._snippets_by_order.copy() 

52 return instance 

53 

54 def append(self, maintscript_snippet: MaintscriptSnippet) -> None: 

55 if maintscript_snippet.snippet_order is None: 

56 self._generic_snippets.append(maintscript_snippet) 

57 else: 

58 if maintscript_snippet.snippet_order not in self._snippets_by_order: 58 ↛ 60line 58 didn't jump to line 60 because the condition on line 58 was always true

59 self._snippets_by_order[maintscript_snippet.snippet_order] = [] 

60 self._snippets_by_order[maintscript_snippet.snippet_order].append( 

61 maintscript_snippet 

62 ) 

63 

64 def has_content(self, snippet_order: Literal["service"] | None = None) -> bool: 

65 if snippet_order is None: 

66 return bool(self._generic_snippets) 

67 if snippet_order not in self._snippets_by_order: 

68 return False 

69 return bool(self._snippets_by_order[snippet_order]) 

70 

71 def all_snippets(self) -> Iterable[MaintscriptSnippet]: 

72 yield from self._generic_snippets 

73 for snippets in self._snippets_by_order.values(): 

74 yield from snippets 

75 

76 def needs_debconf(self) -> bool: 

77 return any(s.uses_debconf for s in self._generic_snippets) or any( 

78 s.uses_debconf 

79 for snippet_lists in self._snippets_by_order.values() 

80 for s in snippet_lists 

81 ) 

82 

83 def generate_snippet( 

84 self, 

85 tool_with_version: str | None = None, 

86 snippet_order: Literal["service"] | None = None, 

87 reverse: bool = False, 

88 ) -> str | None: 

89 inner_content = "" 

90 if snippet_order is None: 

91 snippets = ( 

92 reversed(self._generic_snippets) if reverse else self._generic_snippets 

93 ) 

94 inner_content = "".join(s.script_content() for s in snippets) 

95 elif snippet_order in self._snippets_by_order: 

96 snippets = self._snippets_by_order[snippet_order] 

97 if reverse: 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true

98 snippets = reversed(snippets) 

99 inner_content = "".join(s.script_content() for s in snippets) 

100 

101 if not inner_content: 

102 return None 

103 

104 if tool_with_version: 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true

105 return ( 

106 f"# Automatically added by {tool_with_version}\n" 

107 + inner_content 

108 + "# End automatically added section" 

109 ) 

110 return inner_content 

111 

112 

113class DpkgMaintscriptHelperCommand(DebputyDispatchableType): 

114 __slots__ = ("cmdline", "definition_source") 

115 

116 def __init__(self, cmdline: Sequence[str], definition_source: str) -> None: 

117 super().__init__() 

118 self.cmdline = cmdline 

119 self.definition_source = definition_source 

120 

121 @classmethod 

122 def _finish_cmd( 

123 cls, 

124 definition_source: str, 

125 cmdline: list[str], 

126 prior_version: str | None, 

127 owning_package: str | None, 

128 ) -> Self: 

129 if prior_version is not None: 

130 cmdline.append(prior_version) 

131 if owning_package is not None: 

132 if prior_version is None: 132 ↛ 134line 132 didn't jump to line 134 because the condition on line 132 was never true

133 # Empty is allowed according to `man dpkg-maintscript-helper` 

134 cmdline.append("") 

135 cmdline.append(owning_package) 

136 return cls( 

137 tuple(cmdline), 

138 definition_source, 

139 ) 

140 

141 @classmethod 

142 def rm_conffile( 

143 cls, 

144 definition_source: AttributePath, 

145 conffile: str, 

146 prior_version: str | None = None, 

147 owning_package: str | None = None, 

148 ) -> Self: 

149 cmdline = ["rm_conffile", conffile] 

150 return cls._finish_cmd( 

151 definition_source.path, cmdline, prior_version, owning_package 

152 ) 

153 

154 @classmethod 

155 def mv_conffile( 

156 cls, 

157 definition_source: AttributePath, 

158 old_conffile: str, 

159 new_confile: str, 

160 prior_version: str | None = None, 

161 owning_package: str | None = None, 

162 ) -> Self: 

163 cmdline = ["mv_conffile", old_conffile, new_confile] 

164 return cls._finish_cmd( 

165 definition_source.path, cmdline, prior_version, owning_package 

166 ) 

167 

168 @classmethod 

169 def symlink_to_dir( 

170 cls, 

171 definition_source: AttributePath, 

172 pathname: str, 

173 old_target: str, 

174 prior_version: str | None = None, 

175 owning_package: str | None = None, 

176 ) -> Self: 

177 cmdline = ["symlink_to_dir", pathname, old_target] 

178 return cls._finish_cmd( 

179 definition_source.path, cmdline, prior_version, owning_package 

180 ) 

181 

182 @classmethod 

183 def dir_to_symlink( 

184 cls, 

185 definition_source: AttributePath, 

186 pathname: str, 

187 new_target: str, 

188 prior_version: str | None = None, 

189 owning_package: str | None = None, 

190 ) -> Self: 

191 cmdline = ["dir_to_symlink", pathname, new_target] 

192 return cls._finish_cmd( 

193 definition_source.path, cmdline, prior_version, owning_package 

194 )