Coverage for tsfpga/module.py: 99%

184 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-07 11:31 +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 registers(self) -> Optional[RegisterList]: 

146 """ 

147 Get the registers for this module. 

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

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

150 """ 

151 if self._registers: 

152 # Only create object from TOML once. 

153 return self._registers 

154 

155 toml_file = self.path / f"regs_{self.name}.toml" 

156 if toml_file.exists(): 

157 self._registers = from_toml( 

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

159 ) 

160 

161 self.registers_hook() 

162 return self._registers 

163 

164 def registers_hook(self) -> None: 

165 """ 

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

167 the TOML definition file. 

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

169 will be ``None``. 

170 

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

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

173 

174 .. Note:: 

175 This default method does nothing. 

176 Shall be overridden by modules that utilize this mechanism. 

177 """ 

178 

179 def create_register_synthesis_files(self) -> None: 

180 """ 

181 Create the register artifacts that are needed for synthesis. 

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

183 """ 

184 if self.registers is not None: 

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

186 # outdated definitions. 

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

188 # back when we only created one register artifact. 

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

190 if old_regs_pkg.exists(): 

191 old_regs_pkg.unlink() 

192 

193 if self.create_register_package: 

194 VhdlRegisterPackageGenerator( 

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

196 ).create_if_needed() 

197 

198 if self.create_record_package: 

199 VhdlRecordPackageGenerator( 

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

201 ).create_if_needed() 

202 

203 if self.create_axi_lite_wrapper: 

204 VhdlAxiLiteWrapperGenerator( 

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

206 ).create_if_needed() 

207 

208 def create_register_simulation_files(self) -> None: 

209 """ 

210 Create the register artifacts that are needed for simulation. 

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

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

213 

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

215 """ 

216 if self.registers is not None: 

217 if self.create_simulation_read_write_package: 

218 VhdlSimulationReadWritePackageGenerator( 

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

220 ).create_if_needed() 

221 

222 if self.create_simulation_check_package: 

223 VhdlSimulationCheckPackageGenerator( 

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

225 ).create_if_needed() 

226 

227 if self.create_simulation_wait_until_package: 

228 VhdlSimulationWaitUntilPackageGenerator( 

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

230 ).create_if_needed() 

231 

232 @property 

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

234 """ 

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

236 """ 

237 return [ 

238 self.path, 

239 self.path / "src", 

240 self.path / "rtl", 

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

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

243 self.register_synthesis_folder, 

244 ] 

245 

246 @property 

247 def register_synthesis_folder(self) -> Path: 

248 """ 

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

250 placed in this folder. 

251 """ 

252 return self.path / "regs_src" 

253 

254 @property 

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

256 """ 

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

258 """ 

259 return [ 

260 self.path / "sim", 

261 self.register_simulation_folder, 

262 ] 

263 

264 @property 

265 def register_simulation_folder(self) -> Path: 

266 """ 

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

268 """ 

269 return self.path / "regs_sim" 

270 

271 @property 

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

273 """ 

274 Testbench files will be gathered from these folders. 

275 """ 

276 return [ 

277 self.path / "test", 

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

279 ] 

280 

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

282 self, 

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

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

285 include_vhdl_files: bool = True, 

286 include_verilog_files: bool = True, 

287 include_systemverilog_files: bool = True, 

288 **kwargs: Any, 

289 ) -> list[HdlFile]: 

290 """ 

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

292 

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

294 included. 

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

296 IP core shall be avoided. 

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

298 and call this super method with the arguments supplied. 

299 

300 Arguments: 

301 files_include: Optionally filter to only include these files. 

302 files_avoid: Optionally filter to discard these files. 

303 include_vhdl_files: Optionally disable inclusion of files with VHDL 

304 file endings. 

305 include_verilog_files: Optionally disable inclusion of files with Verilog 

306 file endings. 

307 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

308 file endings. 

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

310 files are included. 

311 

312 Return: 

313 Files that should be included in a synthesis project. 

314 """ 

315 self.create_register_synthesis_files() 

316 

317 return self._get_hdl_file_list( 

318 folders=self.synthesis_folders, 

319 files_include=files_include, 

320 files_avoid=files_avoid, 

321 include_vhdl_files=include_vhdl_files, 

322 include_verilog_files=include_verilog_files, 

323 include_systemverilog_files=include_systemverilog_files, 

324 ) 

325 

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

327 self, 

328 include_tests: bool = True, 

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

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

331 include_vhdl_files: bool = True, 

332 include_verilog_files: bool = True, 

333 include_systemverilog_files: bool = True, 

334 **kwargs: Any, 

335 ) -> list[HdlFile]: 

336 """ 

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

338 and ``files_avoid``. 

339 

340 Arguments: 

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

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

343 files_include: Optionally filter to only include these files. 

344 files_avoid: Optionally filter to discard these files. 

345 include_vhdl_files: Optionally disable inclusion of files with VHDL 

346 file endings. 

347 include_verilog_files: Optionally disable inclusion of files with Verilog 

348 file endings. 

349 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

350 file endings. 

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

352 files are included. 

353 

354 Return: 

355 Files that should be included in a simulation project. 

356 """ 

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

358 sim_and_test_folders = self.sim_folders.copy() 

359 

360 if include_tests: 

361 sim_and_test_folders += self.test_folders 

362 

363 self.create_register_simulation_files() 

364 

365 test_files = self._get_hdl_file_list( 

366 folders=sim_and_test_folders, 

367 files_include=files_include, 

368 files_avoid=files_avoid, 

369 include_vhdl_files=include_vhdl_files, 

370 include_verilog_files=include_verilog_files, 

371 include_systemverilog_files=include_systemverilog_files, 

372 ) 

373 

374 synthesis_files = self.get_synthesis_files( 

375 files_include=files_include, 

376 files_avoid=files_avoid, 

377 include_vhdl_files=include_vhdl_files, 

378 include_verilog_files=include_verilog_files, 

379 include_systemverilog_files=include_systemverilog_files, 

380 **kwargs, 

381 ) 

382 

383 return synthesis_files + test_files 

384 

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

386 self, 

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

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

389 include_vhdl_files: bool = True, 

390 include_verilog_files: bool = True, 

391 include_systemverilog_files: bool = True, 

392 **kwargs: Any, 

393 ) -> list[HdlFile]: 

394 """ 

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

396 

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

398 register package. 

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

400 

401 Arguments: 

402 files_include: Optionally filter to only include these files. 

403 files_avoid: Optionally filter to discard these files. 

404 include_vhdl_files: Optionally disable inclusion of files with VHDL 

405 file endings. 

406 include_verilog_files: Optionally disable inclusion of files with Verilog 

407 file endings. 

408 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

409 file endings. 

410 

411 Return: 

412 Files that should be included in documentation. 

413 """ 

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

415 files_to_avoid = set( 

416 self._get_file_list( 

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

418 file_endings="vhd", 

419 ) 

420 ) 

421 if files_avoid: 

422 files_to_avoid |= files_avoid 

423 

424 return self._get_hdl_file_list( 

425 folders=self.synthesis_folders + self.sim_folders, 

426 files_include=files_include, 

427 files_avoid=files_to_avoid, 

428 include_vhdl_files=include_vhdl_files, 

429 include_verilog_files=include_verilog_files, 

430 include_systemverilog_files=include_systemverilog_files, 

431 ) 

432 

433 # pylint: disable=unused-argument 

434 def get_ip_core_files( 

435 self, 

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

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

438 **kwargs: Any, 

439 ) -> list[IpCoreFile]: 

440 """ 

441 Get IP cores for this module. 

442 

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

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

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

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

447 

448 Arguments: 

449 files_include: Optionally filter to only include these files. 

450 files_avoid: Optionally filter to discard these files. 

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

452 IP cores are included and what their variables are. 

453 

454 Return: 

455 The IP cores for this module. 

456 """ 

457 folders = [ 

458 self.path / "ip_cores", 

459 ] 

460 file_endings = "tcl" 

461 return [ 

462 IpCoreFile(ip_core_file) 

463 for ip_core_file in self._get_file_list( 

464 folders=folders, 

465 file_endings=file_endings, 

466 files_include=files_include, 

467 files_avoid=files_avoid, 

468 ) 

469 ] 

470 

471 # pylint: disable=unused-argument 

472 def get_scoped_constraints( 

473 self, 

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

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

476 **kwargs: Any, 

477 ) -> list[Constraint]: 

478 """ 

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

480 

481 Arguments: 

482 files_include: Optionally filter to only include these files. 

483 files_avoid: Optionally filter to discard these files. 

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

485 constraints are included. 

486 

487 Return: 

488 The constraints. 

489 """ 

490 folders = [ 

491 self.path / "scoped_constraints", 

492 self.path / "entity_constraints", 

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

494 ] 

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

496 constraint_files = self._get_file_list( 

497 folders=folders, 

498 file_endings=file_endings, 

499 files_include=files_include, 

500 files_avoid=files_avoid, 

501 ) 

502 

503 constraints = [] 

504 if constraint_files: 

505 synthesis_files = self.get_synthesis_files() 

506 for constraint_file in constraint_files: 

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

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

509 # this more probable. 

510 constraint = Constraint( 

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

512 ) 

513 constraint.validate_scoped_entity(synthesis_files) 

514 constraints.append(constraint) 

515 

516 return constraints 

517 

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

519 """ 

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

521 

522 .. Note:: 

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

524 any test benches that operate via generics. 

525 

526 Arguments: 

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

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

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

530 location of test files, etc. 

531 """ 

532 

533 def pre_build( 

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

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

536 """ 

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

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

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

540 based on project information, etc. 

541 

542 .. Note:: 

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

544 utilize this mechanism. 

545 

546 Arguments: 

547 project: The project that is being built. 

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

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

550 :meth:`.VivadoProject.__init__`. 

551 

552 Return: 

553 True if everything went well. 

554 """ 

555 return True 

556 

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

558 """ 

559 Get FPGA build projects defined by this module. 

560 

561 .. Note:: 

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

563 build projects. 

564 

565 Return: 

566 FPGA build projects. 

567 """ 

568 return [] 

569 

570 @staticmethod 

571 def test_case_name( 

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

573 ) -> str: 

574 """ 

575 Construct a string suitable for naming test cases. 

576 

577 Arguments: 

578 name: Optional base name. 

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

580 

581 Return: 

582 For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``. 

583 """ 

584 if name: 

585 test_case_name = name 

586 else: 

587 test_case_name = "" 

588 

589 if generics: 

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

591 

592 if test_case_name: 

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

594 else: 

595 test_case_name = generics_string 

596 

597 return test_case_name 

598 

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

600 self, 

601 test: Any, 

602 name: Optional[str] = None, 

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

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

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

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

607 ) -> None: 

608 """ 

609 Add config for VUnit test case. 

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

611 

612 Arguments: 

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

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

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

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

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

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

619 

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

621 be set. 

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

623 generic value will be set. 

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

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

626 

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

628 VHDL type ``natural``. 

629 pre_config: Function to be run before the test. 

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

631 post_check: Function to be run after the test. 

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

633 """ 

634 generics = {} if generics is None else generics 

635 

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

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

638 if isinstance(set_random_seed, bool): 

639 if set_random_seed: 

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

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

642 

643 elif isinstance(set_random_seed, int): 

644 generics["seed"] = set_random_seed 

645 

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

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

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

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

650 name = "test" if name == "" else name 

651 

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

653 

654 def __str__(self) -> str: 

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

656 

657 

658def get_modules( 

659 modules_folders: list[Path], 

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

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

662 library_name_has_lib_suffix: bool = False, 

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

664) -> ModuleList: 

665 """ 

666 Get a list of Module objects based on the source code folders. 

667 

668 Arguments: 

669 modules_folders: A list of paths where your modules are located. 

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

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

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

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

674 default_registers: Default registers. 

675 

676 Return: 

677 List of module objects (:class:`BaseModule` or subclasses thereof) 

678 created from the specified folders. 

679 """ 

680 modules = ModuleList() 

681 

682 for module_folder in _iterate_module_folders(modules_folders): 

683 module_name = module_folder.name 

684 

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

686 continue 

687 

688 if names_avoid is not None and module_name in names_avoid: 

689 continue 

690 

691 modules.append( 

692 _get_module_object( 

693 path=module_folder, 

694 name=module_name, 

695 library_name_has_lib_suffix=library_name_has_lib_suffix, 

696 default_registers=default_registers, 

697 ) 

698 ) 

699 

700 return modules 

701 

702 

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

704 for modules_folder in modules_folders: 

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

706 if module_folder.is_dir(): 

707 yield module_folder 

708 

709 

710def _get_module_object( 

711 path: Path, 

712 name: str, 

713 library_name_has_lib_suffix: bool, 

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

715) -> BaseModule: 

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

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

718 

719 if module_file.exists(): 

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

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

722 path=path, 

723 library_name=library_name, 

724 default_registers=default_registers, 

725 ) 

726 return module 

727 

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