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

110 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-24 01: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 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 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[PosixPath] = [] 

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 = self.lookup_example_info(example_name) 

166 

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

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

169 cout( 

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

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

172 style=INFO, 

173 ) 

174 sys.exit(1) 

175 

176 # -- Get the example dir path. 

177 src_example_path = example_info.path 

178 

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

180 # -- we ignore hidden files and directory. 

181 if dst_dir_path.exists(): 

182 self.check_dst_dir_is_empty(dst_dir_path) 

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.exists(): 

250 self.check_dst_dir_is_empty(dst_dir) 

251 else: 

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

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

254 

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

256 ignore_callback = shutil.ignore_patterns("info") 

257 

258 cout( 

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

260 f"for board '{board_id}'" 

261 ) 

262 

263 for board_example in board_examples: 

264 example_name = board_example.example_name 

265 styled_name = cstyle(example_name, style=EMPH3) 

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

267 shutil.copytree( 

268 src_board_dir / example_name, 

269 dst_dir / example_name, 

270 dirs_exist_ok=False, 

271 ignore=ignore_callback, 

272 ) 

273 

274 cout( 

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

276 "fetched successfully.", 

277 style=SUCCESS, 

278 )