Skip to content

Feat: improve pva support using latest pvi changes#383

Open
ggayDiamond wants to merge 11 commits into
mainfrom
feat/improve-pva-support
Open

Feat: improve pva support using latest pvi changes#383
ggayDiamond wants to merge 11 commits into
mainfrom
feat/improve-pva-support

Conversation

@ggayDiamond
Copy link
Copy Markdown
Contributor

@ggayDiamond ggayDiamond commented May 26, 2026

Summary

Update fastcs to leverage pvi 0.14.0b1 and enhance PVA field name handling for hyphened PV prefixes.
Adds GroupLayout support for controlling sub-controller GUI layout (inline vs. subscreens).

Changes

Core Features

pvi 0.14.0b1 Upgrade: Incorporates improvements to PVI library:

  • Group label preservation through SubScreen reconstructions
  • Flattened Grid group handling for INLINE controller hoisting
  • Enhanced support for dynamic GroupLayout modes

GroupLayout Support: New GroupLayout enum allows controllers to specify:

  • SUBSCREEN: Traditional sub-controller screens (default)
  • INLINE: Embed sub-controllers inline in parent screen

PVA Field Name Sanitization: Replace non-alphanumeric characters (e.g., hyphens from beamline-style PV prefixes like BL04I-EA-E1RIO-01) with underscores to comply with P4P field naming requirements

Improvements

GUI Title Handling: EpicsGUIOptions.title now optional with intelligent fallback:

  • Derives meaningful defaults from controller IDs when not explicitly provided
  • Replaces hardcoded "FastCS Devices" placeholder
  • Improves clarity in GUI screens and documentation

Label Preservation: Fixed downstream sub-controller names now properly preserved as Group display labels through screen transformations

Summary by CodeRabbit

  • New Features

    • Added GroupLayout (SUBSCREEN/INLINE) to control sub-controller layout in EPICS GUIs.
  • Refactor

    • Controllers and controller collections accept an optional path at construction (removes later path-setting calls).
    • GUI generation now falls back to controller ID when no explicit screen title is provided.
  • Chores

    • Pinned PVI dependency to 0.14.0b1.

Review Change Stack

gilesknap and others added 7 commits May 20, 2026 13:33
Closes #373.

`Controller.path` is now a keyword-only ``__init__`` parameter on
``BaseController`` (default ``[]``) and is threaded through user code
rather than seeded post-construction. The launcher builds each root
with ``cls(options, path=[entry.id])`` (or ``cls(path=[entry.id])``
when no options); a parent constructs its subs with the full path
already baked in (e.g. ``Sub(path=self.path + [name])``).

``BaseController.set_path()`` is removed entirely.
``add_sub_controller(name, sub)`` no longer mutates ``sub._path`` --
it sanity-asserts ``sub.path == parent.path + [name]`` and rejects
the call with a clear error if the caller forgot to thread the path.
``_build_api()`` reads ``self._path`` directly, eliminating the
parent-side path argument and the recursive re-prefix it required.

Custom ``Controller.__init__`` (root and sub-controller alike) must
now accept ``path`` and forward it to ``super().__init__``. Demo
controllers and doc snippets are updated to the new shape; tests
either construct with ``path=[...]`` from the start or move the id
declaration above the controller construction so sub paths can be
threaded explicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
It decouples sub-controller's path assignment from controller registration.
The path pre-validation in `add_sub_controller` was removed in 63ffd94
to decouple sub-controller path assignment from registration; the
`does not match parent path` ValueError no longer fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update fastcs to use pvi 0.14.0b1 beta which includes:
- Group label preservation through SubScreen reconstructions
- Flattened Grid group handling for INLINE controller hoisting
- Improved support for dynamic GroupLayout modes
The name passed to Group() in extract_api_components() must satisfy the
PascalCase constraint required by PVI ('^([A-Z][a-z0-9]*)*$').  When a
controller's path segment contains characters outside that set (hyphens,
digits in certain positions, etc.) the name has to be sanitised before
it can be used as the Group key.  PVI then derives the button/screen
label from that sanitised name via to_title_case(), producing something
unreadable (e.g. 'Bl 04i Ea E1rio 01') rather than the intended string.

ControllerAPI.path already holds the original, unsanitised path
segments.  Pass api.path[-1] as the optional label= field on the Group
so that PVI's Component.get_label() returns the original name verbatim
instead of falling back to to_title_case(name).

This is backwards-compatible: label defaults to None, so controllers
whose path is empty are unaffected and existing behaviour is preserved.
Introduce a GroupLayout enum (SUBSCREEN | INLINE) that lets callers
specify, per sub-controller, how its children should be presented on
the parent screen.

SUBSCREEN (default) — children appear on a separate screen opened by a
navigate button, preserving the previous behaviour.

INLINE — children are rendered as an inline Grid block directly on the
parent screen, without an extra navigation level.

The enum is decoupled from PVI: it lives in fastcs.controllers, which
has no dependency on any transport. The EPICS GUI layer (gui.py)
resolves it to the appropriate pvi.device layout object when building
the component tree.

Usage:

    from fastcs.controllers import Controller, GroupLayout
    class MyController(Controller):
        group_layout = GroupLayout.INLINE   # class-level default

    # or per-instance:
    ctrl = MyController(group_layout=GroupLayout.INLINE)
    parent.add_sub_controller("Ctrl", ctrl)
Sanitize child controller names for PVA field names by replacing non-alphanumeric characters (e.g. hyphens from beamline PV prefix formats like 'BL04I-EA-E1RIO-01') with underscores. This prevents P4P validation errors.

Also make EpicsGUIOptions.title optional (defaults to None) and derive a meaningful default from the controller's path when not explicitly provided, improving upon the hardcoded 'FastCS Devices' placeholder. This allows GUI screens and docs to display the controller ID as the title when no explicit title is set.

Updates schema.json files to reflect the title field now accepting null values.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@ggayDiamond, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 29 minutes and 41 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f08de3fb-f9a7-4cf0-ba7e-467969b158c5

📥 Commits

Reviewing files that changed from the base of the PR and between 1380ac2 and 8eb8a11.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • pyproject.toml
  • src/fastcs/controllers/controller_api.py
  • src/fastcs/transports/epics/emission.py
  • src/fastcs/transports/epics/gui.py
  • tests/test_controllers.py
  • tests/test_launch.py
  • tests/transports/epics/pva/test_p4p.py
📝 Walkthrough

Walkthrough

This PR refactors controller path initialization from post-construction (Controller() + set_path()) to constructor-time (Controller(path=[...])), adds a GroupLayout enum to ControllerAPI, updates transports to respect group layout and title fallbacks, and revises tests and demo snippets to use constructor paths.

Changes

Path and Layout Support

Layer / File(s) Summary
GroupLayout enum and ControllerAPI integration
src/fastcs/controllers/controller_api.py, src/fastcs/controllers/__init__.py
New GroupLayout enum (SUBSCREEN, INLINE) and ControllerAPI.group_layout field added; GroupLayout re-exported at package level.
BaseController path and group_layout support
src/fastcs/controllers/base_controller.py
BaseController.__init__ accepts keyword-only path and group_layout; _path normalized; set_path() removed; _build_api() uses stored self._path.
Controller hierarchy path forwarding
src/fastcs/controllers/controller.py, src/fastcs/controllers/controller_vector.py
Controller and ControllerVector accept path/group_layout and forward to BaseController; API/task creation uses _build_api() without external path args.

Demo and Transport Implementation

Layer / File(s) Summary
Demo controller hierarchical paths
src/fastcs/demo/controllers.py, docs/snippets/dynamic.py, docs/snippets/static*.py
TemperatureController and TemperatureRampController accept path and construct/register ramp sub-controllers with hierarchical paths (self.path + [ramp_name]). All demo snippets updated to pass path at construction.
EPICS GUI layout rendering
src/fastcs/transports/epics/gui.py
EpicsGUI chooses group container type from api.group_layout (Grid for INLINE, SubScreen otherwise).
EPICS options and title fallback
src/fastcs/transports/epics/options.py, src/fastcs/transports/epics/emission.py, src/fastcs/demo/schema.json, tests/data/schema.json
EpicsGUIOptions.title is now optional (`str
EPICS ioc sanitization and launch integration
src/fastcs/transports/epics/ca/ioc.py, src/fastcs/launch.py
Sanitize sub-controller names for PVI fields; launch instantiation passes path=[entry.id] into controllers instead of calling set_path() afterward.

Test Updates

Layer / File(s) Summary
Test infrastructure and controller updates
tests/assertable_controller.py, tests/conftest.py, tests/test_controllers.py, tests/test_launch.py, tests/test_multi_controller.py
Test controllers updated to accept path and propagate it; AssertableControllerAPI no longer takes path and calls controller._build_api() without args; error-message expectations updated to reflect new signatures.
Test instantiation and fixtures
tests/benchmarking/controller.py, tests/example_p4p_ioc.py, tests/example_softioc.py, tests/transports/epics/ca/test_gui.py, tests/transports/epics/ca/test_initial_value.py, tests/transports/epics/ca/test_softioc.py, tests/transports/epics/pva/test_p4p.py, tests/transports/epics/test_emission.py, tests/transports/graphQL/test_graphql.py, tests/transports/tango/test_dsr.py
All test controllers and helpers now construct controllers with explicit path=[...]. make_fastcs helper simplified to accept only a Controller instance whose path is preconfigured.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

Suggested Reviewers

  • coretl

🐰 Paths set at birth, not later drawn,

constructors hum from dusk to dawn.
Subcontrollers nest in tidy arrays,
GroupLayout frames the GUI's blaze. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: improve pva support using latest pvi changes' accurately reflects the main objective of the PR, which is to improve PVA support by upgrading to pvi 0.14.0b1 and implementing related enhancements. However, it is somewhat general and does not capture all significant changes (GroupLayout enum, field name sanitization, optional titles, path handling improvements).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/improve-pva-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 26, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.26%. Comparing base (c80a13c) to head (8eb8a11).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #383      +/-   ##
==========================================
+ Coverage   91.20%   91.26%   +0.06%     
==========================================
  Files          72       72              
  Lines        2875     2885      +10     
==========================================
+ Hits         2622     2633      +11     
+ Misses        253      252       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/transports/epics/pva/test_p4p.py (1)

312-312: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix missing colon and verify PV path structure.

The PV string is missing a colon separator between pv_prefix and Child. Additionally, based on the controller setup at lines 280-283, the ControllerVector children don't have child_child attributes—only the direct sub-controllers (child0, child1, etc.) created at lines 286-292 have child_child. This monitor will fail because the PV doesn't exist.

🐛 Proposed fix

If the intent is to monitor the child_child of the first ControllerVector element, you'll need to add child_child attributes to those controllers. However, it's more likely you meant to monitor child0.child_child:

-    f"{pv_prefix}Child:0:ChildChild:PVI", child_child_child_controller_pvi.append
+    f"{pv_prefix}:Child0:ChildChild:PVI", child_child_child_controller_pvi.append
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/transports/epics/pva/test_p4p.py` at line 312, The PV path passed to
child_child_child_controller_pvi is malformed and targets a non-existent
attribute on ControllerVector children: add the missing colon after pv_prefix
and correct the path to reference the first child's child_child (i.e., use
pv_prefix + ":Child:0:child_child:PVI" semantics) or, alternatively, modify the
ControllerVector children (created around ControllerVector and child0/child1) to
expose a child_child attribute so the original PV path becomes valid; ensure you
update the PV string and/or the controller setup to match the actual attribute
hierarchy (symbols to inspect: pv_prefix, child_child_child_controller_pvi,
ControllerVector, child0).
🧹 Nitpick comments (2)
src/fastcs/controllers/controller_vector.py (1)

22-25: ⚡ Quick win

Expose group_layout in ControllerVector.__init__ for API symmetry.

Controller supports per-instance group_layout, but ControllerVector currently does not. Adding it keeps the new layout feature consistent across controller types.

Proposed refactor
 from fastcs.controllers.base_controller import BaseController
 from fastcs.controllers.controller import Controller
+from fastcs.controllers.controller_api import GroupLayout
 from fastcs.util import Controller_T
@@
     def __init__(
         self,
         children: Mapping[int, Controller_T],
         description: str | None = None,
         ios: Sequence[AnyAttributeIO] | None = None,
         *,
         path: list[str] | None = None,
+        group_layout: GroupLayout | None = None,
     ) -> None:
-        super().__init__(description=description, ios=ios, path=path)
+        super().__init__(
+            description=description,
+            ios=ios,
+            path=path,
+            group_layout=group_layout,
+        )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fastcs/controllers/controller_vector.py` around lines 22 - 25,
ControllerVector.__init__ should accept the same per-instance group_layout as
Controller: add a new parameter group_layout: GroupLayout | None = None (or
matching Controller's type) to the ControllerVector.__init__ signature and
forward it into the super().__init__ call (i.e.
super().__init__(description=description, ios=ios, path=path,
group_layout=group_layout)); ensure imports/typing match the Controller's
GroupLayout type and update any callers/tests that construct ControllerVector
without breaking defaults.
tests/transports/epics/pva/test_p4p.py (1)

281-281: 💤 Low value

Consider spread syntax for list concatenation.

For modern Python style, spread syntax is slightly more explicit than concatenation.

♻️ Optional refactor
     sub_controller_vector = ControllerVector(
-        {i: ChildController(path=child_path + [str(i)]) for i in range(3)},
+        {i: ChildController(path=[*child_path, str(i)]) for i in range(3)},
         path=child_path,
     )
         sub_controller.child_child = ChildChildController(
-            path=sub_path + ["child_child"]
+            path=[*sub_path, "child_child"]
         )

Also applies to: 291-291

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/transports/epics/pva/test_p4p.py` at line 281, Replace list
concatenation using child_path + [str(i)] with Python's spread/unpacking syntax
to be more explicit and modern; update the dict comprehension that constructs
ChildController(path=child_path + [str(i)]) to
ChildController(path=[*child_path, str(i)]) and make the identical change at the
other occurrence referenced (the second instance around line 291) so both
comprehensions use [*child_path, str(i)].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pyproject.toml`:
- Around line 37-38: The dependency entries for pvi in pyproject.toml (under
epicsca and epicspva) use an open-ended spec "pvi>=0.14.0b1" which can pull in
future breaking releases; update both entries to constrain the upper bound (for
example "pvi>=0.14.0b1,<0.15") or pin to "pvi==0.14.0b1" depending on whether
you want a range or exact pin so that epicsca and epicspva do not accept
unintended future pvi versions.

In `@src/fastcs/controllers/controller_api.py`:
- Line 45: The docstring in src/fastcs/controllers/controller_api.py (the
triple-quoted string describing "How this controller's children are laid out
when rendered inside a parent screen.") exceeds the configured line length;
shorten or split the sentence into multiple lines to satisfy Ruff E501 (e.g.,
break into two shorter sentences or rephrase) so the docstring lines are within
the max length while preserving the same meaning; update the triple-quoted
docstring in the module/class/method where that text appears.

In `@src/fastcs/transports/epics/emission.py`:
- Around line 148-158: The current call to _render_index_md uses "options.title
or (...)" which treats an empty string as missing; change it to only fall back
when options.title is None (e.g. use a conditional or ternary) so explicit empty
titles "" are preserved; update the invocation of
_render_index_md(controller_apis, options.title) to compute the fallback title
only when options.title is None, leaving options.title unchanged for "" values.

---

Outside diff comments:
In `@tests/transports/epics/pva/test_p4p.py`:
- Line 312: The PV path passed to child_child_child_controller_pvi is malformed
and targets a non-existent attribute on ControllerVector children: add the
missing colon after pv_prefix and correct the path to reference the first
child's child_child (i.e., use pv_prefix + ":Child:0:child_child:PVI" semantics)
or, alternatively, modify the ControllerVector children (created around
ControllerVector and child0/child1) to expose a child_child attribute so the
original PV path becomes valid; ensure you update the PV string and/or the
controller setup to match the actual attribute hierarchy (symbols to inspect:
pv_prefix, child_child_child_controller_pvi, ControllerVector, child0).

---

Nitpick comments:
In `@src/fastcs/controllers/controller_vector.py`:
- Around line 22-25: ControllerVector.__init__ should accept the same
per-instance group_layout as Controller: add a new parameter group_layout:
GroupLayout | None = None (or matching Controller's type) to the
ControllerVector.__init__ signature and forward it into the super().__init__
call (i.e. super().__init__(description=description, ios=ios, path=path,
group_layout=group_layout)); ensure imports/typing match the Controller's
GroupLayout type and update any callers/tests that construct ControllerVector
without breaking defaults.

In `@tests/transports/epics/pva/test_p4p.py`:
- Line 281: Replace list concatenation using child_path + [str(i)] with Python's
spread/unpacking syntax to be more explicit and modern; update the dict
comprehension that constructs ChildController(path=child_path + [str(i)]) to
ChildController(path=[*child_path, str(i)]) and make the identical change at the
other occurrence referenced (the second instance around line 291) so both
comprehensions use [*child_path, str(i)].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2fd66ca9-6858-4e64-be53-ef47bc31744e

📥 Commits

Reviewing files that changed from the base of the PR and between c80a13c and b999582.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (42)
  • docs/snippets/dynamic.py
  • docs/snippets/static04.py
  • docs/snippets/static05.py
  • docs/snippets/static06.py
  • docs/snippets/static07.py
  • docs/snippets/static08.py
  • docs/snippets/static09.py
  • docs/snippets/static10.py
  • docs/snippets/static11.py
  • docs/snippets/static12.py
  • docs/snippets/static13.py
  • docs/snippets/static14.py
  • docs/snippets/static15.py
  • pyproject.toml
  • src/fastcs/controllers/__init__.py
  • src/fastcs/controllers/base_controller.py
  • src/fastcs/controllers/controller.py
  • src/fastcs/controllers/controller_api.py
  • src/fastcs/controllers/controller_vector.py
  • src/fastcs/demo/controllers.py
  • src/fastcs/demo/schema.json
  • src/fastcs/launch.py
  • src/fastcs/transports/epics/ca/ioc.py
  • src/fastcs/transports/epics/emission.py
  • src/fastcs/transports/epics/gui.py
  • src/fastcs/transports/epics/options.py
  • tests/assertable_controller.py
  • tests/benchmarking/controller.py
  • tests/conftest.py
  • tests/data/schema.json
  • tests/example_p4p_ioc.py
  • tests/example_softioc.py
  • tests/test_controllers.py
  • tests/test_launch.py
  • tests/test_multi_controller.py
  • tests/transports/epics/ca/test_gui.py
  • tests/transports/epics/ca/test_initial_value.py
  • tests/transports/epics/ca/test_softioc.py
  • tests/transports/epics/pva/test_p4p.py
  • tests/transports/epics/test_emission.py
  • tests/transports/graphQL/test_graphql.py
  • tests/transports/tango/test_dsr.py

Comment thread pyproject.toml Outdated
Comment thread src/fastcs/controllers/controller_api.py Outdated
Comment thread src/fastcs/transports/epics/emission.py
@ggayDiamond ggayDiamond force-pushed the feat/improve-pva-support branch from 1380ac2 to 267462b Compare May 27, 2026 11:16
- Change return type from Tree to ResolvedTree to match Group.children parameter
- Add cast() to handle list[ComponentUnion | Include] to ResolvedTree conversion
- The method never appends Include objects, so the cast is semantically safe
- Fixes pyright error: Type parameter variance issue with Include in Tree union

This resolves the type checking failure when running 'tox -p' without affecting
the runtime behavior of the code.
…o-options path

Add two targeted tests to improve code coverage:

1. test_controller_with_group_layout() - Tests that the group_layout parameter
   is properly initialized in BaseController.__init__() when explicitly passed
   and that it defaults to GroupLayout.SUBSCREEN. Covers line 57 in base_controller.py.

2. test_launch_single_arg_no_options() - Tests the launch system's handling of
   controllers that take no configuration options. Exercises the code path where
   registered.cls() is called with only path=[entry.id]. Covers line 236 in launch.py.

These tests address CodeCov patch coverage report gaps.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants