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

Changes to ObjectArg and added IdArg #24

Merged
merged 9 commits into from
Mar 30, 2022
Merged
110 changes: 88 additions & 22 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 28 additions & 25 deletions src/ibek/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
from __future__ import annotations

import builtins
import types
from dataclasses import Field, dataclass, field, make_dataclass
from typing import Any, Dict, List, Mapping, Sequence, Tuple, Type, cast
Expand All @@ -16,14 +17,14 @@
deserializer,
identity,
)
from apischema.conversions import Conversion, LazyConversion, reset_deserializers
from apischema.conversions import Conversion, reset_deserializers
from apischema.metadata import conversion
from typing_extensions import Annotated as A
from typing_extensions import Literal

from . import modules
from .globals import T, desc
from .support import Definition, ObjectArg, StrArg, Support
from .support import Definition, IdArg, ObjectArg, Support


class Entity:
Expand All @@ -34,22 +35,22 @@ class Entity:

# a link back to the Definition Object that generated this Definition
__definition__: Definition
__instances__: Dict[str, Entity]

entity_disabled: bool

def __post_init__(self):
# If there is an argument which is an id then allow deserialization by that
args = self.__definition__.args
ids = set(a.name for a in args if isinstance(a, StrArg) and a.is_id)
ids = set(a.name for a in args if isinstance(a, IdArg))
assert len(ids) <= 1, f"Multiple id args {list(ids)} defined in {args}"
if ids:
# A string id, use that
inst_id = getattr(self, ids.pop())
else:
# No string id, make one
inst_id = str(len(self.__instances__))
assert inst_id not in self.__instances__, f"Already got an instance {inst_id}"
self.__instances__[inst_id] = self
assert inst_id not in id_to_entity, f"Already got an instance {inst_id}"
id_to_entity[inst_id] = self


id_to_entity: Dict[str, Entity] = {}


def make_entity_class(definition: Definition, support: Support) -> Type[Entity]:
Expand All @@ -67,24 +68,25 @@ def make_entity_class(definition: Definition, support: Support) -> Type[Entity]:
for arg in definition.args:
# make_dataclass can cope with string types, so cast them here rather
# than lookup
arg_type = cast(type, arg.type)
metadata: Any = None
arg_type: type
if isinstance(arg, ObjectArg):

def make_conversion(name: str = arg.type) -> Conversion:
module_name, entity_name = name.split(".", maxsplit=1)
entity_cls = getattr(getattr(modules, module_name), entity_name)
def lookup_instance(id__):
try:
return id_to_entity[id__]
except KeyError:
raise ValidationError(f"{id__} is not in {list(id_to_entity)}")
niamhdougan marked this conversation as resolved.
Show resolved Hide resolved

def lookup_instance(id):
try:
return entity_cls.__instances__[id]
except KeyError:
raise ValidationError(f"{id} is not a {name}")

return Conversion(lookup_instance, str, Entity)

metadata = conversion(deserialization=LazyConversion(make_conversion))
metadata = conversion(
deserialization=Conversion(lookup_instance, str, Entity)
)
arg_type = Entity
elif isinstance(arg, IdArg):
arg_type = str
else:
# arg.type is str, int, float, etc.
arg_type = getattr(builtins, arg.type)
if arg.description:
arg_type = A[arg_type, desc(arg.description)]
if arg.default is Undefined:
Expand All @@ -102,7 +104,7 @@ def lookup_instance(id):
# it
fields.append(("entity_disabled", bool, field(default=cast(Any, False))))

namespace = dict(__definition__=definition, __instances__={})
namespace = dict(__definition__=definition)

# make the Entity derived dataclass for this EntityClass, with a reference
# to the Definition that created it
Expand All @@ -122,14 +124,15 @@ def make_entity_classes(support: Support) -> types.SimpleNamespace:
setattr(modules, support.module, module)
modules.__all__.append(support.module)
for definition in support.definitions:
entity_cls = make_entity_class(definition, support)
setattr(module, definition.name, entity_cls)
id_to_entity = make_entity_class(definition, support)
setattr(module, definition.name, id_to_entity)
coretl marked this conversation as resolved.
Show resolved Hide resolved
return module


def clear_entity_classes():
"""Reset the modules namespaces, deserializers and caches of defined Entity
subclasses"""
id_to_entity.clear()
while modules.__all__:
delattr(modules, modules.__all__.pop())
reset_deserializers(Entity)
Expand Down
25 changes: 16 additions & 9 deletions src/ibek/support.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
"""
The Support Class represents a deserialized <MODULE_NAME>.ibek.yaml file.
It contains a hierarchy of Entity dataclasses.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Mapping, Optional, Sequence, Type, Union
Expand All @@ -13,6 +10,11 @@

from .globals import T, desc

"""
The Support Class represents a deserialized <MODULE_NAME>.ibek.yaml file.
It contains a hierarchy of Entity dataclasses.
"""

niamhdougan marked this conversation as resolved.
Show resolved Hide resolved

@dataclass
class Arg:
Expand All @@ -23,7 +25,7 @@ class Arg:
type: str
default: Any

# https://wyfo.github.io/apischema/examples/subclass_union/
# https://wyfo.github.io/apischema/latest/examples/subclass_union/
def __init_subclass__(cls):
# Deserializers stack directly as a Union
deserializer(Conversion(identity, source=cls, target=Arg))
Expand Down Expand Up @@ -58,9 +60,6 @@ class StrArg(Arg):

type: Literal["str"] = "str"
default: Default[str] = Undefined
is_id: A[
bool, desc("If true, instances may refer to this instance by this arg")
] = False


@dataclass
Expand All @@ -83,7 +82,15 @@ class BoolArg(Arg):
class ObjectArg(Arg):
"""A reference to another entity defined in this IOC"""

type: A[str, desc("Entity class, <module>.<entity_name>")]
type: Literal["object"] = "object"
default: Default[str] = Undefined


@dataclass
class IdArg(Arg):
"""Explicit ID argument"""

type: Literal["id"] = "id"
default: Default[str] = Undefined


Expand Down
41 changes: 33 additions & 8 deletions tests/samples/schemas/ibek.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@
"null"
],
"description": "If given, and instance doesn't supply argument, what value should be used"
},
"is_id": {
"type": "boolean",
"description": "If true, instances may refer to this instance by this arg",
"default": false
}
},
"required": [
Expand Down Expand Up @@ -156,7 +151,38 @@
},
"type": {
"type": "string",
"description": "Entity class, <module>.<entity_name>"
"const": "object",
"default": "object"
},
"default": {
"type": [
"string",
"null"
],
"description": "If given, and instance doesn't supply argument, what value should be used"
}
},
"required": [
"name",
"description"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the argument that the IOC instance should pass"
},
"description": {
"type": "string",
"description": "Description of what the argument will be used for"
},
"type": {
"type": "string",
"const": "id",
"default": "id"
},
"default": {
"type": [
Expand All @@ -168,8 +194,7 @@
},
"required": [
"name",
"description",
"type"
"description"
],
"additionalProperties": false
}
Expand Down
Loading