Coverage for tsfpga/module.py: 99%

199 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-07 20:51 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://github.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9# Standard libraries 

10import random 

11from pathlib import Path 

12from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, Union 

13 

14# Third party libraries 

15from hdl_registers.generator.vhdl.axi_lite.wrapper import VhdlAxiLiteWrapperGenerator 

16from hdl_registers.generator.vhdl.record_package import VhdlRecordPackageGenerator 

17from hdl_registers.generator.vhdl.register_package import VhdlRegisterPackageGenerator 

18from hdl_registers.generator.vhdl.simulation.check_package import ( 

19 VhdlSimulationCheckPackageGenerator, 

20) 

21from hdl_registers.generator.vhdl.simulation.read_write_package import ( 

22 VhdlSimulationReadWritePackageGenerator, 

23) 

24from hdl_registers.generator.vhdl.simulation.wait_until_package import ( 

25 VhdlSimulationWaitUntilPackageGenerator, 

26) 

27from hdl_registers.parser.toml import from_toml 

28from hdl_registers.register import Register 

29from hdl_registers.register_list import RegisterList 

30 

31# First party libraries 

32from tsfpga.constraint import Constraint 

33from tsfpga.hdl_file import HdlFile 

34from tsfpga.ip_core_file import IpCoreFile 

35from tsfpga.module_list import ModuleList 

36from tsfpga.system_utils import load_python_module 

37 

38if TYPE_CHECKING: 

39 # Local folder libraries 

40 from .vivado.project import VivadoProject 

41 

42 

43class BaseModule: 

44 """ 

45 Base class for handling a HDL module with RTL code, constraints, etc. 

46 

47 Files are gathered from a lot of different sub-folders, to accommodate for projects having 

48 different catalog structure. 

49 """ 

50 

51 # Set to False if you do not want to create these register artifacts for this module. 

52 # Can be done in a child class or on an object instance. 

53 # Note that artifacts will only be created if the module actually has any registers. 

54 create_register_package = True 

55 create_record_package = True 

56 create_axi_lite_wrapper = True 

57 create_simulation_read_write_package = True 

58 create_simulation_check_package = True 

59 create_simulation_wait_until_package = True 

60 

61 def __init__( 

62 self, path: Path, library_name: str, default_registers: Optional[list[Register]] = None 

63 ): 

64 """ 

65 Arguments: 

66 path: Path to the module folder. 

67 library_name: VHDL library name. 

68 default_registers: Default registers. 

69 """ 

70 self.path = path.resolve() 

71 self.name = path.name 

72 self.library_name = library_name 

73 

74 self._default_registers = default_registers 

75 self._registers: Optional[RegisterList] = None 

76 

77 @staticmethod 

78 def _get_file_list( 

79 folders: list[Path], 

80 file_endings: Union[str, tuple[str, ...]], 

81 files_include: Optional[set[Path]] = None, 

82 files_avoid: Optional[set[Path]] = None, 

83 ) -> list[Path]: 

84 """ 

85 Returns a list of files given a list of folders. 

86 

87 Arguments: 

88 folders: The folders to search. 

89 file_endings: File endings to include. 

90 files_include: Optionally filter to only include these files. 

91 files_avoid: Optionally filter to discard these files. 

92 """ 

93 files = [] 

94 for folder in folders: 

95 for file in folder.glob("*"): 

96 if not file.is_file(): 

97 continue 

98 

99 if not file.name.lower().endswith(file_endings): 

100 continue 

101 

102 if files_include is not None and file not in files_include: 

103 continue 

104 

105 if files_avoid is not None and file in files_avoid: 

106 continue 

107 

108 files.append(file) 

109 

110 return files 

111 

112 def _get_hdl_file_list( # pylint: disable=too-many-arguments 

113 self, 

114 folders: list[Path], 

115 files_include: Optional[set[Path]] = None, 

116 files_avoid: Optional[set[Path]] = None, 

117 include_vhdl_files: bool = True, 

118 include_verilog_files: bool = True, 

119 include_systemverilog_files: bool = True, 

120 ) -> list[HdlFile]: 

121 """ 

122 Return a list of HDL file objects. 

123 """ 

124 file_endings: tuple[str, ...] = tuple() 

125 if include_vhdl_files: 

126 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.VHDL] 

127 if include_verilog_files: 

128 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.VERILOG_SOURCE] 

129 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.VERILOG_HEADER] 

130 if include_systemverilog_files: 

131 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.SYSTEMVERILOG_SOURCE] 

132 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.SYSTEMVERILOG_HEADER] 

133 

134 return [ 

135 HdlFile(path=file_path) 

136 for file_path in self._get_file_list( 

137 folders=folders, 

138 file_endings=file_endings, 

139 files_include=files_include, 

140 files_avoid=files_avoid, 

141 ) 

142 ] 

143 

144 @property 

145 def register_data_file(self) -> Path: 

146 """ 

147 The path to this module's register data file (which may or may not exist). 

148 """ 

149 return self.path / f"regs_{self.name}.toml" 

150 

151 @property 

152 def registers(self) -> Optional[RegisterList]: 

153 """ 

154 Get the registers for this module. 

155 Will be ``None`` if the module doesn't have any registers. 

156 I.e. if no TOML file exists and no hook creates registers. 

157 """ 

158 if self._registers: 

159 # Only create object from TOML once. 

160 return self._registers 

161 

162 toml_file = self.register_data_file 

163 if toml_file.exists(): 

164 self._registers = from_toml( 

165 name=self.name, toml_file=toml_file, default_registers=self._default_registers 

166 ) 

167 

168 self.registers_hook() 

169 return self._registers 

170 

171 def registers_hook(self) -> None: 

172 """ 

173 This function will be called directly after creating this module's registers from 

174 the TOML definition file. 

175 If the TOML file does not exist this hook will still be called, but the module's registers 

176 will be ``None``. 

177 

178 This is a good place if you want to add or modify some registers from Python. 

179 Override this method and implement the desired behavior in a subclass. 

180 

181 .. Note:: 

182 This default method does nothing. 

183 Shall be overridden by modules that utilize this mechanism. 

184 """ 

185 

186 def create_register_synthesis_files(self) -> None: 

187 """ 

188 Create the register artifacts that are needed for synthesis. 

189 If this module does not have registers, this method does nothing. 

190 """ 

191 if self.registers is not None: 

192 # Delete any old file that might exist so we don't have multiple and 

193 # outdated definitions. 

194 # This package location was used before the separate register folders were introduced, 

195 # back when we only created one register artifact. 

196 old_regs_pkg = self.path / f"{self.name}_regs_pkg.vhd" 

197 if old_regs_pkg.exists(): 

198 old_regs_pkg.unlink() 

199 

200 if self.create_register_package: 

201 VhdlRegisterPackageGenerator( 

202 register_list=self.registers, output_folder=self.register_synthesis_folder 

203 ).create_if_needed() 

204 

205 if self.create_record_package: 

206 VhdlRecordPackageGenerator( 

207 register_list=self.registers, output_folder=self.register_synthesis_folder 

208 ).create_if_needed() 

209 

210 if self.create_axi_lite_wrapper: 

211 VhdlAxiLiteWrapperGenerator( 

212 register_list=self.registers, output_folder=self.register_synthesis_folder 

213 ).create_if_needed() 

214 

215 def create_register_simulation_files(self) -> None: 

216 """ 

217 Create the register artifacts that are needed for simulation. 

218 Does not create the implementation files, which are also technically needed for simulation. 

219 So a call to :meth:`.create_register_synthesis_files` must also be done. 

220 

221 If this module does not have registers, this method does nothing. 

222 """ 

223 if self.registers is not None: 

224 if self.create_simulation_read_write_package: 

225 VhdlSimulationReadWritePackageGenerator( 

226 register_list=self.registers, output_folder=self.register_simulation_folder 

227 ).create_if_needed() 

228 

229 if self.create_simulation_check_package: 

230 VhdlSimulationCheckPackageGenerator( 

231 register_list=self.registers, output_folder=self.register_simulation_folder 

232 ).create_if_needed() 

233 

234 if self.create_simulation_wait_until_package: 

235 VhdlSimulationWaitUntilPackageGenerator( 

236 register_list=self.registers, output_folder=self.register_simulation_folder 

237 ).create_if_needed() 

238 

239 @property 

240 def synthesis_folders(self) -> list[Path]: 

241 """ 

242 Synthesis/implementation source code files will be gathered from these folders. 

243 """ 

244 return [ 

245 self.path, 

246 self.path / "src", 

247 self.path / "rtl", 

248 self.path / "hdl" / "rtl", 

249 self.path / "hdl" / "package", 

250 self.register_synthesis_folder, 

251 ] 

252 

253 @property 

254 def register_synthesis_folder(self) -> Path: 

255 """ 

256 Generated register artifacts that are needed for synthesis/implementation will be 

257 placed in this folder. 

258 """ 

259 return self.path / "regs_src" 

260 

261 @property 

262 def sim_folders(self) -> list[Path]: 

263 """ 

264 Files with simulation models (the ``sim`` folder) will be gathered from these folders. 

265 """ 

266 return [ 

267 self.path / "sim", 

268 self.register_simulation_folder, 

269 ] 

270 

271 @property 

272 def register_simulation_folder(self) -> Path: 

273 """ 

274 Generated register artifacts that are needed for simulation will be placed in this folder. 

275 """ 

276 return self.path / "regs_sim" 

277 

278 @property 

279 def test_folders(self) -> list[Path]: 

280 """ 

281 Testbench files will be gathered from these folders. 

282 """ 

283 return [ 

284 self.path / "test", 

285 self.path / "rtl" / "tb", 

286 ] 

287 

288 def get_synthesis_files( # pylint: disable=unused-argument 

289 self, 

290 files_include: Optional[set[Path]] = None, 

291 files_avoid: Optional[set[Path]] = None, 

292 include_vhdl_files: bool = True, 

293 include_verilog_files: bool = True, 

294 include_systemverilog_files: bool = True, 

295 **kwargs: Any, 

296 ) -> list[HdlFile]: 

297 """ 

298 Get a list of files that shall be included in a synthesis project. 

299 

300 The ``files_include`` and ``files_avoid`` arguments can be used to filter what files are 

301 included. 

302 This can be useful in many situations, e.g. when encrypted files of files that include an 

303 IP core shall be avoided. 

304 It is recommended to overload this function in a subclass in your ``module_*.py``, 

305 and call this super method with the arguments supplied. 

306 

307 Arguments: 

308 files_include: Optionally filter to only include these files. 

309 files_avoid: Optionally filter to discard these files. 

310 include_vhdl_files: Optionally disable inclusion of files with VHDL 

311 file endings. 

312 include_verilog_files: Optionally disable inclusion of files with Verilog 

313 file endings. 

314 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

315 file endings. 

316 kwargs: Further parameters that can be sent by build flow to control what 

317 files are included. 

318 

319 Return: 

320 Files that should be included in a synthesis project. 

321 """ 

322 self.create_register_synthesis_files() 

323 

324 return self._get_hdl_file_list( 

325 folders=self.synthesis_folders, 

326 files_include=files_include, 

327 files_avoid=files_avoid, 

328 include_vhdl_files=include_vhdl_files, 

329 include_verilog_files=include_verilog_files, 

330 include_systemverilog_files=include_systemverilog_files, 

331 ) 

332 

333 def get_simulation_files( # pylint: disable=too-many-arguments 

334 self, 

335 include_tests: bool = True, 

336 files_include: Optional[set[Path]] = None, 

337 files_avoid: Optional[set[Path]] = None, 

338 include_vhdl_files: bool = True, 

339 include_verilog_files: bool = True, 

340 include_systemverilog_files: bool = True, 

341 **kwargs: Any, 

342 ) -> list[HdlFile]: 

343 """ 

344 See :meth:`.get_synthesis_files` for instructions on how to use ``files_include`` 

345 and ``files_avoid``. 

346 

347 Arguments: 

348 include_tests: When ``False``, the ``test`` files are not included 

349 (the ``sim`` files are always included). 

350 files_include: Optionally filter to only include these files. 

351 files_avoid: Optionally filter to discard these files. 

352 include_vhdl_files: Optionally disable inclusion of files with VHDL 

353 file endings. 

354 include_verilog_files: Optionally disable inclusion of files with Verilog 

355 file endings. 

356 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

357 file endings. 

358 kwargs: Further parameters that can be sent by simulation flow to control what 

359 files are included. 

360 

361 Return: 

362 Files that should be included in a simulation project. 

363 """ 

364 # Shallow copy the list since we might append to it. 

365 sim_and_test_folders = self.sim_folders.copy() 

366 

367 if include_tests: 

368 sim_and_test_folders += self.test_folders 

369 

370 self.create_register_simulation_files() 

371 

372 test_files = self._get_hdl_file_list( 

373 folders=sim_and_test_folders, 

374 files_include=files_include, 

375 files_avoid=files_avoid, 

376 include_vhdl_files=include_vhdl_files, 

377 include_verilog_files=include_verilog_files, 

378 include_systemverilog_files=include_systemverilog_files, 

379 ) 

380 

381 synthesis_files = self.get_synthesis_files( 

382 files_include=files_include, 

383 files_avoid=files_avoid, 

384 include_vhdl_files=include_vhdl_files, 

385 include_verilog_files=include_verilog_files, 

386 include_systemverilog_files=include_systemverilog_files, 

387 **kwargs, 

388 ) 

389 

390 return synthesis_files + test_files 

391 

392 def get_documentation_files( # pylint: disable=unused-argument 

393 self, 

394 files_include: Optional[set[Path]] = None, 

395 files_avoid: Optional[set[Path]] = None, 

396 include_vhdl_files: bool = True, 

397 include_verilog_files: bool = True, 

398 include_systemverilog_files: bool = True, 

399 **kwargs: Any, 

400 ) -> list[HdlFile]: 

401 """ 

402 Get a list of files that shall be included in a documentation build. 

403 

404 It will return all files from the module except testbenches and any generated 

405 register package. 

406 Overwrite in a subclass if you want to change this behavior. 

407 

408 Arguments: 

409 files_include: Optionally filter to only include these files. 

410 files_avoid: Optionally filter to discard these files. 

411 include_vhdl_files: Optionally disable inclusion of files with VHDL 

412 file endings. 

413 include_verilog_files: Optionally disable inclusion of files with Verilog 

414 file endings. 

415 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

416 file endings. 

417 

418 Return: 

419 Files that should be included in documentation. 

420 """ 

421 # Do not include generated register code in the documentation. 

422 files_to_avoid = set( 

423 self._get_file_list( 

424 folders=[self.register_synthesis_folder, self.register_simulation_folder], 

425 file_endings="vhd", 

426 ) 

427 ) 

428 if files_avoid: 

429 files_to_avoid |= files_avoid 

430 

431 return self._get_hdl_file_list( 

432 folders=self.synthesis_folders + self.sim_folders, 

433 files_include=files_include, 

434 files_avoid=files_to_avoid, 

435 include_vhdl_files=include_vhdl_files, 

436 include_verilog_files=include_verilog_files, 

437 include_systemverilog_files=include_systemverilog_files, 

438 ) 

439 

440 # pylint: disable=unused-argument 

441 def get_ip_core_files( 

442 self, 

443 files_include: Optional[set[Path]] = None, 

444 files_avoid: Optional[set[Path]] = None, 

445 **kwargs: Any, 

446 ) -> list[IpCoreFile]: 

447 """ 

448 Get IP cores for this module. 

449 

450 Note that the :class:`.ip_core_file.IpCoreFile` class accepts a ``variables`` argument that 

451 can be used to parameterize IP core creation. By overloading this method in a subclass 

452 you can pass on ``kwargs`` arguments from the build/simulation flow to 

453 :class:`.ip_core_file.IpCoreFile` creation to achieve this parameterization. 

454 

455 Arguments: 

456 files_include: Optionally filter to only include these files. 

457 files_avoid: Optionally filter to discard these files. 

458 kwargs: Further parameters that can be sent by build/simulation flow to control what 

459 IP cores are included and what their variables are. 

460 

461 Return: 

462 The IP cores for this module. 

463 """ 

464 folders = [ 

465 self.path / "ip_cores", 

466 ] 

467 file_endings = "tcl" 

468 return [ 

469 IpCoreFile(ip_core_file) 

470 for ip_core_file in self._get_file_list( 

471 folders=folders, 

472 file_endings=file_endings, 

473 files_include=files_include, 

474 files_avoid=files_avoid, 

475 ) 

476 ] 

477 

478 # pylint: disable=unused-argument 

479 def get_scoped_constraints( 

480 self, 

481 files_include: Optional[set[Path]] = None, 

482 files_avoid: Optional[set[Path]] = None, 

483 **kwargs: Any, 

484 ) -> list[Constraint]: 

485 """ 

486 Constraints that shall be applied to a certain entity within this module. 

487 

488 Arguments: 

489 files_include: Optionally filter to only include these files. 

490 files_avoid: Optionally filter to discard these files. 

491 kwargs: Further parameters that can be sent by build/simulation flow to control what 

492 constraints are included. 

493 

494 Return: 

495 The constraints. 

496 """ 

497 folders = [ 

498 self.path / "scoped_constraints", 

499 self.path / "entity_constraints", 

500 self.path / "hdl" / "constraints", 

501 ] 

502 file_endings = ("tcl", "xdc") 

503 constraint_files = self._get_file_list( 

504 folders=folders, 

505 file_endings=file_endings, 

506 files_include=files_include, 

507 files_avoid=files_avoid, 

508 ) 

509 

510 constraints = [] 

511 if constraint_files: 

512 synthesis_files = self.get_synthesis_files() 

513 for constraint_file in constraint_files: 

514 # Scoped constraints often depend on clocks having been created by another 

515 # constraint file before they can work. Set processing order to "late" to make 

516 # this more probable. 

517 constraint = Constraint( 

518 constraint_file, scoped_constraint=True, processing_order="late" 

519 ) 

520 constraint.validate_scoped_entity(synthesis_files) 

521 constraints.append(constraint) 

522 

523 return constraints 

524 

525 def setup_vunit(self, vunit_proj: Any, **kwargs: Any) -> None: 

526 """ 

527 Setup local configuration of this module's test benches. 

528 

529 .. Note:: 

530 This default method does nothing. Should be overridden by modules that have 

531 any test benches that operate via generics. 

532 

533 Arguments: 

534 vunit_proj: The VUnit project that is used to run simulation. 

535 kwargs: Use this to pass an arbitrary list of arguments from your ``simulate.py`` 

536 to the module where you set up your tests. This could be, e.g., data dimensions, 

537 location of test files, etc. 

538 """ 

539 

540 def pre_build( 

541 self, project: "VivadoProject", **kwargs: Any 

542 ) -> bool: # pylint: disable=unused-argument 

543 """ 

544 This method hook will be called before an FPGA build is run. A typical use case for this 

545 mechanism is to set a register constant or default value based on the generics that 

546 are passed to the project. Could also be used to, e.g., generate BRAM init files 

547 based on project information, etc. 

548 

549 .. Note:: 

550 This default method does nothing. Should be overridden by modules that 

551 utilize this mechanism. 

552 

553 Arguments: 

554 project: The project that is being built. 

555 kwargs: All other parameters to the build flow. Includes arguments to 

556 :meth:`.VivadoProject.build` method as well as other arguments set in 

557 :meth:`.VivadoProject.__init__`. 

558 

559 Return: 

560 True if everything went well. 

561 """ 

562 return True 

563 

564 def get_build_projects(self) -> list["VivadoProject"]: 

565 """ 

566 Get FPGA build projects defined by this module. 

567 

568 .. Note:: 

569 This default method does nothing. Should be overridden by modules that set up 

570 build projects. 

571 

572 Return: 

573 FPGA build projects. 

574 """ 

575 return [] 

576 

577 @staticmethod 

578 def test_case_name( 

579 name: Optional[str] = None, generics: Optional[dict[str, Any]] = None 

580 ) -> str: 

581 """ 

582 Construct a string suitable for naming test cases. 

583 

584 Arguments: 

585 name: Optional base name. 

586 generics: Dictionary of values that will be included in the name. 

587 

588 Return: 

589 For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``. 

590 """ 

591 if name: 

592 test_case_name = name 

593 else: 

594 test_case_name = "" 

595 

596 if generics: 

597 generics_string = ".".join([f"{key}_{value}" for key, value in generics.items()]) 

598 

599 if test_case_name: 

600 test_case_name = f"{name}.{generics_string}" 

601 else: 

602 test_case_name = generics_string 

603 

604 return test_case_name 

605 

606 def add_vunit_config( # pylint: disable=too-many-arguments 

607 self, 

608 test: Any, 

609 name: Optional[str] = None, 

610 generics: Optional[dict[str, Any]] = None, 

611 set_random_seed: Optional[Union[bool, int]] = False, 

612 pre_config: Optional[Callable[..., bool]] = None, 

613 post_check: Optional[Callable[..., bool]] = None, 

614 ) -> None: 

615 """ 

616 Add config for VUnit test case. 

617 Wrapper that sets a suitable name and can set a random seed generic. 

618 

619 Arguments: 

620 test: VUnit test object. Can be testbench or test case. 

621 name: Optional designated name for this config. Will be used to form the name of 

622 the config together with the ``generics`` value. 

623 generics: Generic values that will be applied to the testbench entity. The values 

624 will also be used to form the name of the config. 

625 set_random_seed: Controls setting of the ``seed`` generic: 

626 

627 * When this argument is not assigned, or assigned ``False``, the generic will not 

628 be set. 

629 * When set to boolean ``True``, a random natural (non-negative integer) 

630 generic value will be set. 

631 * When set to an integer value, that value will be set for the generic. 

632 This is useful to get a static test case name for waveform inspection. 

633 

634 If the generic is to be set it must exist in the testbench entity, and should have 

635 VHDL type ``natural``. 

636 pre_config: Function to be run before the test. 

637 See `VUnit documentation <https://vunit.github.io/py/ui.html>`_ for details. 

638 post_check: Function to be run after the test. 

639 See `VUnit documentation <https://vunit.github.io/py/ui.html>`_ for details. 

640 """ 

641 generics = {} if generics is None else generics 

642 

643 # Note that "bool" is a sub-class of "int" in python, so isinstance(set_random_seed, int) 

644 # returns True if it is an integer or a bool. 

645 if isinstance(set_random_seed, bool): 

646 if set_random_seed: 

647 # Use the maximum range for a natural in VHDL-2008 

648 generics["seed"] = random.randint(0, 2**31 - 1) 

649 

650 elif isinstance(set_random_seed, int): 

651 generics["seed"] = set_random_seed 

652 

653 name = self.test_case_name(name=name, generics=generics) 

654 # VUnit does not allow an empty name, which can happen if both 'name' and 'generics' to 

655 # this method are None, but the user sets for example a 'pre_config'. 

656 # Avoid this error mode by setting a default name when it happens. 

657 name = "test" if name == "" else name 

658 

659 test.add_config(name=name, generics=generics, pre_config=pre_config, post_check=post_check) 

660 

661 def __str__(self) -> str: 

662 return f"{self.name}:{self.path}" 

663 

664 

665def get_modules( 

666 modules_folder: Optional[Path] = None, 

667 modules_folders: Optional[list[Path]] = None, 

668 names_include: Optional[set[str]] = None, 

669 names_avoid: Optional[set[str]] = None, 

670 library_name_has_lib_suffix: bool = False, 

671 default_registers: Optional[list[Register]] = None, 

672) -> ModuleList: 

673 """ 

674 Get a list of module objects (:class:`BaseModule` or subclasses thereof) based on the source 

675 code folders. 

676 

677 Arguments: 

678 modules_folder: The path to the folder containing modules. 

679 modules_folders: Optionally, you can specify many folders with modules that will all 

680 be searched. 

681 names_include: If specified, only modules with these names will be included. 

682 names_avoid: If specified, modules with these names will be discarded. 

683 library_name_has_lib_suffix: If set, the library name will be ``<module name>_lib``, 

684 otherwise it is just ``<module name>``. 

685 default_registers: Default registers. 

686 

687 Return: 

688 The modules created from the specified folders. 

689 """ 

690 modules = ModuleList() 

691 

692 folders = [] 

693 if modules_folder is not None: 

694 folders.append(modules_folder) 

695 if modules_folders is not None: 

696 folders += modules_folders 

697 

698 for module_folder in _iterate_module_folders(folders): 

699 module_name = module_folder.name 

700 

701 if names_include is not None and module_name not in names_include: 

702 continue 

703 

704 if names_avoid is not None and module_name in names_avoid: 

705 continue 

706 

707 modules.append( 

708 _get_module_object( 

709 path=module_folder, 

710 name=module_name, 

711 library_name_has_lib_suffix=library_name_has_lib_suffix, 

712 default_registers=default_registers, 

713 ) 

714 ) 

715 

716 return modules 

717 

718 

719def get_module( 

720 name: str, 

721 modules_folder: Optional[Path] = None, 

722 modules_folders: Optional[list[Path]] = None, 

723 library_name_has_lib_suffix: bool = False, 

724 default_registers: Optional[list[Register]] = None, 

725) -> BaseModule: 

726 """ 

727 Get a single module object, for a module found in one of the specified source code folders. 

728 Is a wrapper around :func:`.get_modules`. 

729 

730 Arguments: 

731 name: The name of the module. 

732 modules_folder: The path to the folder containing modules. 

733 modules_folders: Optionally, you can specify many folders with modules that will all 

734 be searched. 

735 library_name_has_lib_suffix: If set, the library name will be ``<module name>_lib``, 

736 otherwise it is just ``<module name>``. 

737 default_registers: Default registers. 

738 

739 Return: 

740 The requested module. 

741 """ 

742 modules = get_modules( 

743 modules_folder=modules_folder, 

744 modules_folders=modules_folders, 

745 names_include={name}, 

746 library_name_has_lib_suffix=library_name_has_lib_suffix, 

747 default_registers=default_registers, 

748 ) 

749 

750 if not modules: 

751 raise RuntimeError(f'Could not find module "{name}".') 

752 

753 if len(modules) > 1: 

754 raise RuntimeError(f'Found multiple modules named "{name}".') 

755 

756 return modules[0] 

757 

758 

759def _iterate_module_folders(modules_folders: list[Path]) -> Iterable[Path]: 

760 for modules_folder in modules_folders: 

761 for module_folder in modules_folder.glob("*"): 

762 if module_folder.is_dir(): 

763 yield module_folder 

764 

765 

766def _get_module_object( 

767 path: Path, 

768 name: str, 

769 library_name_has_lib_suffix: bool, 

770 default_registers: Optional[list["Register"]], 

771) -> BaseModule: 

772 module_file = path / f"module_{name}.py" 

773 library_name = f"{name}_lib" if library_name_has_lib_suffix else name 

774 

775 if module_file.exists(): 

776 # We assume that the user lets their 'Module' class inherit from 'BaseModule'. 

777 module: BaseModule = load_python_module(module_file).Module( 

778 path=path, 

779 library_name=library_name, 

780 default_registers=default_registers, 

781 ) 

782 return module 

783 

784 return BaseModule(path=path, library_name=library_name, default_registers=default_registers)