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

MNT: add EntryVisitor and refactor search methods to use SearchVisitor #110

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
51 changes: 3 additions & 48 deletions superscore/backends/core.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,12 @@
"""
Base superscore data storage backend interface
"""
import re
from collections.abc import Container, Generator
from typing import NamedTuple, Union
from collections.abc import Generator
from typing import Union
from uuid import UUID

from superscore.model import Entry, Root
from superscore.type_hints import AnyEpicsType

SearchTermValue = Union[AnyEpicsType, Container[AnyEpicsType], tuple[AnyEpicsType, ...]]
SearchTermType = tuple[str, str, SearchTermValue]


class SearchTerm(NamedTuple):
attr: str
operator: str
value: SearchTermValue
from superscore.search_term import SearchTermType


class _Backend:
Expand Down Expand Up @@ -67,41 +57,6 @@ def search(self, *search_terms: SearchTermType) -> Generator[Entry, None, None]:
"""
raise NotImplementedError

@staticmethod
def compare(op: str, data: AnyEpicsType, target: SearchTermValue) -> bool:
"""
Return whether data and target satisfy the op comparator, typically during application
of a search filter. Possible values of op are detailed in _Backend.search

Parameters
----------
op: str
one of the comparators that all backends must support, detailed in _Backend.search
data: AnyEpicsType | Tuple[AnyEpicsType]
data from an Entry that is being used to decide whether the Entry passes a filter
target: AnyEpicsType | Tuple[AnyEpicsType]
the filter value

Returns
-------
bool
whether data and target satisfy the op condition
"""
if op == "eq":
return data == target
elif op == "lt":
return data <= target
elif op == "gt":
return data >= target
elif op == "in":
return data in target
elif op == "like":
if isinstance(data, UUID):
data = str(data)
return re.search(target, data)
else:
raise ValueError(f"SearchTerm does not support operator \"{op}\"")

@property
def root(self) -> Root:
"""Return the Root Entry in this backend"""
Expand Down
21 changes: 5 additions & 16 deletions superscore/backends/filestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from superscore.errors import BackendError
from superscore.model import Entry, Root
from superscore.utils import build_abs_path
from superscore.visitor import SearchVisitor

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -285,22 +286,10 @@ def search(self, *search_terms: SearchTermType) -> Generator[Entry, None, None]:
Keys are attributes on `Entry` subclasses, or special keywords.
Values can be a single value or a tuple of values depending on operator.
"""
with self._load_and_store_context() as db:
for entry in db.values():
conditions = []
for attr, op, target in search_terms:
# TODO: search for child pvs?
if attr == "entry_type":
conditions.append(isinstance(entry, target))
else:
try:
# check entry attribute by name
value = getattr(entry, attr)
conditions.append(self.compare(op, value, target))
except AttributeError:
conditions.append(False)
if all(conditions):
yield entry
visitor = SearchVisitor(self, *search_terms)
root = self.root
visitor.visit(root)
yield from visitor.matches

@contextlib.contextmanager
def _load_and_store_context(self) -> Generator[Dict[UUID, Any], None, None]:
Expand Down
20 changes: 5 additions & 15 deletions superscore/backends/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from superscore.errors import (BackendError, EntryExistsError,
EntryNotFoundError)
from superscore.model import Entry, Nestable, Root
from superscore.visitor import SearchVisitor


class TestBackend(_Backend):
Expand Down Expand Up @@ -75,18 +76,7 @@ def root(self) -> Root:
return self._root

def search(self, *search_terms: SearchTermType):
for entry in self._entry_cache.values():
conditions = []
for attr, op, target in search_terms:
# TODO: search for child pvs?
if attr == "entry_type":
conditions.append(isinstance(entry, target))
else:
try:
# check entry attribute by name
value = getattr(entry, attr)
conditions.append(self.compare(op, value, target))
except AttributeError:
conditions.append(False)
if all(conditions):
yield entry
visitor = SearchVisitor(self, *search_terms)
root = self.root
visitor.visit(root)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm seeing this elsewhere as starting with an "accept" call, such as root.acept(visitor). These are functionally equivalent right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're right, looks like I made up .visit without realizing. It does give us a benefit though: we can use .visit for tracking state during traversal. For example, SearchVisitor adds the Entry to the path and pops it off the path in .visit because .visit doesn't return until all of the reachable entries have been traversed, whereas .visit* return before the reachable entries are traversed. Without .visit, we might have to move the traversal logic into .visit*, so we can track the state we need.

I'll try out removing .visit and seeing what changes need to be made.

Copy link
Contributor

Choose a reason for hiding this comment

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

For reference, a sequence diagram and class diagram from the source of all my knowledge: https://en.wikipedia.org/wiki/Visitor_pattern#Structure

yield from visitor.matches
3 changes: 2 additions & 1 deletion superscore/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
from uuid import UUID

from superscore.backends import get_backend
from superscore.backends.core import SearchTerm, SearchTermType, _Backend
from superscore.backends.core import _Backend
from superscore.compare import DiffItem, EntryDiff, walk_find_diff
from superscore.control_layers import ControlLayer, EpicsData
from superscore.control_layers.status import TaskStatus
from superscore.errors import CommunicationError
from superscore.model import (Collection, Entry, Nestable, Parameter, Readback,
Setpoint, Snapshot)
from superscore.search_term import SearchTerm, SearchTermType
from superscore.utils import build_abs_path

logger = logging.getLogger(__name__)
Expand Down
30 changes: 29 additions & 1 deletion superscore/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ def validate(self, toplevel: bool = True) -> bool:
readback_is_valid = self.readback is None or self.readback.validate(toplevel=False)
return readback_is_valid and super().validate(toplevel=toplevel)

def accept(self, visitor) -> None:
visitor.visitParameter(self)
if self.readback is not None:
visitor.visit(self.readback)


@dataclass
class Setpoint(Entry):
Expand Down Expand Up @@ -142,6 +147,11 @@ def validate(self, toplevel: bool = True) -> bool:
readback_is_valid = self.readback is None or self.readback.validate(toplevel=False)
return readback_is_valid and super().validate(toplevel=toplevel)

def accept(self, visitor) -> None:
visitor.visitSetpoint(self)
if self.readback is not None:
visitor.visit(self.readback)


@dataclass
class Readback(Entry):
Expand Down Expand Up @@ -183,6 +193,9 @@ def from_parameter(
timeout=timeout,
)

def accept(self, visitor) -> None:
visitor.visitReadback(self)


class Nestable:
"""Mix-in class that provides methods for nested container Entries"""
Expand Down Expand Up @@ -237,6 +250,11 @@ def swap_to_uuids(self) -> List[Entry]:
self.children = new_children
return ref_list

def accept(self, visitor) -> None:
visitor.visitCollection(self)
for entry in self.children:
visitor.visit(entry)


@dataclass
class Snapshot(Nestable, Entry):
Expand Down Expand Up @@ -274,9 +292,19 @@ def swap_to_uuids(self) -> List[Union[Entry, UUID]]:

return ref_list

def accept(self, visitor) -> None:
visitor.visitSnapshot(self)
for entry in self.children:
visitor.visit(entry)


@dataclass
class Root:
"""Top level structure holding ``Entry``'s. Denotes the top of the tree"""
meta_id: UUID = _root_uuid
uuid: UUID = _root_uuid
entries: List[Entry] = field(default_factory=list)

def accept(self, visitor) -> None:
visitor.visitRoot(self)
for entry in self.entries:
visitor.visit(entry)
12 changes: 12 additions & 0 deletions superscore/search_term.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import Container, NamedTuple, Union

from superscore.type_hints import AnyEpicsType

SearchTermValue = Union[AnyEpicsType, Container[AnyEpicsType], tuple[AnyEpicsType, ...]]
SearchTermType = tuple[str, str, SearchTermValue]


class SearchTerm(NamedTuple):
attr: str
operator: str
value: SearchTermValue
2 changes: 1 addition & 1 deletion superscore/tests/db/filestore.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"meta_id": "a28cd77d-cc92-46cc-90cb-758f0f36f041",
"uuid": "a28cd77d-cc92-46cc-90cb-758f0f36f041",
"entries": [
{
"Parameter": {
Expand Down
3 changes: 2 additions & 1 deletion superscore/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@

import pytest

from superscore.backends.core import SearchTerm, _Backend
from superscore.backends.core import _Backend
from superscore.errors import (BackendError, EntryExistsError,
EntryNotFoundError)
from superscore.model import Collection, Parameter, Snapshot
from superscore.search_term import SearchTerm


class TestTestBackend:
Expand Down
22 changes: 19 additions & 3 deletions superscore/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@

import pytest

from superscore.backends.core import SearchTerm
from superscore.backends.filestore import FilestoreBackend
from superscore.backends.test import TestBackend
from superscore.client import Client
from superscore.control_layers import EpicsData
from superscore.errors import CommunicationError
from superscore.model import (Collection, Entry, Nestable, Parameter, Readback,
Root, Setpoint)
from superscore.search_term import SearchTerm
from superscore.tests.conftest import MockTaskStatus, nest_depth

SAMPLE_CFG = Path(__file__).parent / 'config.cfg'
Expand Down Expand Up @@ -211,8 +211,24 @@ def test_fill_depth(fill_depth: int):


@pytest.mark.parametrize("filestore_backend", [("linac_with_comparison_snapshot",)], indirect=True)
def test_parametrized_filestore(sample_client: Client):
assert len(list(sample_client.search())) > 0
def test_search_entries_by_ancestor(sample_client: Client):
entries = tuple(sample_client.search(
("entry_type", "eq", Setpoint),
("pv_name", "eq", "LASR:GUNB:TEST1"),
))
assert len(entries) == 2
entries = tuple(sample_client.search(
("entry_type", "eq", Setpoint),
("pv_name", "eq", "LASR:GUNB:TEST1"),
("ancestor", "eq", UUID("06282731-33ea-4270-ba14-098872e627dc")), # top-level snapshot
))
assert len(entries) == 1
entries = tuple(sample_client.search(
("entry_type", "eq", Setpoint),
("pv_name", "eq", "LASR:GUNB:TEST1"),
("ancestor", "eq", UUID("2f709b4b-79da-4a8b-8693-eed2c389cb3a")), # direct parent
))
assert len(entries) == 1


def test_parametrized_filestore_empty(sample_client: Client):
Expand Down
2 changes: 1 addition & 1 deletion superscore/tests/test_compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

import pytest

from superscore.backends.core import SearchTerm
from superscore.client import Client
from superscore.compare import (AttributePath, DiffItem, EntryDiff,
walk_find_diff)
from superscore.model import (Collection, Entry, Parameter, Readback, Setpoint,
Severity, Snapshot, Status)
from superscore.search_term import SearchTerm


def simplify_path(path: AttributePath) -> AttributePath:
Expand Down
Loading