Coverage for apio/managers/examples.py: 81%
112 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-06 10:20 +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 is_dir_empty(self, path: Path) -> bool:
51 """Return true if the given dir is empty, ignoring hidden entry.
52 That is, the dir may contain only hidden entries.
53 We use this relaxed criteria of emptiness to avoid user confusion.
54 We could use glob.glob() but in python 3.10 and earlier it doesn't
55 have the 'include_hidden' argument.
56 """
57 # -- Check prerequisites.
58 assert path.is_dir(), f"Not a dir: {path}"
60 # -- Iterate directory entries
61 for name in os.listdir(path): 61 ↛ 63line 61 didn't jump to line 63 because the loop on line 61 never started
62 # -- If not a hidden entry, answer is no.
63 if not name.startswith("."):
64 return False
65 # -- Non hidden entries not found. Directory is empty.
66 return True
68 def get_examples_infos(self) -> List[ExampleInfo]:
69 """Scans the examples and returns a list of ExampleInfos.
70 Returns null if an error."""
72 # -- Collect the examples home dir each board.
73 boards_dirs: List[PosixPath] = []
75 for board_dir in self.examples_dir.iterdir():
76 if board_dir.is_dir():
77 boards_dirs.append(board_dir)
79 # -- Collect the examples of each boards.
80 examples: List[ExampleInfo] = []
81 for board_dir in boards_dirs:
83 # -- Iterate board's example subdirectories.
84 for example_dir in board_dir.iterdir():
86 # -- Skip files. We care just about directories.
87 if not example_dir.is_dir(): 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true
88 continue
90 # -- Try to load description from the example info file.
91 info_file = example_dir / "info"
92 if info_file.exists(): 92 ↛ 96line 92 didn't jump to line 96 because the condition on line 92 was always true
93 with open(info_file, "r", encoding="utf-8") as f:
94 description = f.read().replace("\n", "")
95 else:
96 description = ""
98 # -- Extract the fpga arch and part number, with "" as
99 # -- default value if not found.
100 board_info = self.apio_ctx.boards.get(board_dir.name, {})
101 fpga_id = board_info.get("fpga-id", "")
102 fpga_info = self.apio_ctx.fpgas.get(fpga_id, {})
103 fpga_arch = fpga_info.get("arch", "")
104 fpga_part_num = fpga_info.get("part-num", "")
105 fpga_size = fpga_info.get("size", "")
107 # -- Append this example to the list.
108 example_info = ExampleInfo(
109 board_id=board_dir.name,
110 example_name=example_dir.name,
111 path=example_dir,
112 description=description,
113 fpga_arch=fpga_arch,
114 fpga_part_num=fpga_part_num,
115 fpga_size=fpga_size,
116 )
117 examples.append(example_info)
119 # -- Sort in-place by acceding example name, case insensitive.
120 examples.sort(key=lambda x: x.name.lower())
122 return examples
124 def count_examples_by_board(self) -> Dict[str, int]:
125 """Returns a dictionary with example count per board. Boards
126 that have no examples are not included in the dictionary."""
128 # -- Get list of examples.
129 examples: List[ExampleInfo] = self.get_examples_infos()
131 # -- Count examples by board
132 counts: Dict[str, int] = {}
133 for example in examples:
134 board = example.board_id
135 old_count = counts.get(board, 0)
136 counts[board] = old_count + 1
138 # -- All done
139 return counts
141 def lookup_example_info(self, example_name) -> Optional[ExampleInfo]:
142 """Return the example info for given example or None if not found.
143 Example_name looks like 'alhambra-ii/ledon'.
144 """
146 example_infos = self.get_examples_infos()
147 for ex in example_infos: 147 ↛ 150line 147 didn't jump to line 150 because the loop on line 147 didn't complete
148 if example_name == ex.name:
149 return ex
150 return None
152 def copy_example_files(self, example_name: str, dst_dir_path: Path):
153 """Copy the files from the given example to the destination dir.
154 If destination dir exists, it must be empty.
155 If it doesn't exist, it's created with any necessary parent.
156 The arg 'example_name' looks like 'alhambra-ii/ledon'.
157 """
159 # Check that the example name exists.
160 example_info: ExampleInfo = self.lookup_example_info(example_name)
162 if not example_info: 162 ↛ 163line 162 didn't jump to line 163 because the condition on line 162 was never true
163 cerror(f"Example '{example_name}' not found.")
164 cout(
165 "Run 'apio example list' for the list of examples.",
166 "Expecting an example name like alhambra-ii/ledon.",
167 style=INFO,
168 )
169 sys.exit(1)
171 # -- Get the example dir path.
172 src_example_path = example_info.path
174 # -- Prepare an empty destination directory. To avoid confusion,
175 # -- we ignore hidden files and directory.
176 if dst_dir_path.is_dir():
177 if not self.is_dir_empty(dst_dir_path): 177 ↛ 178line 177 didn't jump to line 178 because the condition on line 177 was never true
178 cerror(
179 f"Destination directory '{str(dst_dir_path)}' "
180 "is not empty."
181 )
182 sys.exit(1)
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.is_dir():
250 # -- To avoid confusion from the user, we ignore hidden files.
251 if not self.is_dir_empty(dst_dir): 251 ↛ 252line 251 didn't jump to line 252 because the condition on line 251 was never true
252 cerror(
253 f"Destination directory '{str(dst_dir)}' " "is not empty."
254 )
255 sys.exit(1)
256 else:
257 cout(f"Creating directory {dst_dir}.")
258 dst_dir.mkdir(parents=True, exist_ok=False)
260 # -- Create an ignore callback to skip 'info' files.
261 ignore_callback = shutil.ignore_patterns("info")
263 cout(
264 f'Found {util.plurality(board_examples, "example")} '
265 f"for board '{board_id}'"
266 )
268 for board_example in board_examples:
269 example_name = board_example.example_name
270 styled_name = cstyle(example_name, style=EMPH3)
271 cout(f"Fetching {board_id}/{styled_name}")
272 shutil.copytree(
273 src_board_dir / example_name,
274 dst_dir / example_name,
275 dirs_exist_ok=False,
276 ignore=ignore_callback,
277 )
279 cout(
280 f"{util.plurality(board_examples, 'Example', include_num=False)} "
281 "fetched successfully.",
282 style=SUCCESS,
283 )