Repository navigation
Conversation
| constant python_logger : logger_t := get_logger(p_python_id); | ||
|
|
||
| -- Whether sessions other than the default one, and so Python objects, are supported | ||
| constant p_supports_sessions : boolean := true; |
There was a problem hiding this comment.
To claude: Why is this required?
There was a problem hiding this comment.
It wasn't required, so it's removed in 7306ba5. I'd added it only so that the Riviera-PRO/Active-HDL failure would say "Python objects require …". Their FFI already fails on the first use of a non-default session ("Python sessions are only supported with NVC, GHDL and Questa"), and every object uses a session of its own, so that message already covers it.
| .. code-block:: vhdl | ||
|
|
||
| constant model : python_object_t := new_python_object( | ||
| "my_models.uart:UartModel", kwarg("baud", 115200), id => get_id("uart") |
There was a problem hiding this comment.
Why is the colon required, cant we just have regular python namespace conventions?
There was a problem hiding this comment.
Also mentions that this calls the constructor
There was a problem hiding this comment.
Agreed, the colon is gone in 7306ba5. A class is now a plain dotted name, "package.module.Class". It's resolved by importing the longest prefix that is a module and taking the rest as attributes, so a nested class ("package.module.Outer.Inner") works without a separator. An import error inside the user's module is still reported as such, not mistaken for a missing module; pytest covers both cases.
There was a problem hiding this comment.
Done. The paragraph now says that new_python_object names the class and the arguments of its constructor, and that the constructor is called on first use (or right away by create).
| The model of a verification component is best a Python package next to it, | ||
| named after it since module names are global to the simulation: | ||
|
|
||
| .. code-block:: text | ||
|
|
||
| my_vc/ | ||
| my_vc.vhd | ||
| python/ | ||
| my_vc_model/ | ||
| __init__.py | ||
|
|
There was a problem hiding this comment.
Is this required or implicit behaviour?
There was a problem hiding this comment.
Partly implicit. The run script's directory is the first entry of sys.path, and the packages of VUnit's Python environment (including pip install -e) are importable without anything extra. PYTHONPATH is only needed for code somewhere else, such as a VC's own Python folder. The new "Where Python code is found" chapter now spells this out.
| The run script prepends the Python folder to ``PYTHONPATH`` before VUnit | ||
| starts. The simulator runs Python in the environment of VUnit, so the setting | ||
| reaches the interpreter in the simulator and the class is | ||
| ``"my_vc_model.Model"`` wherever the component is used: |
There was a problem hiding this comment.
We should document the namespacing conventions (directories added to paths etc) in it's own chapter.
There was a problem hiding this comment.
Done: a new "Where Python code is found" chapter, before Sessions. It covers what the interpreter finds without setup (the run script directory, VUnit's Python environment), adding folders with PYTHONPATH from the run script, how exec_file/import_module_from_file resolve file names (and that exec_file temporarily adds the file's directory), and that sys.path/sys.modules are global to the simulation. The VC package layout moved there from the objects section, which now links to it.
| ``get_python_object(object, class_name, args, id)`` returns ``object`` when it | ||
| is given, and a new object otherwise, like ``get_logger`` and ``get_id``. A | ||
| verification component made the way VUnit's own are creates its object in the | ||
| constructor of its handle, with the identity of the component. Every instance | ||
| then gets a backend of its own by default and the object of the testbench when | ||
| given one: |
There was a problem hiding this comment.
Rewrite this for better readability.
There was a problem hiding this comment.
Rewritten: it now leads with where a VC makes its object (the handle constructor, like VUnit's own components) and then what get_python_object does, before the example.
| The handle is a constant made during elaboration, and so is the object, which | ||
| is not created in Python until its first call. ``create_std_cfg`` gives | ||
| instances without an ``id`` the identities ``my_vcs:uart_master:1``, | ||
| ``my_vcs:uart_master:2`` and so on, which become the identities of their | ||
| objects. ``filter_vc_pkg.vhd`` and ``filter_vc.vhd`` of the `embedded_python | ||
| example <https://github.com/VUnit/vunit-python-bridge/tree/main/examples/embedded_python>`__ | ||
| are a complete component of this kind, with a Python model in | ||
| ``filter_model.py``. Its ``Test Python objects as backends of verification | ||
| components`` test case drives three instances: two with objects of their own | ||
| and one given an object of the testbench, which the testbench queries too. |
There was a problem hiding this comment.
This could also do with some readability improvements.
There was a problem hiding this comment.
Split into two shorter paragraphs. The first covers elaboration versus first call and the create_std_cfg numbering; the second points to filter_vc in the example and lists which file holds what.
| A testbench giving several components, or a component and itself, the same | ||
| object shares its state between them, for example a scoreboard filled by a | ||
| monitor and checked by the test: |
There was a problem hiding this comment.
Reworded: "Several components can share an object, and so can a component and the testbench: the testbench makes the object and gives it to each of them." The scoreboard example follows. The closing sentence about a class-name generic is clearer too.
| """ | ||
| raise RuntimeError(message) | ||
|
|
||
| def create(self, class_name, *args, **kwargs): |
There was a problem hiding this comment.
This method should be named more of what it does.
There was a problem hiding this comment.
Renamed to __vunit__.instantiate_as_self, since it instantiates the named class and binds the instance to self in the current session.
| .. code-block:: text | ||
|
|
||
| my_vc/ | ||
| my_vc.vhd | ||
| python/ | ||
| my_vc_model/ | ||
| __init__.py | ||
|
|
||
| and the run script puts that folder on the path, after which the model is | ||
| ``my_vc_model`` wherever the component is used: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
| from vunit import VUnit | ||
|
|
||
| my_vc_python = Path(__file__).parent / "my_vc" / "python" | ||
| os.environ["PYTHONPATH"] = os.pathsep.join( | ||
| filter(None, [str(my_vc_python), os.environ.get("PYTHONPATH")]) | ||
| ) | ||
|
|
||
| vu = VUnit.from_argv() | ||
| ... |
There was a problem hiding this comment.
This is not very clear if the model is found automatically or not, since I think these snippets are in the wrong order.
There was a problem hiding this comment.
Agreed, the order hid it. Reworked in f10700d. The chapter now has a subsection per topic: "Found without setup", "Adding a folder", "Files given by name" and "Module names". "Adding a folder" opens with "Python code anywhere else is not found until its folder is on PYTHONPATH". It then shows the VC layout, says outright that my_vc/python can't be imported as it is, shows the run script snippet, and only then says that my_vc_model is importable everywhere. pip install -e is mentioned there as the alternative.
ebe868a to
a3f43d6
Compare
38fa0cc to
3e4393e
Compare
| Several components can share an object, and so can a component and the | ||
| testbench: the testbench makes the object and gives it to each of them. A | ||
| scoreboard filled by a monitor and checked by the test is a typical case: | ||
|
|
||
| .. code-block:: vhdl | ||
|
|
||
| constant scoreboard : python_object_t := new_python_object("my_models.Scoreboard"); | ||
|
|
||
| ... | ||
|
|
||
| monitor_inst : entity work.my_monitor | ||
| generic map ( | ||
| monitor => new_my_monitor(model => scoreboard) | ||
| ) | ||
| port map ( | ||
| ... | ||
| ); | ||
|
|
||
| ... | ||
|
|
||
| check_equal(integer'(call(scoreboard, "num_mismatches")), 0); |
There was a problem hiding this comment.
Use a different example than scoreboard
| * Objects need sessions, and so the Python bridge: NVC, GHDL or Questa. On | ||
| Riviera-PRO/Active-HDL, ``new_python_object`` reports that as a failure on | ||
| the logger of the object and returns ``null_python_object``. |
There was a problem hiding this comment.
Remove, this is part of the bigger picture of whether riviera pro is supported.
| -- Private, the primitives the bridge operations of python_pkg are built on | ||
| ----------------------------------------------------------------------------- | ||
| -- Whether this simulator has sessions, and with them Python objects | ||
| constant p_sessions_supported : boolean := true; |
There was a problem hiding this comment.
Why do we need this?
There was a problem hiding this comment.
Claude (AI assistant) here, on behalf of @ru551n:
It was only there to fail at new_python_object on Riviera-PRO/Active-HDL. That is not needed: the first use of an object already fails in the VHPI primitives (<operation> requires NVC, GHDL or Questa). Fixed in a158cf7: removed p_sessions_supported and its check.
| python_pkg with the object first, the method or expression second. | ||
| """ | ||
| lines = [ | ||
| " -- Call a method of the object, like call", |
There was a problem hiding this comment.
| " -- Call a method of the object, like call", | |
| " -- Call a method of the object", |
|
|
||
| count := call(model, "push", arg(byte)); -- calls self.push(byte) | ||
| call(model, "reset"); -- no return value | ||
| check_true(eval_boolean(model, "self.is_idle()")); |
There was a problem hiding this comment.
I think we should consider other names than self to avoid potential name collisions. What if user code creates another class in the model's name space? self would have another meaning for methods within that class. Maybe we can borrow from other languages? this is common but it collides with the Python package this. Very unlikely to be used though. me is another option. Or we can simply use obj (but not object since that is already defined by Python). I'm leaning towards obj
There was a problem hiding this comment.
There was a problem hiding this comment.
Agree, renamed to obj
| ``get_logger(object)``, which reports the errors of its operations. It is | ||
| optional and mainly makes the log readable. Without an | ||
| ``id``, objects are enumerated the way VUnit enumerates verification | ||
| components: ``python_bridge:python:object:1``, |
There was a problem hiding this comment.
What about using the class name? The hierarchy of the class is reflected in the parent/child hierarchy of the id concept.
There was a problem hiding this comment.
Fixed, it now resolves as python_bridge:python:object:package:module:Class:<n>
There was a problem hiding this comment.
Do we need all of the python_bridge:python:object: prefix. Generally speaking we want the shortest naming scheme that gives uniqueness.
There was a problem hiding this comment.
Good point, renamed as python_object:my_models.uart.UartModel:1. Should we use the dots or colon in the python dotted name? I think dots since it is exactly what python sees, colons would be confusing.
|
|
||
| A verification component usually makes its object in the constructor of its | ||
| handle, the way VUnit's own components make everything they need there. | ||
| ``get_python_object(object, class_name, args, id)`` returns ``object`` when one |
There was a problem hiding this comment.
get_logger and get_id takes a string as input and returns the logger/id. Creating something similar would be to call get_python_object with the class name. However, that doesn't work since we can have several instances of that class. Instead, get_python_object should take the id as input or the id name. If that name doesn't exist, a new object can't be created as that requires arguments to the constructor of that class. In that case it should return null_python_object and the user has to create the object "manually"
There was a problem hiding this comment.
Partially fixed in 96758a3. Unknown objects now also return as null_python_object without failure.
| impure function new_uart_master( | ||
| baud : positive; model : python_object_t := null_python_object; id : id_t := null_id | ||
| ) return uart_master_t is | ||
| constant std_cfg : std_cfg_t := create_std_cfg(id, "my_vcs", "uart_master"); |
There was a problem hiding this comment.
Maybe we should elaborate on design patterns for ID assignment. If id is provided, we should use it. The question is what we use when it isn't provided. Are there cases where we should use the model ID? It seems like the only use case for providing a model is when it is shared among several VCs and the VCs should have different names. If the VC isn't shared, we might as well let the function create it. I guess that means we should never take the ID from the incoming model and the example can remain as is.
There was a problem hiding this comment.
Fixed together with the other comment on the same topic, now split into two functions. Also added doc section on id assignment.
| end record; | ||
|
|
||
| -- A Counter of its own, with the identity of the VC, unless given a model | ||
| impure function new_counter_vc( |
There was a problem hiding this comment.
After seeing a few of these, I think the design pattern should be that we have different new functions rather than trying to solve two use cases with the same function:
impure function new_counter_vc(model : python_object_t; id : id_t := null_id) return counter_vc_t;impure function new_counter_vc(step : natural := 1; id : id_t := null_id) return counter_vc_t;
| context python_bridge.python_context; | ||
|
|
||
| entity path_counter_vc is | ||
| generic( |
There was a problem hiding this comment.
I would say that this is a bit of an anti-pattern. Some of the updates we've done recently is to change VCs that have more than one generic into having just the handle. Multiple generics will still work of course but I would avoid promoting it.
There was a problem hiding this comment.
Fixed, merged into one handle.
|
|
||
| elsif run("Test the result types of the calls of an object") then | ||
| object := new_python_object("models.counter_model.Counter", kwarg("start", 5)); | ||
| check_equal(integer'(eval(object, "self.count")), 5); |
There was a problem hiding this comment.
Use the alias eval_integer
| check_equal(integer'(eval(object, "self.count")), 5); | ||
| check_equal(real'(call(object, "ratio")), 1.25); | ||
| call(object, "add", arg(1)); | ||
| call_unsigned(object, "value", byte); |
There was a problem hiding this comment.
Have you considered call functions for the vector types? Similar to vec := to_integer(17, vec) and vec := to_integer(17, vec'length). With VHDL-2019, vec := call_unsigned(object, "value") is another option.
There was a problem hiding this comment.
Good idea. Added both 'length and res size type overloads.
I left out the VHDL-2019 type for now.
| @@ -0,0 +1 @@ | |||
| `python_object_t`: an instance of a Python class owned by VHDL, made with `new_python_object` and created on first use in a session of its own and called with `call(object, "method", ...)` and `eval(object, "expr")`. The identity of an object can be given as an `id_t` or by its full name, and `get_python_object(id)` or `get_python_object(name)` returns the object made with it. See "Python objects" in the user guide. | |||
There was a problem hiding this comment.
Shorten this, we don't need to supply the technical details in a news fragment. Point the user to the documentation.
| @@ -0,0 +1 @@ | |||
| `call_integer` is an alias of `call_integer_w_arg`, the `call` returning `integer`, named like `eval_integer` and `call_real`. | |||
There was a problem hiding this comment.
Remove, nothing a user needs to know.
| ``get_logger(object)``, which reports the errors of its operations. It is | ||
| optional and mainly makes the log readable. Without an | ||
| ``id``, objects are enumerated the way VUnit enumerates verification | ||
| components: ``python_bridge:python:object:1``, |
There was a problem hiding this comment.
Good point, renamed as python_object:my_models.uart.UartModel:1. Should we use the dots or colon in the python dotted name? I think dots since it is exactly what python sees, colons would be confusing.
python_object_t is an instance of a Python class living in a session of its own, created on first use and called with call(object, "method", args). Every VC instance can get an object of its own without namespace or path handling, and a bench can create one and pass it to a VC through a generic. Hand-written prototype for review of the API; the full set of result types would be generated once it is agreed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
python_object_pkg.vhd is generated from python_object_pkg.vhd.in by generate_python_pkg.py, from the same result tables as python_pkg, so an object has every result type of call and eval, the procedure results and the same aliases. A test checks that both generated packages are up to date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like get_logger and get_id, it gives the object it is given or a new one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An object whose class cannot be imported or constructed reports that error once and is then bound to a stand-in raising the reason on every use, rather than leaving self undefined. The user guide gets a Python objects section, and the README and the release notes mention them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
new_object is now new_python_object and get is get_python_object, as VUnit names the constructors and getters of its types. An object without an id is python_bridge:python:object:<n>, enumerated like VUnit's verification components. The user guide makes the handle constructor of a VUnit style verification component the place to create its object, which tests/counter_vc_pkg.vhd now shows; the handle-less component taking its instance path is tests/path_counter_vc.vhd. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exec(object, code) executes code with the object as self, and get_logger(object) is the logger reporting its errors. On Riviera-PRO/Active-HDL, which have no sessions, the first use of an object says that objects require NVC, GHDL or Questa. The user guide notes that an integer_array_t cannot be a constructor argument of an object made during elaboration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
filter_vc is a verification component whose handle, made by new_filter_vc, holds the Python object that is its behaviour. The new test case drives three instances: two with objects of their own and one given an object of the testbench, which the testbench queries too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the verification component pattern of VUnit#14: the model of a verification component as a package next to it, put on PYTHONPATH by the run script or installed with pip install -e. The class example of the independent models section gets its file from the model_file generic of the component instead of runner_cfg, which a component does not have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Name the failures of objects after what failed: new_python_object("<class>")
for a failed creation and call("<method>") for the procedure form of a
method call, rather than an exec of their session. p_create and p_self are
private to the package body, and the PROTOTYPE marker is gone. The user
guide shows a component calling its object in a process and how errors are
named.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One association per line in the generic and port maps of the tests, the example and the user guide, the layout speja gives them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A class is named by a plain dotted name, resolved from the longest prefix that is a module, so nested classes work without the module:Class form. - __vunit__.create is __vunit__.instantiate_as_self, after what it does. - p_supports_sessions is gone: the FFI of Riviera-PRO/Active-HDL already reports the first use of a session other than the default one. - filter_vc of the example takes a clock and a valid input. - The user guide gets a "Where Python code is found" chapter on the run script directory, the Python environment, PYTHONPATH, file names and global module names, and clearer paragraphs on backends of verification components and shared objects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Where Python code is found chapter gets a subsection per topic, and the verification component example states that its folder is not importable until the run script adds it to PYTHONPATH, before showing how. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pydoc.locate resolves dotted class names instead of a hand-written loop. - The identity check is in VHDL only, which also catches a duplicate at elaboration; the Python check of the session was a second copy of it. - Runtime.namespace is the property itself rather than a wrapper of it. - create is the procedure the generated code calls, without p_create behind it, p_self is inlined and the operation of a failed creation is stored rather than rebuilt from the class name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ery use A class name now starts in the default session, so a class reached through a name defined there, like "np.ndarray", is found, and is otherwise imported with importlib rather than pydoc, which is no longer imported at every simulation start. A name that is not found, or not a class, is reported in one line rather than with the traceback of the bridge. The stand-in of an object that could not be created also fails on assignments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- new_python_object reports, on the logger of the identity, an identity
already used by an object or by a session made with new_session, and
a simulator without sessions (Riviera-PRO/Active-HDL), and then
returns null_python_object rather than carrying on. Using
null_python_object is reported on python_logger.
- The VHPI p_exec stub reports and executes nothing, rather than
running the code in the default session.
- Every form of call on an object reports its errors as
call("<method>"), like the procedure did: the call overloads evaluate
with p_eval and the result getters, which the FFI packages now all
have.
- create is named create_now, which is what it does.
- Tests of the error paths: a used identity, an object that cannot be
created, the naming of call errors, separate sessions and numbering,
exec and get_logger before creation, null_python_object, the eval
procedures and the identities of create_std_cfg.
- The generated lines are at most 120 characters, and the docstring of
the generator describes both packages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The section on how modules are found is now the "Python modules" part of "File paths", whose table already covered file names, with one recipe for PYTHONPATH. The objects chapter describes the new identity rule, the failure on Riviera-PRO/Active-HDL, the class lookup and the error names of call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Converting a stream-width model kept in module globals to an object per source, in a project that used exec_file models before, showed: - A misspelled class in a module that does import said only "No class named", which reads like a missing module. It now names the module and the missing attribute. - The user guide gets a recipe for moving from a model with module state to an object, says that the id is optional, and recommends create_now to report wrong constructor arguments at the start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
new_python_object makes an object that Python constructs on first use;
create_python_object makes one that is constructed by the call, so a
wrong class or constructor argument is reported there. They replace
new_python_object with create_now, and the step that constructs an
object on first use is private.
create_python_object only works in a process: NVC cannot find the
foreign functions of the bridge during elaboration ("foreign function
vpy_buffer_clear not found") and GHDL cannot call them then. The user
guide has a table of when to use each, and errors name the one called.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Python objects chapter moves from Running Python code to its own page, after Sessions, and python_object_pkg gets a page in the VHDL API reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove p_sessions_supported: on Riviera-PRO/Active-HDL, the first use of an object already fails in the VHPI primitives, and whether Riviera-PRO is supported is a question of its own. The shared object example of the docs is a memory model instead of a scoreboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_python_object(id) and get_python_object(name) return the object made with that identity, like get_logger, but report an identity without an object as a failure instead of making one. new_python_object and create_python_object take the identity by its full name as well. The get_python_object giving a component a default backend is replaced by an if in the constructor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
self is also the first parameter of the methods of classes defined in the session, which made code mixing the two hard to read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Objects without an id are enumerated per class, the dotted class name giving the identity hierarchy under python_bridge:python:object, e.g. python_bridge:python:object:models:counter_model:Counter:1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
get_python_object no longer reports a failure for an identity without an object, so a testbench can check whether the object is made and make it otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A component making its own object and a component given one have constructors of their own instead of an optional model parameter. The user guide describes the identities of components and their objects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handle holds the step and the id. Without an id the component still takes its instance path as the identity, in its process, and the testbench reaches the object by that path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests and the user guide use eval_integer, call_real and the like rather than qualified expressions. The call returning integer, call_integer_w_arg, gets the alias call_integer in python_pkg and python_object_pkg, named like the other results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eval and call of std_ulogic_vector, signed and unsigned results, in python_pkg and for objects, are also functions taking the width as a length or as size_res. They call the procedure forms, so an error returns a vector of 'U'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failure stops the test before the function returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The python_pkg comment and the example no longer say that signed and unsigned results are procedures only; the example shows the sized function too. - The aliasing of call is described for the functions without a width. - The new features of the release notes are listed under New features. - One class name in the identity examples of the user guide, and a class name given to a constructor rather than as a generic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- counter_vc and filter_vc read p_model of their handle, like the user guide and VUnit's own verification components, instead of a get_model accessor. - The Python objects page no longer repeats that string arguments are passed verbatim, a semantic of python_pkg that VUnit#16 changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The details are in the user guide, and the call_integer alias is nothing a user needs to know about. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default identity was python_bridge:python:object:package:module:Class:<n>. python_object:package.module.Class:<n> is shorter and as unique: the counter separates the objects of a class and the dotted class name the classes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
14f2902 to
526d295
Compare
Adds Python objects to the bridge: instances of Python classes owned by VHDL, each living in a session of its own with state of its own. One can be a reference model, a memory model shared between a bus slave and the test, a helper class in a testbench, or the backend of a verification component. That last use is the one that motivated the design. It replaces #14 and covers most of #8.
The problem
When a VC's behaviour lives in Python, every VC author has to solve the same problems:
exec_fileresolves relative to the testbench, which a VC deep in the hierarchy doesn't know. That leads tosys.pathmanipulation, passed-around paths, and ordering workarounds aroundtest_runner_setup.The API
The class is named, not loaded from a file. A dotted name like
"package.module.Class"is imported from the module it names, found like any import: the run script directory, the Python environment, orPYTHONPATH. A bare"Class"is a class defined in the default session, and a name like"np.ndarray"follows a name defined there. A name that isn't found, or isn't a class, is reported in one line.One object, one session. The instance is bound to
objin a session named by the object's identity, so objects never share names or state.Constructed on first use, or right away.
new_python_objectonly records the class and the constructor arguments, so an object can be a constant or a generic, independent oftest_runner_setup.create_python_objecttakes the same arguments and constructs the object in the call, so a wrong class or argument is reported there; it works only in a process, since NVC and GHDL can't run the bridge during elaboration. The user guide has a table of when to use each.Identity.
idis numbered per class like VUnit's VCs:python_object:my_vcs.uart.UartModel:1,:2, and so on.new_session, is reported on that identity's logger, andnew_python_objectreturnsnull_python_object. Usingnull_python_objectis reported onpython_logger.get_logger(object)reports the object's errors, named after what failed:call("push") failed:for every form ofcall,eval(...)andexec(...)for those, andnew_python_object("<class>") failed:.Lookup by identity.
get_python_object(id)andget_python_object(name)return the object made with that identity, so a testbench can reach a VC's object without being given it. Unlikeget_logger, it doesn't make one: an identity without an object returnsnull_python_object.new_python_objectandcreate_python_objectalso take the identity by its full name (name => "tb:uart_model").VC backends. A VUnit-style VC has one constructor making its own object, with the id from
create_std_cfg, and one taking an object, e.g. shared with the testbench or other VCs. A given object keeps its own identity:Generated overloads.
callandevalon an object come from the same result tables aspython_pkg: every result type, the out-parameter procedures, the vector functions sized by a length orsize_res, and the same aliases.python_object_pkg.vhdis generated fromhdl/tools/python_object_pkg.vhd.inbygenerate_python_pkg.py, and a pytest checks that both generated packages are up to date.The identity comes from the handle rather than
'path_name, because GHDL drops the instance labels from'path_name/'instance_namewhen they're evaluated during elaboration. A VC whose handle has no id can make its object in a process with its instance path as the identity; the user guide shows both.Changes
hdl/tools/python_object_pkg.vhd.inand the generatedhdl/src/python_object_pkg.vhd, added topython_context.runtime.py:__vunit__.instantiate_as_obj. If the class can't be found or constructed, the error is reported once andobjbecomes a stand-in that raises the reason on every later use, assignments included. Classes are found withimportlib;pydocis not imported.python_pkggets the vector functions sized by a length orsize_res, and the aliascall_integerofcall_integer_w_arg.p_execwith an operation name, which only reports, and the result getters the generated object overloads use.examples/embedded_pythongetsfilter_vc, a clocked, handle-based VC whose behaviour is aMovingAverageobject, with a test case driving two instances that have their own objects and one given the testbench's object.PYTHONPATH, and global module names,python_object_pkgpage in the VHDL API reference,runner_cfginside a component,release_notes/news.d/15.feature*.md.Tests
tests/tb_python_objects.vhdcovers:tests/counter_vc_pkg.vhd,tests/counter_vc.vhd) with their own id, an enumerated id and a shared object,tests/path_counter_vc.vhd),get_python_objectby identity, full name and instance path, andnull_python_objectfor an identity without an object,create_python_objectright away and by the first use of anew_python_object, then on every use,exec,get_logger,create_python_objectandget_session, also before creation,null_python_object,create_std_cfg, as in the user guide.__vunit__.instantiate_as_obj: dotted names including nested classes, names followed from the default session, default-session classes, a missing name, a name that isn't a class, import errors inside a model not being mistaken for a missing class, and the failed-creation stand-in.PYTHONPATHsetup from the docs works with the run script started from another directory,python_pkgandpython_object_pkg, apart from its foreign bodies.Coordination with #18
The "Errors and limitations" bullet that
integer_array_tconstructor arguments can't be made during elaboration goes if #18 merges. #29 keeps it true, reporting such an argument as a failure.🤖 Generated with Claude Code