|
1 | 1 | import json |
| 2 | +from inspect import signature |
2 | 3 | from pathlib import Path |
3 | 4 | from typing import Annotated, Any, get_type_hints |
4 | 5 |
|
|
16 | 17 | class FastCS: |
17 | 18 | def __init__( |
18 | 19 | self, |
19 | | - controller: type[Controller], |
20 | | - controller_options: Any, |
| 20 | + controller: Controller, |
21 | 21 | transport_options: EpicsOptions | TangoOptions, |
22 | 22 | ): |
23 | | - self._controller = controller() |
24 | | - self._controller.startup(controller_options) |
25 | | - |
26 | | - self._backend = Backend(self._controller) |
| 23 | + self._backend = Backend(controller) |
27 | 24 | self._transport: TransportAdapter |
28 | 25 | match transport_options: |
29 | 26 | case TangoOptions(): |
@@ -54,20 +51,64 @@ def run(self) -> None: |
54 | 51 | self._transport.run() |
55 | 52 |
|
56 | 53 |
|
57 | | -def launch(controller_class: type[Controller]): |
58 | | - try: |
59 | | - options_type = get_type_hints(controller_class.startup)["config"] |
60 | | - except KeyError as e: |
61 | | - raise KeyError( |
62 | | - "Missing type hint for config argument, expected: " |
63 | | - "`def startup(self, config: <type hint>)`" |
64 | | - ) from e |
| 54 | +class LaunchError(Exception): |
| 55 | + pass |
65 | 56 |
|
66 | | - fastcs_options = create_model( |
67 | | - f"{controller_class.__name__}", |
68 | | - controller=(options_type, ...), |
69 | | - transport=(EpicsOptions | TangoOptions, ...), |
70 | | - ) |
| 57 | + |
| 58 | +def launch(controller_class: type[Controller]) -> None: |
| 59 | + """ |
| 60 | + Serves as an entry point for starting FastCS applications. |
| 61 | +
|
| 62 | + By utilizing type hints in a Controller's __init__ method, this |
| 63 | + function provides a command-line interface to describe and gather the |
| 64 | + required configuration before instantiating the application. |
| 65 | +
|
| 66 | + Args: |
| 67 | + controller_class (type[Controller]): The FastCS Controller to instantiate. |
| 68 | + It must have a type-hinted __init__ method and no more than 2 arguments. |
| 69 | +
|
| 70 | + Raises: |
| 71 | + LaunchError: If the class's __init__ is not as expected |
| 72 | +
|
| 73 | + Example of the expected Controller implementation: |
| 74 | + class MyController(Controller): |
| 75 | + def __init__(self, my_arg: MyControllerOptions) -> None: |
| 76 | + ... |
| 77 | +
|
| 78 | + Typical usage: |
| 79 | + if __name__ == "__main__": |
| 80 | + launch(MyController) |
| 81 | + """ |
| 82 | + args_len = controller_class.__init__.__code__.co_argcount |
| 83 | + sig = signature(controller_class.__init__) |
| 84 | + if args_len == 1: |
| 85 | + fastcs_options = create_model( |
| 86 | + f"{controller_class.__name__}", |
| 87 | + transport=(EpicsOptions | TangoOptions, ...), |
| 88 | + __config__={"extra": "forbid"}, |
| 89 | + ) |
| 90 | + elif args_len == 2: |
| 91 | + hints = get_type_hints(controller_class.__init__) |
| 92 | + if "self" in hints: |
| 93 | + del hints["self"] |
| 94 | + if hints: |
| 95 | + options_type = list(hints.values())[-1] |
| 96 | + else: |
| 97 | + raise LaunchError( |
| 98 | + f"Expected typehinting in {controller_class.__name__}" |
| 99 | + f".__init__ but received {sig}" |
| 100 | + ) |
| 101 | + fastcs_options = create_model( |
| 102 | + f"{controller_class.__name__}", |
| 103 | + controller=(options_type, ...), |
| 104 | + transport=(EpicsOptions | TangoOptions, ...), |
| 105 | + __config__={"extra": "forbid"}, |
| 106 | + ) |
| 107 | + else: |
| 108 | + raise LaunchError( |
| 109 | + f"Expected up to 2 arguments for {controller_class.__name__}.__init__ " |
| 110 | + f"but received {args_len} {sig}" |
| 111 | + ) |
71 | 112 |
|
72 | 113 | _launch_typer = typer.Typer() |
73 | 114 |
|
@@ -109,11 +150,14 @@ def run( |
109 | 150 | yaml = YAML(typ="safe") |
110 | 151 | options_yaml = yaml.load(config) |
111 | 152 | # To do: Handle a k8s "values.yaml" file |
112 | | - instance_options = fastcs_options.parse_obj(options_yaml) |
| 153 | + instance_options = fastcs_options.model_validate(options_yaml) |
| 154 | + if hasattr(instance_options, "controller"): |
| 155 | + controller = controller_class(instance_options.controller) |
| 156 | + else: |
| 157 | + controller = controller_class() |
113 | 158 |
|
114 | 159 | instance = FastCS( |
115 | | - controller_class, |
116 | | - instance_options.controller, |
| 160 | + controller, |
117 | 161 | instance_options.transport, |
118 | 162 | ) |
119 | 163 |
|
|
0 commit comments