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

111 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-07 05:08 +0000

1# This file is part of the Apio project 

2# (C) 2016-present FPGAwars 

3# Project authors: 

4# * Jesús Arroyo (Jesus89) (2016-2019) 

5# * Juan Gonzalez (obijuan) (2019-2024) 

6# * The Apio Team (2024-present) 

7# License: GPLv2 

8 

9"""Manage apio examples""" 

10 

11import shutil 

12import os 

13from pathlib import Path 

14from dataclasses import dataclass 

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

16from apio.common.apio_styles import SUCCESS, EMPH3 

17from apio.common.proto.stubs.apio_common_pb2 import ApioArch 

18from apio.apio_context import ApioContext 

19from apio.utils import util 

20from apio.common import proto_util 

21 

22 

23@dataclass 

24class ExampleInfo: 

25 """Information about a single example.""" 

26 

27 board_id: str 

28 example_name: str 

29 path: Path 

30 description: str 

31 fpga_arch: str 

32 fpga_vendor_part: str 

33 fpga_size: str 

34 

35 @property 

36 def name(self) -> str: 

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

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

39 

40 

41class Examples: 

42 """Manage the apio examples""" 

43 

44 def __init__(self, apio_ctx: ApioContext): 

45 

46 # -- Save the apio context. 

47 self.apio_ctx = apio_ctx 

48 

49 # -- Folder where the example packages was installed 

50 self.examples_dir = ( 

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

52 ) 

53 

54 def check_dst_dir_is_empty(self, path: Path): 

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

56 print an error and exit. 

57 """ 

58 

59 # -- Check prerequisites. 

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

61 

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

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

64 

65 # -- We don't care about macOS 

66 ignore_list = [".DS_Store"] 

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

68 

69 # -- Error if not empty. 

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

71 fatal_error( 

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

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

74 ) 

75 

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

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

78 Returns null if an error.""" 

79 

80 # pylint: disable=too-many-locals 

81 

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

83 # -- definitions. 

84 assert self.apio_ctx.definitions is not None 

85 

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

87 boards_dirs: list[Path] = [] 

88 

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

90 if board_dir.is_dir(): 

91 boards_dirs.append(board_dir) 

92 

93 # -- Collect the examples of each boards. 

94 examples: list[ExampleInfo] = [] 

95 for board_dir in boards_dirs: 

96 # -- Convert board dir to board id 

97 board_id = board_dir.name 

98 

99 # -- Verify that the board id exists. 

100 definitions = self.apio_ctx.definitions 

101 boards_definitions = definitions.boards 

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

103 fatal_error( 

104 "Apio examples contain an invalid board " 

105 + f"id '{board_id}'" 

106 ) 

107 

108 # -- Get board definitions 

109 board_definition = boards_definitions[board_id] 

110 

111 # -- Iterate board's example subdirectories. 

112 for example_dir in board_dir.iterdir(): 

113 

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

115 if not example_dir.is_dir(): 

116 continue 

117 

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

119 info_file = example_dir / "info" 

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

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

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

123 else: 

124 description = "" 

125 

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

127 # -- default value if not found. 

128 proto_util.check_is_required(board_definition, "fpga_id") 

129 fpga_id = board_definition.fpga_id 

130 fpga_definition = definitions.fpgas[fpga_id] 

131 proto_util.check_is_required( 

132 fpga_definition, "arch", "vendor_part", "size" 

133 ) 

134 fpga_arch = ApioArch.Name(fpga_definition.arch) 

135 fpga_vendor_part = fpga_definition.vendor_part 

136 fpga_size = fpga_definition.size 

137 

138 # -- Append this example to the list. 

139 example_info = ExampleInfo( 

140 board_id=board_id, 

141 example_name=example_dir.name, 

142 path=example_dir, 

143 description=description, 

144 fpga_arch=fpga_arch, 

145 fpga_vendor_part=fpga_vendor_part, 

146 fpga_size=fpga_size, 

147 ) 

148 examples.append(example_info) 

149 

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

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

152 

153 return examples 

154 

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

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

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

158 

159 # -- Get list of examples. 

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

161 

162 # -- Count examples by board 

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

164 for example in examples: 

165 board = example.board_id 

166 old_count = counts.get(board, 0) 

167 counts[board] = old_count + 1 

168 

169 # -- All done 

170 return counts 

171 

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

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

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

175 """ 

176 

177 example_infos = self.get_examples_infos() 

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

179 if example_name == ex.name: 

180 return ex 

181 return None 

182 

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

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

185 If destination dir exists, it must be empty. 

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

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

188 """ 

189 

190 # Check that the example name exists. 

191 example_info: ExampleInfo | None = self.lookup_example_info( 

192 example_name 

193 ) 

194 

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

196 fatal_error( 

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

198 info=[ 

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

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

201 ], 

202 ) 

203 

204 # -- Get the example dir path. 

205 src_example_path = example_info.path 

206 

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

208 # -- we ignore hidden files and directory. 

209 if dst_dir_path.exists(): 

210 self.check_dst_dir_is_empty(dst_dir_path) 

211 else: 

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

213 

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

215 

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

217 for entry_path in src_example_path.iterdir(): 

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

219 if entry_path.name == "info": 

220 continue 

221 # -- Case 2: Copy subdirectory. 

222 if entry_path.is_dir(): 

223 shutil.copytree( 

224 entry_path, # src 

225 dst_dir_path / entry_path.name, # dst 

226 dirs_exist_ok=False, 

227 ) 

228 continue 

229 # -- Case 3: Copy file. 

230 shutil.copy(entry_path, dst_dir_path) 

231 

232 # -- Inform the user. 

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

234 

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

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

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

238 

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

240 """Copy the example creating the folder 

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

242 is created 

243 * INPUTS: 

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

245 * dst_dir: (optional) destination directory. 

246 """ 

247 

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

249 # dst_dir = util.resolve_project_dir( 

250 # dst_dir, create_if_missing=True 

251 # ) 

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

253 

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

255 fatal_error( 

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

257 info=[ 

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

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

260 ], 

261 ) 

262 

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

264 src_board_dir = self.examples_dir / board_id 

265 

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

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

268 fatal_error( 

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

270 info=[ 

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

272 + "examples.", 

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

274 ], 

275 ) 

276 

277 if dst_dir.exists(): 

278 self.check_dst_dir_is_empty(dst_dir) 

279 else: 

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

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

282 

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

284 ignore_callback = shutil.ignore_patterns("info") 

285 

286 cout( 

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

288 f"for board '{board_id}'" 

289 ) 

290 

291 for board_example in board_examples: 

292 example_name = board_example.example_name 

293 styled_name = cstyle(example_name, style=EMPH3) 

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

295 shutil.copytree( 

296 src_board_dir / example_name, 

297 dst_dir / example_name, 

298 dirs_exist_ok=False, 

299 ignore=ignore_callback, 

300 ) 

301 

302 cout( 

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

304 "fetched successfully.", 

305 style=SUCCESS, 

306 )