Coverage for tsfpga/module.py: 96%

200 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-21 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 

9from __future__ import annotations 

10 

11import random 

12from typing import TYPE_CHECKING, Any, Callable 

13 

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

15from hdl_registers.generator.vhdl.record_package import VhdlRecordPackageGenerator 

16from hdl_registers.generator.vhdl.register_package import VhdlRegisterPackageGenerator 

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

18 VhdlSimulationCheckPackageGenerator, 

19) 

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

21 VhdlSimulationReadWritePackageGenerator, 

22) 

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

24 VhdlSimulationWaitUntilPackageGenerator, 

25) 

26from hdl_registers.parser.toml import from_toml 

27 

28from tsfpga.constraint import Constraint 

29from tsfpga.hdl_file import HdlFile 

30from tsfpga.ip_core_file import IpCoreFile 

31from tsfpga.module_list import ModuleList 

32from tsfpga.system_utils import load_python_module 

33 

34if TYPE_CHECKING: 

35 from collections.abc import Iterable 

36 from pathlib import Path 

37 

38 from hdl_registers.register import Register 

39 from hdl_registers.register_list import RegisterList 

40 from vunit.ui import VUnit 

41 from vunit.ui.test import Test 

42 from vunit.ui.testbench import TestBench 

43 

44 from .vivado.project import VivadoProject 

45 

46 

47class BaseModule: 

48 """ 

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

50 

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

52 different catalog structure. 

53 """ 

54 

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

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

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

58 create_register_package = True 

59 create_record_package = True 

60 create_axi_lite_wrapper = True 

61 create_simulation_read_write_package = True 

62 create_simulation_check_package = True 

63 create_simulation_wait_until_package = True 

64 

65 def __init__( 

66 self, path: Path, library_name: str, default_registers: list[Register] | None = None 

67 ) -> None: 

68 """ 

69 Arguments: 

70 path: Path to the module folder. 

71 library_name: VHDL library name. 

72 default_registers: Default registers. 

73 """ 

74 self.path = path.resolve() 

75 self.name = path.name 

76 self.library_name = library_name 

77 

78 self._default_registers = default_registers 

79 self._registers: RegisterList | None = None 

80 

81 @staticmethod 

82 def _get_file_list( 

83 folders: list[Path], 

84 file_endings: str | tuple[str, ...], 

85 files_include: set[Path] | None = None, 

86 files_avoid: set[Path] | None = None, 

87 ) -> list[Path]: 

88 """ 

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

90 

91 Arguments: 

92 folders: The folders to search. 

93 file_endings: File endings to include. 

94 files_include: Optionally filter to only include these files. 

95 files_avoid: Optionally filter to discard these files. 

96 """ 

97 files = [] 

98 for folder in folders: 

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

100 if not file.is_file(): 

101 continue 

102 

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

104 continue 

105 

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

107 continue 

108 

109 if files_avoid is not None and file in files_avoid: 

110 continue 

111 

112 files.append(file) 

113 

114 return files 

115 

116 def _get_hdl_file_list( 

117 self, 

118 folders: list[Path], 

119 files_include: set[Path] | None = None, 

120 files_avoid: set[Path] | None = None, 

121 include_vhdl_files: bool = True, 

122 include_verilog_files: bool = True, 

123 include_systemverilog_files: bool = True, 

124 ) -> list[HdlFile]: 

125 """ 

126 Return a list of HDL file objects. 

127 """ 

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

129 if include_vhdl_files: 

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

131 if include_verilog_files: 

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

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

134 if include_systemverilog_files: 

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

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

137 

138 return [ 

139 HdlFile(path=file_path) 

140 for file_path in self._get_file_list( 

141 folders=folders, 

142 file_endings=file_endings, 

143 files_include=files_include, 

144 files_avoid=files_avoid, 

145 ) 

146 ] 

147 

148 @property 

149 def register_data_file(self) -> Path: 

150 """ 

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

152 """ 

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

154 

155 @property 

156 def registers(self) -> RegisterList | None: 

157 """ 

158 Get the registers for this module. 

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

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

161 """ 

162 if self._registers: 

163 # Only create object from TOML once. 

164 return self._registers 

165 

166 toml_file = self.register_data_file 

167 if toml_file.exists(): 

168 self._registers = from_toml( 

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

170 ) 

171 

172 self.registers_hook() 

173 return self._registers 

174 

175 def registers_hook(self) -> None: 

176 """ 

177 Will be called directly after creating this module's registers from 

178 the TOML definition file. 

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

180 will be ``None``. 

181 

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

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

184 

185 .. Note:: 

186 This default method does nothing. 

187 Shall be overridden by modules that utilize this mechanism. 

188 """ 

189 

190 def create_register_synthesis_files(self) -> None: 

191 """ 

192 Create the register artifacts that are needed for synthesis. 

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

194 """ 

195 if self.registers is not None: 

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

197 # outdated definitions. 

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

199 # back when we only created one register artifact. 

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

201 if old_regs_pkg.exists(): 

202 old_regs_pkg.unlink() 

203 

204 if self.create_register_package: 

205 VhdlRegisterPackageGenerator( 

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

207 ).create_if_needed() 

208 

209 if self.create_record_package: 

210 VhdlRecordPackageGenerator( 

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

212 ).create_if_needed() 

213 

214 if self.create_axi_lite_wrapper: 

215 VhdlAxiLiteWrapperGenerator( 

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

217 ).create_if_needed() 

218 

219 def create_register_simulation_files(self) -> None: 

220 """ 

221 Create the register artifacts that are needed for simulation. 

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

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

224 

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

226 """ 

227 if self.registers is not None: 

228 if self.create_simulation_read_write_package: 

229 VhdlSimulationReadWritePackageGenerator( 

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

231 ).create_if_needed() 

232 

233 if self.create_simulation_check_package: 

234 VhdlSimulationCheckPackageGenerator( 

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

236 ).create_if_needed() 

237 

238 if self.create_simulation_wait_until_package: 

239 VhdlSimulationWaitUntilPackageGenerator( 

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

241 ).create_if_needed() 

242 

243 @property 

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

245 """ 

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

247 """ 

248 return [ 

249 self.path, 

250 self.path / "src", 

251 self.path / "rtl", 

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

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

254 self.register_synthesis_folder, 

255 ] 

256 

257 @property 

258 def register_synthesis_folder(self) -> Path: 

259 """ 

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

261 placed in this folder. 

262 """ 

263 return self.path / "regs_src" 

264 

265 @property 

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

267 """ 

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

269 """ 

270 return [ 

271 self.path / "sim", 

272 self.register_simulation_folder, 

273 ] 

274 

275 @property 

276 def register_simulation_folder(self) -> Path: 

277 """ 

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

279 """ 

280 return self.path / "regs_sim" 

281 

282 @property 

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

284 """ 

285 Testbench files will be gathered from these folders. 

286 """ 

287 return [ 

288 self.path / "test", 

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

290 ] 

291 

292 def get_synthesis_files( 

293 self, 

294 files_include: set[Path] | None = None, 

295 files_avoid: set[Path] | None = None, 

296 include_vhdl_files: bool = True, 

297 include_verilog_files: bool = True, 

298 include_systemverilog_files: bool = True, 

299 **kwargs: Any, # noqa: ANN401, ARG002 

300 ) -> list[HdlFile]: 

301 """ 

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

303 

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

305 included. 

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

307 IP core shall be avoided. 

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

309 and call this super method with the arguments supplied. 

310 

311 Arguments: 

312 files_include: Optionally filter to only include these files. 

313 files_avoid: Optionally filter to discard these files. 

314 include_vhdl_files: Optionally disable inclusion of files with VHDL 

315 file endings. 

316 include_verilog_files: Optionally disable inclusion of files with Verilog 

317 file endings. 

318 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

319 file endings. 

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

321 files are included. 

322 

323 Return: 

324 Files that should be included in a synthesis project. 

325 """ 

326 self.create_register_synthesis_files() 

327 

328 return self._get_hdl_file_list( 

329 folders=self.synthesis_folders, 

330 files_include=files_include, 

331 files_avoid=files_avoid, 

332 include_vhdl_files=include_vhdl_files, 

333 include_verilog_files=include_verilog_files, 

334 include_systemverilog_files=include_systemverilog_files, 

335 ) 

336 

337 def get_simulation_files( 

338 self, 

339 include_tests: bool = True, 

340 files_include: set[Path] | None = None, 

341 files_avoid: set[Path] | None = None, 

342 include_vhdl_files: bool = True, 

343 include_verilog_files: bool = True, 

344 include_systemverilog_files: bool = True, 

345 **kwargs: Any, # noqa: ANN401 

346 ) -> list[HdlFile]: 

347 """ 

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

349 and ``files_avoid``. 

350 

351 Arguments: 

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

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

354 files_include: Optionally filter to only include these files. 

355 files_avoid: Optionally filter to discard these files. 

356 include_vhdl_files: Optionally disable inclusion of files with VHDL 

357 file endings. 

358 include_verilog_files: Optionally disable inclusion of files with Verilog 

359 file endings. 

360 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

361 file endings. 

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

363 files are included. 

364 

365 Return: 

366 Files that should be included in a simulation project. 

367 """ 

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

369 sim_and_test_folders = self.sim_folders.copy() 

370 

371 if include_tests: 

372 sim_and_test_folders += self.test_folders 

373 

374 self.create_register_simulation_files() 

375 

376 test_files = self._get_hdl_file_list( 

377 folders=sim_and_test_folders, 

378 files_include=files_include, 

379 files_avoid=files_avoid, 

380 include_vhdl_files=include_vhdl_files, 

381 include_verilog_files=include_verilog_files, 

382 include_systemverilog_files=include_systemverilog_files, 

383 ) 

384 

385 synthesis_files = self.get_synthesis_files( 

386 files_include=files_include, 

387 files_avoid=files_avoid, 

388 include_vhdl_files=include_vhdl_files, 

389 include_verilog_files=include_verilog_files, 

390 include_systemverilog_files=include_systemverilog_files, 

391 **kwargs, 

392 ) 

393 

394 return synthesis_files + test_files 

395 

396 def get_documentation_files( 

397 self, 

398 files_include: set[Path] | None = None, 

399 files_avoid: set[Path] | None = None, 

400 include_vhdl_files: bool = True, 

401 include_verilog_files: bool = True, 

402 include_systemverilog_files: bool = True, 

403 **kwargs: Any, # noqa: ANN401, ARG002 

404 ) -> list[HdlFile]: 

405 """ 

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

407 

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

409 register package. 

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

411 

412 Arguments: 

413 files_include: Optionally filter to only include these files. 

414 files_avoid: Optionally filter to discard these files. 

415 include_vhdl_files: Optionally disable inclusion of files with VHDL 

416 file endings. 

417 include_verilog_files: Optionally disable inclusion of files with Verilog 

418 file endings. 

419 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

420 file endings. 

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

422 files are included. 

423 

424 Return: 

425 Files that should be included in documentation. 

426 """ 

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

428 files_to_avoid = set( 

429 self._get_file_list( 

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

431 file_endings="vhd", 

432 ) 

433 ) 

434 if files_avoid: 

435 files_to_avoid |= files_avoid 

436 

437 return self._get_hdl_file_list( 

438 folders=self.synthesis_folders + self.sim_folders, 

439 files_include=files_include, 

440 files_avoid=files_to_avoid, 

441 include_vhdl_files=include_vhdl_files, 

442 include_verilog_files=include_verilog_files, 

443 include_systemverilog_files=include_systemverilog_files, 

444 ) 

445 

446 def get_ip_core_files( 

447 self, 

448 files_include: set[Path] | None = None, 

449 files_avoid: set[Path] | None = None, 

450 **kwargs: Any, # noqa: ANN401, ARG002 

451 ) -> list[IpCoreFile]: 

452 """ 

453 Get IP cores for this module. 

454 

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

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

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

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

459 

460 Arguments: 

461 files_include: Optionally filter to only include these files. 

462 files_avoid: Optionally filter to discard these files. 

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

464 IP cores are included and what their variables are. 

465 

466 Return: 

467 The IP cores for this module. 

468 """ 

469 folders = [ 

470 self.path / "ip_cores", 

471 ] 

472 file_endings = "tcl" 

473 return [ 

474 IpCoreFile(ip_core_file) 

475 for ip_core_file in self._get_file_list( 

476 folders=folders, 

477 file_endings=file_endings, 

478 files_include=files_include, 

479 files_avoid=files_avoid, 

480 ) 

481 ] 

482 

483 def get_scoped_constraints( 

484 self, 

485 files_include: set[Path] | None = None, 

486 files_avoid: set[Path] | None = None, 

487 **kwargs: Any, # noqa: ANN401, ARG002 

488 ) -> list[Constraint]: 

489 """ 

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

491 

492 Arguments: 

493 files_include: Optionally filter to only include these files. 

494 files_avoid: Optionally filter to discard these files. 

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

496 constraints are included. 

497 

498 Return: 

499 The constraints. 

500 """ 

501 folders = [ 

502 self.path / "scoped_constraints", 

503 self.path / "entity_constraints", 

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

505 ] 

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

507 constraint_files = self._get_file_list( 

508 folders=folders, 

509 file_endings=file_endings, 

510 files_include=files_include, 

511 files_avoid=files_avoid, 

512 ) 

513 

514 constraints = [] 

515 if constraint_files: 

516 synthesis_files = self.get_synthesis_files() 

517 for constraint_file in constraint_files: 

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

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

520 # this more probable. 

521 constraint = Constraint( 

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

523 ) 

524 constraint.validate_scoped_entity(synthesis_files) 

525 constraints.append(constraint) 

526 

527 return constraints 

528 

529 def setup_vunit( 

530 self, 

531 vunit_proj: VUnit, 

532 **kwargs: Any, # noqa: ANN401 

533 ) -> None: 

534 """ 

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

536 

537 .. Note:: 

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

539 any test benches that operate via generics. 

540 

541 Arguments: 

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

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

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

545 location of test files, etc. 

546 """ 

547 

548 def pre_build( 

549 self, 

550 project: VivadoProject, # noqa: ARG002 

551 **kwargs: Any, # noqa: ANN401, ARG002 

552 ) -> bool: 

553 """ 

554 Will be called before an FPGA build is run. A typical use case for this 

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

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

557 based on project information, etc. 

558 

559 .. Note:: 

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

561 utilize this mechanism. 

562 

563 Arguments: 

564 project: The project that is being built. 

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

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

567 :meth:`.VivadoProject.__init__`. 

568 

569 Return: 

570 True if everything went well. 

571 """ 

572 return True 

573 

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

575 """ 

576 Get FPGA build projects defined by this module. 

577 

578 .. Note:: 

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

580 build projects. 

581 

582 Return: 

583 FPGA build projects. 

584 """ 

585 return [] 

586 

587 @staticmethod 

588 def test_case_name(name: str | None = None, generics: dict[str, Any] | None = None) -> str: 

589 """ 

590 Construct a string suitable for naming test cases. 

591 

592 Arguments: 

593 name: Optional base name. 

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

595 

596 Return: 

597 For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``. 

598 """ 

599 test_case_name = name if name else "" 

600 

601 if generics: 

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

603 

604 test_case_name = f"{name}.{generics_string}" if test_case_name else generics_string 

605 

606 return test_case_name 

607 

608 def add_vunit_config( 

609 self, 

610 test: Test | TestBench, 

611 name: str | None = None, 

612 generics: dict[str, Any] | None = None, 

613 set_random_seed: bool | int | None = False, 

614 pre_config: Callable[..., bool] | None = None, 

615 post_check: Callable[..., bool] | None = None, 

616 ) -> None: 

617 """ 

618 Add a VUnit test configuration. 

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

620 

621 Arguments: 

622 test: VUnit test or testbench object. 

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

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

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

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

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

628 

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

630 be set. 

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

632 generic value will be set. 

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

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

635 

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

637 VHDL type ``natural``. 

638 pre_config: Function to be run before the test. 

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

640 post_check: Function to be run after the test. 

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

642 """ 

643 generics = {} if generics is None else generics 

644 

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

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

647 if isinstance(set_random_seed, bool): 

648 if set_random_seed: 

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

650 # Does not need to be cryptographically secure. 

651 generics["seed"] = random.randint(0, 2**31 - 1) # noqa: S311 

652 

653 elif isinstance(set_random_seed, int): 

654 generics["seed"] = set_random_seed 

655 

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

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

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

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

660 name = "test" if name == "" else name 

661 

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

663 

664 def __str__(self) -> str: 

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

666 

667 

668def get_modules( 

669 modules_folder: Path | None = None, 

670 modules_folders: list[Path] | None = None, 

671 names_include: set[str] | None = None, 

672 names_avoid: set[str] | None = None, 

673 library_name_has_lib_suffix: bool = False, 

674 default_registers: list[Register] | None = None, 

675) -> ModuleList: 

676 """ 

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

678 code folders. 

679 

680 Arguments: 

681 modules_folder: The path to the folder containing modules. 

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

683 be searched. 

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

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

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

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

688 default_registers: Default registers. 

689 

690 Return: 

691 The modules created from the specified folders. 

692 """ 

693 modules = ModuleList() 

694 

695 folders = [] 

696 if modules_folder is not None: 

697 folders.append(modules_folder) 

698 if modules_folders is not None: 

699 folders += modules_folders 

700 

701 for module_folder in _iterate_module_folders(folders): 

702 module_name = module_folder.name 

703 

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

705 continue 

706 

707 if names_avoid is not None and module_name in names_avoid: 

708 continue 

709 

710 modules.append( 

711 _get_module_object( 

712 path=module_folder, 

713 name=module_name, 

714 library_name_has_lib_suffix=library_name_has_lib_suffix, 

715 default_registers=default_registers, 

716 ) 

717 ) 

718 

719 return modules 

720 

721 

722def get_module( 

723 name: str, 

724 modules_folder: Path | None = None, 

725 modules_folders: list[Path] | None = None, 

726 library_name_has_lib_suffix: bool = False, 

727 default_registers: list[Register] | None = None, 

728) -> BaseModule: 

729 """ 

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

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

732 

733 Arguments: 

734 name: The name of the module. 

735 modules_folder: The path to the folder containing modules. 

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

737 be searched. 

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

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

740 default_registers: Default registers. 

741 

742 Return: 

743 The requested module. 

744 """ 

745 modules = get_modules( 

746 modules_folder=modules_folder, 

747 modules_folders=modules_folders, 

748 names_include={name}, 

749 library_name_has_lib_suffix=library_name_has_lib_suffix, 

750 default_registers=default_registers, 

751 ) 

752 

753 if not modules: 

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

755 

756 if len(modules) > 1: 

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

758 

759 return modules[0] 

760 

761 

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

763 for modules_folder in modules_folders: 

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

765 if module_folder.is_dir(): 

766 yield module_folder 

767 

768 

769def _get_module_object( 

770 path: Path, 

771 name: str, 

772 library_name_has_lib_suffix: bool, 

773 default_registers: list[Register] | None, 

774) -> BaseModule: 

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

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

777 

778 if module_file.exists(): 

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

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

781 path=path, 

782 library_name=library_name, 

783 default_registers=default_registers, 

784 ) 

785 return module 

786 

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