Coverage for apio/managers/examples.py: 81%

112 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-06 10:20 +0000

1"""Manage apio examples""" 

2 

3# -*- coding: utf-8 -*- 

4# -- This file is part of the Apio project 

5# -- (C) 2016-2019 FPGAwars 

6# -- Author Jesús Arroyo, Juan González 

7# -- License GPLv2 

8 

9import shutil 

10import sys 

11import os 

12from pathlib import Path, PosixPath 

13from dataclasses import dataclass 

14from typing import Optional, List, Dict 

15from apio.common.apio_console import cout, cstyle, cerror 

16from apio.common.apio_styles import INFO, SUCCESS, EMPH3 

17from apio.apio_context import ApioContext 

18from apio.utils import util 

19 

20 

21@dataclass 

22class ExampleInfo: 

23 """Information about a single example.""" 

24 

25 board_id: str 

26 example_name: str 

27 path: PosixPath 

28 description: str 

29 fpga_arch: str 

30 fpga_part_num: str 

31 fpga_size: str 

32 

33 @property 

34 def name(self) -> str: 

35 """Returns the full id of the example.""" 

36 return self.board_id + "/" + self.example_name 

37 

38 

39class Examples: 

40 """Manage the apio examples""" 

41 

42 def __init__(self, apio_ctx: ApioContext): 

43 

44 # -- Save the apio context. 

45 self.apio_ctx = apio_ctx 

46 

47 # -- Folder where the example packages was installed 

48 self.examples_dir = apio_ctx.get_package_dir("examples") 

49 

50 def is_dir_empty(self, path: Path) -> bool: 

51 """Return true if the given dir is empty, ignoring hidden entry. 

52 That is, the dir may contain only hidden entries. 

53 We use this relaxed criteria of emptiness to avoid user confusion. 

54 We could use glob.glob() but in python 3.10 and earlier it doesn't 

55 have the 'include_hidden' argument. 

56 """ 

57 # -- Check prerequisites. 

58 assert path.is_dir(), f"Not a dir: {path}" 

59 

60 # -- Iterate directory entries 

61 for name in os.listdir(path): 61 ↛ 63line 61 didn't jump to line 63 because the loop on line 61 never started

62 # -- If not a hidden entry, answer is no. 

63 if not name.startswith("."): 

64 return False 

65 # -- Non hidden entries not found. Directory is empty. 

66 return True 

67 

68 def get_examples_infos(self) -> List[ExampleInfo]: 

69 """Scans the examples and returns a list of ExampleInfos. 

70 Returns null if an error.""" 

71 

72 # -- Collect the examples home dir each board. 

73 boards_dirs: List[PosixPath] = [] 

74 

75 for board_dir in self.examples_dir.iterdir(): 

76 if board_dir.is_dir(): 

77 boards_dirs.append(board_dir) 

78 

79 # -- Collect the examples of each boards. 

80 examples: List[ExampleInfo] = [] 

81 for board_dir in boards_dirs: 

82 

83 # -- Iterate board's example subdirectories. 

84 for example_dir in board_dir.iterdir(): 

85 

86 # -- Skip files. We care just about directories. 

87 if not example_dir.is_dir(): 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true

88 continue 

89 

90 # -- Try to load description from the example info file. 

91 info_file = example_dir / "info" 

92 if info_file.exists(): 92 ↛ 96line 92 didn't jump to line 96 because the condition on line 92 was always true

93 with open(info_file, "r", encoding="utf-8") as f: 

94 description = f.read().replace("\n", "") 

95 else: 

96 description = "" 

97 

98 # -- Extract the fpga arch and part number, with "" as 

99 # -- default value if not found. 

100 board_info = self.apio_ctx.boards.get(board_dir.name, {}) 

101 fpga_id = board_info.get("fpga-id", "") 

102 fpga_info = self.apio_ctx.fpgas.get(fpga_id, {}) 

103 fpga_arch = fpga_info.get("arch", "") 

104 fpga_part_num = fpga_info.get("part-num", "") 

105 fpga_size = fpga_info.get("size", "") 

106 

107 # -- Append this example to the list. 

108 example_info = ExampleInfo( 

109 board_id=board_dir.name, 

110 example_name=example_dir.name, 

111 path=example_dir, 

112 description=description, 

113 fpga_arch=fpga_arch, 

114 fpga_part_num=fpga_part_num, 

115 fpga_size=fpga_size, 

116 ) 

117 examples.append(example_info) 

118 

119 # -- Sort in-place by acceding example name, case insensitive. 

120 examples.sort(key=lambda x: x.name.lower()) 

121 

122 return examples 

123 

124 def count_examples_by_board(self) -> Dict[str, int]: 

125 """Returns a dictionary with example count per board. Boards 

126 that have no examples are not included in the dictionary.""" 

127 

128 # -- Get list of examples. 

129 examples: List[ExampleInfo] = self.get_examples_infos() 

130 

131 # -- Count examples by board 

132 counts: Dict[str, int] = {} 

133 for example in examples: 

134 board = example.board_id 

135 old_count = counts.get(board, 0) 

136 counts[board] = old_count + 1 

137 

138 # -- All done 

139 return counts 

140 

141 def lookup_example_info(self, example_name) -> Optional[ExampleInfo]: 

142 """Return the example info for given example or None if not found. 

143 Example_name looks like 'alhambra-ii/ledon'. 

144 """ 

145 

146 example_infos = self.get_examples_infos() 

147 for ex in example_infos: 147 ↛ 150line 147 didn't jump to line 150 because the loop on line 147 didn't complete

148 if example_name == ex.name: 

149 return ex 

150 return None 

151 

152 def copy_example_files(self, example_name: str, dst_dir_path: Path): 

153 """Copy the files from the given example to the destination dir. 

154 If destination dir exists, it must be empty. 

155 If it doesn't exist, it's created with any necessary parent. 

156 The arg 'example_name' looks like 'alhambra-ii/ledon'. 

157 """ 

158 

159 # Check that the example name exists. 

160 example_info: ExampleInfo = self.lookup_example_info(example_name) 

161 

162 if not example_info: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true

163 cerror(f"Example '{example_name}' not found.") 

164 cout( 

165 "Run 'apio example list' for the list of examples.", 

166 "Expecting an example name like alhambra-ii/ledon.", 

167 style=INFO, 

168 ) 

169 sys.exit(1) 

170 

171 # -- Get the example dir path. 

172 src_example_path = example_info.path 

173 

174 # -- Prepare an empty destination directory. To avoid confusion, 

175 # -- we ignore hidden files and directory. 

176 if dst_dir_path.is_dir(): 

177 if not self.is_dir_empty(dst_dir_path): 177 ↛ 178line 177 didn't jump to line 178 because the condition on line 177 was never true

178 cerror( 

179 f"Destination directory '{str(dst_dir_path)}' " 

180 "is not empty." 

181 ) 

182 sys.exit(1) 

183 else: 

184 dst_dir_path.mkdir(parents=True, exist_ok=False) 

185 

186 cout("Copying " + example_name + " example files.") 

187 

188 # -- Go though all the files in the example folder. 

189 for entry_path in src_example_path.iterdir(): 

190 # -- Case 1: Skip 'info' files. 

191 if entry_path.name == "info": 

192 continue 

193 # -- Case 2: Copy subdirectory. 

194 if entry_path.is_dir(): 

195 shutil.copytree( 

196 entry_path, # src 

197 dst_dir_path / entry_path.name, # dst 

198 dirs_exist_ok=False, 

199 ) 

200 continue 

201 # -- Case 3: Copy file. 

202 shutil.copy(entry_path, dst_dir_path) 

203 

204 # -- Inform the user. 

205 cout(f"Example '{example_name}' fetched successfully.", style=SUCCESS) 

206 

207 def get_board_examples(self, board_id) -> List[ExampleInfo]: 

208 """Returns the list of examples with given board id.""" 

209 return [x for x in self.get_examples_infos() if x.board_id == board_id] 

210 

211 def copy_board_examples(self, board_id: str, dst_dir: Path): 

212 """Copy the example creating the folder 

213 Ex. The example alhambra-ii/ledon --> the folder alhambra-ii/ledon 

214 is created 

215 * INPUTS: 

216 * board_id: e.g. 'alhambra-ii. 

217 * dst_dir: (optional) destination directory. 

218 """ 

219 

220 # -- Get the working dir (current or given) 

221 # dst_dir = util.resolve_project_dir( 

222 # dst_dir, create_if_missing=True 

223 # ) 

224 board_examples: List[ExampleInfo] = self.get_board_examples(board_id) 

225 

226 if not board_examples: 226 ↛ 227line 226 didn't jump to line 227 because the condition on line 226 was never true

227 cerror(f"No examples for board '{board_id}.") 

228 cout( 

229 "Run 'apio examples list' for the list of examples.", 

230 "Expecting a board id such as 'alhambra-ii.", 

231 style=INFO, 

232 ) 

233 sys.exit(1) 

234 

235 # -- Build the source example path (where the example was installed) 

236 src_board_dir = self.examples_dir / board_id 

237 

238 # -- If the source example path is not a folder... it is an error 

239 if not src_board_dir.is_dir(): 239 ↛ 240line 239 didn't jump to line 240 because the condition on line 239 was never true

240 cerror(f"Examples for board [{board_id}] not found.") 

241 cout( 

242 "Run 'apio examples list' for the list of available " 

243 "examples.", 

244 "Expecting a board id such as 'alhambra-ii'.", 

245 style=INFO, 

246 ) 

247 sys.exit(1) 

248 

249 if dst_dir.is_dir(): 

250 # -- To avoid confusion from the user, we ignore hidden files. 

251 if not self.is_dir_empty(dst_dir): 251 ↛ 252line 251 didn't jump to line 252 because the condition on line 251 was never true

252 cerror( 

253 f"Destination directory '{str(dst_dir)}' " "is not empty." 

254 ) 

255 sys.exit(1) 

256 else: 

257 cout(f"Creating directory {dst_dir}.") 

258 dst_dir.mkdir(parents=True, exist_ok=False) 

259 

260 # -- Create an ignore callback to skip 'info' files. 

261 ignore_callback = shutil.ignore_patterns("info") 

262 

263 cout( 

264 f'Found {util.plurality(board_examples, "example")} ' 

265 f"for board '{board_id}'" 

266 ) 

267 

268 for board_example in board_examples: 

269 example_name = board_example.example_name 

270 styled_name = cstyle(example_name, style=EMPH3) 

271 cout(f"Fetching {board_id}/{styled_name}") 

272 shutil.copytree( 

273 src_board_dir / example_name, 

274 dst_dir / example_name, 

275 dirs_exist_ok=False, 

276 ignore=ignore_callback, 

277 ) 

278 

279 cout( 

280 f"{util.plurality(board_examples, 'Example', include_num=False)} " 

281 "fetched successfully.", 

282 style=SUCCESS, 

283 )