Skip to content
Merged
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
56 changes: 7 additions & 49 deletions src/fastcs/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .backend import Backend
from .controller import Controller
from .exceptions import LaunchError
from .transport.adapter import TransportAdapter
from .transport import Transport
from .transport.epics.ca.options import EpicsCAOptions
from .transport.epics.pva.options import EpicsPVAOptions
from .transport.graphql.options import GraphQLOptions
Expand All @@ -33,57 +33,15 @@
class FastCS:
"""For launching a controller with given transport(s)."""

def __init__(
self,
controller: Controller,
transport_options: TransportOptions,
):
def __init__(self, controller: Controller, transports: list[Transport]):
self._loop = asyncio.get_event_loop()
self._controller = controller
self._backend = Backend(controller, self._loop)
transport: TransportAdapter
self._transports: list[TransportAdapter] = []
for option in transport_options:
match option:
case EpicsPVAOptions():
from .transport.epics.pva.adapter import EpicsPVATransport

transport = EpicsPVATransport(
self._backend.controller_api,
option,
)
case EpicsCAOptions():
from .transport.epics.ca.adapter import EpicsCATransport

transport = EpicsCATransport(
self._backend.controller_api,
self._loop,
option,
)
case TangoOptions():
from .transport.tango.adapter import TangoTransport

transport = TangoTransport(
self._backend.controller_api,
self._loop,
option,
)
case RestOptions():
from .transport.rest.adapter import RestTransport

transport = RestTransport(
self._backend.controller_api,
option,
)
case GraphQLOptions():
from .transport.graphql.adapter import GraphQLTransport

transport = GraphQLTransport(
self._backend.controller_api,
option,
)

self._transports.append(transport)
self._transports = transports
for transport in self._transports:
transport.initialise(
controller_api=self._backend.controller_api, loop=self._loop
)

def create_docs(self) -> None:
for transport in self._transports:
Expand Down
1 change: 1 addition & 0 deletions src/fastcs/transport/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
from .rest.options import RestServerOptions as RestServerOptions
from .tango.options import TangoDSROptions as TangoDSROptions
from .tango.options import TangoOptions as TangoOptions
from .transport import Transport as Transport
19 changes: 10 additions & 9 deletions src/fastcs/transport/epics/ca/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,29 @@
from softioc import softioc

from fastcs.controller_api import ControllerAPI
from fastcs.transport.adapter import TransportAdapter
from fastcs.transport import Transport
from fastcs.transport.epics.ca.ioc import EpicsCAIOC
from fastcs.transport.epics.ca.options import EpicsCAOptions
from fastcs.transport.epics.docs import EpicsDocs
from fastcs.transport.epics.gui import EpicsGUI


class EpicsCATransport(TransportAdapter):
class EpicsCATransport(Transport):
"""Channel access transport."""

def __init__(
def __init__(self, options: EpicsCAOptions | None = None):
self._options = options or EpicsCAOptions()

def initialise(
self,
controller_api: ControllerAPI,
loop: asyncio.AbstractEventLoop,
options: EpicsCAOptions | None = None,
) -> None:
self._controller_api = controller_api
self._loop = loop
self._options = options or EpicsCAOptions()
self._pv_prefix = self.options.ca_ioc.pv_prefix
self._pv_prefix = self._options.ca_ioc.pv_prefix
self._ioc = EpicsCAIOC(
self.options.ca_ioc.pv_prefix,
self._options.ca_ioc.pv_prefix,
controller_api,
self._options.ca_ioc,
)
Expand All @@ -35,10 +36,10 @@ def options(self) -> EpicsCAOptions:
return self._options

def create_docs(self) -> None:
EpicsDocs(self._controller_api).create_docs(self.options.docs)
EpicsDocs(self._controller_api).create_docs(self._options.docs)

def create_gui(self) -> None:
EpicsGUI(self._controller_api, self._pv_prefix).create_gui(self.options.gui)
EpicsGUI(self._controller_api, self._pv_prefix).create_gui(self._options.gui)

async def serve(self) -> None:
print(f"Running FastCS IOC: {self._pv_prefix}")
Expand Down
14 changes: 9 additions & 5 deletions src/fastcs/transport/epics/pva/adapter.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import asyncio

from fastcs.controller_api import ControllerAPI
from fastcs.transport.adapter import TransportAdapter
from fastcs.transport import Transport
from fastcs.transport.epics.docs import EpicsDocs
from fastcs.transport.epics.gui import PvaEpicsGUI
from fastcs.transport.epics.pva.options import EpicsPVAOptions

from .ioc import P4PIOC


class EpicsPVATransport(TransportAdapter):
class EpicsPVATransport(Transport):
"""PV access transport."""

def __init__(
def __init__(self, options: EpicsPVAOptions | None = None):
self._options = options or EpicsPVAOptions()

def initialise(
self,
controller_api: ControllerAPI,
options: EpicsPVAOptions | None = None,
loop: asyncio.AbstractEventLoop,
) -> None:
self._controller_api = controller_api
self._options = options or EpicsPVAOptions()
self._pv_prefix = self.options.pva_ioc.pv_prefix
self._ioc = P4PIOC(self.options.pva_ioc.pv_prefix, controller_api)

Expand Down
14 changes: 9 additions & 5 deletions src/fastcs/transport/graphql/adapter.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import asyncio

from fastcs.controller_api import ControllerAPI
from fastcs.transport.adapter import TransportAdapter
from fastcs.transport import Transport

from .graphql import GraphQLServer
from .options import GraphQLOptions


class GraphQLTransport(TransportAdapter):
class GraphQLTransport(Transport):
"""GraphQL transport."""

def __init__(
def __init__(self, options: GraphQLOptions | None = None):
self._options = options or GraphQLOptions()

def initialise(
self,
controller_api: ControllerAPI,
options: GraphQLOptions | None = None,
loop: asyncio.AbstractEventLoop,
):
self._options = options or GraphQLOptions()
self._server = GraphQLServer(controller_api)

@property
Expand Down
13 changes: 10 additions & 3 deletions src/fastcs/transport/rest/adapter.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import asyncio

from fastcs.controller_api import ControllerAPI
from fastcs.transport.adapter import TransportAdapter
from fastcs.transport import Transport

from .options import RestOptions
from .rest import RestServer


class RestTransport(TransportAdapter):
class RestTransport(Transport):
"""Rest Transport Adapter."""

def __init__(
self,
controller_api: ControllerAPI,
options: RestOptions | None = None,
):
self._options = options or RestOptions()

def initialise(
self,
controller_api: ControllerAPI,
loop: asyncio.AbstractEventLoop,
):
self._server = RestServer(controller_api)

@property
Expand Down
13 changes: 8 additions & 5 deletions src/fastcs/transport/tango/adapter.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import asyncio

from fastcs.controller_api import ControllerAPI
from fastcs.transport.adapter import TransportAdapter
from fastcs.transport import Transport

from .dsr import TangoDSR
from .options import TangoOptions


class TangoTransport(TransportAdapter):
class TangoTransport(Transport):
"""Tango transport."""

def __init__(
def __init__(self, options: TangoOptions | None = None):
self._options = options or TangoOptions()

def initialise(
self,
controller_api: ControllerAPI,
loop: asyncio.AbstractEventLoop,
options: TangoOptions | None = None,
):
self._options = options or TangoOptions()
if loop is None:
raise ValueError("TangoTransport expects a non-None loop")
self._dsr = TangoDSR(controller_api, loop)

@property
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import asyncio
from abc import ABC, abstractmethod
from typing import Any

from fastcs.controller_api import ControllerAPI

class TransportAdapter(ABC):
"""A base class for adapting a transport's implementation to

class Transport(ABC):
"""A base class for transport's implementation
so it can be used in FastCS."""

@property
Expand All @@ -26,3 +29,11 @@ def create_gui(self) -> None:
@property
def context(self) -> dict[str, Any]:
return {}

@abstractmethod
def initialise(
self,
controller_api: ControllerAPI,
loop: asyncio.AbstractEventLoop,
) -> None:
pass
3 changes: 2 additions & 1 deletion tests/example_p4p_ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from fastcs.transport.epics.options import (
EpicsIOCOptions,
)
from fastcs.transport.epics.pva.adapter import EpicsPVATransport
from fastcs.transport.epics.pva.options import EpicsPVAOptions
from fastcs.wrappers import command, scan

Expand Down Expand Up @@ -82,7 +83,7 @@ def run(pv_prefix="P4P_TEST_DEVICE"):
controller.register_sub_controller(
"Child2", ChildController(description="another sub controller")
)
fastcs = FastCS(controller, [p4p_options])
fastcs = FastCS(controller, [EpicsPVATransport(p4p_options)])
fastcs.run()


Expand Down
3 changes: 2 additions & 1 deletion tests/example_softioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from fastcs.controller import Controller, SubController
from fastcs.datatypes import Int
from fastcs.launch import FastCS
from fastcs.transport.epics.ca.adapter import EpicsCATransport
from fastcs.transport.epics.ca.options import EpicsCAOptions
from fastcs.transport.epics.options import EpicsIOCOptions
from fastcs.wrappers import command
Expand All @@ -24,7 +25,7 @@ def run(pv_prefix="SOFTIOC_TEST_DEVICE"):
epics_options = EpicsCAOptions(ca_ioc=EpicsIOCOptions(pv_prefix=pv_prefix))
controller = ParentController()
controller.register_sub_controller("Child", ChildController())
fastcs = FastCS(controller, [epics_options])
fastcs = FastCS(controller, [EpicsCATransport(epics_options)])
fastcs.run()


Expand Down
2 changes: 1 addition & 1 deletion tests/test_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def test_error_if_identical_context_in_transports(mocker: MockerFixture, data):
mocker.patch("fastcs.launch.FastCS.create_gui")
mocker.patch("fastcs.launch.FastCS.create_docs")
mocker.patch(
"fastcs.transport.adapter.TransportAdapter.context",
"fastcs.transport.Transport.context",
new_callable=mocker.PropertyMock,
return_value={"controller": "test"},
)
Expand Down
2 changes: 1 addition & 1 deletion tests/transport/epics/ca/test_softioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ def test_update_datatype(mocker: MockerFixture):


def test_ca_context_contains_softioc_commands(mocker: MockerFixture):
transport = EpicsCATransport(mocker.MagicMock(), mocker.MagicMock())
transport = EpicsCATransport(mocker.MagicMock())

softioc_commands = {
command: getattr(softioc, command) for command in softioc.command_names
Expand Down
3 changes: 2 additions & 1 deletion tests/transport/epics/pva/test_p4p.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from fastcs.datatypes import Bool, Enum, Float, Int, String, Table, Waveform
from fastcs.launch import FastCS
from fastcs.transport.epics.options import EpicsIOCOptions
from fastcs.transport.epics.pva.adapter import EpicsPVATransport
from fastcs.transport.epics.pva.options import EpicsPVAOptions
from fastcs.wrappers import command

Expand Down Expand Up @@ -227,7 +228,7 @@ async def test_numerical_alarms(p4p_subprocess: tuple[str, Queue]):

def make_fastcs(pv_prefix: str, controller: Controller) -> FastCS:
epics_options = EpicsPVAOptions(pva_ioc=EpicsIOCOptions(pv_prefix=pv_prefix))
return FastCS(controller, [epics_options])
return FastCS(controller, [EpicsPVATransport(epics_options)])


def test_read_signal_set():
Expand Down
5 changes: 4 additions & 1 deletion tests/transport/graphQL/test_graphql.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import copy
import json
from typing import Any
Expand Down Expand Up @@ -66,7 +67,9 @@ def nest_response(path: list[str], value: Any) -> dict:


def create_test_client(gql_controller_api: AssertableControllerAPI) -> TestClient:
return TestClient(GraphQLTransport(gql_controller_api)._server._app)
graphql_transport = GraphQLTransport()
graphql_transport.initialise(gql_controller_api, asyncio.AbstractEventLoop())
return TestClient(graphql_transport._server._app)


class TestGraphQLServer:
Expand Down
5 changes: 4 additions & 1 deletion tests/transport/rest/test_rest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import enum

import numpy as np
Expand Down Expand Up @@ -36,7 +37,9 @@ def rest_controller_api(class_mocker: MockerFixture):


def create_test_client(rest_controller_api: ControllerAPI) -> TestClient:
return TestClient(RestTransport(rest_controller_api)._server._app)
rest_transport = RestTransport()
rest_transport.initialise(rest_controller_api, asyncio.AbstractEventLoop())
return TestClient(rest_transport._server._app)


class TestRestServer:
Expand Down
6 changes: 4 additions & 2 deletions tests/transport/tango/test_dsr.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ def tango_controller_api(class_mocker: MockerFixture) -> AssertableControllerAPI


def create_test_context(tango_controller_api: AssertableControllerAPI):
device = TangoTransport(
tango_transport = TangoTransport()
tango_transport.initialise(
tango_controller_api,
# This is passed to enable instantiating the transport, but tests must avoid
# using via patching of functions. It will raise NotImplementedError if used.
asyncio.AbstractEventLoop(),
)._dsr._device
)
device = tango_transport._dsr._device
# https://tango-controls.readthedocs.io/projects/pytango/en/v9.5.1/testing/test_context.html
with DeviceTestContext(device, debug=0) as proxy:
yield proxy
Expand Down
Loading