Skip to content

Commit fe8f290

Browse files
committed
Initial commit
1 parent d0e9763 commit fe8f290

9 files changed

Lines changed: 763 additions & 1 deletion

File tree

.github/workflows/build.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: p4p_ext
2+
3+
on: [push, pull_request, workflow_dispatch]
4+
5+
jobs:
6+
lint:
7+
name: Use Ruff to perform linting, formatting, and other code quality tests
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: actions/setup-python@v5
12+
with:
13+
python-version: "3.x"
14+
- run: pip install .[test]
15+
- run: |
16+
ruff check --no-fix
17+
ruff format --diff
18+
19+
test:
20+
name: Run tests on multiple Python versions
21+
needs:
22+
- lint
23+
strategy:
24+
matrix:
25+
os: [ubuntu-latest] #, mac-latest]
26+
python-version: ["3.10", "3.11", "3.12"]
27+
runs-on: ${{ matrix.os }}
28+
continue-on-error: true
29+
steps:
30+
- uses: actions/checkout@v4
31+
- name: Set up Python ${{ matrix.python-version }}
32+
uses: actions/setup-python@v5
33+
with:
34+
python-version: ${{ matrix.python-version }}
35+
- name: Install dependencies
36+
run: |
37+
pip install .[test]
38+
PIP_PATH=$(which pip)
39+
$PIP_PATH install .[test]
40+
- name: Run tests
41+
run: |
42+
PYTHON_PATH=$(which python)
43+
$PYTHON_PATH -m coverage run --source=. -m unittest discover tests/
44+
- name: Gather coverage statistics
45+
if: ${{ always() }}
46+
run: |
47+
coverage report -m
48+
coverage xml
49+
- name: Upload pytest test results
50+
if: ${{ always() }}
51+
uses: actions/upload-artifact@v4
52+
with:
53+
name: coverage-results-${{ matrix.os }}-${{ matrix.python-version }}
54+
path: coverage.xml
55+
# Use always() to always run this step to publish test results when there are test failures

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

.vscode/settings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"python.testing.unittestArgs": [
3+
"-v",
4+
"-s",
5+
"./tests",
6+
"-p",
7+
"test_*.py"
8+
],
9+
"python.testing.pytestEnabled": false,
10+
"python.testing.unittestEnabled": true
11+
}

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,16 @@
11
# p4p_ext
2-
Extensions and additional functionality for the p4p library
2+
Opinionated extensions and additional functionality for the p4p library.
3+
4+
### Python Version
5+
These extensions make extensive use of typing. As such they require Python 3.9 or later.
6+
7+
### p4p Version
8+
These extensions require a version of p4p with [PR172](https://github.com/epics-base/p4p/pull/172). This will be installed as a dependency with the package, but note there is a potential for conflict with other installed instances.
9+
10+
## Extensions
11+
### CompositeHandler
12+
The base version of p4p only allows a single Handler class to be associated with a SharedPV. This makes it complex to combine multiple handlers from different sources. It also means that if a Handler is used to handle NormativeType logic then it either precludes a user handler or requires subclassing.
13+
14+
The supplied CompositeHandler has the same interface as the base Handler. It uses an OrderedDict to store componenent Handlers (standard Handlers, per base p4p) and calls them in the specified order.
15+
16+
It is designed to work with the Handler class, and is **not** designed to work with the Handler decorators.

p4p_ext/__init__.py

Whitespace-only changes.

p4p_ext/handler.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Composite Handler allows multiple standard handlers to be combined into a single handler.
3+
4+
And ordered dictionary is used to make the component handlers accessible by name.
5+
The ordered dictionary also controls the order in which the handlers are called.
6+
"""
7+
8+
from collections import OrderedDict
9+
from typing import Optional
10+
11+
from p4p import Value
12+
from p4p.server import ServerOperation
13+
from p4p.server.thread import Handler, SharedPV
14+
from typing_extensions import deprecated
15+
16+
17+
class HandlerException(Exception):
18+
"""Exception raised for errors in the handler operations."""
19+
20+
21+
class AbortHandlerException(HandlerException):
22+
"""Exception raised to abort the current operation in the handler."""
23+
24+
def __init__(self, message: str = "Operation aborted"):
25+
super().__init__(message)
26+
self.message = message
27+
28+
29+
class CompositeHandler(Handler):
30+
"""Composite Handler for combining multiple component handlers into a single handler."""
31+
32+
def __init__(self, handlers: Optional[OrderedDict[str, Handler]] = None):
33+
"""Initialize the CompositeHandler with an optional list of handlers."""
34+
super().__init__()
35+
self.handlers = handlers
36+
37+
def __getitem__(self, name: str) -> Handler | None:
38+
if self.handlers:
39+
return self.handlers[name]
40+
41+
return None
42+
43+
def open(self, value: Value):
44+
"""Open all handlers in the composite handler."""
45+
if self.handlers:
46+
for handler in self.handlers.values():
47+
handler.open(value)
48+
49+
def put(self, pv: SharedPV, op: ServerOperation):
50+
errmsg = None
51+
52+
if self.handlers:
53+
for handler in self.handlers.values():
54+
try:
55+
handler.put(pv, op)
56+
except AbortHandlerException as e:
57+
errmsg = e.message
58+
break
59+
60+
op.done(error=errmsg)
61+
62+
def post(self, pv: SharedPV, value: Value):
63+
if self.handlers:
64+
for handler in self.handlers.values():
65+
handler.post(pv, value)
66+
67+
def rpc(self, pv: SharedPV, op: ServerOperation):
68+
errmsg = None
69+
70+
if self.handlers:
71+
for handler in self.handlers.values():
72+
try:
73+
handler.rpc(pv, op)
74+
except AbortHandlerException as e:
75+
errmsg = e.message
76+
break
77+
78+
op.done(error=errmsg)
79+
80+
def on_first_connect(self, pv: SharedPV):
81+
"""Called when the first client connects to the PV."""
82+
if self.handlers:
83+
for handler in self.handlers.values():
84+
handler.onFirstConnect(pv)
85+
86+
@deprecated("onFirstConnect is deprecated, use on_first_connect instead")
87+
def onFirstConnect(self, pv: Value):
88+
self.on_first_connect(pv)
89+
90+
def on_last_connect(self, pv: SharedPV):
91+
"""Called when the last client channel is closed."""
92+
if self.handlers:
93+
for handler in self.handlers.values():
94+
handler.onFirstConnect(pv)
95+
96+
@deprecated("onLastDisconnect is deprecated, use on_last_connect instead")
97+
def onLastDisconnect(self, pv: Value):
98+
self.on_last_connect(pv)
99+
100+
def close(self, pv: SharedPV):
101+
if self.handlers:
102+
for handler in self.handlers.values():
103+
handler.close(pv)

pyproject.toml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[project]
2+
name = "p4p-ext"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.9"
7+
dependencies = [
8+
"p4p",
9+
"typing-extensions>=4.14.1",
10+
]
11+
classifiers = [
12+
"Development Status :: 2 - Pre-Alpha",
13+
"Environment :: Console",
14+
"Intended Audience :: Developers",
15+
"License :: OSI Approved :: BSD License",
16+
"Programming Language :: Python",
17+
"Typing :: Typed"
18+
]
19+
license = {text = "BSD-3-Clause"}
20+
keywords = ["p4p", "epics"]
21+
authors = [
22+
{name = "Ivan Finch", email = "ivan.finch@stfc.ac.uk"},
23+
{name = "Ajit Kurup"},
24+
{name = "Kathryn Baker", email = "k.baker@stfc.ac.uk"},
25+
{name = "Aqeel AlShafei", email = "aqeel.alshafei@stfc.ac.uk"}
26+
]
27+
maintainers = [
28+
{name = "Ivan Finch", email = "ivan.finch@stfc.ac.uk"},
29+
]
30+
31+
[project.optional-dependencies]
32+
test = [
33+
"ruff>=0.12",
34+
]
35+
36+
37+
[tool.uv.sources]
38+
p4p = { git = "https://github.com/ISISNeutronMuon/p4p" }
39+
40+
[tool.ruff]
41+
# Allow lines to be as long as 120.
42+
line-length = 120
43+
44+
[tool.ruff.lint]
45+
extend-select = [
46+
"UP", # pyupgrade
47+
"I", # isort
48+
]

0 commit comments

Comments
 (0)