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.
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>
ebe868a to
a3f43d6
Compare
…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>
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 scoreboard shared between a monitor 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
selfin 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 like VUnit's VCs:python_bridge:python:object: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:.VC backends.
get_python_object(object, class_name, args, id)returns the given object or a new one, likeget_logger/get_id. A VUnit-style VC calls it in its handle constructor, using the id fromcreate_std_cfg. Each instance then gets its own backend, and a testbench can pass in, and keep using, its own:Generated overloads.
callandevalon an object come from the same result tables aspython_pkg: every result type, the out-parameter procedures, 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 without a handle can use its instance path in a process body; 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_self. If the class can't be found or constructed, the error is reported once andselfbecomes a stand-in that raises the reason on every later use, assignments included. Classes are found withimportlib;pydocis not imported.new_python_objectfails right away on the object's logger and returnsnull_python_object. The FFI packages tell the object package whether sessions are supported and which identities have sessions. The VHPI FFI gets ap_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,runner_cfginside a component,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),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_self: 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 #16 and #18
Two "Errors and limitations" bullets in the user guide describe behaviour those PRs change. Whichever PR merges second should drop the matching bullet:
integer_array_tconstructor arguments can't be made during elaboration goes when Transfer integer_array_t arguments when they are first used #18 merges.Question
selfin its session. Is that the name we want?🤖 Generated with Claude Code