From 5d8649763cbdd62fa095bb090c68c4879dbed9f9 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sat, 3 Oct 2026 11:10:39 +0200 Subject: [PATCH] Resolve relative paths of import_module_from_file against the testbench directory Like exec_file, a relative path is now taken from tb_path rather than from the working directory of the simulator, so verification components can import their models without runner_cfg. Fixes #7. Co-Authored-By: Claude Opus 5.5 --- docs/user_guide.rst | 8 ++++++-- release_notes/0.2.0.md | 3 +++ src/vunit_python_bridge/hdl/src/python_pkg.vhd | 12 +++++++++--- .../hdl/tools/python_pkg.vhd.in | 12 +++++++++--- tests/tb_python_pkg_bridge.vhd | 16 ++++++++++++++++ 5 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 release_notes/0.2.0.md diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 47d731d..792a2a1 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -459,11 +459,15 @@ import_module_from_file and to_py_list_str ``import_module_from_file`` imports any Python file as a module by path (also taking an optional :ref:`session ` parameter), and is -what ``import_run_script`` uses internally: +what ``import_run_script`` uses internally. Like the file name of +``exec_file``, a relative path is relative to the directory of the testbench +file, ``tb_path``, so ``test_runner_setup`` must have been called; this works +from verification components without ``runner_cfg``. An absolute path is used +as it is given: .. code-block:: vhdl - import_module_from_file(join(tb_path(runner_cfg), "reference_model.py"), "reference_model"); + import_module_from_file("reference_model.py", "reference_model"); exec("reference_model.configure(gain=4)"); ``to_py_list_str`` converts an ``integer_vector``, ``integer_vector_ptr_t`` or diff --git a/release_notes/0.2.0.md b/release_notes/0.2.0.md new file mode 100644 index 0000000..7de24f5 --- /dev/null +++ b/release_notes/0.2.0.md @@ -0,0 +1,3 @@ +## Changes + +- `import_module_from_file` resolves a relative path against the directory of the testbench file, like `exec_file`, rather than against the working directory of the simulator. It must be called after `test_runner_setup`. Callers that relied on a path relative to the working directory must pass an absolute path instead. See #7. diff --git a/src/vunit_python_bridge/hdl/src/python_pkg.vhd b/src/vunit_python_bridge/hdl/src/python_pkg.vhd index c238622..e703595 100644 --- a/src/vunit_python_bridge/hdl/src/python_pkg.vhd +++ b/src/vunit_python_bridge/hdl/src/python_pkg.vhd @@ -28,6 +28,9 @@ use vunit_lib.logger_pkg.all; ------------------------------------------------------------------------------ package python_pkg is + -- Import a Python file as a module. A relative path is relative to the + -- directory of the testbench file, tb_path, so test_runner_setup must have + -- been called. An absolute path is used as it is given. procedure import_module_from_file( module_path, as_module_name : string; session : python_session_t := default_session ); @@ -274,6 +277,8 @@ package python_pkg is end package; package body python_pkg is + impure function p_file_path(file_name : string) return string; + -- @formatter:off procedure import_module_from_file( module_path, as_module_name : string; session : python_session_t := default_session @@ -283,7 +288,7 @@ package body python_pkg is "from importlib.util import spec_from_file_location, module_from_spec" & LF & "from pathlib import Path" & LF & "import sys" & LF & - spec_name & " = spec_from_file_location('" & as_module_name & "', str(Path('" & module_path & "')))" & LF & + spec_name & " = spec_from_file_location('" & as_module_name & "', str(Path('" & p_file_path(module_path) & "')))" & LF & as_module_name & " = module_from_spec(" & spec_name & ")" & LF & "sys.modules['" & as_module_name & "'] = " & as_module_name & LF & spec_name & ".loader.exec_module(" & as_module_name & ")"; @@ -893,8 +898,9 @@ package body python_pkg is return false; end; - -- The file name of exec_file, a relative one taken from the directory of the - -- testbench file like the file names of the other VUnit subprograms + -- The file name of exec_file and import_module_from_file, a relative one taken + -- from the directory of the testbench file like the file names of the other + -- VUnit subprograms impure function p_file_path(file_name : string) return string is begin if p_is_absolute(file_name) then diff --git a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in index f5261e0..7c2e311 100644 --- a/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in +++ b/src/vunit_python_bridge/hdl/tools/python_pkg.vhd.in @@ -28,6 +28,9 @@ use vunit_lib.logger_pkg.all; ------------------------------------------------------------------------------ package python_pkg is + -- Import a Python file as a module. A relative path is relative to the + -- directory of the testbench file, tb_path, so test_runner_setup must have + -- been called. An absolute path is used as it is given. procedure import_module_from_file( module_path, as_module_name : string; session : python_session_t := default_session ); @@ -176,6 +179,8 @@ $call_declarations end package; package body python_pkg is + impure function p_file_path(file_name : string) return string; + -- @formatter:off procedure import_module_from_file( module_path, as_module_name : string; session : python_session_t := default_session @@ -185,7 +190,7 @@ package body python_pkg is "from importlib.util import spec_from_file_location, module_from_spec" & LF & "from pathlib import Path" & LF & "import sys" & LF & - spec_name & " = spec_from_file_location('" & as_module_name & "', str(Path('" & module_path & "')))" & LF & + spec_name & " = spec_from_file_location('" & as_module_name & "', str(Path('" & p_file_path(module_path) & "')))" & LF & as_module_name & " = module_from_spec(" & spec_name & ")" & LF & "sys.modules['" & as_module_name & "'] = " & as_module_name & LF & spec_name & ".loader.exec_module(" & as_module_name & ")"; @@ -737,8 +742,9 @@ $arg_functions return false; end; - -- The file name of exec_file, a relative one taken from the directory of the - -- testbench file like the file names of the other VUnit subprograms + -- The file name of exec_file and import_module_from_file, a relative one taken + -- from the directory of the testbench file like the file names of the other + -- VUnit subprograms impure function p_file_path(file_name : string) return string is begin if p_is_absolute(file_name) then diff --git a/tests/tb_python_pkg_bridge.vhd b/tests/tb_python_pkg_bridge.vhd index 203e238..f99f28e 100644 --- a/tests/tb_python_pkg_bridge.vhd +++ b/tests/tb_python_pkg_bridge.vhd @@ -143,6 +143,12 @@ begin check_equal(integer'(call("bump", data)), 5); end; + -- Imports the way a verification component would, without runner_cfg + procedure import_filters is + begin + import_module_from_file("models/filters.py", "filters_model"); + end; + begin test_runner_setup(runner, runner_cfg); @@ -219,6 +225,16 @@ begin exec_file("models/counter.py"); check_equal(integer'(call("get_call_count")), 2); + elsif run("Test importing a module with a relative file name") then + -- The simulator runs in the VUnit output path, not the testbench directory + check_false(eval_boolean("__import__('os').path.samefile('.', '" & tb_path(runner_cfg) & "')")); + import_filters; + check_equal(integer'(call("filters_model.fir", arg(2))), 3); + + elsif run("Test importing a module with an absolute file name") then + import_module_from_file(join(tb_path(runner_cfg), "models/filters.py"), "filters_model"); + check_equal(integer'(call("filters_model.fir", arg(2))), 3); + elsif run("Test that exec_file with a missing file fails") then -- The error message shows the path in the native format of the OS exec("import os" + "def native_repr(path):" + " return repr(os.path.normpath(path))");