Skip to content

Commit

Permalink
Merge pull request #216 from epics-containers/add-collection-tests2
Browse files Browse the repository at this point in the history
Add collections of Entities inside of Definitions
  • Loading branch information
gilesknap authored May 15, 2024
2 parents c25f882 + aba1b13 commit 5477c59
Show file tree
Hide file tree
Showing 46 changed files with 14,726 additions and 627 deletions.
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):
"""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 EntityDefinition(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
8 changes: 4 additions & 4 deletions src/ibek/dev_cmds/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import typer

from ibek.globals import CONFIG_DIR_NAME, IOC_FOLDER, NaturalOrderGroup
from ibek.globals import GLOBALS, NaturalOrderGroup

log = logging.getLogger(__name__)
dev_cli = typer.Typer(cls=NaturalOrderGroup)
Expand Down Expand Up @@ -34,9 +34,9 @@ def instance(
"""

# validate the instance folder has a config folder
ioc_folder = IOC_FOLDER
config_folder = ioc_folder / CONFIG_DIR_NAME
instance_config = instance / CONFIG_DIR_NAME
ioc_folder = GLOBALS.IOC_FOLDER
config_folder = ioc_folder / GLOBALS.CONFIG_DIR_NAME
instance_config = instance / GLOBALS.CONFIG_DIR_NAME

# verify that the expected folder exists
if not ioc_folder.exists():
Expand Down
Loading

0 comments on commit 5477c59

Please sign in to comment.