Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 58 additions & 15 deletions src/techui_builder/autofill.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from pathlib import Path

from lxml import objectify
from lxml.objectify import ObjectifiedElement
from lxml.etree import Element, SubElement, tostring
from lxml.objectify import ObjectifiedElement, fromstring

from techui_builder.builder import Builder, _get_action_group
from techui_builder.models import Component
Expand All @@ -17,7 +18,9 @@
@dataclass
class Autofiller:
path: Path
macros: list[str] = field(default_factory=lambda: ["prefix", "desc", "file"])
macros: list[str] = field(
default_factory=lambda: ["prefix", "desc", "file", "macros"]
)
widgets: dict[str, ObjectifiedElement] = field(
default_factory=defaultdict, init=False, repr=False
)
Expand Down Expand Up @@ -69,25 +72,51 @@ def replace_content(
component: Component,
):
for macro in self.macros:
# Get current component attribute
component_attr = getattr(component, macro)

# Fix to make sure widget is reverted back to widget that was passed in
current_widget = widget

match macro:
case "prefix":
tag_name = "pv_name"
component_attr += ":DEVSTA"
case "desc":
tag_name = "description"
current_widget = _get_action_group(widget)
if component_attr is None:
component_attr = component_name
case "file":
tag_name = "file"
component_attr = f"{component.P}:DEVSTA"

case "desc" | "file" | "macros":
# Get current component attribute
component_attr = getattr(component, macro, None)

current_widget = _get_action_group(widget)
if component_attr is None:
component_attr = f"{component_name}.bob"
match macro:
case "desc":
tag_name = "description"

if component_attr is None:
component_attr = component_name

case "file":
tag_name = "file"

if component_attr is None:
component_attr = f"{component_name}.bob"

case "macros":
tag_name = "macros"

if component_attr is None:
# If no custom macros are provided, don't run this code
# As this will overwrite generated macros
continue

assert current_widget is not None
# Remove all existing macros if they exist
if current_widget.macros is not None:
current_widget.remove(current_widget.macros)
# Create new macros element
current_widget.append(
self._create_macro_element(component_attr)
)
# Break out of the loop
continue

case _:
raise ValueError("The provided macro type is not supported.")

Expand All @@ -100,3 +129,17 @@ def replace_content(

# Set component's tag text to the corresponding widget tag
current_widget[tag_name] = component_attr

def _create_macro_element(self, macros: dict):
# You cannot set a text tag of an ObjectifiedElement,
# so we need to make an etree.Element and convert it ...

macros_element = Element("macros")
for macro, val in macros.items():
macro_element = SubElement(macros_element, macro)
macro_element.text = val

# ... which requires this horror
obj_macros_element = fromstring(tostring(macros_element))

return obj_macros_element
36 changes: 31 additions & 5 deletions src/techui_builder/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
StringConstraints,
computed_field,
field_validator,
model_validator,
)

logger_ = logging.getLogger(__name__)
Expand Down Expand Up @@ -104,8 +105,30 @@ class Component(BaseModel):
desc: str | None = None
extras: list[str] | None = None
file: str | None = None
macros: dict[str, str | int | float] | None = None
devsta: list[str] | None = None
model_config = ConfigDict(extra="forbid")
model_config = ConfigDict(
# Makes sure that 'macros' is only allowed if 'file' is present
# (this is required for VSCode checks)
json_schema_extra={
"allOf": [
{
"if": {"required": ["macros"]},
"then": {
"required": ["file"],
"title": "'macros' is only allowed if 'file' is defined.",
},
}
]
},
)

@model_validator(mode="after")
def _check_macros_if_file_present(self):
if self.macros is not None:
if self.file is None:
raise AssertionError("'macros' is only allowed if 'file' is defined.")
return self

@field_validator("prefix")
@classmethod
Expand Down Expand Up @@ -150,21 +173,21 @@ def _check_devsta(cls, v: list[str]) -> list[str]:
raise ValueError("devsta must contain unique items")
return v

@computed_field
@computed_field(repr=False, return_type=str | None)
@property
def P(self) -> str | None: # noqa: N802
match = re.match(_DLS_PREFIX_RE, self.prefix)
if match:
return match.group(1)

@computed_field
@computed_field(repr=False, return_type=str | None)
@property
def R(self) -> str | None: # noqa: N802
match = re.match(_DLS_PREFIX_RE, self.prefix)
if match:
return match.group(2)

@computed_field
@computed_field(repr=False, return_type=str | None)
@property
def attribute(self) -> str | None:
match = re.match(_DLS_PREFIX_RE, self.prefix)
Expand All @@ -175,7 +198,10 @@ def attribute(self) -> str | None:
class TechUi(BaseModel):
beamline: Beamline
components: dict[str, Component]
model_config = ConfigDict(extra="forbid")
model_config = ConfigDict(
extra="forbid",
hide_input_in_errors=True,
)


"""
Expand Down
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ def example_related_widget():
)
desc_element = SubElement(action_element, "description")
desc_element.text = "placeholder description"
macros_element = SubElement(action_element, "macros")
macro_element = SubElement(macros_element, "P")
macro_element.text = "placeholder P"

# ... which requires this horror
widget_element = fromstring(tostring(widget_element))
Expand Down
32 changes: 26 additions & 6 deletions tests/test_autofiller.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,25 @@ def test_autofiller_write_bob(autofiller):


@pytest.mark.parametrize(
"prefix, description, filename, expected_desc, expected_file",
"prefix, description, filename, macros, expected_desc, expected_file",
[
("TEST_1", None, None, "test_component", "test_component.bob"),
("TEST_2", "test_desc", "test_file.bob", "test_desc", "test_file.bob"),
("BL01T-TS-TEST-01", None, None, None, "test_component", "test_component.bob"),
(
"BL01T-TS-TEST-02",
"test_desc",
"test_file.bob",
None,
"test_desc",
"test_file.bob",
),
(
"BL01T-TS-TEST-03",
"test_desc",
"test_file.bob",
{"TEST": "TEST3"},
"test_desc",
"test_file.bob",
),
],
)
def test_autofiller_replace_content(
Expand All @@ -65,28 +80,33 @@ def test_autofiller_replace_content(
prefix,
description,
filename,
macros,
expected_desc,
expected_file,
):
with patch("techui_builder.autofill._get_action_group") as mock_get:
mock_get.return_value = example_related_widget.actions.action

mock_component = Mock(
spec=Component,
# Cannot use a Mock object as need P to be computed
fake_component = Component(
prefix=prefix,
desc=description,
file=filename,
macros=macros,
)

autofiller.replace_content(
example_related_widget,
"test_component",
mock_component,
fake_component,
)

assert example_related_widget.pv_name == f"{prefix}:DEVSTA"
assert example_related_widget.actions.action.description.text == expected_desc
assert example_related_widget.actions.action.file.text == expected_file
if macros is not None:
for k, v in macros.items():
assert example_related_widget.actions.action.macros[k] == macros[k] == v


def test_autofiller_replace_content_no_action_group(autofiller, caplog):
Expand Down
3 changes: 1 addition & 2 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ def test_component_repr(component: Component):
assert (
str(component)
== "prefix='BL01T-EA-TEST-02' desc='Test Device' extras=None\
file=None devsta=['BL01T-MO-MOTOR-01:Y'] P='BL01T-EA-TEST-02' R=None\
attribute=None"
file=None macros=None devsta=['BL01T-MO-MOTOR-01:Y']"
)


Expand Down