Skip to content

Commit a20dc57

Browse files
Now, changing a reference in the catalog updates it everywhere (including across multiple control systems). The link within the control system therefore needs to be refactored.
1 parent 74ddb76 commit a20dc57

File tree

10 files changed

+705
-379
lines changed

10 files changed

+705
-379
lines changed

pyaml/accelerator.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,13 @@ def __init__(self, cfg: ConfigModel):
7878

7979
if cfg.controls is not None:
8080
for c in cfg.controls:
81-
if c.get_catalog_name():
82-
c.set_catalog(self.__catalogs.get(c.get_catalog_name()))
81+
if (
82+
c.get_catalog_name()
83+
and c.get_catalog_name() in self.__catalogs.keys()
84+
):
85+
catalog = self.__catalogs.get(c.get_catalog_name())
86+
view = catalog.view(c)
87+
c.set_catalog(view)
8388
if c.name() == "live":
8489
self.__live = c
8590
else:
@@ -126,6 +131,21 @@ def set_energy(self, E: float):
126131
for c in self._cfg.controls:
127132
c.set_energy(E)
128133

134+
def get_catalog(self, catalog_name: str) -> Catalog | None:
135+
"""
136+
137+
Parameters
138+
----------
139+
catalog_name: str
140+
The name of the catalog
141+
142+
Returns
143+
-------
144+
The catalog instance or None
145+
146+
"""
147+
return self.__catalogs.get(catalog_name)
148+
129149
def post_init(self):
130150
"""
131151
Method triggered after all initialisations are done

pyaml/configuration/catalog.py

Lines changed: 89 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import re
2-
from typing import Pattern
3-
41
from pydantic import BaseModel, ConfigDict, model_validator
52

63
from pyaml import PyAMLException
7-
from pyaml.configuration.catalog_entry import CatalogEntry, CatalogValue
8-
from pyaml.configuration.catalog_entry import ConfigModel as CatalogEntryConfigModel
4+
from pyaml.configuration.catalog_entry import CatalogEntry, CatalogTarget
5+
from pyaml.control.catalog_view import CatalogView
96
from pyaml.control.deviceaccess import DeviceAccess
107

118
# Define the main class name for this module
@@ -46,249 +43,153 @@ def _validate_unique_references(self) -> "ConfigModel":
4643

4744
class Catalog:
4845
"""
49-
A simple registry mapping reference keys to DeviceAccess objects.
46+
Shared configuration catalog.
47+
48+
This catalog stores *prototypes* (not attached) and can be shared across
49+
multiple ControlSystem instances.
5050
51-
The catalog is intentionally minimal:
52-
- It resolves references to DeviceAccess or list[DeviceAccess]
53-
- It does NOT expose any DeviceAccess-like interface (no get/set/readback/etc.)
51+
Key points
52+
----------
53+
- sr.get_catalog(name) returns this configuration object (as you described).
54+
- Each ControlSystem (identified by name) uses a per-CS runtime view:
55+
cfg_catalog.view(control_system)
56+
- Updates are propagated to all existing views eagerly (refresh occurs
57+
immediately), while actual device connections remain lazy inside
58+
DeviceAccess backends.
59+
60+
Update semantics
61+
---------------
62+
update_proto(reference, proto) updates the shared prototype and triggers a
63+
refresh of that reference in every existing CatalogView.
5464
"""
5565

5666
def __init__(self, cfg: ConfigModel):
5767
self._cfg = cfg
58-
self._entries: dict[str, CatalogValue] = {}
68+
self._protos: dict[str, CatalogTarget] = {}
69+
70+
# Views indexed by ControlSystem name (instance identity rule).
71+
self._views: dict[str, CatalogView] = {}
72+
5973
for ref in cfg.refs:
60-
self.add(ref.get_reference(), ref.get_value())
74+
self.add_proto(ref.get_reference(), ref.get_value())
6175

6276
# ------------------------------------------------------------------
6377

6478
def get_name(self) -> str:
79+
"""Return the catalog name."""
6580
return self._cfg.name
6681

6782
# ------------------------------------------------------------------
6883

69-
def add(self, reference: str, value: CatalogValue):
70-
"""
71-
Register a reference in the catalog.
84+
def keys(self) -> list[str]:
85+
"""Return all reference keys in this catalog."""
86+
return list(self._protos.keys())
7287

73-
Raises
74-
------
75-
PyAMLException
76-
If the reference already exists.
77-
"""
78-
if reference in self._entries:
79-
raise PyAMLException(f"Duplicate catalog reference: '{reference}'")
80-
self._entries[reference] = value
88+
# ------------------------------------------------------------------
89+
90+
def has_reference(self, reference: str) -> bool:
91+
"""Return True if a prototype exists for this reference."""
92+
return reference in self._protos
8193

8294
# ------------------------------------------------------------------
8395

84-
def get(self, reference: str) -> CatalogValue:
96+
def add_proto(self, reference: str, proto: CatalogTarget):
8597
"""
86-
Resolve a reference key.
98+
Add a new prototype entry.
8799
88-
Returns
89-
-------
90-
DeviceAccess | list[DeviceAccess]
100+
Parameters
101+
----------
102+
reference : str
103+
Reference key.
104+
proto : DeviceAccess | list[DeviceAccess]
105+
Prototype device(s), not attached.
91106
92107
Raises
93108
------
94109
PyAMLException
95-
If the reference does not exist.
110+
If the reference already exists.
96111
"""
97-
try:
98-
return self._entries[reference]
99-
except KeyError as exc:
100-
raise PyAMLException(f"Catalog reference '{reference}' not found.") from exc
112+
if reference in self._protos:
113+
raise PyAMLException(f"Duplicate catalog reference: '{reference}'")
114+
self._protos[reference] = proto
115+
self._notify_update(reference)
101116

102117
# ------------------------------------------------------------------
103118

104-
def get_one(self, reference: str) -> DeviceAccess:
119+
def update_proto(self, reference: str, proto: CatalogTarget):
105120
"""
106-
Resolve a reference and ensure it corresponds to a single DeviceAccess.
121+
Update an existing prototype entry.
122+
123+
Parameters
124+
----------
125+
reference : str
126+
Existing reference key.
127+
proto : DeviceAccess | list[DeviceAccess]
128+
New prototype device(s), not attached.
107129
108130
Raises
109131
------
110132
PyAMLException
111-
If the reference does not exist or is multi-device.
133+
If the reference does not exist.
112134
"""
113-
value = self.get(reference)
114-
115-
if isinstance(value, list):
116-
raise PyAMLException(
117-
f"Catalog reference '{reference}' is multi-device; use get_many()."
118-
)
119-
120-
return value
135+
if reference not in self._protos:
136+
raise PyAMLException(f"Catalog reference '{reference}' not found.")
137+
self._protos[reference] = proto
138+
self._notify_update(reference)
121139

122140
# ------------------------------------------------------------------
123141

124-
def get_many(self, reference: str) -> list[DeviceAccess]:
142+
def get_proto(self, reference: str) -> CatalogTarget:
125143
"""
126-
Resolve a reference and ensure it corresponds to multiple DeviceAccess.
127-
128-
Returns
129-
-------
130-
list[DeviceAccess]
144+
Return the prototype for a given reference.
131145
132146
Raises
133147
------
134148
PyAMLException
135-
If the reference does not exist or is single-device.
136-
"""
137-
value = self.get(reference)
138-
139-
if not isinstance(value, list):
140-
raise PyAMLException(
141-
f"Catalog reference '{reference}' is single-device; use get_one()."
142-
)
143-
144-
return value
145-
146-
# ------------------------------------------------------------------
147-
148-
def find_by_prefix(self, prefix: str) -> dict[str, CatalogValue]:
149-
"""
150-
Return all catalog entries whose reference starts with
151-
the given prefix.
152-
153-
Parameters
154-
----------
155-
prefix : str
156-
Prefix to match at the beginning of reference keys.
157-
158-
Returns
159-
-------
160-
dict[str, CatalogValue]
161-
Mapping {reference -> DeviceAccess or list[DeviceAccess]}.
162-
163-
Notes
164-
-----
165-
- The prefix is escaped using re.escape() to avoid
166-
unintended regular expression behavior.
167-
- This is a convenience wrapper around `find()`.
168-
"""
169-
return self.find(rf"^{re.escape(prefix)}")
170-
171-
# ------------------------------------------------------------------
172-
173-
def find(self, pattern: str) -> dict[str, CatalogValue]:
174-
"""
175-
Resolve references matching a regular expression.
176-
177-
Parameters
178-
----------
179-
pattern : str
180-
Regular expression applied to reference keys.
181-
182-
Returns
183-
-------
184-
dict[str, DeviceAccess | list[DeviceAccess]]
185-
Mapping {reference -> value}.
186-
"""
187-
regex: Pattern[str] = re.compile(pattern)
188-
return {k: v for k, v in self._entries.items() if regex.search(k)}
189-
190-
# ------------------------------------------------------------------
191-
192-
def get_sub_catalog_by_prefix(self, prefix: str) -> "Catalog":
193-
"""
194-
Create a new Catalog containing only the references
195-
that start with the given prefix, and remove the prefix
196-
from the keys in the returned catalog.
197-
198-
Parameters
199-
----------
200-
prefix : str
201-
Prefix to match at the beginning of reference keys.
202-
203-
Returns
204-
-------
205-
Catalog
206-
A new Catalog instance containing only the matching
207-
references, with the prefix removed from their keys.
208-
209-
Notes
210-
-----
211-
- The prefix is matched literally (no regex behavior).
212-
- The underlying DeviceAccess instances are NOT copied;
213-
the same objects are reused.
214-
- If no references match, an empty Catalog is returned.
215-
- If removing the prefix results in duplicate keys,
216-
a PyAMLException is raised.
149+
If the reference does not exist.
217150
"""
218-
sub_catalog = Catalog(ConfigModel(name=self.get_name() + "/" + prefix, refs=[]))
219-
220-
for key, value in self._entries.items():
221-
if key.startswith(prefix):
222-
# Remove prefix from key
223-
new_key = key[len(prefix) :]
224-
225-
if not new_key:
226-
raise PyAMLException(
227-
f"Removing prefix '{prefix}' from '{key}' "
228-
"results in an empty reference."
229-
)
230-
231-
sub_catalog.add(new_key, value)
232-
233-
return sub_catalog
151+
try:
152+
return self._protos[reference]
153+
except KeyError as exc:
154+
raise PyAMLException(f"Catalog reference '{reference}' not found.") from exc
234155

235156
# ------------------------------------------------------------------
236157

237-
def get_sub_catalog(self, pattern: str) -> "Catalog":
158+
def view(self, control_system: "ControlSystem") -> CatalogView: # noqa: F821
238159
"""
239-
Create a new Catalog containing only the references
240-
matching the given regular expression.
160+
Return a per-ControlSystem runtime view of this catalog.
241161
242162
Parameters
243163
----------
244-
pattern : str
245-
Regular expression applied to reference keys.
164+
control_system : ControlSystem
165+
ControlSystem instance. Its name identifies the view.
246166
247167
Returns
248168
-------
249-
Catalog
250-
A new Catalog instance containing only the matching
251-
references and their associated DeviceAccess objects.
169+
CatalogView
170+
Runtime view bound to the provided ControlSystem.
252171
253172
Notes
254173
-----
255-
- The returned catalog is independent from the original one.
256-
- The underlying DeviceAccess objects are not copied; the
257-
same instances are reused.
258-
- If no references match, an empty Catalog is returned.
174+
- A view is created once per ControlSystem name and cached.
175+
- The view is immediately refreshed (all entries attached in that CS context).
259176
"""
260-
data = self.find(pattern)
261-
262-
# Create a new empty catalog with a derived name
263-
sub_catalog = Catalog(
264-
ConfigModel(name=self.get_name() + "/" + pattern, refs=[])
265-
)
266-
267-
# Re-register matching entries in the new catalog
268-
for k, v in data.items():
269-
sub_catalog.add(k, v)
270-
return sub_catalog
271-
272-
# ------------------------------------------------------------------
273-
274-
def keys(self) -> list[str]:
275-
"""Return all catalog reference keys."""
276-
return list(self._entries.keys())
177+
cs_name = control_system.name()
178+
view = self._views.get(cs_name)
179+
if view is None:
180+
view = CatalogView(config_catalog=self, control_system=control_system)
181+
self._views[cs_name] = view
182+
view.refresh_all()
183+
return view
277184

278185
# ------------------------------------------------------------------
279186

280-
def has_reference(self, reference: str) -> bool:
187+
def _notify_update(self, reference: str):
281188
"""
282-
Return True if the reference exists in the catalog.
189+
Notify all existing views that a reference prototype has changed.
283190
284-
Parameters
285-
----------
286-
reference : str
287-
Catalog reference key.
288-
289-
Returns
290-
-------
291-
bool
292-
True if the reference exists, False otherwise.
191+
We choose eager propagation: every view refreshes that reference immediately.
192+
DeviceAccess backends remain lazy, so this is typically cheap.
293193
"""
294-
return reference in self._entries
194+
for view in self._views.values():
195+
view.refresh_reference(reference)

0 commit comments

Comments
 (0)