Skip to content

Commit

Permalink
Merge pull request #64 from pymeasure/old-python
Browse files Browse the repository at this point in the history
Old python
  • Loading branch information
BenediktBurger authored Mar 1, 2024
2 parents d337a29 + b5f29d6 commit db1e795
Show file tree
Hide file tree
Showing 13 changed files with 94 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pyleco_CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
fail-fast: true
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
with:
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ _Use self defined objects instead of jsonrpc2-objects and jsonrpc2-pyclient._

### Changed

- Rename `cls` parameter to `device_class` in `Actor` and `TransparentDirector`.
- Substitute `jsonrpc2-objects` and `jsonrpc2-pyclient` by self written objects.
- Python requirement lowered to Python 3.8

### Added

Expand Down
2 changes: 1 addition & 1 deletion GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def task(stop_event) -> None:
"""The task which is run by the starter."""
with Actor(
name="fiberAmp", # you can access it under this name
cls=YAR, # the class to instantiate later on
device_class=YAR, # the class to instantiate later on
) as actor:
actor.connect(adapter) # create an instance `actor.device = YAR(adapter)`

Expand Down
4 changes: 2 additions & 2 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ name: pyleco
channels:
- conda-forge
dependencies:
- pyzmq=25.1.2
- pyzmq #=25.1.2 don't pin version for python<3.9
- pip # don't pin, to gain newest conda compatibility fixes
- pip:
- openrpc==8.1.0
# - openrpc==8.1.0 don't pin presence for python<3.9
- uuid7==0.1.0
# Development dependencies below
- pytest=7.2.0
Expand Down
2 changes: 1 addition & 1 deletion examples/pymeasure_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def readout(device: YAR, publisher: DataPublisher) -> None:
def task(stop_event) -> None:
"""The task which is run by the starter."""
# Initialize
with Actor(name="pymeasure_actor", cls=YAR, periodic_reading=interval) as actor:
with Actor(name="pymeasure_actor", device_class=YAR, periodic_reading=interval) as actor:
actor.read_publish = readout # define the regular readout function
actor.connect(adapter) # connect to the device

Expand Down
37 changes: 29 additions & 8 deletions pyleco/actors/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from __future__ import annotations

from typing import Any, Callable, Generic, Optional, Sequence, TypeVar, Union
from warnings import warn

import zmq

Expand Down Expand Up @@ -57,22 +58,39 @@ class Actor(MessageHandler, Generic[Device]):
is available via RPC as well.
:param str name: Name to listen to and to publish values with.
:param class cls: Instrument class.
:param class device_class: Instrument class.
:param int port: Port number to connect to.
:param periodic_reading: Interval between periodic readouts in s.
:param dict auto_connect: Kwargs to automatically connect to the device.
:param class cls: See :code:`device_class`.
.. deprecated:: 0.3
Deprecated, use :code:`device_class` instead.
:param \\**kwargs: Keywoard arguments for the general message handling.
"""

device: Device

def __init__(self, name: str, cls: type[Device], periodic_reading: float = -1,
auto_connect: Optional[dict] = None,
context: Optional[zmq.Context] = None,
**kwargs):
def __init__(
self,
name: str,
device_class: Optional[type[Device]] = None,
periodic_reading: float = -1,
auto_connect: Optional[dict] = None,
context: Optional[zmq.Context] = None,
cls: Optional[type[Device]] = None,
**kwargs,
):
context = context or zmq.Context.instance()
super().__init__(name=name, context=context, **kwargs)
self.cls = cls
if cls is not None:
warn("Parameter `cls` is deprecated, use `device_class` instead.", FutureWarning)
device_class = cls
if device_class is None:
# Keep this check as long as device_class is optional due to deprecated cls parameter
raise ValueError("You have to specify a `device_class`!")
self.device_class = device_class

# Pipe for the periodic readout timer
self.pipe: zmq.Socket = context.socket(zmq.PAIR)
Expand Down Expand Up @@ -171,7 +189,10 @@ def start_timer(self, interval: Optional[float] = None) -> None:

def stop_timer(self) -> None:
"""Stop the readout timer."""
self.timer.cancel()
try:
self.timer.cancel()
except AttributeError:
pass

def start_polling(self, polling_interval: Optional[float] = None) -> None:
self.start_timer(interval=polling_interval)
Expand All @@ -198,7 +219,7 @@ def connect(self, *args, **kwargs) -> None:
"""Connect to the device with the given arguments and keyword arguments."""
# TODO read auto_connect?
self.log.info("Connecting")
self.device = self.cls(*args, **kwargs)
self.device = self.device_class(*args, **kwargs)
self.start_timer()

def disconnect(self) -> None:
Expand Down
25 changes: 19 additions & 6 deletions pyleco/directors/transparent_director.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from __future__ import annotations
import logging
from typing import Generic, Optional, TypeVar, Union
from warnings import warn

from .director import Director

Expand Down Expand Up @@ -117,15 +118,27 @@ class TransparentDirector(Director, Generic[Device]):
It has a :attr:`device` attribute. Whenever you get/set an attribute of `device`, the Director
will call the Actor and try to get/set the corresponding attribute of the Actor's device.
If you want to add method calls, you might use the :class:`RemoteCall` Descriptor to add methods
to a subclass of :class:`TransparentDevice` and give that class to the `cls` parameter.
to a subclass of :class:`TransparentDevice` and give that class to the `device_class` parameter.
For example :code:`method = RemoteCall()` in the class definition will make sure,
that :code:`device.method(*args, **kwargs)` will be executed remotely.
:param cls: Subclass of :class:`TransparentDevice` to use as a device dummy.
:param actor: Name of the actor to direct.
:param device_class: Subclass of :class:`TransparentDevice` to use as a device dummy.
:param cls: see :code:`device_class`.
.. deprecated:: 0.3
Use :code:`device_class` instead.
"""

def __init__(self, actor: Optional[Union[bytes, str]] = None,
cls: type[Device] = TransparentDevice, # type: ignore[assignment]
**kwargs):
def __init__(
self,
actor: Optional[Union[bytes, str]] = None,
device_class: type[Device] = TransparentDevice, # type: ignore[assignment]
cls: Optional[type[Device]] = None,
**kwargs,
):
super().__init__(actor=actor, **kwargs)
self.device = cls(director=self) # type: ignore[call-arg]
if cls is not None:
warn("Parameter `cls` is deprecated, use `device_class` instead.", FutureWarning)
device_class = cls
self.device = device_class(director=self) # type: ignore[call-arg]
3 changes: 2 additions & 1 deletion pyleco/management/test_tasks/test_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def task(stop_event: Event) -> None:
while stop_event.wait(.5):
sleep(.1)
return
with Actor(name="pymeasure_actor", cls=FakeInstrument, periodic_reading=-1) as actor:
with Actor(name="pymeasure_actor", device_class=FakeInstrument,
periodic_reading=-1) as actor:
actor.connect() # connect to the device

# Continuous loop
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ classifiers = [
"Topic :: System :: Networking",
]

requires-python = ">=3.9"
requires-python = ">=3.8"
dependencies = [
"pyzmq >= 22.3.0",
"openrpc >= 8.1.0; python_version >= '3.9'",
Expand Down
26 changes: 22 additions & 4 deletions tests/actors/test_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#

import logging
from sys import version_info
import time

from unittest.mock import MagicMock
Expand Down Expand Up @@ -112,7 +113,8 @@ class ExtendedActorProtocol(ExtendedComponentProtocol, PollingActorProtocol, Pro

@pytest.fixture()
def actor() -> FakeActor:
actor = FakeActor("test", FantasyInstrument, auto_connect={'adapter': MagicMock()}, port=1234,
actor = FakeActor("test", FantasyInstrument, auto_connect={'adapter': MagicMock()},
port=1234,
protocol="inproc")
actor.next_beat = float("inf")
return actor
Expand All @@ -124,7 +126,7 @@ class TestProtocolImplemented:
def static_test_methods_are_present(self):
def testing(component: ExtendedActorProtocol):
pass
testing(FakeActor(name="test", cls=FantasyInstrument))
testing(FakeActor(name="test", device_class=FantasyInstrument)) # type: ignore

@pytest.fixture
def component_methods(self, actor: Actor):
Expand All @@ -141,6 +143,21 @@ def test_method_is_available(self, component_methods, method):
raise AssertionError(f"Method {method} is not available.")


@pytest.mark.skipif(version_info.minor < 9,
reason="It is deprecated, because it does not work for python<3.9.")
def test_deprecated_cls_argument():
with pytest.warns(FutureWarning, match="`cls` is deprecated"):
actor = FakeActor("test", cls=FantasyInstrument, auto_connect={'adapter': MagicMock()},
port=1234,
protocol="inproc")
assert actor.device_class == FantasyInstrument


def test_device_class_or_cls_is_necessary():
with pytest.raises(ValueError, match="`device_class`"):
FakeActor("test", protocol="inproc")


def test_get_properties(actor: Actor):
assert actor.get_parameters(['prop']) == {'prop': 5}

Expand Down Expand Up @@ -198,7 +215,8 @@ def test_register_device_method(actor: Actor):
class Test_disconnect:
@pytest.fixture
def disconnected_actor(self):
actor = FakeActor("name", cls=FantasyInstrument, auto_connect={"adapter": MagicMock()})
actor = FakeActor("name", device_class=FantasyInstrument,
auto_connect={"adapter": MagicMock()})
actor._device = actor.device # type: ignore
actor.device.adapter.close = MagicMock()
actor.disconnect()
Expand All @@ -215,7 +233,7 @@ def test_device_closed(self, disconnected_actor: Actor):


def test_exit_calls_disconnect():
with FakeActor("name", cls=FantasyInstrument) as actor:
with FakeActor("name", device_class=FantasyInstrument) as actor:
actor.disconnect = MagicMock()
actor.disconnect.assert_called_once()

Expand Down
2 changes: 1 addition & 1 deletion tests/directors/test_transparent_director.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class FantasyDevice(TransparentDevice):

@pytest.fixture
def director() -> TransparentDirector:
director = FakeDirector(cls=FantasyDevice,
director = FakeDirector(device_class=FantasyDevice,
communicator=FakeCommunicator(name="Communicator")) # type: ignore
return director

Expand Down
3 changes: 2 additions & 1 deletion tests/json_utils/test_rpc_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

try:
# Load openrpc server for comparison, if available.
from openrpc import RPCServer as RPCServerOpen
from openrpc import RPCServer as RPCServerOpen # type: ignore
except ModuleNotFoundError:
rpc_server_classes: list = [RPCServer]
else:
Expand Down Expand Up @@ -133,6 +133,7 @@ def test_process_response(rpc_server: RPCServer, rpc_generator: RPCGenerator):
error = exc_info.value.rpc_error
assert error.code == INVALID_REQUEST.code
assert error.message == INVALID_REQUEST.message
# ignore the following test, which depends on the openrpc version
# assert error.data == request.model_dump() # type: ignore


Expand Down
20 changes: 11 additions & 9 deletions tests/utils/test_message_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# THE SOFTWARE.
#

from __future__ import annotations
import logging
from unittest.mock import MagicMock
import time
Expand All @@ -36,7 +37,8 @@
from pyleco.core.serialization import serialize_data
from pyleco.test import FakeContext, FakePoller
from pyleco.errors import NOT_SIGNED_IN, DUPLICATE_NAME, NODE_UNKNOWN, RECEIVER_UNKNOWN
from pyleco.json_utils.json_objects import Request, ResultResponse, ErrorResponse, DataError
from pyleco.json_utils.json_objects import Request, ResultResponse, ErrorResponse
from pyleco.json_utils.errors import JSONRPCError, INVALID_REQUEST

from pyleco.utils.message_handler import MessageHandler, SimpleEvent

Expand Down Expand Up @@ -480,14 +482,14 @@ def test_handle_json_not_request(self, handler: MessageHandler):
data=data,
conversation_id=cid, message_type=MessageTypes.JSON)
result = handler.process_json_message(message=message)
error_message = Message(receiver=remote_name, conversation_id=cid,
message_type=MessageTypes.JSON,
data=ErrorResponse(id=5, error=DataError(
code=-32600,
message="Invalid Request",
data=data)))
# assert
assert result == error_message
assert result.receiver == remote_name.encode()
assert result.conversation_id == cid
assert result.header_elements.message_type == MessageTypes.JSON
with pytest.raises(JSONRPCError) as exc_info:
handler.rpc_generator.get_result_from_response(result.data) # type: ignore
error = exc_info.value.rpc_error
assert error.code == INVALID_REQUEST.code
assert error.message == INVALID_REQUEST.message


class Test_listen:
Expand Down

0 comments on commit db1e795

Please sign in to comment.