Coverage for tsfpga/module.py: 95%
209 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 20:51 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 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# --------------------------------------------------------------------------------------------------
9from __future__ import annotations
11import random
12from typing import TYPE_CHECKING, Any, Callable
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
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
34if TYPE_CHECKING:
35 from collections.abc import Iterable
36 from pathlib import Path
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
44 from .vivado.project import VivadoProject
47class BaseModule:
48 """
49 Base class for handling a HDL module with RTL code, constraints, etc.
51 Files are gathered from a lot of different sub-folders, to accommodate for projects having
52 different catalog structure.
53 """
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
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
78 self._default_registers = default_registers
79 self._registers: RegisterList | None = None
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.
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
103 if not file.name.lower().endswith(file_endings):
104 continue
106 if files_include is not None and file not in files_include:
107 continue
109 if files_avoid is not None and file in files_avoid:
110 continue
112 files.append(file)
114 return files
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]
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 ]
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"
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
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 )
172 self.registers_hook()
173 return self._registers
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``.
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.
185 .. Note::
186 This default method does nothing.
187 Shall be overridden by modules that utilize this mechanism.
188 """
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()
204 if self.create_register_package:
205 VhdlRegisterPackageGenerator(
206 register_list=self.registers, output_folder=self.register_synthesis_folder
207 ).create_if_needed()
209 if self.create_record_package:
210 VhdlRecordPackageGenerator(
211 register_list=self.registers, output_folder=self.register_synthesis_folder
212 ).create_if_needed()
214 if self.create_axi_lite_wrapper:
215 VhdlAxiLiteWrapperGenerator(
216 register_list=self.registers, output_folder=self.register_synthesis_folder
217 ).create_if_needed()
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.
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()
233 if self.create_simulation_check_package:
234 VhdlSimulationCheckPackageGenerator(
235 register_list=self.registers, output_folder=self.register_simulation_folder
236 ).create_if_needed()
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()
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 ]
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"
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 ]
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"
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 ]
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.
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.
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.
323 Return:
324 Files that should be included in a synthesis project.
325 """
326 self.create_register_synthesis_files()
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 )
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``.
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.
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()
371 if include_tests:
372 sim_and_test_folders += self.test_folders
374 self.create_register_simulation_files()
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 )
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 )
394 return synthesis_files + test_files
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.
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.
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.
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
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 )
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.
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.
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.
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 ]
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.
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.
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 )
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)
527 return constraints
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.
537 .. Note::
538 This default method does nothing. Should be overridden by modules that have
539 any test benches that operate via generics.
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 """
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.
559 .. Note::
560 This default method does nothing. Should be overridden by modules that
561 utilize this mechanism.
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__`.
569 Return:
570 True if everything went well.
571 """
572 return True
574 def get_build_projects(self) -> list[VivadoProject]:
575 """
576 Get FPGA build projects defined by this module.
578 .. Note::
579 This default method does nothing. Should be overridden by modules that set up
580 build projects.
582 Return:
583 FPGA build projects.
584 """
585 return []
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.
592 Arguments:
593 name: Optional base name.
594 generics: Dictionary of values that will be included in the name.
596 Return:
597 For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``.
598 """
599 test_case_name = name if name else ""
601 if generics:
602 generics_string = ".".join([f"{key}_{value}" for key, value in generics.items()])
604 test_case_name = f"{name}.{generics_string}" if test_case_name else generics_string
606 return test_case_name
608 def netlist_build_name(self, name: str, generics: dict[str, Any] | None = None) -> str:
609 """
610 Construct a string suitable for naming a netlist build project.
612 Arguments:
613 name: Base name.
614 generics: Dictionary of values that will be included in the name.
616 Return:
617 For example ``LibraryName.MyBaseName.GenericA_ValueA.GenericB_ValueB``.
618 """
619 return self.test_case_name(name=f"{self.library_name}.{name}", generics=generics)
621 def add_vunit_config( # noqa: PLR0913
622 self,
623 test: Test | TestBench,
624 name: str | None = None,
625 generics: dict[str, Any] | None = None,
626 set_random_seed: bool | int | None = False,
627 count: int = 1,
628 pre_config: Callable[..., bool] | None = None,
629 post_check: Callable[..., bool] | None = None,
630 ) -> None:
631 """
632 Convenient wrapper to add VUnit test configuration(s).
634 Arguments:
635 test: VUnit test or testbench object.
636 name: Optional designated name for this config.
637 Will be used to form the name of the config together with the ``generics`` value.
638 generics: Generic values that will be applied to the testbench entity.
639 The values will also be used to form the name of the config.
640 set_random_seed: Controls setting of the ``seed`` generic:
642 * When this argument is not assigned, or assigned ``False``, the generic will not
643 be set.
644 * When set to boolean ``True``, a random natural (non-negative integer)
645 generic value will be set.
646 * When set to an integer value, that specific value will be set for the generic.
647 This is useful to get a static test case name for waveform inspection.
649 If the generic is to be set it must exist in the testbench entity, and should have
650 VHDL type ``natural``.
652 .. deprecated:: 13.2.2
653 Use VUnit mechanism for random seed instead rather than this clunky variant.
654 count: Number of times to setup the same test configuration.
655 Is useful for testbenches that randomize their behavior based on the VUnit seed.
656 pre_config: Function to be run before the test.
657 See `VUnit documentation <https://vunit.github.io/py/ui.html>`_ for details.
658 post_check: Function to be run after the test.
659 See `VUnit documentation <https://vunit.github.io/py/ui.html>`_ for details.
660 """
661 generics = {} if generics is None else generics
663 if set_random_seed != False: # noqa: E712
664 print(
665 f"DEPRECATED: {self.__class__.__name__}.add_vunit_config() argument "
666 "'set_random_seed' is deprecated and will be removed."
667 )
669 # Note that "bool" is a sub-class of "int" in python, so isinstance(set_random_seed, int)
670 # returns True if it is an integer or a bool.
671 if isinstance(set_random_seed, bool):
672 if set_random_seed:
673 # Use the maximum range for a natural in VHDL-2008
674 # Does not need to be cryptographically secure.
675 generics["seed"] = random.randint(0, 2**31 - 1) # noqa: S311
677 elif isinstance(set_random_seed, int):
678 generics["seed"] = set_random_seed
680 base_name = self.test_case_name(name=name, generics=generics)
682 for count_index in range(count):
683 if base_name == "":
684 # VUnit does not allow an empty name, which can happen if both 'name' and 'generics'
685 # to this method are 'None', but the user sets for example a 'pre_config'.
686 # Avoid this error mode by setting the count index, even if count is 1.
687 name = str(count_index)
688 elif count > 1:
689 name = f"{base_name}.{count_index}"
690 else:
691 name = base_name
693 test.add_config(
694 name=name, generics=generics, pre_config=pre_config, post_check=post_check
695 )
697 def __str__(self) -> str:
698 return f"{self.name}:{self.path}"
701def get_modules(
702 modules_folder: Path | None = None,
703 modules_folders: list[Path] | None = None,
704 names_include: set[str] | None = None,
705 names_avoid: set[str] | None = None,
706 library_name_has_lib_suffix: bool = False,
707 default_registers: list[Register] | None = None,
708) -> ModuleList:
709 """
710 Get a list of module objects (:class:`BaseModule` or subclasses thereof) based on the source
711 code folders.
713 Arguments:
714 modules_folder: The path to the folder containing modules.
715 modules_folders: Optionally, you can specify many folders with modules that will all
716 be searched.
717 names_include: If specified, only modules with these names will be included.
718 names_avoid: If specified, modules with these names will be discarded.
719 library_name_has_lib_suffix: If set, the library name will be ``<module name>_lib``,
720 otherwise it is just ``<module name>``.
721 default_registers: Default registers.
723 Return:
724 The modules created from the specified folders.
725 """
726 modules = ModuleList()
728 folders = []
729 if modules_folder is not None:
730 folders.append(modules_folder)
731 if modules_folders is not None:
732 folders += modules_folders
734 for module_folder in _iterate_module_folders(folders):
735 module_name = module_folder.name
737 if names_include is not None and module_name not in names_include:
738 continue
740 if names_avoid is not None and module_name in names_avoid:
741 continue
743 modules.append(
744 _get_module_object(
745 path=module_folder,
746 name=module_name,
747 library_name_has_lib_suffix=library_name_has_lib_suffix,
748 default_registers=default_registers,
749 )
750 )
752 return modules
755def get_module(
756 name: str,
757 modules_folder: Path | None = None,
758 modules_folders: list[Path] | None = None,
759 library_name_has_lib_suffix: bool = False,
760 default_registers: list[Register] | None = None,
761) -> BaseModule:
762 """
763 Get a single module object, for a module found in one of the specified source code folders.
764 Is a wrapper around :func:`.get_modules`.
766 Arguments:
767 name: The name of the module.
768 modules_folder: The path to the folder containing modules.
769 modules_folders: Optionally, you can specify many folders with modules that will all
770 be searched.
771 library_name_has_lib_suffix: If set, the library name will be ``<module name>_lib``,
772 otherwise it is just ``<module name>``.
773 default_registers: Default registers.
775 Return:
776 The requested module.
777 """
778 modules = get_modules(
779 modules_folder=modules_folder,
780 modules_folders=modules_folders,
781 names_include={name},
782 library_name_has_lib_suffix=library_name_has_lib_suffix,
783 default_registers=default_registers,
784 )
786 if not modules:
787 raise RuntimeError(f'Could not find module "{name}".')
789 if len(modules) > 1:
790 raise RuntimeError(f'Found multiple modules named "{name}".')
792 return modules[0]
795def _iterate_module_folders(modules_folders: list[Path]) -> Iterable[Path]:
796 for modules_folder in modules_folders:
797 for module_folder in modules_folder.glob("*"):
798 if module_folder.is_dir():
799 yield module_folder
802def _get_module_object(
803 path: Path,
804 name: str,
805 library_name_has_lib_suffix: bool,
806 default_registers: list[Register] | None,
807) -> BaseModule:
808 module_file = path / f"module_{name}.py"
809 library_name = f"{name}_lib" if library_name_has_lib_suffix else name
811 if module_file.exists():
812 # We assume that the user lets their 'Module' class inherit from 'BaseModule'.
813 module: BaseModule = load_python_module(module_file).Module(
814 path=path,
815 library_name=library_name,
816 default_registers=default_registers,
817 )
818 return module
820 return BaseModule(path=path, library_name=library_name, default_registers=default_registers)