DiamondLightSource / DiamondLightSource/fastcs

Plugin mechanism for `Controller` subclasses via entry points

Open
#376 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
6
Forks
8
Avg merge
2d 12h
Merged PRs (30d)
3

Description

After #360 we should do this

## Summary

We need a way for external packages (e.g. `fastcs_foo`) to register `Controller` subclasses so that `fastcs` can discover and instantiate them dynamically (e.g. from YAML configuration), without requiring explicit imports of those packages.

This would enable a proper plugin ecosystem for controllers.

---

## Proposed approach

Use Python packaging **entry points** as an extension mechanism.

- Define an entry point group: `fastcs.controllers`
- Each plugin package registers its `Controller` subclasses there
- `fastcs` discovers them at runtime using `importlib.metadata.entry_points`

---

## Example plugin (`fastcs_foo`)

### Implementation

```python
# fastcs_foo/foo_controller.py

from fastcs.controller import Controller

class FooController(Controller):
def run(self):
print(f"Foo running with {self.config}")
```

### Registration

```toml
# pyproject.toml

[project.entry-points."fastcs.controllers"]
FooController = "fastcs_foo.foo_controller:FooController"
```

---

## YAML configuration

Controllers are referenced by:

```
.
```

Example:

```yaml
controllers:
- type: fastcs_foo.FooController
config:
speed: 10
```

---

## Discovery + resolution (in `fastcs`)

```python
from importlib.metadata import entry_points
from fastcs.controller import Controller

def discover_controllers():
registry = {}

for ep in entry_points(group="fastcs.controllers"):
cls = ep.load()

namespace = cls.__module__.split(".", 1)[0]
name = ep.name

registry[(namespace, name)] = cls

return registry

def resolve_controller(type_string: str):
namespace, class_name = type_string.split(".", 1)

registry = discover_controllers()

key = (namespace, class_name)

if key not in registry:
raise ValueError(f"Unknown controller '{type_string}'")

cls = registry[key]

if not issubclass(cls, Controller):
raise TypeError(f"{type_string} is not a Controller subclass")

return cls
```

---

## Benefits

- No explicit imports required
- Clean separation between core and plugins
- Works with standard Python packaging
- Controllers can live in independently installable packages
- YAML remains simple and explicit (`fastcs_foo.FooController`)

---

## Notes / future considerations

- Could validate distribution name vs module root more strictly
- Could support aliases or versioning
- Could cache discovery results for performance

---

## Outcome

This would provide a robust, extensible plugin system for `Controller` implementations, similar to how pytest and other tools manage plugins.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.