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
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-24 01: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 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
21@dataclass
22class ExampleInfo:
23 """Information about a single example."""
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
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[PosixPath] = []
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 = self.lookup_example_info(example_name)
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)
176 # -- Get the example dir path.
177 src_example_path = example_info.path
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)
186 cout("Copying " + example_name + " example files.")
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)
204 # -- Inform the user.
205 cout(f"Example '{example_name}' fetched successfully.", style=SUCCESS)
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]
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 """
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)
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)
235 # -- Build the source example path (where the example was installed)
236 src_board_dir = self.examples_dir / board_id
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)
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)
255 # -- Create an ignore callback to skip 'info' files.
256 ignore_callback = shutil.ignore_patterns("info")
258 cout(
259 f'Found {util.plurality(board_examples, "example")} '
260 f"for board '{board_id}'"
261 )
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 )
274 cout(
275 f"{util.plurality(board_examples, 'Example', include_num=False)} "
276 "fetched successfully.",
277 style=SUCCESS,
278 )