Skip to content

Commit

Permalink
ENH: Add validation methods for all Entries
Browse files Browse the repository at this point in the history
Entry.validate includes an apischema serialization roundtrip to enforce
type checks

Nestable validation includes cycle detection and making sure Nestables
aren't empty.
  • Loading branch information
shilorigins committed May 31, 2024
1 parent 040e64a commit 61871f2
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 5 deletions.
6 changes: 3 additions & 3 deletions superscore/backends/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from superscore.backends.core import _Backend
from superscore.errors import (BackendError, EntryExistsError,
EntryNotFoundError)
from superscore.model import Collection, Entry, Snapshot
from superscore.model import Entry, Nestable


class TestBackend(_Backend):
Expand All @@ -28,7 +28,7 @@ def get_entry(self, uuid: UUID) -> Entry:
entry = stack.pop()
if entry.uuid == uuid:
return entry
if isinstance(entry, Collection) or isinstance(entry, Snapshot):
if isinstance(entry, Nestable):
stack.extend(entry.children)
raise EntryNotFoundError(f"Entry {entry.uuid} could not be found")

Expand All @@ -45,4 +45,4 @@ def delete_entry(self, to_delete: Entry) -> None:
children.remove(entry)
elif entry.uuid == to_delete.uuid:
raise BackendError(f"Can't delete: entry {to_delete.uuid} is out of sync with the version in the backend")
stack.extend([entry.children for entry in children if isinstance(entry, Collection) or isinstance(entry, Snapshot)])
stack.extend([entry.children for entry in children if isinstance(entry, Nestable)])
58 changes: 56 additions & 2 deletions superscore/model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Classes for representing data"""
from __future__ import annotations

import apischema
import logging
from dataclasses import dataclass, field
from datetime import datetime
Expand Down Expand Up @@ -59,6 +60,20 @@ class Entry:
description: str = ''
creation_time: datetime = field(default_factory=utcnow)

def validate(self) -> bool:
"""
Ensures Entry is properly formed.
Raises apischema.ValidationError if type checking fails during
deserialization
"""
try:
serial = apischema.serialize(self)
apischema.deserialize(type(self), serial)
return True
except apischema.ValidationError:
return False


@dataclass
class Parameter(Entry):
Expand All @@ -69,6 +84,10 @@ class Parameter(Entry):
readback: Optional[Parameter] = None
read_only: bool = False

def validate(self) -> bool:
readback_is_valid = self.readback is None or self.readback.validate()
return readback_is_valid and super().validate()


@dataclass
class Value(Entry):
Expand All @@ -84,6 +103,10 @@ class Setpoint(Value):
"""A Value that can be written to the EPICS environment"""
readback: Optional[Readback] = None

def validate(self) -> bool:
readback_is_valid = self.readback is None or self.readback.validate()
return readback_is_valid and super().validate()


@dataclass
class Readback(Value):
Expand All @@ -102,8 +125,39 @@ class Readback(Value):
timeout: Optional[float] = None


class Nestable:
"""Abstract class that provides methods for nested container Entries"""
def validate(self):
"""
Validates structure of Entry tree, then uses validate_nodes to validate
individual Entries. This separation avoids redundant work by only performing
tree-level validation once. Overrides Entry.validate().
"""
return not self.has_cycle() and super().validate() and self.validate_nodes()

def validate_nodes(self):
"""
Validates self and all children at the Entry level. See Nestable.validate() for
more information.
"""
not_empty = len(self.children) > 0
all_children_are_valid = all(child.validate_nodes() if isinstance(child, Nestable) else child.validate() for child in self.children)
return not_empty and all_children_are_valid

def has_cycle(self, parents=set()) -> bool:
if self.uuid in parents:
return True
else:
parents.add(self.uuid)
for child in self.children:
if isinstance(child, type(self)) and child.has_cycle(parents=parents):
return True
parents.remove(self.uuid)
return False


@dataclass
class Collection(Entry):
class Collection(Nestable, Entry):
"""Nestable group of Parameters and Collections"""
meta_pvs: ClassVar[List[Parameter]] = []
all_tags: ClassVar[Set[Tag]] = set()
Expand All @@ -114,7 +168,7 @@ class Collection(Entry):


@dataclass
class Snapshot(Entry):
class Snapshot(Nestable, Entry):
"""
Nestable group of Values and Snapshots. Effectively a data-filled Collection
"""
Expand Down

0 comments on commit 61871f2

Please sign in to comment.