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
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 03:51 +0000
1"""Manage apio examples"""
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
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
21@dataclass
22class ExampleInfo:
23 """Information about a single example."""
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
33 @property
34 def name(self) -> str:
35 """Returns the full id of the example."""
36 return self.board_id + "/" + self.example_name
39class Examples:
40 """Manage the apio examples"""
42 def __init__(self, apio_ctx: ApioContext):
44 # -- Save the apio context.
45 self.apio_ctx = apio_ctx
47 # -- Folder where the example packages was installed
48 self.examples_dir = apio_ctx.get_package_dir("examples")
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 """
55 # -- Check prerequisites.
56 assert path.is_dir(), f"Not a dir: {path}"
58 # -- Get the dir content, including hidden entries.
59 dir_content: List[str] = os.listdir(path)
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]
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)
73 def get_examples_infos(self) -> List[ExampleInfo]:
74 """Scans the examples and returns a list of ExampleInfos.
75 Returns null if an error."""
77 # -- Collect the examples home dir each board.
78 boards_dirs: List[Path] = []
80 for board_dir in self.examples_dir.iterdir():
81 if board_dir.is_dir():
82 boards_dirs.append(board_dir)
84 # -- Collect the examples of each boards.
85 examples: List[ExampleInfo] = []
86 for board_dir in boards_dirs:
88 # -- Iterate board's example subdirectories.
89 for example_dir in board_dir.iterdir():
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
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 = ""
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", "")
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)
124 # -- Sort in-place by acceding example name, case insensitive.
125 examples.sort(key=lambda x: x.name.lower())
127 return examples
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."""
133 # -- Get list of examples.
134 examples: List[ExampleInfo] = self.get_examples_infos()
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
143 # -- All done
144 return counts
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 """
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
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 """
164 # Check that the example name exists.
165 example_info: ExampleInfo | None = self.lookup_example_info(
166 example_name
167 )
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)
178 # -- Get the example dir path.
179 src_example_path = example_info.path
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)
188 cout("Copying " + example_name + " example files.")
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)
206 # -- Inform the user.
207 cout(f"Example '{example_name}' fetched successfully.", style=SUCCESS)
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]
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 """
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)
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)
237 # -- Build the source example path (where the example was installed)
238 src_board_dir = self.examples_dir / board_id
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)
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)
257 # -- Create an ignore callback to skip 'info' files.
258 ignore_callback = shutil.ignore_patterns("info")
260 cout(
261 f'Found {util.plurality(board_examples, "example")} '
262 f"for board '{board_id}'"
263 )
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 )
276 cout(
277 f"{util.plurality(board_examples, 'Example', include_num=False)} "
278 "fetched successfully.",
279 style=SUCCESS,
280 )