Coverage for debputy/transformation_rules.py: 0%

121 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-22 14:29 +0100

1import os 

2from typing import NoReturn, Iterable, Optional 

3 

4from debputy.filesystem_scan import FSPath, VirtualDirectoryFSPath, FSPathVirtualPath 

5from debputy.manifest_conditions import ConditionContext, ManifestCondition 

6from debputy.manifest_path_rules import ManifestPathRule, EnsureDirectoryPathRule 

7from debputy.path_matcher import MatchRule 

8from debputy.util import _error 

9 

10 

11def _match_rule_had_no_matches(match_rule: MatchRule, definition_source: str) -> NoReturn: 

12 _error(f'The match rule "{match_rule.describe_match_short()}" in transformation "{definition_source}" did not' 

13 ' match any paths. Either the definition is redundant (and can be omitted) or the match rule is' 

14 ' incorrect.') 

15 

16 

17def _fs_path_as_dir(path: FSPath, force_convert: bool, definition_source: str) -> FSPath: 

18 if path.is_dir: 

19 return path 

20 if not force_convert: 

21 path_type = 'file' if path.is_file else 'symlink/"special file system object"' 

22 _error(f"The path {path.path} was expected to be a directory (or non-existing) due to {definition_source}." 

23 f"However that path existed and is a {path_type}.") 

24 return VirtualDirectoryFSPath(path.basename, path.parent_dir) 

25 

26 

27def _ensure_is_directory(fs_root: FSPath, 

28 path_to_directory: str, 

29 definition_source: str, 

30 force_convert=False, 

31 ) -> FSPath: 

32 current, missing_parts = fs_root.attempt_lookup(path_to_directory) 

33 current = _fs_path_as_dir(current, force_convert, definition_source) 

34 for missing_part in missing_parts: 

35 assert missing_part not in ('.', '..') 

36 current = VirtualDirectoryFSPath(missing_part, current) 

37 return current 

38 

39 

40class TransformationRule: 

41 

42 @property 

43 def is_builtin_rule(self) -> bool: 

44 return False 

45 

46 def describe_transformation(self) -> str: 

47 raise NotImplementedError 

48 

49 def transform_file_system(self, fs_root: FSPath, condition_context: ConditionContext) -> None: 

50 raise NotImplementedError 

51 

52 

53class ExclusionTransformationRule(TransformationRule): 

54 

55 def __init__(self, match_rule: MatchRule, definition_source: str) -> None: 

56 self._match_rule = match_rule 

57 self._definition_source = definition_source 

58 

59 def describe_transformation(self) -> str: 

60 return f"Exclude {self._match_rule.describe_match_short()} (from: {self._definition_source})" 

61 

62 def transform_file_system(self, fs_root: FSPath, condition_context: ConditionContext) -> None: 

63 matched_any = False 

64 for m in self._match_rule.finditer(fs_root): 

65 matched_any = True 

66 parent = m.parent_dir 

67 if parent is None: 

68 _error(f"Cannot exclude the root directory (triggered by {self._definition_source}") 

69 del parent[m.basename] 

70 if not matched_any: 

71 _match_rule_had_no_matches(self._match_rule, self._definition_source) 

72 

73 

74class MoveTransformationRule(TransformationRule): 

75 

76 def __init__(self, 

77 match_rule: MatchRule, 

78 dest_path: str, 

79 dest_is_dir: bool, 

80 definition_source: str, 

81 condition: Optional[ManifestCondition], 

82 ) -> None: 

83 self._match_rule = match_rule 

84 self._dest_path = dest_path 

85 self._dest_is_dir = dest_is_dir 

86 self._definition_source = definition_source 

87 self._condition = condition 

88 

89 def describe_transformation(self) -> str: 

90 dest_path = self._dest_path 

91 if self._dest_is_dir: 

92 dest_path += '/' 

93 return f'Rename {self._match_rule.describe_match_short()} -> "{dest_path}"' \ 

94 f" (from: {self._definition_source})" 

95 

96 def transform_file_system(self, fs_root: FSPath, condition_context: ConditionContext) -> None: 

97 if self._condition is not None and not self._condition.evaluate(condition_context): 

98 return 

99 # Eager resolve is necessary to avoid "self-recursive" matching in special cases (e.g., **/*.la) 

100 matches = list(self._match_rule.finditer(fs_root)) 

101 if not matches: 

102 _match_rule_had_no_matches(self._match_rule, self._definition_source) 

103 

104 if self._dest_is_dir: 

105 target_dir = _ensure_is_directory(fs_root, self._dest_path, self._definition_source) 

106 else: 

107 dir_part, basename = os.path.split(self._dest_path) 

108 target_parent_dir = _ensure_is_directory(fs_root, dir_part, self._definition_source) 

109 target_dir = target_parent_dir.get(basename) 

110 

111 if target_dir is None or not target_dir.is_dir: 

112 if len(matches) > 1: 

113 _error(f"Could not rename {self._match_rule.describe_match_short()} to {self._dest_path}" 

114 f" (from: {self._definition_source}). Multiple paths matched the pattern and the destination" 

115 " was not a directory. Either correct the pattern to only match ony source OR define the" 

116 f' destination to be a directory (E.g., add a trailing slash - example:' 

117 f' "{self._dest_path}/")') 

118 p = matches[0] 

119 if p.path == self._dest_path: 

120 _error(f"Error in {self._definition_source}, the source {self._match_rule.describe_match_short()}" 

121 f"matched {self._dest_path} making the rename redundant!?") 

122 p.parent_dir = target_parent_dir 

123 p.basename = basename 

124 return 

125 

126 assert target_dir is not None and target_dir.is_dir 

127 basenames = dict() 

128 target_dir_path = target_dir.path 

129 

130 for m in matches: 

131 if m.path == target_dir_path: 

132 _error(f"Error in {self._definition_source}, the source {self._match_rule.describe_match_short()}" 

133 f"matched {self._dest_path} (among other), but it is not possible to copy a directory into" 

134 " itself") 

135 if m.basename in basenames: 

136 alt_path = basenames[m.basename] 

137 # We document "two *distinct*" paths. However, as the glob matches are written, it should not be 

138 # possible for a *single* glob to match the same path twice. 

139 assert alt_path is not m 

140 _error(f"Could not rename {self._match_rule.describe_match_short()} to {self._dest_path}" 

141 f" (from: {self._definition_source}). Multiple paths matched the pattern had the same basename" 

142 f' "{m.basename}" ("{m.path}" vs. "{alt_path.path}"). Please correct the pattern, so it only' 

143 f' matches one path with that basename to avoid this conflict.') 

144 existing = m.get(m.basename) 

145 if existing and existing.is_dir: 

146 _error(f"Could not rename {self._match_rule.describe_match_short()} to {self._dest_path}" 

147 f" (from: {self._definition_source}). The pattern matched {m.path} which would replace" 

148 f" the existing directory {existing.path}. If this replacement is intentional, then please" 

149 f' exclude {existing.path} first (e.g., via `- exclude: "{existing.path}"`)') 

150 basenames[m.basename] = m 

151 m.parent_dir = target_dir 

152 

153 

154class CreateDirectoryTransformationRule(TransformationRule): 

155 

156 def __init__(self, directories: Iterable[EnsureDirectoryPathRule]): 

157 self._directories = directories 

158 

159 @property 

160 def is_builtin_rule(self) -> bool: 

161 return True 

162 

163 def describe_transformation(self) -> str: 

164 return "<built-in: Insert implicitly and explicitly declared directories>" 

165 

166 def transform_file_system(self, fs_root: FSPath, condition_context: ConditionContext) -> None: 

167 for path_definition in self._directories: 

168 if path_definition.condition is not None and not path_definition.condition.evaluate(condition_context): 

169 continue 

170 _ensure_is_directory(fs_root, 

171 path_definition.member_path, 

172 '<Built-in; implementation of "directories">', 

173 force_convert=True) 

174 

175 

176class CreateVirtualPathTransformationRule(TransformationRule): 

177 

178 def __init__(self, path_definitions: Iterable[ManifestPathRule]): 

179 self._path_definitions = path_definitions 

180 

181 @property 

182 def is_builtin_rule(self) -> bool: 

183 return True 

184 

185 def describe_transformation(self) -> str: 

186 return "<built-in: Insert explicitly declared virtual paths>" 

187 

188 def transform_file_system(self, fs_root: FSPath, condition_context: ConditionContext) -> None: 

189 for path_definition in self._path_definitions: 

190 if path_definition.condition is not None and not path_definition.condition.evaluate(condition_context): 

191 continue 

192 parent_path = os.path.dirname(path_definition.member_path) 

193 parent_dir = fs_root.lookup(parent_path) 

194 assert parent_dir is not None and parent_dir.is_dir 

195 FSPathVirtualPath(path_definition, parent_dir) 

196