Skip to content

Commit 0457353

Browse files
committed
Simplify use of IOCRecordServer by unpacking multiple providers provided through .providers
1 parent d6deeef commit 0457353

3 files changed

Lines changed: 50 additions & 9 deletions

File tree

examples/asyncio/ioc_fields_server.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
Uses `IOCRecordServer`, which builds "<name>.<FIELD>" sub-PVs for free for
66
any base PV in a providers=[] entry -- either an explicit `IOCRecordProvider`
77
(needed for per-PV overrides, as for EXAMPLE:PV's dtyp_choices/fields below;
8-
note it isn't itself a single provider, so it's spread via `*base.providers`
9-
below), or, for the common case needing no overrides, a plain {name: pv} dict,
8+
passed directly below, same as any other provider -- `IOCRecordServer`
9+
unpacks it internally even though it isn't itself a single provider), or,
10+
for the common case needing no overrides, a plain {name: pv} dict,
1011
same as any ordinary p4p server (see EXAMPLE:PV2 below). Every sub-PV is built
1112
lazily, on first client connection, as a plain thread-flavored SharedPV --
1213
regardless of the base PV's own flavor (the asyncio `SharedPV` imported below,
@@ -47,7 +48,7 @@ async def main():
4748
)
4849
pvs = {"EXAMPLE:PV2": SharedPV(nt=NTScalar("i"), initial=0)}
4950

50-
with IOCRecordServer(providers=[*base.providers, pvs]):
51+
with IOCRecordServer(providers=[base, pvs]):
5152
print("Serving:", list(base.providers[0].keys()) + list(pvs.keys()))
5253
print("Also serving <PV>.<FIELD> for FIELD in:", ", ".join(sorted(FIELD_NAMES)))
5354
try:

p4pillon/server/records/server.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
__all__ = ("IOCRecordServer",)
1919

2020

21+
def _with_order(item: Any, order: int | None) -> Any:
22+
# p4p.server.Server's providers= entries are either a bare provider or a
23+
# (provider, order) tuple -- reattach whichever `order` this entry came
24+
# in with (or none) to each provider it expands to.
25+
return (item, order) if order is not None else item
26+
27+
2128
def _dynamic_fields_provider(provider: Any) -> _DynamicProvider | None:
2229
# Builds the *additional* DynamicRecordFields-backed provider for a
2330
# plain-dict entry's "<name>.<FIELD>" sub-PVs (see the class docstring
@@ -66,13 +73,22 @@ class IOCRecordServer(_Server):
6673
`StaticRecordProvider`'s `~p4p.server.StaticProvider` -- maintains no
6774
enumerable list of the names it can serve.
6875
76+
An `IOCRecordProvider` instance can also be passed directly, same as a
77+
plain dict or a `StaticRecordProvider` -- unlike `~p4p.server.Server`
78+
itself, which has no concept of a single `providers=` entry backed by two
79+
providers underneath (see `IOCRecordProvider`'s docstring for why it
80+
isn't itself a `~p4p.server.StaticProvider`/`~p4p.server.DynamicProvider`),
81+
`IOCRecordServer` unpacks it into its `.providers` pair automatically, so
82+
``providers=[base, pvs]`` works the same as
83+
``providers=[*base.providers, pvs]``.
84+
6985
For DESC updates after add(), per-PV overrides, matching sub-PV flavor to
7086
an asyncio base PV, or `pvlist` visibility, build a `StaticRecordProvider`
7187
explicitly (see its own docstring) and pass that instead of a dict;
72-
entries that aren't a plain dict (a provider name string, or an
73-
already-constructed provider instance, including a `StaticRecordProvider`)
74-
are passed through to `~p4p.server.Server` unchanged, with no extra
75-
"<name>.<FIELD>" handling added.
88+
entries that aren't a plain dict or an `IOCRecordProvider` (a provider
89+
name string, or an already-constructed provider instance, including a
90+
`StaticRecordProvider`) are passed through to `~p4p.server.Server`
91+
unchanged, with no extra "<name>.<FIELD>" handling added.
7692
"""
7793

7894
def __init__(self, providers: list[Any], isolate: bool = False, **kws: Any) -> None:
@@ -86,9 +102,17 @@ def __init__(self, providers: list[Any], isolate: bool = False, **kws: Any) -> N
86102
wrapped: list[Any] = []
87103
for entry in providers:
88104
provider, order = entry if isinstance(entry, tuple) else (entry, None)
89-
wrapped.append((provider, order) if order is not None else provider)
105+
sub_providers = getattr(provider, "providers", None)
106+
if isinstance(sub_providers, tuple):
107+
# Already backed by its own provider pair (e.g.
108+
# IOCRecordProvider's static + DynamicRecordFields pair) --
109+
# unpack rather than treating it as a single provider (it
110+
# isn't one) or a dict (it has no .items()).
111+
wrapped.extend(_with_order(sub_provider, order) for sub_provider in sub_providers)
112+
continue
113+
wrapped.append(_with_order(provider, order))
90114
fields_provider = _dynamic_fields_provider(provider)
91115
if fields_provider is not None:
92116
self._field_providers.append(fields_provider)
93-
wrapped.append((fields_provider, order) if order is not None else fields_provider)
117+
wrapped.append(_with_order(fields_provider, order))
94118
super().__init__(wrapped, isolate=isolate, **kws)

tests/unit/server/test_records.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,22 @@ def test_mixed_dict_and_provider_entries(self):
431431
assert C.get("EXAMPLE:PV.RTYP") == "ai"
432432
assert C.get("EXPLICIT:PV.RTYP") == "ai"
433433

434+
def test_ioc_record_provider_passed_directly(self):
435+
# An IOCRecordProvider isn't itself a single provider (it holds a
436+
# StaticProvider + DynamicProvider pair, see its own docstring) --
437+
# IOCRecordServer should unpack it automatically, so passing it bare
438+
# works the same as spreading it via *base.providers.
439+
base = IOCRecordProvider("base")
440+
base.add("EXAMPLE:PV", _pv(), dtyp_choices=["Soft Channel", "Raw Soft Channel"])
441+
pvs = {"EXAMPLE:PV2": _pv()}
442+
443+
with IOCRecordServer(providers=[base, pvs], isolate=True) as S:
444+
with Context("pva", conf=S.conf(), useenv=False) as C:
445+
assert C.get("EXAMPLE:PV") == 1.234
446+
assert C.get("EXAMPLE:PV.RTYP") == "ai"
447+
assert C.get("EXAMPLE:PV.DTYP").raw["value.choices"] == ["Soft Channel", "Raw Soft Channel"]
448+
assert C.get("EXAMPLE:PV2.RTYP") == "ai"
449+
434450

435451
class TestDynamicRecordFields:
436452
def test_unknown_fields_key_warns_eagerly_at_construction(self):

0 commit comments

Comments
 (0)