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

Add collections of Entities inside of Definitions #216

Merged
merged 24 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d0bb484
add CollectionDefinition and refactor stage 1
gilesknap May 9, 2024
3c3741a
refactor 2
gilesknap May 9, 2024
7e68b3f
refactor 3
gilesknap May 9, 2024
a6e9848
make support.defs = [Definition|CollectionDefinition]
gilesknap May 9, 2024
7674437
allow object ids in Arg defaults
gilesknap May 10, 2024
5f4f435
first pass of SubEntity implementation
gilesknap May 10, 2024
4002584
combine CollectionDefs and Defs in make_entity_models
gilesknap May 10, 2024
7cb291c
move model functions into entity_model
gilesknap May 11, 2024
e066669
move entity_model functions into IocFactory class
gilesknap May 11, 2024
65554c1
tests passing but SubEntity Args still need work
gilesknap May 11, 2024
e3fa27a
CollectionDefinition fully working
gilesknap May 11, 2024
f941f3c
merge Definition and CollectionDefinition back into Definition
gilesknap May 11, 2024
338b70a
demonstrate yaml anchors and aliases in quadem test
gilesknap May 12, 2024
7e882f8
support recursive SubEntities
gilesknap May 12, 2024
a1631b6
simplify Entity Model validator
gilesknap May 12, 2024
c3892df
refactor parts of ioc_factory into entity_factory
gilesknap May 14, 2024
d11b619
update remaining tests to work with refactor
gilesknap May 14, 2024
e75acab
final changes from Garys review
gilesknap May 14, 2024
d13a171
linting
gilesknap May 14, 2024
10ba7a6
fix dependencies test
gilesknap May 14, 2024
1aca3ea
overhaul globals and test patching
gilesknap May 14, 2024
49ee13b
some more tests needed epics_root fixture
gilesknap May 14, 2024
2d841f7
apply Garys PR comments
gilesknap May 15, 2024
aba1b13
rewrite process_collectiohns as resolve_sub_entities
gilesknap May 15, 2024
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ lint.ignore = [
"E501", # Line too long, should be fixed by black.
]
line-length = 88
select = [
lint.select = [
"C4", # flake8-comprehensions - https://beta.ruff.rs/docs/rules/#flake8-comprehensions-c4
"E", # pycodestyle errors - https://beta.ruff.rs/docs/rules/#error-e
"F", # pyflakes rules - https://beta.ruff.rs/docs/rules/#pyflakes-f
Expand Down
87 changes: 87 additions & 0 deletions src/ibek/args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Classes to specify arguments to Definitions
"""

from __future__ import annotations

from typing import Any, Dict, Optional

from pydantic import Field
from typing_extensions import Literal

from .globals import BaseSettings


class Value(BaseSettings):
"""A calculated string value for a definition"""

name: str = Field(description="Name of the value that the IOC instance will expose")
description: str = Field(
description="Description of what the value will be used for"
)
value: str = Field(description="The contents of the value")


class Arg(BaseSettings):
"""Base class for all Argument Types"""

type: str
name: str = Field(
description="Name of the argument that the IOC instance should pass"
)
description: str = Field(
description="Description of what the argument will be used for"
)


class FloatArg(Arg):
"""An argument with a float value"""

type: Literal["float"] = "float"
default: Optional[float] = None


class StrArg(Arg):
"""An argument with a str value"""

type: Literal["str"] = "str"
default: Optional[str] = None


class IntArg(Arg):
"""An argument with an int value"""

type: Literal["int"] = "int"
default: Optional[int] = None


class BoolArg(Arg):
"""An argument with an bool value"""

type: Literal["bool"] = "bool"
default: Optional[bool] = None


class ObjectArg(Arg):
"""A reference to another entity defined in this IOC"""

type: Literal["object"] = "object"
default: Optional[str] = None


class IdArg(Arg):
"""Explicit ID argument that an object can refer to"""

type: Literal["id"] = "id"
default: Optional[str] = None


class EnumArg(Arg):
"""An argument with an enum value"""

type: Literal["enum"] = "enum"
default: Optional[Any] = None

values: Dict[str, Any] = Field(
description="provides a list of values to make this argument an Enum",
)
168 changes: 168 additions & 0 deletions src/ibek/definition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
The Definition class describes what a given support module can instantiate.
"""

from __future__ import annotations

from enum import Enum
from typing import Any, Mapping, Optional, Sequence, Union

from pydantic import Field, PydanticUndefinedAnnotation
from typing_extensions import Literal

from .args import Arg, IdArg, Value
from .globals import BaseSettings
from .sub_entity import SubEntity


def default(T: type):
"""
defines a default type which may be
"""
return Field(
Union[Optional[T], PydanticUndefinedAnnotation],
description="If given, instance doesn't supply argument, what value should be used",
)


class When(Enum):
first = "first"
every = "every"
last = "last"


class Database(BaseSettings):
"""
A database file that should be loaded by the startup script and its args
"""

file: str = Field(
description="Filename of the database template in <module_root>/db"
)

enabled: str = Field(
description="Set to False to disable loading this database", default="True"
)

args: Mapping[str, Optional[str]] = Field(
description=(
"Dictionary of args and values to pass through to database. "
"A value of None is equivalent to ARG: '{{ ARG }}'. "
"See `UTILS.render_map` for more details."
)
)


class EnvironmentVariable(BaseSettings):
"""
An environment variable that should be set in the startup script
"""

name: str = Field(description="Name of environment variable")
value: str = Field(description="Value to set")


class Comment(BaseSettings):
"""
A script snippet that will have '# ' prepended to every line
for insertion into the startup script
"""

type: Literal["comment"] = "comment"
when: When = Field(description="One of first / every / last", default="every")
value: str = Field(
description="A comment to add into the startup script", default=""
)


class Text(BaseSettings):
"""
A script snippet to insert into the startup script
"""

type: Literal["text"] = "text"
when: str = Field(description="One of first / every / last", default="every")
value: str = Field(description="raw text to add to the startup script", default="")


Script = Sequence[Union[Text, Comment]]


class EntityPVI(BaseSettings):
gilesknap marked this conversation as resolved.
Show resolved Hide resolved
"""Entity PVI definition"""

yaml_path: str = Field(
description="Path to .pvi.device.yaml - absolute or relative to PVI_DEFS"
)
ui_index: bool = Field(
True,
description="Whether to add the UI to the IOC index.",
)
ui_macros: dict[str, str | None] = Field(
None,
description=(
"Macros to launch the UI on the IOC index. "
"These must be args of the Entity this is attached to."
),
)
pv: bool = Field(
False,
description=(
"Whether to generate a PVI PV. This adds a database template with info "
"tags that create a PVAccess PV representing the device structure."
),
)
pv_prefix: str = Field("", description='PV prefix for PVI PV - e.g. "$(P)"')


class Definition(BaseSettings):
"""
A single definition of a class of Entity that an IOC instance may instantiate
"""

name: str = Field(
description="Publish Definition as type <module>.<name> for IOC instances"
)
description: str = Field(
description="A description of the Support module defined here"
)
# declare Arg as Union of its subclasses for Pydantic to be able to deserialize
args: Sequence[Union[tuple(Arg.__subclasses__())]] = Field( # type: ignore
description="The arguments IOC instance should supply", default=()
)
values: Sequence[Value] = Field(
description="Calculated values to use as additional arguments", default=()
)
databases: Sequence[Database] = Field(
description="Databases to instantiate", default=[]
)
pre_init: Script = Field(
description="Startup script snippets to add before iocInit()", default=()
)
post_init: Script = Field(
description="Startup script snippets to add post iocInit(), such as dbpf",
default=(),
)
env_vars: Sequence[EnvironmentVariable] = Field(
description="Environment variables to set in the boot script", default=()
)
pvi: Optional[EntityPVI] = Field(
description="PVI definition for Entity", default=None
)

# list of additional entities to instantiate for each instance of this definition
sub_entities: Sequence[SubEntity] = Field(
description="The sub-entity instances that this collection is to instantiate",
default=(),
)

shared: Sequence[Any] = Field(
description="A place to create any anchors required for repeating YAML",
default=(),
)

def _get_id_arg(self):
"""Returns the name of the ID argument for this definition, if it exists"""
for arg in self.args:
if isinstance(arg, IdArg):
return arg.name
74 changes: 9 additions & 65 deletions src/ibek/gen_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,38 @@
"""

import logging
import re
from pathlib import Path
from typing import List, Tuple, Type
from typing import List, Sequence, Tuple

from jinja2 import Template
from ruamel.yaml.main import YAML

from ibek.utils import UTILS

from .definition import Database
from .globals import TEMPLATES
from .ioc import IOC, Entity, clear_entity_model_ids, make_entity_models, make_ioc_model
from .ioc import Entity
from .render import Render
from .render_db import RenderDb
from .support import Database, Support

log = logging.getLogger(__name__)


schema_modeline = re.compile(r"# *yaml-language-server *: *\$schema=([^ ]*)")
url_f = r"file://"


def ioc_create_model(definitions: List[Path]) -> Type[IOC]:
"""
Take a list of definitions YAML and create an IOC model from it
"""
entity_models = []

clear_entity_model_ids()
for definition in definitions:
support_dict = YAML(typ="safe").load(definition)

Support.model_validate(support_dict)

# deserialize the support module definition file
support = Support(**support_dict)
# make Entity classes described in the support module definition file
entity_models += make_entity_models(support)

# Save the schema for IOC
model = make_ioc_model(entity_models)

return model


def ioc_deserialize(ioc_instance_yaml: Path, definition_yaml: List[Path]) -> IOC:
"""
Takes an ioc instance entities file, list of generic ioc definitions files.

Returns a model of the resulting ioc instance
"""
ioc_model = ioc_create_model(definition_yaml)

# extract the ioc instance yaml into a dict
ioc_instance_dict = YAML(typ="safe").load(ioc_instance_yaml)
if ioc_instance_dict is None or "ioc_name" not in ioc_instance_dict:
raise RuntimeError(
f"Failed to load a valid ioc config from {ioc_instance_yaml}"
)

# extract the ioc name into UTILS for use in jinja renders
name = UTILS.render({}, ioc_instance_dict["ioc_name"])
UTILS.set_ioc_name(name)
ioc_instance_dict["ioc_name"] = name

# Create an IOC instance from the instance dict and the model
ioc_instance = ioc_model(**ioc_instance_dict)

return ioc_instance


def create_db_script(
ioc_instance: IOC, extra_databases: List[Tuple[Database, Entity]]
entities: Sequence[Entity], extra_databases: List[Tuple[Database, Entity]]
) -> str:
"""
Create make_db.sh script for expanding the database templates
"""
with open(TEMPLATES / "ioc.subst.jinja", "r") as f:
jinja_txt = f.read()

renderer = RenderDb(ioc_instance)
renderer = RenderDb(entities)

templates = renderer.render_database(extra_databases)

return Template(jinja_txt).render(templates=templates)


def create_boot_script(ioc_instance: IOC) -> str:
def create_boot_script(entities: Sequence[Entity]) -> str:
"""
Create the boot script for an IOC
"""
Expand All @@ -101,7 +45,7 @@ def create_boot_script(ioc_instance: IOC) -> str:

return template.render(
__utils__=UTILS,
env_var_elements=renderer.render_environment_variable_elements(ioc_instance),
script_elements=renderer.render_pre_ioc_init_elements(ioc_instance),
post_ioc_init_elements=renderer.render_post_ioc_init_elements(ioc_instance),
env_var_elements=renderer.render_environment_variable_elements(entities),
script_elements=renderer.render_pre_ioc_init_elements(entities),
post_ioc_init_elements=renderer.render_post_ioc_init_elements(entities),
)
Loading
Loading