Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename defs to entity_models. Simplify Jinja rendering #232

Merged
merged 6 commits into from
Jun 18, 2024
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
21 changes: 3 additions & 18 deletions src/ibek/entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
from ibek.globals import JINJA

from .ioc import Entity, EnumVal, clear_entity_model_ids
from .params import Define, EnumParam, IdParam, ObjectParam
from .support import EntityDefinition, Support
from .params import EnumParam, IdParam, ObjectParam
from .support import EntityModel, Support
from .utils import UTILS


Expand Down Expand Up @@ -56,19 +56,12 @@ def make_entity_models(self, definition_yaml: List[Path]) -> List[Type[Entity]]:
return list(self._entity_models.values())

def _make_entity_model(
self, definition: EntityDefinition, support: Support
self, definition: EntityModel, support: Support
) -> Type[Entity]:
"""
Create an Entity Model from a Definition instance and a Support instance.
"""

def add_defines(s: dict[str, Define]) -> None:
if s:
# add in the pre_defines or post_defines as Args in the Entity
for name, value in s.items():
typ = getattr(builtins, str(value.type))
add_arg(name, typ, value.description, value.value)

def add_arg(name, typ, description, default):
if default is None:
default = PydanticUndefined
Expand Down Expand Up @@ -100,11 +93,6 @@ def add_arg(name, typ, description, default):
# fully qualified name of the Entity class including support module
full_name = f"{support.module}.{definition.name}"

# add in the calculated values Jinja Templates as Fields in the Entity
# these are the pre_values that should be Jinja rendered before any
# Args (or post values)
add_defines(definition.pre_defines)

# add in each of the arguments as a Field in the Entity
for name, arg in definition.params.items():
type: Any
Expand Down Expand Up @@ -132,9 +120,6 @@ def add_arg(name, typ, description, default):
type = getattr(builtins, arg.type)
add_arg(name, type, arg.description, getattr(arg, "default")) # type: ignore

# add in the calculated values Jinja Templates as Fields in the Entity
add_defines(definition.post_defines)

# add the type literal which discriminates between the different Entity classes
typ = Literal[full_name] # type: ignore
args["type"] = (typ, Field(description=definition.description))
Expand Down
4 changes: 2 additions & 2 deletions src/ibek/definition.py → src/ibek/entity_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ class EntityPVI(BaseSettings):
]


class EntityDefinition(BaseSettings):
class EntityModel(BaseSettings):
"""
A single definition of a class of Entity that an IOC instance may instantiate
A Model for a class of Entity that an IOC instance may instantiate
"""

name: str = Field(
Expand Down
2 changes: 1 addition & 1 deletion src/ibek/gen_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from ibek.utils import UTILS

from .definition import Database
from .entity_model import Database
from .globals import TEMPLATES
from .ioc import Entity
from .render import Render
Expand Down
106 changes: 58 additions & 48 deletions src/ibek/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@

from __future__ import annotations

import json
import ast
import builtins
from enum import Enum
from typing import Any, Dict, List, Sequence

from pydantic import (
Field,
model_validator,
)
from pydantic.fields import FieldInfo

from .definition import EntityDefinition
from .entity_model import EntityModel
from .globals import BaseSettings
from .params import IdParam
from .utils import UTILS

# a global dict of all entity instances indexed by their ID
Expand Down Expand Up @@ -54,60 +55,69 @@ class Entity(BaseSettings):
entity_enabled: bool = Field(
description="enable or disable this entity instance", default=True
)
__definition__: EntityDefinition
__definition__: EntityModel
Copy link
Member

@GDYendell GDYendell Jun 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field should probably have a new name too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not getting you - that is the new name we settled on


def _process_field(self: Entity, name: str, value: Any, typ: str):
"""
Process an Entity field - doing jinja rendering, type coercion and
object id storage/lookup as required.
"""

if isinstance(value, str):
# Jinja expansion always performed on string fields
value = UTILS.render(self, value)
if typ in ["list", "int", "float", "bool"]:
# coerce the rendered parameter to its intended type
try:
cast_type = getattr(builtins, typ)
value = cast_type(ast.literal_eval(value))
except:
print(f"ERROR: decoding field '{name}', value '{value}' as {typ}")
raise

if typ == "object":
# look up the actual object by it's id
if isinstance(value, str):
value = get_entity_by_id(value)

# If this field is not pre-existing, add it into the model instance.
# This is how pre/post_defines are added.
if name not in self.model_fields:
self.model_fields[name] = FieldInfo(annotation=str, default=value)

# update the model instance attribute with the rendered value
setattr(self, name, value)

if typ == "id":
# add this entity to the global id index
if value in id_to_entity:
raise ValueError(f"Duplicate id {value} in {list(id_to_entity)}")
id_to_entity[value] = self

@model_validator(mode="after")
def add_ibek_attributes(self):
"""
Whole Entity model validation

Do jinja rendering of pre_defines/ parameters / post_defines
in the correct order.

Also adds pre_define and post_defines to the model instance, making
them available for the phase 2 (final) jinja rendering performed in
ibek.runtime_cmds.generate().
"""

# find the id field in this Entity if it has one
ids = {
name
for name, value in self.__definition__.params.items()
if isinstance(value, IdParam)
}
if self.__definition__.pre_defines:
for name, define in self.__definition__.pre_defines.items():
self._process_field(name, define.value, define.type)

entity_dict = self.model_dump()
for arg, value in entity_dict.items():
model_field = self.model_fields[arg]
if self.__definition__.params:
for name, parameter in self.__definition__.params.items():
self._process_field(name, getattr(self, name), parameter.type)

if isinstance(value, str):
# Jinja expansion of any of the Entity's string args/values
value = UTILS.render(entity_dict, value)
# this is a cheesy test - any better ideas please let me know
if "Union" in str(model_field.annotation):
# Args that were non strings and have been rendered by Jinja
# must be coerced back into their original type
try:
# The following replace are to make the string json compatible
# (maybe we should python decode instead of json.loads)
value = value.replace("'", '"')
value = value.replace("True", "true")
value = value.replace("False", "false")
value = json.loads(value)
except:
print(
f"ERROR: fail to decode {value} as a {model_field.annotation}"
)
raise

if model_field.annotation == object:
# look up the actual object by it's id
if isinstance(value, str):
value = get_entity_by_id(value)

# update this entity instance with the rendered value
setattr(self, arg, value)
# update the entity_dict with the rendered value
entity_dict[arg] = value

if arg in ids:
# add this entity to the global id index
if value in id_to_entity:
raise ValueError(f"Duplicate id {value} in {list(id_to_entity)}")
id_to_entity[value] = self
if self.__definition__.post_defines:
for name, define in self.__definition__.post_defines.items():
self._process_field(name, define.value, define.type)

return self

Expand Down
8 changes: 4 additions & 4 deletions src/ibek/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
]


class DefinesTypes(Enum):
class DefineTypes(Enum):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this still has one too many ss

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

"""The type of a value"""

string = "str"
Expand All @@ -34,14 +34,14 @@ def __repr__(self):


class Define(BaseSettings):
"""A calculated string value for a definition"""
"""A calculated value for an Entity Model"""

description: str = Field(
description="Description of what the value will be used for"
)
value: Any = Field(description="The contents of the value")
type: DefinesTypes = Field(
description="The type of the value", default=DefinesTypes.string
type: DefineTypes | None = Field(
description="The type of the value", default=DefineTypes.string
)


Expand Down
2 changes: 1 addition & 1 deletion src/ibek/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import Callable, List, Optional, Sequence, Union

from .definition import Comment, Script, Text, When
from .entity_model import Comment, Script, Text, When
from .ioc import Entity
from .utils import UTILS

Expand Down
2 changes: 1 addition & 1 deletion src/ibek/render_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from dataclasses import dataclass
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple

from ibek.definition import Database
from ibek.entity_model import Database
from ibek.ioc import Entity
from ibek.utils import UTILS

Expand Down
2 changes: 1 addition & 1 deletion src/ibek/runtime_cmds/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from pvi._format.template import format_template
from pvi.device import Device

from ibek.definition import Database
from ibek.entity_factory import EntityFactory
from ibek.entity_model import Database
from ibek.gen_scripts import create_boot_script, create_db_script
from ibek.globals import GLOBALS, NaturalOrderGroup
from ibek.ioc import IOC, Entity
Expand Down
4 changes: 2 additions & 2 deletions src/ibek/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from pydantic import Field

from .definition import EntityDefinition
from .entity_model import EntityModel
from .globals import BaseSettings


Expand All @@ -26,7 +26,7 @@ class Support(BaseSettings):
)

module: str = Field(description="Support module name, normally the repo name")
defs: Sequence[EntityDefinition] = Field(
defs: Sequence[EntityModel] = Field(
description="The definitions an IOC can create using this module"
)

Expand Down
74 changes: 1 addition & 73 deletions tests/samples/schemas/fastVacuum.ibek.ioc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,46 +95,6 @@
"default": 0,
"description": "union of <class 'int'> and jinja representation of {typ}",
"title": "Timeout"
},
"gaugeNum": {
"anyOf": [
{
"description": "jinja that renders to <class 'int'>",
"pattern": ".*\\{\\{.*\\}\\}.*",
"type": "string"
},
{
"description": "auto gauge count",
"type": "integer"
}
],
"default": "{{ _global.incrementor(master, start=1) }}",
"description": "union of <class 'int'> and jinja representation of {typ}",
"title": "Gaugenum"
},
"fan": {
"default": "{{ \"%02d\" % (gaugeNum / 7 + 1) }}",
"description": "fan number",
"title": "Fan",
"type": "string"
},
"mask": {
"default": "{{ _global.incrementor(\"mask_{}\".format(master), 2, 2**gaugeNum) }}",
"description": "mask for the channel",
"title": "Mask",
"type": "string"
},
"lnk_no": {
"default": "{{ ((gaugeNum - 1) % 6) + 1 }}",
"description": "link number",
"title": "Lnk No",
"type": "string"
},
"gaugePV": {
"default": "{{ master.device }}:GAUGE:{{id}}_0",
"description": "Gauge PV",
"title": "Gaugepv",
"type": "string"
}
},
"required": [
Expand Down Expand Up @@ -186,41 +146,9 @@
},
"device": {
"default": "{{dom}}-VA-FAST-01",
"description": "device name",
"description": "Device prefix",
"title": "Device",
"type": "string"
},
"waveform_nelm": {
"anyOf": [
{
"description": "jinja that renders to <class 'int'>",
"pattern": ".*\\{\\{.*\\}\\}.*",
"type": "string"
},
{
"description": "waveform element count",
"type": "integer"
}
],
"default": 500,
"description": "union of <class 'int'> and jinja representation of {typ}",
"title": "Waveform Nelm"
},
"combined_nelm": {
"anyOf": [
{
"description": "jinja that renders to <class 'int'>",
"pattern": ".*\\{\\{.*\\}\\}.*",
"type": "string"
},
{
"description": "total waveform element count",
"type": "integer"
}
],
"default": "{{ 6 * waveform_nelm }}",
"description": "union of <class 'int'> and jinja representation of {typ}",
"title": "Combined Nelm"
}
},
"required": [
Expand Down
6 changes: 3 additions & 3 deletions tests/samples/schemas/gauges.ibek.ioc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,17 +212,17 @@
"title": "Gauge1"
},
"gauge2": {
"default": "{{gauge1.name}}",
"default": "{{gauge1}}",
"description": "Second gauge",
"title": "Gauge2"
},
"gauge3": {
"default": "{{gauge1.name}}\n",
"default": "{{gauge1}}\n",
"description": "Third gauge",
"title": "Gauge3"
},
"gauge4": {
"default": "{{gauge1.name}}\n",
"default": "{{gauge1}}\n",
"description": "Fourth gauge",
"title": "Gauge4"
}
Expand Down
Loading
Loading