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
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-23 03:53 +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 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
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 = (
49 apio_ctx.get_package_dir("definitions") / "examples"
50 )
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 """
57 # -- Check prerequisites.
58 assert path.is_dir(), f"Not a dir: {path}"
60 # -- Get the dir content, including hidden entries.
61 dir_content: list[str] = os.listdir(path)
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]
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 )
74 def get_examples_infos(self) -> list[ExampleInfo]:
75 """Scans the examples and returns a list of ExampleInfos.
76 Returns null if an error."""
78 # pylint: disable=too-many-locals
80 # -- The context should have the board, fpgas, and programmer
81 # -- definitions.
82 assert self.apio_ctx.definitions is not None
84 # -- Collect the examples home dir each board.
85 boards_dirs: list[Path] = []
87 for board_dir in self.examples_dir.iterdir():
88 if board_dir.is_dir():
89 boards_dirs.append(board_dir)
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
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 )
106 # -- Get board definitions
107 board_definition = boards_definitions[board_id]
109 # -- Iterate board's example subdirectories.
110 for example_dir in board_dir.iterdir():
112 # -- Skip files. We care just about directories.
113 if not example_dir.is_dir():
114 continue
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 = ""
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
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)
148 # -- Sort in-place by acceding example name, case insensitive.
149 examples.sort(key=lambda x: x.name.lower())
151 return examples
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."""
157 # -- Get list of examples.
158 examples: list[ExampleInfo] = self.get_examples_infos()
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
167 # -- All done
168 return counts
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 """
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
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 """
188 # Check that the example name exists.
189 example_info: ExampleInfo | None = self.lookup_example_info(
190 example_name
191 )
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 )
202 # -- Get the example dir path.
203 src_example_path = example_info.path
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)
212 cout("Copying " + example_name + " example files.")
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)
230 # -- Inform the user.
231 cout(f"Example '{example_name}' fetched successfully.", style=SUCCESS)
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]
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 """
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)
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 )
261 # -- Build the source example path (where the example was installed)
262 src_board_dir = self.examples_dir / board_id
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 )
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)
281 # -- Create an ignore callback to skip 'info' files.
282 ignore_callback = shutil.ignore_patterns("info")
284 cout(
285 f'Found {util.plurality(board_examples, "example")} '
286 f"for board '{board_id}'"
287 )
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 )
300 cout(
301 f"{util.plurality(board_examples, 'Example', include_num=False)} "
302 "fetched successfully.",
303 style=SUCCESS,
304 )