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

110 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-24 03:51 +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 

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: 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 = apio_ctx.get_package_dir("examples") 

49 

50 def check_dst_dir_is_empty(self, path: Path): 

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

52 print an error and exit. 

53 """ 

54 

55 # -- Check prerequisites. 

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

57 

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

59 dir_content: List[str] = os.listdir(path) 

60 

61 # -- We don't care about macOS 

62 ignore_list = [".DS_Store"] 

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

64 

65 # -- Error if not empty. 

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

67 cerror( 

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

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

70 ) 

71 sys.exit(1) 

72 

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

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

75 Returns null if an error.""" 

76 

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

78 boards_dirs: List[Path] = [] 

79 

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

81 if board_dir.is_dir(): 

82 boards_dirs.append(board_dir) 

83 

84 # -- Collect the examples of each boards. 

85 examples: List[ExampleInfo] = [] 

86 for board_dir in boards_dirs: 

87 

88 # -- Iterate board's example subdirectories. 

89 for example_dir in board_dir.iterdir(): 

90 

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

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

93 continue 

94 

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

96 info_file = example_dir / "info" 

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

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

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

100 else: 

101 description = "" 

102 

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

104 # -- default value if not found. 

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

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

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

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

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

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

111 

112 # -- Append this example to the list. 

113 example_info = ExampleInfo( 

114 board_id=board_dir.name, 

115 example_name=example_dir.name, 

116 path=example_dir, 

117 description=description, 

118 fpga_arch=fpga_arch, 

119 fpga_part_num=fpga_part_num, 

120 fpga_size=fpga_size, 

121 ) 

122 examples.append(example_info) 

123 

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

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

126 

127 return examples 

128 

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

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

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

132 

133 # -- Get list of examples. 

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

135 

136 # -- Count examples by board 

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

138 for example in examples: 

139 board = example.board_id 

140 old_count = counts.get(board, 0) 

141 counts[board] = old_count + 1 

142 

143 # -- All done 

144 return counts 

145 

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

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

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

149 """ 

150 

151 example_infos = self.get_examples_infos() 

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

153 if example_name == ex.name: 

154 return ex 

155 return None 

156 

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

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

159 If destination dir exists, it must be empty. 

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

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

162 """ 

163 

164 # Check that the example name exists. 

165 example_info: ExampleInfo | None = self.lookup_example_info( 

166 example_name 

167 ) 

168 

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

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

171 cout( 

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

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

174 style=INFO, 

175 ) 

176 sys.exit(1) 

177 

178 # -- Get the example dir path. 

179 src_example_path = example_info.path 

180 

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

182 # -- we ignore hidden files and directory. 

183 if dst_dir_path.exists(): 

184 self.check_dst_dir_is_empty(dst_dir_path) 

185 else: 

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

187 

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

189 

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

191 for entry_path in src_example_path.iterdir(): 

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

193 if entry_path.name == "info": 

194 continue 

195 # -- Case 2: Copy subdirectory. 

196 if entry_path.is_dir(): 

197 shutil.copytree( 

198 entry_path, # src 

199 dst_dir_path / entry_path.name, # dst 

200 dirs_exist_ok=False, 

201 ) 

202 continue 

203 # -- Case 3: Copy file. 

204 shutil.copy(entry_path, dst_dir_path) 

205 

206 # -- Inform the user. 

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

208 

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

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

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

212 

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

214 """Copy the example creating the folder 

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

216 is created 

217 * INPUTS: 

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

219 * dst_dir: (optional) destination directory. 

220 """ 

221 

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

223 # dst_dir = util.resolve_project_dir( 

224 # dst_dir, create_if_missing=True 

225 # ) 

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

227 

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

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

230 cout( 

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

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

233 style=INFO, 

234 ) 

235 sys.exit(1) 

236 

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

238 src_board_dir = self.examples_dir / board_id 

239 

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

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

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

243 cout( 

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

245 "examples.", 

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

247 style=INFO, 

248 ) 

249 sys.exit(1) 

250 

251 if dst_dir.exists(): 

252 self.check_dst_dir_is_empty(dst_dir) 

253 else: 

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

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

256 

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

258 ignore_callback = shutil.ignore_patterns("info") 

259 

260 cout( 

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

262 f"for board '{board_id}'" 

263 ) 

264 

265 for board_example in board_examples: 

266 example_name = board_example.example_name 

267 styled_name = cstyle(example_name, style=EMPH3) 

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

269 shutil.copytree( 

270 src_board_dir / example_name, 

271 dst_dir / example_name, 

272 dirs_exist_ok=False, 

273 ignore=ignore_callback, 

274 ) 

275 

276 cout( 

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

278 "fetched successfully.", 

279 style=SUCCESS, 

280 )