Create a builder2ibek converter for a support module#
When builder2ibek xml2yaml converts a builder XML file it applies a
converter — a small Python module — for each XML component prefix it
encounters. If no converter exists, the entity is passed through unchanged
with whatever attributes the XML had.
This how-to explains when and how to write a new converter.
Why fix builder2ibek rather than editing ioc.yaml by hand#
It is tempting to run xml2yaml, fix up the resulting ioc.yaml manually,
and move on. Resist this. Writing a converter instead has important
advantages:
Others benefit immediately. Anyone converting the same module gets correct output without knowing about the quirk.
Re-conversion is safe. If the original builder XML is modified (a device address changes, a new instance is added) you can re-run
xml2yamland get a correct result. Manual edits toioc.yamlwould have to be re-applied by hand.The fix is tested. Once you add a sample XML/YAML pair to
tests/samples/the CI suite catches any future regressions.The knowledge is captured. The converter documents exactly what builder.py was doing implicitly, in executable form.
The rule of thumb: if you find yourself editing ioc.yaml to fix something
that came from xml2yaml, ask yourself whether that fix belongs in a
converter instead.
When a converter is needed#
A converter is needed when the ibek entity requires different field names, values, or structure from the raw XML attributes. Common cases:
Situation |
Converter action |
|---|---|
An XML attribute has been renamed in ibek |
|
An XML attribute is GUI-only or builder-only and absent from ibek |
|
An XML string |
Already handled by |
A numeric value must be transformed (e.g. |
Arithmetic on |
The entity type itself must change (module renamed in ibek) |
|
An XML element maps to a different ibek module entirely |
|
One XML element should be deleted (e.g. replaced by an ibek default) |
|
One XML element should expand to two ibek entities |
|
Converter anatomy#
Every converter is a .py file in src/builder2ibek/converters/.
moduleinfos.py discovers them automatically at import time — no
registration step is needed.
The minimal structure is:
from builder2ibek.converters.globalHandler import globalHandler
from builder2ibek.types import Entity, Generic_IOC
# Must match the XML element prefix: <myModule.SomeClass .../>
xml_component = "myModule"
@globalHandler
def handler(entity: Entity, entity_type: str, ioc: Generic_IOC):
"""Convert myModule XML entities to ibek YAML."""
if entity_type == "SomeClass":
entity.rename("oldParam", "newParam")
entity.remove("builderOnlyParam")
Required module-level names#
Name |
Type |
Purpose |
|---|---|---|
|
|
XML prefix(es) this converter handles |
|
callable |
Called once per matching entity during conversion |
Optional module-level names#
Name |
Type |
Purpose |
|---|---|---|
|
|
Override the ibek module prefix in the output (if different from |
|
|
Default parameter values injected for specific entity types |
|
|
URL of the ibek IOC schema for the Generic IOC (informational) |
The @globalHandler decorator#
@globalHandler wraps your handler function so that the global
pre-processing runs first. The global handler:
Removes
gda_nameandgda_descfrom every entityConverts string values
"True"/"False"/""to PythonTrue/False/None
Always use @globalHandler unless you have a specific reason not to.
The Entity API#
Entity is a dict subclass that also supports attribute-style access.
All XML attributes arrive as string values; the global handler has already
converted booleans and empty strings.
entity.rename("old", "new") # rename a key; no-op if absent
entity.remove("key") # delete a key; no-op if absent
entity.type = "mod.Class" # change the ibek entity type
entity.delete_me() # mark this entity for removal from output
entity.add_entity(new) # insert an extra entity after this one
Read-only access to the IOC-level context:
ioc.ioc_name # IOC name string
ioc.entities # list of all entity dicts already processed
Example 1: simple field rename and removal#
The autosave XML element has several DLS-specific attributes that do not
exist in the ibek entity model:
<autosave.Autosave iocName="BL11I-CS-IOC-09" bl="True"
path="/dls_sw/i11/epics/autosave" skip_1="True" name="AS"/>
The ibek model uses P for the PV prefix (derived from iocName with a
trailing colon added):
# src/builder2ibek/converters/autosave.py
xml_component = "autosave"
@globalHandler
def handler(entity: Entity, entity_type: str, ioc: Generic_IOC):
if entity_type == "Autosave":
entity.rename("iocName", "P") # rename the field
entity.P += ":" # append the colon
entity.remove("bl") # DLS-only flags not in ibek model
entity.remove("name")
entity.remove("path")
entity.remove("skip_1")
entity.debug = bool(entity.debug)
Example 2: type change and value transformation#
vacuumValve.vacuumValve maps to dlsPLC.vacValve with the valve number
multiplied by 10 to get addr, and the crate short name looked up to
obtain the full vlvcc PV device name:
# src/builder2ibek/converters/vacuumValve.py
xml_component = "vacuumValve"
read100Objects = {} # shared state to resolve crate name → port
@globalHandler
def handler(entity: Entity, entity_type: str, ioc: Generic_IOC):
if entity_type == "vacuumValveRead":
read100Objects[entity.name] = entity.port
entity.type = "dlsPLC.read100"
entity.century = 0
entity.remove("name")
elif entity_type in ["vacuumValve", "vacuumValve_callback"]:
entity.type = "dlsPLC.vacValve"
entity.rename("crate", "vlvcc")
entity.addr = int(entity.valve) * 10
entity.remove("valve")
entity.port = read100Objects[entity.vlvcc]
Note the module-level read100Objects dict: entities are processed in XML
document order, so a vacuumValveRead entity always appears before any
vacuumValve that references it.
Example 3: deleting an entity#
devIocStats.devIocStatsHelper is superseded by an ibek default that
auto-generates the equivalent entity. The converter simply removes it:
@globalHandler
def handler(entity: Entity, entity_type: str, ioc: Generic_IOC):
if entity_type == "devIocStatsHelper":
entity.delete_me()
return
Example 4: handling multiple XML prefixes#
One converter can handle several XML prefixes by setting xml_component to
a list. This is useful for support modules that were renamed:
xml_component = ["vacuumValve", "oldVacuum"]
@globalHandler
def handler(entity: Entity, entity_type: str, ioc: Generic_IOC):
...
Development iteration loop#
Open the builder2ibek repository in the devcontainer (first run
git submodule update --init to check out ibek-support and
ibek-support-dls). The typical inner loop is:
Edit
src/builder2ibek/converters/<module>.pyOptionally edit the ibek support YAML in
ibek-support-dls/oribek-support/if the entity model also needs updatingRe-convert all sample XML files and update expected outputs:
cd tests/samples && ./make_samples.sh
Rebuild the global IOC YAML schema so VSCode validation reflects any support YAML changes:
./update-schema
Open the generated
.yamlfiles intests/samples/in VSCode and check for schema validation errors (requires the Red Hat YAML extension). If the extension does not pick up schema changes immediately, toggle Yaml: Validate off and on in the extension settings.Once the output looks correct, commit both the converter and the updated sample files.
Adding a test#
Add a sample XML and the expected YAML to tests/samples/ so the CI will
catch regressions.
cp MY-IOC.xml tests/samples/
cd tests/samples && ./make_samples.sh
Review the diff carefully — make_samples.sh overwrites the .yaml files
from the current converter output, so only commit when the output is correct.
The test suite runs all sample conversions automatically:
uv run pytest tests/test_file_conversion.py
Submitting the converter#
Converters are part of the builder2ibek source tree, not the
ibek-support* repositories. Open a pull request against
epics-containers/builder2ibek
including:
src/builder2ibek/converters/<module>.pytests/samples/<EXAMPLE>.xmlandtests/samples/<example>.yaml