From 01d090ae1f1b92e1209106df5af865112ca1a2ab Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Sun, 19 Jan 2025 20:11:00 +0000 Subject: [PATCH 1/4] Remove mock dependency --- pyproject.toml | 1 - src/genie_python/genie_dae.py | 7 ++-- .../ibex_websocket_backend.py | 2 +- tests/py3_test_genie_experimental_data.py | 2 +- tests/test_block_names.py | 2 +- tests/test_genie.py | 2 +- tests/test_genie_alerts.py | 3 +- tests/test_genie_api_setup.py | 2 +- tests/test_genie_blockserver_tests.py | 3 +- tests/test_genie_dae.py | 5 ++- tests/test_genie_epics_api.py | 2 +- tests/test_matplotlib_backend.py | 5 ++- tests/test_mysql_abstraction_layer.py | 3 +- tests/test_script_checker.py | 33 ++++++++----------- tests/test_simulation.py | 2 +- tests/test_utilities.py | 3 +- 16 files changed, 31 insertions(+), 46 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 743597a2..ecc25696 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,7 +71,6 @@ doc = [ dev = [ "genie_python[plot,doc]", - "mock", "parameterized", "pyhamcrest", "pytest", diff --git a/src/genie_python/genie_dae.py b/src/genie_python/genie_dae.py index 474b79c5..1c98c864 100644 --- a/src/genie_python/genie_dae.py +++ b/src/genie_python/genie_dae.py @@ -458,9 +458,7 @@ def end_run( prepost: run pre and post commands [optional] """ if self.get_run_state() == "ENDING" and not immediate: - print( - "Please specify the 'immediate=True' flag to end a run " "while in the ENDING state" - ) + print("Please specify the 'immediate=True' flag to end a run while in the ENDING state") return run_number = self.get_run_number() @@ -601,8 +599,7 @@ def pause_run(self, immediate: bool = False, prepost: bool = True) -> None: """ if self.get_run_state() == "PAUSING" and not immediate: print( - "Please specify the 'immediate=True' flag " - "to pause a run while in the PAUSING state" + "Please specify the 'immediate=True' flag to pause a run while in the PAUSING state" ) return diff --git a/src/genie_python/matplotlib_backend/ibex_websocket_backend.py b/src/genie_python/matplotlib_backend/ibex_websocket_backend.py index 29b0b04e..e7c9000f 100644 --- a/src/genie_python/matplotlib_backend/ibex_websocket_backend.py +++ b/src/genie_python/matplotlib_backend/ibex_websocket_backend.py @@ -68,7 +68,7 @@ def _asyncio_send_exceptions_to_logfile_only(loop, context): exception = context.get("exception") try: GenieLogger().log_info_msg( - f"Caught (non-fatal) asyncio exception: " f"{exception.__class__.__name__}: {exception}" + f"Caught (non-fatal) asyncio exception: {exception.__class__.__name__}: {exception}" ) except Exception: # Exception while logging, ignore... diff --git a/tests/py3_test_genie_experimental_data.py b/tests/py3_test_genie_experimental_data.py index cad9505a..a3878fee 100644 --- a/tests/py3_test_genie_experimental_data.py +++ b/tests/py3_test_genie_experimental_data.py @@ -17,9 +17,9 @@ import datetime import unittest +from unittest.mock import call, patch from hamcrest import assert_that, calling, raises -from mock import call, patch from parameterized import parameterized from genie_python.genie_experimental_data import ( diff --git a/tests/test_block_names.py b/tests/test_block_names.py index a5554bc4..d3c8c3e2 100644 --- a/tests/test_block_names.py +++ b/tests/test_block_names.py @@ -21,9 +21,9 @@ import json import unittest from time import sleep +from unittest.mock import Mock, patch from hamcrest import assert_that, has_key, has_length, is_, only_contains -from mock import Mock, patch from genie_python.block_names import BlockNames, BlockNamesManager from genie_python.channel_access_exceptions import UnableToConnectToPVException diff --git a/tests/test_genie.py b/tests/test_genie.py index 5b273406..517dafce 100644 --- a/tests/test_genie.py +++ b/tests/test_genie.py @@ -19,9 +19,9 @@ import unittest from datetime import datetime, timedelta from io import StringIO +from unittest.mock import MagicMock, call, patch from hamcrest import assert_that, contains_exactly, has_length, is_ -from mock import MagicMock, call, patch import genie_python.genie_api_setup from genie_python import genie diff --git a/tests/test_genie_alerts.py b/tests/test_genie_alerts.py index 23e7c382..643b804e 100644 --- a/tests/test_genie_alerts.py +++ b/tests/test_genie_alerts.py @@ -16,8 +16,7 @@ from __future__ import absolute_import, print_function import unittest - -from mock import patch +from unittest.mock import patch import genie_python.genie_alerts import genie_python.genie_api_setup diff --git a/tests/test_genie_api_setup.py b/tests/test_genie_api_setup.py index 283ed64e..3b11db95 100644 --- a/tests/test_genie_api_setup.py +++ b/tests/test_genie_api_setup.py @@ -20,9 +20,9 @@ import os import unittest +from unittest.mock import patch from hamcrest import assert_that, is_ -from mock import patch from parameterized import parameterized import genie_python.genie_api_setup diff --git a/tests/test_genie_blockserver_tests.py b/tests/test_genie_blockserver_tests.py index c45b5eec..9511502a 100644 --- a/tests/test_genie_blockserver_tests.py +++ b/tests/test_genie_blockserver_tests.py @@ -17,8 +17,7 @@ from __future__ import absolute_import import unittest - -from mock import MagicMock, Mock +from unittest.mock import MagicMock, Mock from genie_python.genie_blockserver import BlockServer from genie_python.utilities import compress_and_hex diff --git a/tests/test_genie_dae.py b/tests/test_genie_dae.py index 2a670ff5..f6ba3a4e 100644 --- a/tests/test_genie_dae.py +++ b/tests/test_genie_dae.py @@ -17,10 +17,10 @@ from __future__ import absolute_import import unittest +from unittest.mock import MagicMock, patch import numpy as np from hamcrest import assert_that, calling, close_to, is_, raises -from mock import MagicMock, patch from parameterized import parameterized_class from genie_python.genie_change_cache import ChangeCache @@ -295,8 +295,7 @@ def test_GIVEN_in_transition_WHEN_change_finish_called_THEN_value_error_with_cor self.assertRaisesRegexp( ValueError, - "Another DAE change operation is currently in progress - values will be " - "inconsistent", + "Another DAE change operation is currently in progress - values will be inconsistent", self.dae.change_finish, ) diff --git a/tests/test_genie_epics_api.py b/tests/test_genie_epics_api.py index 1016598b..cd1a0d2f 100644 --- a/tests/test_genie_epics_api.py +++ b/tests/test_genie_epics_api.py @@ -17,9 +17,9 @@ from __future__ import absolute_import import unittest +from unittest.mock import MagicMock, patch from hamcrest import assert_that, calling, is_, raises -from mock import MagicMock, patch from parameterized import parameterized from genie_python.channel_access_exceptions import UnableToConnectToPVException diff --git a/tests/test_matplotlib_backend.py b/tests/test_matplotlib_backend.py index 465099b3..5e2a9f9a 100644 --- a/tests/test_matplotlib_backend.py +++ b/tests/test_matplotlib_backend.py @@ -1,7 +1,6 @@ import time import unittest - -import mock +from unittest.mock import MagicMock from genie_python.matplotlib_backend import ibex_websocket_backend @@ -22,7 +21,7 @@ class TestMatplotlibBackend(unittest.TestCase): def test_WHEN_plotting_thread_fails_to_start_THEN_script_does_not_hang(self): ibex_websocket_backend.WebAggApplication = ErroringWebAggApplication ibex_websocket_backend.ibex_open_plot_window = lambda *a, **k: None - ibex_websocket_backend.Gcf = mock.MagicMock() + ibex_websocket_backend.Gcf = MagicMock() start = time.time() ibex_websocket_backend._BackendIbexWebAgg.show() diff --git a/tests/test_mysql_abstraction_layer.py b/tests/test_mysql_abstraction_layer.py index e27681c2..6a5cb8c0 100644 --- a/tests/test_mysql_abstraction_layer.py +++ b/tests/test_mysql_abstraction_layer.py @@ -16,10 +16,9 @@ import unittest from typing import List -from unittest.mock import patch +from unittest.mock import Mock, patch from hamcrest import assert_that, equal_to -from mock import Mock from mysql.connector.connection import MySQLConnection from mysql.connector.cursor import MySQLCursor from mysql.connector.errors import InternalError diff --git a/tests/test_script_checker.py b/tests/test_script_checker.py index 3b1b86bd..97ba99f5 100644 --- a/tests/test_script_checker.py +++ b/tests/test_script_checker.py @@ -54,7 +54,7 @@ def assertSymbolsDefined(self, script_lines, expected_symbols): def test_GIVEN_end_without_brackets_WHEN_check_THEN_error_message(self): script_lines = [ - "from genie_python import genie as g\n" "def test():\n", + "from genie_python import genie as g\ndef test():\n", " g.begin()\n", " g.end\n", ] @@ -65,14 +65,14 @@ def test_GIVEN_end_without_brackets_WHEN_check_THEN_error_message(self): ) def test_GIVEN_end_as_start_of_another_word_WHEN_check_THEN_no_error_message(self): - script_lines = ["from genie_python import genie as g\n" "def test():\n", " endAngle = 1"] + script_lines = ["from genie_python import genie as g\ndef test():\n", " endAngle = 1"] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: self.assertEqual(errors, []) def test_GIVEN_end_as_end_of_another_word_WHEN_check_THEN_no_error_message(self): script_lines = [ - "from genie_python import genie as g\n" "def test():\n", + "from genie_python import genie as g\ndef test():\n", " angle_end = 1", ] @@ -82,7 +82,7 @@ def test_GIVEN_end_as_end_of_another_word_WHEN_check_THEN_no_error_message(self) self.assertEqual(errors, []) def test_GIVEN_end_without_brackets_at_start_of_line_WHEN_check_THEN_error_message(self): - script_lines = ["from genie_python import genie as g\n" "def test():\n" " g.end"] + script_lines = ["from genie_python import genie as g\ndef test():\n g.end"] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: self.assertEqual( @@ -92,7 +92,7 @@ def test_GIVEN_end_without_brackets_at_start_of_line_WHEN_check_THEN_error_messa def test_GIVEN_end_without_brackets_on_line_with_fn_with_brackets_WHEN_check_THEN_error_message( self, ): - script_lines = ["from genie_python import genie as g\n" "g.begin(); g.end "] + script_lines = ["from genie_python import genie as g\ng.begin(); g.end "] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: self.assertEqual( @@ -100,7 +100,7 @@ def test_GIVEN_end_without_brackets_on_line_with_fn_with_brackets_WHEN_check_THE ) def test_GIVEN_end_in_string_without_brackets_WHEN_check_THEN_no_message(self): - script_lines = ["def test():\n" ' " a string containing end "'] + script_lines = ['def test():\n " a string containing end "'] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: self.assertEqual(errors, []) @@ -202,9 +202,7 @@ def test_GIVEN_variable_assignment_with_g__WHEN_check_THEN_no_message(self): self.assertEqual(errors, []) def test_GIVEN_function_with_g_WHEN_check_THEN_warn_user(self): - script_lines = [ - "from genie_python import genie as g\n" "def test():\n" " g.test_function()\n" - ] + script_lines = ["from genie_python import genie as g\ndef test():\n g.test_function()\n"] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: self.assertEqual( @@ -214,7 +212,7 @@ def test_GIVEN_function_with_g_WHEN_check_THEN_warn_user(self): def test_GIVEN_2_g_assignments_WHEN_check_THEN_warning_message(self): script_lines = [ - "from genie_python import genie as g\n" "def test():\n" " g=16\n", + "from genie_python import genie as g\ndef test():\n g=16\n", " g=17", ] @@ -224,9 +222,7 @@ def test_GIVEN_2_g_assignments_WHEN_check_THEN_warning_message(self): ) def test_GIVEN_g_non_existing_command_WHEN_call_THEN_error_message(self): - script_lines = [ - "from genie_python import genie as g\n" "def test():\n" " g.aitfor_time(10)" - ] + script_lines = ["from genie_python import genie as g\ndef test():\n g.aitfor_time(10)"] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: self.assertEqual( @@ -539,8 +535,7 @@ def test_GIVEN_trying_to_index_var_of_optional_type_WHEN_pyright_script_checker_ ): script_lines = [ "from typing import Optional, List\n", - "def get_first_element(elements: Optional[List[int]]) -> int:\n" - " return elements[0]\n", + "def get_first_element(elements: Optional[List[int]]) -> int:\n return elements[0]\n", ] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: @@ -563,7 +558,7 @@ def test_GIVEN_trying_to_iterate_over_var_of_optional_type_WHEN_pyright_script_c ): script_lines = [ "from typing import Optional, List\n", - "def iter_elements(elements: Optional[List[int]]):\n" " for element in elements:\n", + "def iter_elements(elements: Optional[List[int]]):\n for element in elements:\n", " pass\n", ] @@ -573,7 +568,7 @@ def test_GIVEN_trying_to_iterate_over_var_of_optional_type_WHEN_pyright_script_c def test_GIVEN_trying_to_define_function_with_none_type_args_type_WHEN_pyright_script_checker_called_THEN_no_error( self, ): - script_lines = ["def none_func(arg: int = None):\n" " print(arg)\n"] + script_lines = ["def none_func(arg: int = None):\n print(arg)\n"] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: self.assertEqual(errors, []) @@ -581,7 +576,7 @@ def test_GIVEN_trying_to_define_function_with_none_type_args_type_WHEN_pyright_s def test_GIVEN_trying_to_use_optional_operand__WHEN_pyright_script_checker_called_THEN_no_error( self, ): - script_lines = ["def none_func(arg1: int, arg2: int = None):\n" " print(arg2 + arg1)\n"] + script_lines = ["def none_func(arg1: int, arg2: int = None):\n print(arg2 + arg1)\n"] with CreateTempScriptAndReturnErrors(self.checker, self.machine, script_lines) as errors: self.assertEqual(errors, []) @@ -589,7 +584,7 @@ def test_GIVEN_trying_to_use_optional_operand__WHEN_pyright_script_checker_calle def test_GIVEN_trying_to_use_undefined_variable_WHEN_pyright_script_checker_called_THEN_no_error( self, ): - script_lines = ["def func():\n" " print(arg)\n"] + script_lines = ["def func():\n print(arg)\n"] with CreateTempScriptAndReturnErrors( self.checker, self.machine, script_lines, no_pylint=True diff --git a/tests/test_simulation.py b/tests/test_simulation.py index f2d4955c..93071b8c 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -18,8 +18,8 @@ import unittest from collections import OrderedDict from datetime import datetime +from unittest.mock import patch -from mock import patch from parameterized import parameterized from genie_python import genie diff --git a/tests/test_utilities.py b/tests/test_utilities.py index 6464bef3..0f349bf6 100644 --- a/tests/test_utilities.py +++ b/tests/test_utilities.py @@ -17,8 +17,7 @@ import json import unittest - -from mock import Mock +from unittest.mock import Mock from genie_python.utilities import ( EnvironmentDetails, From c8c2f1d83bc412c3641995363debd978b121838a Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Sun, 19 Jan 2025 22:08:48 +0000 Subject: [PATCH 2/4] ruff/pyright mpl backend (TO BE CHECKED) --- .../ibex_websocket_backend.py | 139 +++++++++++------- 1 file changed, 85 insertions(+), 54 deletions(-) diff --git a/src/genie_python/matplotlib_backend/ibex_websocket_backend.py b/src/genie_python/matplotlib_backend/ibex_websocket_backend.py index e7c9000f..03b625c3 100644 --- a/src/genie_python/matplotlib_backend/ibex_websocket_backend.py +++ b/src/genie_python/matplotlib_backend/ibex_websocket_backend.py @@ -12,14 +12,17 @@ import threading from functools import wraps from time import sleep +from typing import Any, Callable, Mapping, ParamSpec, TypeVar, cast import tornado +import tornado.websocket from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import _Backend from matplotlib.backends import backend_webagg from matplotlib.backends import backend_webagg_core as core from py4j.java_collections import ListConverter from py4j.java_gateway import JavaGateway +from tornado.ioloop import IOLoop from tornado.websocket import WebSocketClosedError from genie_python.genie_logging import GenieLogger @@ -37,20 +40,26 @@ _is_primary = True -def _ignore_if_websocket_closed(func): +T = TypeVar("T") +P = ParamSpec("P") + + +def _ignore_if_websocket_closed(func: Callable[P, T]) -> Callable[P, T | None]: """ Decorator which ignores exceptions that were caused by websockets being closed. """ @wraps(func) - def wrapper(*a, **kw): + def wrapper(*a: P.args, **kw: P.kwargs) -> T | None: try: return func(*a, **kw) except WebSocketClosedError: pass except Exception as e: - # Plotting multiple graphs quickly can cause an error where pyplot tries to access a plot which - # has been removed. This error does not break anything, so log it and continue. It is better for the plot + # Plotting multiple graphs quickly can cause an error where + # pyplot tries to access a plot which + # has been removed. This error does not break anything, so log it + # and continue. It is better for the plot # to fail to update than for the whole user script to crash. try: GenieLogger().log_info_msg( @@ -64,7 +73,9 @@ def wrapper(*a, **kw): return wrapper -def _asyncio_send_exceptions_to_logfile_only(loop, context): +def _asyncio_send_exceptions_to_logfile_only( + loop: asyncio.AbstractEventLoop, context: Mapping[str, Any] +) -> None: exception = context.get("exception") try: GenieLogger().log_info_msg( @@ -75,13 +86,19 @@ def _asyncio_send_exceptions_to_logfile_only(loop, context): pass -def set_up_plot_default(is_primary=True, should_open_ibex_window_on_show=True, max_figures=None): +def set_up_plot_default( + is_primary: bool = True, + should_open_ibex_window_on_show: bool = True, + max_figures: int | None = None, +) -> None: """ Set the plot defaults for when show is called Args: - is_primary: True display plot on primary web port; False display plot on secondary web port - should_open_ibex_window_on_show: Does nothing; provided for backwards-compatibility with older backend + is_primary: True display plot on primary web port; False display + plot on secondary web port + should_open_ibex_window_on_show: Does nothing; provided for + backwards-compatibility with older backend max_figures: Maximum number of figures to plot simultaneously (int) """ global _web_backend_port @@ -99,71 +116,76 @@ def set_up_plot_default(is_primary=True, should_open_ibex_window_on_show=True, m class WebAggApplication(backend_webagg.WebAggApplication): - class WebSocket(tornado.websocket.WebSocketHandler): + class WebSocket(tornado.websocket.WebSocketHandler): # pyright: ignore supports_binary = True - def write_message(self, *args, **kwargs): + def write_message(self, *args: Any, **kwargs: Any) -> asyncio.Future[None]: f = super().write_message(*args, **kwargs) @_ignore_if_websocket_closed - def _cb(*args, **kwargs): + def _cb(*args: Any, **kwargs: Any) -> None: return f.result() f.add_done_callback(_cb) + return f @_ignore_if_websocket_closed - def open(self, fignum): + def open(self, fignum: int, *args: Any, **kwargs: Any) -> None: self.fignum = int(fignum) - self.manager = Gcf.figs.get(self.fignum, None) + self.manager = cast(_FigureManager | None, Gcf.figs.get(self.fignum, None)) if self.manager is not None: self.manager.add_web_socket(self) if hasattr(self, "set_nodelay"): self.set_nodelay(True) @_ignore_if_websocket_closed - def on_close(self): - self.manager.remove_web_socket(self) + def on_close(self) -> None: + if self.manager is not None: + self.manager.remove_web_socket(self) @_ignore_if_websocket_closed - def on_message(self, message): - message = json.loads(message) + def on_message(self, message: str | bytes) -> None: + parsed_message: dict[str, Any] = json.loads(message) # The 'supports_binary' message is on a client-by-client # basis. The others affect the (shared) canvas as a # whole. - if message["type"] == "supports_binary": - self.supports_binary = message["value"] + if parsed_message["type"] == "supports_binary": + self.supports_binary = parsed_message["value"] else: - manager = Gcf.figs.get(self.fignum, None) + manager = cast(_FigureManager | None, Gcf.figs.get(self.fignum, None)) # It is possible for a figure to be closed, # but a stale figure UI is still sending messages # from the browser. if manager is not None: - manager.handle_json(message) + manager.handle_json(parsed_message) @_ignore_if_websocket_closed - def send_json(self, content): + def send_json(self, content: dict[str, str]) -> None: self.write_message(json.dumps(content)) @_ignore_if_websocket_closed - def send_binary(self, blob): + def send_binary(self, blob: str) -> None: if self.supports_binary: self.write_message(blob, binary=True) else: - blob_code = blob.encode("base64").replace("\n", "") + blob_code = blob.encode("base64").replace(b"\n", b"") data_uri = f"data:image/png;base64,{blob_code}" self.write_message(data_uri) - ioloop = None + ioloop: IOLoop | None = None asyncio_loop = None started = False app = None @classmethod - def initialize(cls, url_prefix="", port=None, address=None): + def initialize( + cls, url_prefix: str = "", port: int | None = None, address: str | None = None + ) -> None: """ Create the class instance - We use a constant, hard-coded port as we will only ever have one plot going at the same time. + We use a constant, hard-coded port as we will only + ever have one plot going at the same time. """ cls.app = cls(url_prefix=url_prefix) cls.url_prefix = url_prefix @@ -171,7 +193,7 @@ def initialize(cls, url_prefix="", port=None, address=None): cls.address = address @classmethod - def start(cls): + def start(cls) -> None: """ IOLoop.running() was removed as of Tornado 2.4; see for example https://groups.google.com/forum/#!topic/python-tornado/QLMzkpQBGOY @@ -191,6 +213,8 @@ def start(cls): asyncio.set_event_loop(loop) cls.asyncio_loop = loop cls.ioloop = tornado.ioloop.IOLoop.current() + if cls.port is None or cls.address is None or cls.app is None: + raise RuntimeError("port, address and app must be set") cls.app.listen(cls.port, cls.address) # Set the flag to True *before* blocking on ioloop.start() @@ -202,22 +226,26 @@ def start(cls): traceback.print_exc() @classmethod - def stop(cls): + def stop(cls) -> None: try: - def _stop(): - cls.ioloop.stop() - sys.stdout.flush() - cls.started = False + def _stop() -> None: + if cls.ioloop is not None: + cls.ioloop.stop() + sys.stdout.flush() + cls.started = False - cls.ioloop.add_callback(_stop) + if cls.ioloop is not None: + cls.ioloop.add_callback(_stop) except Exception: import traceback traceback.print_exc() -def ibex_open_plot_window(figures, is_primary=True, host=None): +def ibex_open_plot_window( + figures: list[int], is_primary: bool = True, host: str | None = None +) -> None: """ Open the plot window in ibex gui through py4j. With sensible defaults Args: @@ -230,12 +258,14 @@ def ibex_open_plot_window(figures, is_primary=True, host=None): url = f"{host}:{port}" try: gateway = JavaGateway() - figures = ListConverter().convert(figures, gateway._gateway_client) - gateway.entry_point.openMplRenderer(figures, url, is_primary) + converted_figures = ListConverter().convert(figures, gateway._gateway_client) + gateway.entry_point.openMplRenderer(converted_figures, url, is_primary) # pyright: ignore (rpc) except Exception as e: - # We need this try-except to be very broad as various exceptions can, in principle, + # We need this try-except to be very broad as various + # exceptions can, in principle, # be thrown while translating between python <-> java. - # If any exceptions occur, it is better to log and continue rather than crashing the entire script. + # If any exceptions occur, it is better to log and + # continue rather than crashing the entire script. print(f"Failed to open plot in IBEX due to: {e}") @@ -248,25 +278,25 @@ class _FigureManager(core.FigureManagerWebAgg): _toolbar2_class = core.NavigationToolbar2WebAgg @_ignore_if_websocket_closed - def _send_event(self, *args, **kwargs): + def _send_event(self, *args: Any, **kwargs: Any) -> None: with _IBEX_FIGURE_MANAGER_LOCK: super()._send_event(*args, **kwargs) - def remove_web_socket(self, *args, **kwargs): + def remove_web_socket(self, *args: Any, **kwargs: Any) -> None: with _IBEX_FIGURE_MANAGER_LOCK: super().remove_web_socket(*args, **kwargs) - def add_web_socket(self, *args, **kwargs): + def add_web_socket(self, *args: Any, **kwargs: Any) -> None: with _IBEX_FIGURE_MANAGER_LOCK: super().add_web_socket(*args, **kwargs) @_ignore_if_websocket_closed - def refresh_all(self, *args, **kwargs): + def refresh_all(self) -> None: with _IBEX_FIGURE_MANAGER_LOCK: - super().refresh_all(*args, **kwargs) + super().refresh_all() @classmethod - def pyplot_show(cls, *args, **kwargs): + def pyplot_show(cls, *args: Any, **kwargs: Any) -> None: """ Show a plot. @@ -305,18 +335,18 @@ def pyplot_show(cls, *args, **kwargs): class _FigureCanvas(backend_webagg.FigureCanvasWebAgg): manager_class = _FigureManager - def set_image_mode(self, mode): + def set_image_mode(self, mode: str) -> None: """ Always send full images to ibex. """ self._current_image_mode = "full" - def get_diff_image(self, *args, **kwargs): + def get_diff_image(self) -> bytes | None: """ Always send full images to ibex. """ self._force_full = True - return super().get_diff_image(*args, **kwargs) + return super().get_diff_image() def draw_idle(self) -> None: """ @@ -341,17 +371,17 @@ class _BackendIbexWebAgg(_Backend): FigureManager = _FigureManager @classmethod - def trigger_manager_draw(cls, manager): + def trigger_manager_draw(cls, manager: FigureManager) -> None: with IBEX_BACKEND_LOCK: manager.canvas.draw_idle() @classmethod - def draw_if_interactive(cls): + def draw_if_interactive(cls) -> None: with IBEX_BACKEND_LOCK: - super(_BackendIbexWebAgg, cls).draw_if_interactive() + super(_BackendIbexWebAgg, cls).draw_if_interactive() # pyright: ignore @classmethod - def new_figure_manager(cls, num, *args, **kwargs): + def new_figure_manager(cls, num: int, *args: Any, **kwargs: Any) -> _FigureManager: with IBEX_BACKEND_LOCK: for x in list(figure_numbers): if x not in Gcf.figs.keys(): @@ -360,7 +390,8 @@ def new_figure_manager(cls, num, *args, **kwargs): if len(figure_numbers) > max_number_of_figures: Gcf.destroy(figure_numbers[0]) print( - f"There are too many figures so deleted the oldest figure, which was {figure_numbers[0]}." + f"There are too many figures so deleted " + f"the oldest figure, which was {figure_numbers[0]}." ) figure_numbers.pop(0) - return super(_BackendIbexWebAgg, cls).new_figure_manager(num, *args, **kwargs) + return super(_BackendIbexWebAgg, cls).new_figure_manager(num, *args, **kwargs) # pyright: ignore From 9b0377ed03f05d0732636bc3d1fd8effe2b7e152 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Sun, 19 Jan 2025 22:10:06 +0000 Subject: [PATCH 3/4] ruff --- tests/test_block_names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_block_names.py b/tests/test_block_names.py index d3c8c3e2..16c64acc 100644 --- a/tests/test_block_names.py +++ b/tests/test_block_names.py @@ -218,7 +218,7 @@ def test_GIVEN_request_invalid_block_WHEN_inspect_block_THEN_attribute_error_thr block_names, _ = create_block_names(get_pv_value_mock, []) try: - result = block_names._blocks_cant_start_with_hash + block_names._blocks_cant_start_with_hash self.fail("No exception thrown") except AttributeError: pass From 3a71bfa4e5ee222e7d7cf3147373e5de8d883f07 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Mon, 20 Jan 2025 09:29:31 +0000 Subject: [PATCH 4/4] Move mpl changes to a different branch --- .../ibex_websocket_backend.py | 141 +++++++----------- 1 file changed, 55 insertions(+), 86 deletions(-) diff --git a/src/genie_python/matplotlib_backend/ibex_websocket_backend.py b/src/genie_python/matplotlib_backend/ibex_websocket_backend.py index 03b625c3..29b0b04e 100644 --- a/src/genie_python/matplotlib_backend/ibex_websocket_backend.py +++ b/src/genie_python/matplotlib_backend/ibex_websocket_backend.py @@ -12,17 +12,14 @@ import threading from functools import wraps from time import sleep -from typing import Any, Callable, Mapping, ParamSpec, TypeVar, cast import tornado -import tornado.websocket from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import _Backend from matplotlib.backends import backend_webagg from matplotlib.backends import backend_webagg_core as core from py4j.java_collections import ListConverter from py4j.java_gateway import JavaGateway -from tornado.ioloop import IOLoop from tornado.websocket import WebSocketClosedError from genie_python.genie_logging import GenieLogger @@ -40,26 +37,20 @@ _is_primary = True -T = TypeVar("T") -P = ParamSpec("P") - - -def _ignore_if_websocket_closed(func: Callable[P, T]) -> Callable[P, T | None]: +def _ignore_if_websocket_closed(func): """ Decorator which ignores exceptions that were caused by websockets being closed. """ @wraps(func) - def wrapper(*a: P.args, **kw: P.kwargs) -> T | None: + def wrapper(*a, **kw): try: return func(*a, **kw) except WebSocketClosedError: pass except Exception as e: - # Plotting multiple graphs quickly can cause an error where - # pyplot tries to access a plot which - # has been removed. This error does not break anything, so log it - # and continue. It is better for the plot + # Plotting multiple graphs quickly can cause an error where pyplot tries to access a plot which + # has been removed. This error does not break anything, so log it and continue. It is better for the plot # to fail to update than for the whole user script to crash. try: GenieLogger().log_info_msg( @@ -73,32 +64,24 @@ def wrapper(*a: P.args, **kw: P.kwargs) -> T | None: return wrapper -def _asyncio_send_exceptions_to_logfile_only( - loop: asyncio.AbstractEventLoop, context: Mapping[str, Any] -) -> None: +def _asyncio_send_exceptions_to_logfile_only(loop, context): exception = context.get("exception") try: GenieLogger().log_info_msg( - f"Caught (non-fatal) asyncio exception: {exception.__class__.__name__}: {exception}" + f"Caught (non-fatal) asyncio exception: " f"{exception.__class__.__name__}: {exception}" ) except Exception: # Exception while logging, ignore... pass -def set_up_plot_default( - is_primary: bool = True, - should_open_ibex_window_on_show: bool = True, - max_figures: int | None = None, -) -> None: +def set_up_plot_default(is_primary=True, should_open_ibex_window_on_show=True, max_figures=None): """ Set the plot defaults for when show is called Args: - is_primary: True display plot on primary web port; False display - plot on secondary web port - should_open_ibex_window_on_show: Does nothing; provided for - backwards-compatibility with older backend + is_primary: True display plot on primary web port; False display plot on secondary web port + should_open_ibex_window_on_show: Does nothing; provided for backwards-compatibility with older backend max_figures: Maximum number of figures to plot simultaneously (int) """ global _web_backend_port @@ -116,76 +99,71 @@ def set_up_plot_default( class WebAggApplication(backend_webagg.WebAggApplication): - class WebSocket(tornado.websocket.WebSocketHandler): # pyright: ignore + class WebSocket(tornado.websocket.WebSocketHandler): supports_binary = True - def write_message(self, *args: Any, **kwargs: Any) -> asyncio.Future[None]: + def write_message(self, *args, **kwargs): f = super().write_message(*args, **kwargs) @_ignore_if_websocket_closed - def _cb(*args: Any, **kwargs: Any) -> None: + def _cb(*args, **kwargs): return f.result() f.add_done_callback(_cb) - return f @_ignore_if_websocket_closed - def open(self, fignum: int, *args: Any, **kwargs: Any) -> None: + def open(self, fignum): self.fignum = int(fignum) - self.manager = cast(_FigureManager | None, Gcf.figs.get(self.fignum, None)) + self.manager = Gcf.figs.get(self.fignum, None) if self.manager is not None: self.manager.add_web_socket(self) if hasattr(self, "set_nodelay"): self.set_nodelay(True) @_ignore_if_websocket_closed - def on_close(self) -> None: - if self.manager is not None: - self.manager.remove_web_socket(self) + def on_close(self): + self.manager.remove_web_socket(self) @_ignore_if_websocket_closed - def on_message(self, message: str | bytes) -> None: - parsed_message: dict[str, Any] = json.loads(message) + def on_message(self, message): + message = json.loads(message) # The 'supports_binary' message is on a client-by-client # basis. The others affect the (shared) canvas as a # whole. - if parsed_message["type"] == "supports_binary": - self.supports_binary = parsed_message["value"] + if message["type"] == "supports_binary": + self.supports_binary = message["value"] else: - manager = cast(_FigureManager | None, Gcf.figs.get(self.fignum, None)) + manager = Gcf.figs.get(self.fignum, None) # It is possible for a figure to be closed, # but a stale figure UI is still sending messages # from the browser. if manager is not None: - manager.handle_json(parsed_message) + manager.handle_json(message) @_ignore_if_websocket_closed - def send_json(self, content: dict[str, str]) -> None: + def send_json(self, content): self.write_message(json.dumps(content)) @_ignore_if_websocket_closed - def send_binary(self, blob: str) -> None: + def send_binary(self, blob): if self.supports_binary: self.write_message(blob, binary=True) else: - blob_code = blob.encode("base64").replace(b"\n", b"") + blob_code = blob.encode("base64").replace("\n", "") data_uri = f"data:image/png;base64,{blob_code}" self.write_message(data_uri) - ioloop: IOLoop | None = None + ioloop = None asyncio_loop = None started = False app = None @classmethod - def initialize( - cls, url_prefix: str = "", port: int | None = None, address: str | None = None - ) -> None: + def initialize(cls, url_prefix="", port=None, address=None): """ Create the class instance - We use a constant, hard-coded port as we will only - ever have one plot going at the same time. + We use a constant, hard-coded port as we will only ever have one plot going at the same time. """ cls.app = cls(url_prefix=url_prefix) cls.url_prefix = url_prefix @@ -193,7 +171,7 @@ def initialize( cls.address = address @classmethod - def start(cls) -> None: + def start(cls): """ IOLoop.running() was removed as of Tornado 2.4; see for example https://groups.google.com/forum/#!topic/python-tornado/QLMzkpQBGOY @@ -213,8 +191,6 @@ def start(cls) -> None: asyncio.set_event_loop(loop) cls.asyncio_loop = loop cls.ioloop = tornado.ioloop.IOLoop.current() - if cls.port is None or cls.address is None or cls.app is None: - raise RuntimeError("port, address and app must be set") cls.app.listen(cls.port, cls.address) # Set the flag to True *before* blocking on ioloop.start() @@ -226,26 +202,22 @@ def start(cls) -> None: traceback.print_exc() @classmethod - def stop(cls) -> None: + def stop(cls): try: - def _stop() -> None: - if cls.ioloop is not None: - cls.ioloop.stop() - sys.stdout.flush() - cls.started = False + def _stop(): + cls.ioloop.stop() + sys.stdout.flush() + cls.started = False - if cls.ioloop is not None: - cls.ioloop.add_callback(_stop) + cls.ioloop.add_callback(_stop) except Exception: import traceback traceback.print_exc() -def ibex_open_plot_window( - figures: list[int], is_primary: bool = True, host: str | None = None -) -> None: +def ibex_open_plot_window(figures, is_primary=True, host=None): """ Open the plot window in ibex gui through py4j. With sensible defaults Args: @@ -258,14 +230,12 @@ def ibex_open_plot_window( url = f"{host}:{port}" try: gateway = JavaGateway() - converted_figures = ListConverter().convert(figures, gateway._gateway_client) - gateway.entry_point.openMplRenderer(converted_figures, url, is_primary) # pyright: ignore (rpc) + figures = ListConverter().convert(figures, gateway._gateway_client) + gateway.entry_point.openMplRenderer(figures, url, is_primary) except Exception as e: - # We need this try-except to be very broad as various - # exceptions can, in principle, + # We need this try-except to be very broad as various exceptions can, in principle, # be thrown while translating between python <-> java. - # If any exceptions occur, it is better to log and - # continue rather than crashing the entire script. + # If any exceptions occur, it is better to log and continue rather than crashing the entire script. print(f"Failed to open plot in IBEX due to: {e}") @@ -278,25 +248,25 @@ class _FigureManager(core.FigureManagerWebAgg): _toolbar2_class = core.NavigationToolbar2WebAgg @_ignore_if_websocket_closed - def _send_event(self, *args: Any, **kwargs: Any) -> None: + def _send_event(self, *args, **kwargs): with _IBEX_FIGURE_MANAGER_LOCK: super()._send_event(*args, **kwargs) - def remove_web_socket(self, *args: Any, **kwargs: Any) -> None: + def remove_web_socket(self, *args, **kwargs): with _IBEX_FIGURE_MANAGER_LOCK: super().remove_web_socket(*args, **kwargs) - def add_web_socket(self, *args: Any, **kwargs: Any) -> None: + def add_web_socket(self, *args, **kwargs): with _IBEX_FIGURE_MANAGER_LOCK: super().add_web_socket(*args, **kwargs) @_ignore_if_websocket_closed - def refresh_all(self) -> None: + def refresh_all(self, *args, **kwargs): with _IBEX_FIGURE_MANAGER_LOCK: - super().refresh_all() + super().refresh_all(*args, **kwargs) @classmethod - def pyplot_show(cls, *args: Any, **kwargs: Any) -> None: + def pyplot_show(cls, *args, **kwargs): """ Show a plot. @@ -335,18 +305,18 @@ def pyplot_show(cls, *args: Any, **kwargs: Any) -> None: class _FigureCanvas(backend_webagg.FigureCanvasWebAgg): manager_class = _FigureManager - def set_image_mode(self, mode: str) -> None: + def set_image_mode(self, mode): """ Always send full images to ibex. """ self._current_image_mode = "full" - def get_diff_image(self) -> bytes | None: + def get_diff_image(self, *args, **kwargs): """ Always send full images to ibex. """ self._force_full = True - return super().get_diff_image() + return super().get_diff_image(*args, **kwargs) def draw_idle(self) -> None: """ @@ -371,17 +341,17 @@ class _BackendIbexWebAgg(_Backend): FigureManager = _FigureManager @classmethod - def trigger_manager_draw(cls, manager: FigureManager) -> None: + def trigger_manager_draw(cls, manager): with IBEX_BACKEND_LOCK: manager.canvas.draw_idle() @classmethod - def draw_if_interactive(cls) -> None: + def draw_if_interactive(cls): with IBEX_BACKEND_LOCK: - super(_BackendIbexWebAgg, cls).draw_if_interactive() # pyright: ignore + super(_BackendIbexWebAgg, cls).draw_if_interactive() @classmethod - def new_figure_manager(cls, num: int, *args: Any, **kwargs: Any) -> _FigureManager: + def new_figure_manager(cls, num, *args, **kwargs): with IBEX_BACKEND_LOCK: for x in list(figure_numbers): if x not in Gcf.figs.keys(): @@ -390,8 +360,7 @@ def new_figure_manager(cls, num: int, *args: Any, **kwargs: Any) -> _FigureManag if len(figure_numbers) > max_number_of_figures: Gcf.destroy(figure_numbers[0]) print( - f"There are too many figures so deleted " - f"the oldest figure, which was {figure_numbers[0]}." + f"There are too many figures so deleted the oldest figure, which was {figure_numbers[0]}." ) figure_numbers.pop(0) - return super(_BackendIbexWebAgg, cls).new_figure_manager(num, *args, **kwargs) # pyright: ignore + return super(_BackendIbexWebAgg, cls).new_figure_manager(num, *args, **kwargs)