Coverage for tsfpga/module.py: 96%
200 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-21 20:51 +0000
« 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# --------------------------------------------------------------------------------------------------
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 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.
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:
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.
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
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
653 elif isinstance(set_random_seed, int):
654 generics["seed"] = set_random_seed
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
662 test.add_config(name=name, generics=generics, pre_config=pre_config, post_check=post_check)
664 def __str__(self) -> str:
665 return f"{self.name}:{self.path}"
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.
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.
690 Return:
691 The modules created from the specified folders.
692 """
693 modules = ModuleList()
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
701 for module_folder in _iterate_module_folders(folders):
702 module_name = module_folder.name
704 if names_include is not None and module_name not in names_include:
705 continue
707 if names_avoid is not None and module_name in names_avoid:
708 continue
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 )
719 return modules
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`.
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.
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 )
753 if not modules:
754 raise RuntimeError(f'Could not find module "{name}".')
756 if len(modules) > 1:
757 raise RuntimeError(f'Found multiple modules named "{name}".')
759 return modules[0]
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
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
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
787 return BaseModule(path=path, library_name=library_name, default_registers=default_registers)