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

111 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-23 03:53 +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 os 

11from pathlib import Path 

12from dataclasses import dataclass 

13from apio.common.apio_console import cout, cstyle, fatal_error 

14from apio.common.apio_styles import SUCCESS, EMPH3 

15from apio.common.proto.apio_common_pb2 import ApioArch 

16from apio.apio_context import ApioContext 

17from apio.utils import util 

18from apio.common import proto_util 

19 

20 

21@dataclass 

22class ExampleInfo: 

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

24 

25 board_id: str 

26 example_name: str 

27 path: Path 

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 = ( 

49 apio_ctx.get_package_dir("definitions") / "examples" 

50 ) 

51 

52 def check_dst_dir_is_empty(self, path: Path): 

53 """Check that the destination directory at the path is empty. If not, 

54 print an error and exit. 

55 """ 

56 

57 # -- Check prerequisites. 

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

59 

60 # -- Get the dir content, including hidden entries. 

61 dir_content: list[str] = os.listdir(path) 

62 

63 # -- We don't care about macOS 

64 ignore_list = [".DS_Store"] 

65 dir_content = [f for f in dir_content if f not in ignore_list] 

66 

67 # -- Error if not empty. 

68 if dir_content: 68 ↛ 69line 68 didn't jump to line 69 because the condition on line 68 was never true

69 fatal_error( 

70 f"Destination directory '{str(path)}' " 

71 + f"is not empty (e.g, '{dir_content[0]}')." 

72 ) 

73 

74 def get_examples_infos(self) -> list[ExampleInfo]: 

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

76 Returns null if an error.""" 

77 

78 # pylint: disable=too-many-locals 

79 

80 # -- The context should have the board, fpgas, and programmer 

81 # -- definitions. 

82 assert self.apio_ctx.definitions is not None 

83 

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

85 boards_dirs: list[Path] = [] 

86 

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

88 if board_dir.is_dir(): 

89 boards_dirs.append(board_dir) 

90 

91 # -- Collect the examples of each boards. 

92 examples: list[ExampleInfo] = [] 

93 for board_dir in boards_dirs: 

94 # -- Convert board dir to board id 

95 board_id = board_dir.name 

96 

97 # -- Verify that the board id exists. 

98 definitions = self.apio_ctx.definitions 

99 boards_definitions = definitions.boards 

100 if board_id not in boards_definitions: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true

101 fatal_error( 

102 "Apio examples contain an invalid board " 

103 + f"id '{board_id}'" 

104 ) 

105 

106 # -- Get board definitions 

107 board_definition = boards_definitions[board_id] 

108 

109 # -- Iterate board's example subdirectories. 

110 for example_dir in board_dir.iterdir(): 

111 

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

113 if not example_dir.is_dir(): 

114 continue 

115 

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

117 info_file = example_dir / "info" 

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

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

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

121 else: 

122 description = "" 

123 

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

125 # -- default value if not found. 

126 proto_util.check_is_required(board_definition, "fpga_id") 

127 fpga_id = board_definition.fpga_id 

128 fpga_definition = definitions.fpgas[fpga_id] 

129 proto_util.check_is_required( 

130 fpga_definition, "arch", "part_num", "size" 

131 ) 

132 fpga_arch = ApioArch.Name(fpga_definition.arch) 

133 fpga_part_num = fpga_definition.part_num 

134 fpga_size = fpga_definition.size 

135 

136 # -- Append this example to the list. 

137 example_info = ExampleInfo( 

138 board_id=board_id, 

139 example_name=example_dir.name, 

140 path=example_dir, 

141 description=description, 

142 fpga_arch=fpga_arch, 

143 fpga_part_num=fpga_part_num, 

144 fpga_size=fpga_size, 

145 ) 

146 examples.append(example_info) 

147 

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

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

150 

151 return examples 

152 

153 def count_examples_by_board(self) -> dict[str, int]: 

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

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

156 

157 # -- Get list of examples. 

158 examples: list[ExampleInfo] = self.get_examples_infos() 

159 

160 # -- Count examples by board 

161 counts: dict[str, int] = {} 

162 for example in examples: 

163 board = example.board_id 

164 old_count = counts.get(board, 0) 

165 counts[board] = old_count + 1 

166 

167 # -- All done 

168 return counts 

169 

170 def lookup_example_info(self, example_name) -> ExampleInfo | None: 

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

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

173 """ 

174 

175 example_infos = self.get_examples_infos() 

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

177 if example_name == ex.name: 

178 return ex 

179 return None 

180 

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

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

183 If destination dir exists, it must be empty. 

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

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

186 """ 

187 

188 # Check that the example name exists. 

189 example_info: ExampleInfo | None = self.lookup_example_info( 

190 example_name 

191 ) 

192 

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

194 fatal_error( 

195 f"Example '{example_name}' not found.", 

196 info=[ 

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

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

199 ], 

200 ) 

201 

202 # -- Get the example dir path. 

203 src_example_path = example_info.path 

204 

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

206 # -- we ignore hidden files and directory. 

207 if dst_dir_path.exists(): 

208 self.check_dst_dir_is_empty(dst_dir_path) 

209 else: 

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

211 

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

213 

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

215 for entry_path in src_example_path.iterdir(): 

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

217 if entry_path.name == "info": 

218 continue 

219 # -- Case 2: Copy subdirectory. 

220 if entry_path.is_dir(): 

221 shutil.copytree( 

222 entry_path, # src 

223 dst_dir_path / entry_path.name, # dst 

224 dirs_exist_ok=False, 

225 ) 

226 continue 

227 # -- Case 3: Copy file. 

228 shutil.copy(entry_path, dst_dir_path) 

229 

230 # -- Inform the user. 

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

232 

233 def get_board_examples(self, board_id) -> list[ExampleInfo]: 

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

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

236 

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

238 """Copy the example creating the folder 

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

240 is created 

241 * INPUTS: 

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

243 * dst_dir: (optional) destination directory. 

244 """ 

245 

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

247 # dst_dir = util.resolve_project_dir( 

248 # dst_dir, create_if_missing=True 

249 # ) 

250 board_examples: list[ExampleInfo] = self.get_board_examples(board_id) 

251 

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

253 fatal_error( 

254 f"No examples for board '{board_id}.", 

255 info=[ 

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

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

258 ], 

259 ) 

260 

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

262 src_board_dir = self.examples_dir / board_id 

263 

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

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

266 fatal_error( 

267 f"Examples for board [{board_id}] not found.", 

268 info=[ 

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

270 + "examples.", 

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

272 ], 

273 ) 

274 

275 if dst_dir.exists(): 

276 self.check_dst_dir_is_empty(dst_dir) 

277 else: 

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

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

280 

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

282 ignore_callback = shutil.ignore_patterns("info") 

283 

284 cout( 

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

286 f"for board '{board_id}'" 

287 ) 

288 

289 for board_example in board_examples: 

290 example_name = board_example.example_name 

291 styled_name = cstyle(example_name, style=EMPH3) 

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

293 shutil.copytree( 

294 src_board_dir / example_name, 

295 dst_dir / example_name, 

296 dirs_exist_ok=False, 

297 ignore=ignore_callback, 

298 ) 

299 

300 cout( 

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

302 "fetched successfully.", 

303 style=SUCCESS, 

304 )