From 221f4f7cd47599b79c70c649e4a1e21edc816a9c Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Mon, 19 Feb 2024 02:43:24 -0500 Subject: [PATCH 1/3] improve jsonrpc --- pyflowlauncher/__init__.py | 9 ++-- pyflowlauncher/api.py | 63 +++++++++++++------------ pyflowlauncher/jsonrpc.py | 28 ----------- pyflowlauncher/jsonrpc/__init__.py | 5 ++ pyflowlauncher/jsonrpc/client.py | 33 +++++++++++++ pyflowlauncher/jsonrpc/id_generation.py | 6 +++ pyflowlauncher/jsonrpc/models.py | 35 ++++++++++++++ pyflowlauncher/jsonrpc/server.py | 28 +++++++++++ pyflowlauncher/method.py | 11 +++-- pyflowlauncher/plugin.py | 31 +++++------- pyflowlauncher/result.py | 38 +++++---------- pyflowlauncher/settings.py | 7 ++- 12 files changed, 178 insertions(+), 116 deletions(-) delete mode 100644 pyflowlauncher/jsonrpc.py create mode 100644 pyflowlauncher/jsonrpc/__init__.py create mode 100644 pyflowlauncher/jsonrpc/client.py create mode 100644 pyflowlauncher/jsonrpc/id_generation.py create mode 100644 pyflowlauncher/jsonrpc/models.py create mode 100644 pyflowlauncher/jsonrpc/server.py diff --git a/pyflowlauncher/__init__.py b/pyflowlauncher/__init__.py index 22bbdfa..28fb88c 100644 --- a/pyflowlauncher/__init__.py +++ b/pyflowlauncher/__init__.py @@ -1,8 +1,10 @@ import logging from .plugin import Plugin -from .result import JsonRPCAction, Result, send_results, ResultResponse +from .result import Result, send_results from .method import Method +from .jsonrpc.models import JsonRPCRequest, JsonRPCResult +from .jsonrpc.client import send_request logger = logging.getLogger(__name__) @@ -10,9 +12,10 @@ __all__ = [ "Plugin", - "ResultResponse", "send_results", "Result", - "JsonRPCAction", + "JsonRPCRequest", + "JsonRPCResult", "Method", + "send_request", ] diff --git a/pyflowlauncher/api.py b/pyflowlauncher/api.py index f0b36cb..7580d5e 100644 --- a/pyflowlauncher/api.py +++ b/pyflowlauncher/api.py @@ -1,79 +1,80 @@ from typing import Optional -from .result import JsonRPCAction +from .jsonrpc.models import JsonRPCRequest +from .jsonrpc.client import create_request NAME_SPACE = 'Flow.Launcher' -def _send_action(method: str, *parameters) -> JsonRPCAction: - return {"method": f"{NAME_SPACE}.{method}", "parameters": parameters} +def _get_namespace(method: str) -> str: + return f"{NAME_SPACE}.{method}" -def change_query(query: str, requery: bool = False) -> JsonRPCAction: +def change_query(query: str, requery: bool = False) -> JsonRPCRequest: """Change the query in Flow Launcher.""" - return _send_action("ChangeQuery", query, requery) + return create_request(_get_namespace("ChangeQuery"), [query, requery]) -def shell_run(command: str, filename: str = 'cmd.exe') -> JsonRPCAction: +def shell_run(command: str, filename: str = 'cmd.exe') -> JsonRPCRequest: """Run a shell command.""" - return _send_action("ShellRun", command, filename) + return create_request(_get_namespace("ShellRun"), [command, filename]) -def close_app() -> JsonRPCAction: +def close_app() -> JsonRPCRequest: """Close Flow Launcher.""" - return _send_action("CloseApp") + return create_request(_get_namespace("CloseApp")) -def hide_app() -> JsonRPCAction: +def hide_app() -> JsonRPCRequest: """Hide Flow Launcher.""" - return _send_action("HideApp") + return create_request(_get_namespace("HideApp")) -def show_app() -> JsonRPCAction: +def show_app() -> JsonRPCRequest: """Show Flow Launcher.""" - return _send_action("ShowApp") + return create_request(_get_namespace("ShowApp")) -def show_msg(title: str, sub_title: str, ico_path: str = "") -> JsonRPCAction: +def show_msg(title: str, sub_title: str, ico_path: str = "") -> JsonRPCRequest: """Show a message in Flow Launcher.""" - return _send_action("ShowMsg", title, sub_title, ico_path) + return create_request(_get_namespace("ShowMsg"), [title, sub_title, ico_path]) -def open_setting_dialog() -> JsonRPCAction: +def open_setting_dialog() -> JsonRPCRequest: """Open the settings window in Flow Launcher.""" - return _send_action("OpenSettingDialog") + return create_request(_get_namespace("OpenSettingDialog")) -def start_loading_bar() -> JsonRPCAction: +def start_loading_bar() -> JsonRPCRequest: """Start the loading bar in Flow Launcher.""" - return _send_action("StartLoadingBar") + return create_request(_get_namespace("StartLoadingBar")) -def stop_loading_bar() -> JsonRPCAction: +def stop_loading_bar() -> JsonRPCRequest: """Stop the loading bar in Flow Launcher.""" - return _send_action("StopLoadingBar") + return create_request(_get_namespace("StopLoadingBar")) -def reload_plugins() -> JsonRPCAction: +def reload_plugins() -> JsonRPCRequest: """Reload the plugins in Flow Launcher.""" - return _send_action("ReloadPlugins") + return create_request(_get_namespace("ReloadPlugins")) -def copy_to_clipboard(text: str, direct_copy: bool = False, show_default_notification=True) -> JsonRPCAction: +def copy_to_clipboard(text: str, direct_copy: bool = False, show_default_notification=True) -> JsonRPCRequest: """Copy text to the clipboard.""" - return _send_action("CopyToClipboard", text, direct_copy, show_default_notification) + return create_request(_get_namespace("CopyToClipboard"), [text, direct_copy, show_default_notification]) -def open_directory(directory_path: str, filename_or_filepath: Optional[str] = None) -> JsonRPCAction: +def open_directory(directory_path: str, filename_or_filepath: Optional[str] = None) -> JsonRPCRequest: """Open a directory.""" - return _send_action("OpenDirectory", directory_path, filename_or_filepath) + return create_request(_get_namespace("OpenDirectory"), [directory_path, filename_or_filepath]) -def open_url(url: str, in_private: bool = False) -> JsonRPCAction: +def open_url(url: str, in_private: bool = False) -> JsonRPCRequest: """Open a URL.""" - return _send_action("OpenUrl", url, in_private) + return create_request(_get_namespace("OpenUrl"), [url, in_private]) -def open_uri(uri: str) -> JsonRPCAction: +def open_uri(uri: str) -> JsonRPCRequest: """Open a URI.""" - return _send_action("OpenAppUri", uri) + return create_request(_get_namespace("OpenUri"), [uri]) diff --git a/pyflowlauncher/jsonrpc.py b/pyflowlauncher/jsonrpc.py deleted file mode 100644 index 89aa567..0000000 --- a/pyflowlauncher/jsonrpc.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -import json -import sys -from typing import Any, Mapping - -if sys.version_info < (3, 11): - from typing_extensions import NotRequired, TypedDict -else: - from typing import NotRequired, TypedDict - - -class JsonRPCRequest(TypedDict): - method: str - parameters: list - settings: NotRequired[dict[Any, Any]] - - -class JsonRPCClient: - - def send(self, data: Mapping) -> None: - json.dump(data, sys.stdout) - - def recieve(self) -> JsonRPCRequest: - try: - return json.loads(sys.argv[1]) - except (IndexError, json.JSONDecodeError): - return {'method': 'query', 'parameters': ['']} diff --git a/pyflowlauncher/jsonrpc/__init__.py b/pyflowlauncher/jsonrpc/__init__.py new file mode 100644 index 0000000..39e780a --- /dev/null +++ b/pyflowlauncher/jsonrpc/__init__.py @@ -0,0 +1,5 @@ +from .id_generation import incremental_int + +JSONRPC_VER = "2.0" + +ids = incremental_int() diff --git a/pyflowlauncher/jsonrpc/client.py b/pyflowlauncher/jsonrpc/client.py new file mode 100644 index 0000000..7e09ef9 --- /dev/null +++ b/pyflowlauncher/jsonrpc/client.py @@ -0,0 +1,33 @@ +import json +from typing import List, Optional + +from . import JSONRPC_VER, ids +from .models import JsonRPCRequest + + +def create_request( + method: str, + parameters: Optional[List] = None, + id: Optional[int] = None, + jsonrpc: str = JSONRPC_VER +) -> JsonRPCRequest: + return { + "jsonrpc": jsonrpc, + "method": method, + "parameters": parameters or [], + "id": id or next(ids) + } + + +def request_from_string( + method: str, + parameters: Optional[List] = None, + id: Optional[int] = None, +) -> str: + return json.dumps( + create_request(method, parameters, id) + ) + + +def send_request(request: JsonRPCRequest) -> None: + print(json.dumps(request)) diff --git a/pyflowlauncher/jsonrpc/id_generation.py b/pyflowlauncher/jsonrpc/id_generation.py new file mode 100644 index 0000000..18b4723 --- /dev/null +++ b/pyflowlauncher/jsonrpc/id_generation.py @@ -0,0 +1,6 @@ +import itertools +from typing import Iterator + + +def incremental_int(start: int = 1) -> Iterator[int]: + return itertools.count(start) diff --git a/pyflowlauncher/jsonrpc/models.py b/pyflowlauncher/jsonrpc/models.py new file mode 100644 index 0000000..4d4033c --- /dev/null +++ b/pyflowlauncher/jsonrpc/models.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import sys +from typing import Any, List, Optional + +if sys.version_info < (3, 11): + from typing_extensions import NotRequired, TypedDict +else: + from typing import NotRequired, TypedDict + + +class BaseJsonRPCRequest(TypedDict): + """Standard JsonRPC Request""" + id: NotRequired[int] + jsonrpc: NotRequired[str] + method: str + parameters: List + + +class JsonRPCRequest(BaseJsonRPCRequest): + """Flow Launcher JsonRPC Request""" + dontHideAfterAction: NotRequired[bool] + settings: NotRequired[dict] + + +class BaseJsonRPCResult(TypedDict): + """Standard JsonRPC Result""" + id: NotRequired[int] + jsonrpc: str + result: Any + + +class JsonRPCResult(BaseJsonRPCResult): + """Flow Launcher JsonRPC Result""" + SettingsChange: NotRequired[Optional[dict]] diff --git a/pyflowlauncher/jsonrpc/server.py b/pyflowlauncher/jsonrpc/server.py new file mode 100644 index 0000000..1c05602 --- /dev/null +++ b/pyflowlauncher/jsonrpc/server.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import json +from typing import Any, Dict, Optional + +from .models import JsonRPCRequest, JsonRPCResult + +from . import JSONRPC_VER, ids + + +def parse_request(message: str) -> JsonRPCRequest: + request = json.loads(message) + if "id" not in request: + request["id"] = next(ids) + return request + + +def create_response(result: Any, id: int, SettingsChange: Optional[Dict] = None) -> JsonRPCResult: + return { + "jsonrpc": JSONRPC_VER, + "result": result, + "id": id, + "SettingsChange": SettingsChange + } + + +def response(result: Any, id: int, SettingsChange: Optional[Dict] = None) -> str: + return json.dumps(create_response(result, id, SettingsChange)) diff --git a/pyflowlauncher/method.py b/pyflowlauncher/method.py index 50d3e0a..09bdf3d 100644 --- a/pyflowlauncher/method.py +++ b/pyflowlauncher/method.py @@ -1,10 +1,11 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional -from .result import JsonRPCAction, Result, ResultResponse, send_results +from .result import Result, send_results from .shared import logger +from .jsonrpc.models import JsonRPCResult class Method(ABC): @@ -16,9 +17,9 @@ def __init__(self) -> None: def add_result(self, result: Result) -> None: self._results.append(result) - def return_results(self, settings: Optional[Dict[str, Any]] = None) -> ResultResponse: - return send_results(self._results, settings) + def return_results(self) -> List[Dict[str, Any]]: + return send_results(self._results) @abstractmethod - def __call__(self, *args, **kwargs) -> ResultResponse | JsonRPCAction: + def __call__(self, *args, **kwargs) -> Optional[JsonRPCResult]: pass diff --git a/pyflowlauncher/plugin.py b/pyflowlauncher/plugin.py index 9f26ee6..9532bb5 100644 --- a/pyflowlauncher/plugin.py +++ b/pyflowlauncher/plugin.py @@ -10,18 +10,17 @@ from pyflowlauncher.shared import logger from .event import EventHandler -from .jsonrpc import JsonRPCClient -from .result import JsonRPCAction, ResultResponse +from .jsonrpc import server +from .jsonrpc.models import JsonRPCRequest, JsonRPCResult from .manifest import PluginManifestSchema, MANIFEST_FILE -Method = Callable[..., Union[ResultResponse, JsonRPCAction, None]] +Method = Callable[..., Union[JsonRPCResult, None]] class Plugin: def __init__(self, methods: list[Method] | None = None) -> None: self._logger = logger(self) - self._client = JsonRPCClient() self._event_handler = EventHandler() self._settings: dict[str, Any] = {} if methods: @@ -56,30 +55,22 @@ def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]: return handler return wrapper - def action(self, method: Method, parameters: Optional[Iterable] = None) -> JsonRPCAction: + def action(self, method: Method, parameters: Optional[Iterable] = None) -> JsonRPCRequest: """Register a method and return a JsonRPCAction that calls it.""" method_name = self.add_method(method) - return {"method": method_name, "parameters": parameters or []} + return {"method": method_name, "parameters": list(parameters or [])} - @property - def settings(self) -> dict: - if self._settings is None: - self._settings = {} - self._settings = self._client.recieve().get('settings', {}) - return self._settings + async def run_async(self) -> None: + request = server.parse_request(sys.argv[1]) + feedback = await self._event_handler.trigger_event(request["method"], *request["parameters"]) + print(server.response(feedback, request["id"])) def run(self) -> None: - request = self._client.recieve() - method = request["method"] - parameters = request.get('parameters', []) if sys.version_info >= (3, 10, 0): - feedback = asyncio.run(self._event_handler.trigger_event(method, *parameters)) + asyncio.run(self.run_async()) else: loop = asyncio.get_event_loop() - feedback = loop.run_until_complete(self._event_handler.trigger_event(method, *parameters)) - if not feedback: - return - self._client.send(feedback) + loop.run_until_complete(self.run_async()) @property def run_dir(self) -> Path: diff --git a/pyflowlauncher/result.py b/pyflowlauncher/result.py index 84a3dc6..35653f6 100644 --- a/pyflowlauncher/result.py +++ b/pyflowlauncher/result.py @@ -1,27 +1,16 @@ from __future__ import annotations -import sys from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union - -if sys.version_info < (3, 11): - from typing_extensions import NotRequired, TypedDict -else: - from typing import NotRequired, TypedDict +from typing import TYPE_CHECKING, TypedDict, Any, Dict, Iterable, List, Optional, Union +from .jsonrpc.models import JsonRPCRequest +from .jsonrpc.client import create_request if TYPE_CHECKING: from .plugin import Method -class JsonRPCAction(TypedDict): - """Flow Launcher JsonRPCAction""" - method: str - parameters: Iterable - dontHideAfterAction: NotRequired[bool] - - class Glyph(TypedDict): """Flow Launcher Glyph""" Glyph: str @@ -42,7 +31,7 @@ class Result: SubTitle: Optional[str] = None IcoPath: Optional[Union[str, Path]] = None Score: int = 0 - JsonRPCAction: Optional[JsonRPCAction] = None + JsonRPCAction: Optional[JsonRPCRequest] = None ContextData: Optional[Iterable] = None Glyph: Optional[Glyph] = None CopyText: Optional[str] = None @@ -57,18 +46,13 @@ def add_action(self, method: Method, parameters: Optional[Iterable[Any]] = None, *, dont_hide_after_action: bool = False) -> None: - self.JsonRPCAction = { - "method": method.__name__, - "parameters": parameters or [], - "dontHideAfterAction": dont_hide_after_action - } - - -class ResultResponse(TypedDict): - result: List[Dict[str, Any]] - SettingsChange: NotRequired[Optional[Dict[str, Any]]] + self.JsonRPCAction = create_request( + method.__name__, + list(parameters or []), + ) + self.JsonRPCAction["dontHideAfterAction"] = dont_hide_after_action -def send_results(results: Iterable[Result], settings: Optional[Dict[str, Any]] = None) -> ResultResponse: +def send_results(results: Iterable[Result]) -> List[Dict[str, Any]]: """Formats and returns results as a JsonRPCResponse""" - return {'result': [result.as_dict() for result in results], 'SettingsChange': settings} + return [result.as_dict() for result in results] diff --git a/pyflowlauncher/settings.py b/pyflowlauncher/settings.py index ba5cbf9..215b29d 100644 --- a/pyflowlauncher/settings.py +++ b/pyflowlauncher/settings.py @@ -1,7 +1,10 @@ +import sys from typing import Any, Dict -from .jsonrpc import JsonRPCClient + +from .jsonrpc.server import parse_request def settings() -> Dict[str, Any]: """Retrieve the settings from Flow Launcher.""" - return JsonRPCClient().recieve().get('settings', {}) + request = parse_request(sys.argv[1]) + return request.get("settings", {}) From 9bacbe8efbab13fd7b3acb03d7eb87962ebd47e2 Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Mon, 19 Feb 2024 06:15:50 -0500 Subject: [PATCH 2/3] Update tests --- tests/test_api.py | 38 +++++++++++++++++++++--------------- tests/test_id_generation.py | 7 +++++++ tests/test_jsonrpc.py | 30 ---------------------------- tests/test_jsonrpc_client.py | 17 ++++++++++++++++ tests/test_jsonrpc_server.py | 23 ++++++++++++++++++++++ 5 files changed, 69 insertions(+), 46 deletions(-) create mode 100644 tests/test_id_generation.py delete mode 100644 tests/test_jsonrpc.py create mode 100644 tests/test_jsonrpc_client.py create mode 100644 tests/test_jsonrpc_server.py diff --git a/tests/test_api.py b/tests/test_api.py index 3e2afe9..7b45a0a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,61 +1,67 @@ from pyflowlauncher import api -def test_send_action(): - assert api._send_action("Test", "Test") == {"method": "Flow.Launcher.Test", "parameters": ("Test",)} +def check_output(func, method, parameters): + req = func(*parameters) + assert req["method"] == method + assert req["parameters"] == parameters + + +def test_get_namespace(): + assert api._get_namespace("Test") == "Flow.Launcher.Test" def test_change_query(): - assert api.change_query("Test", False) == {"method": "Flow.Launcher.ChangeQuery", "parameters": ("Test", False)} + check_output(api.change_query, "Flow.Launcher.ChangeQuery", ["Test", False]) def test_shell_run(): - assert api.shell_run("Test", "Test") == {"method": "Flow.Launcher.ShellRun", "parameters": ("Test", "Test")} + check_output(api.shell_run, "Flow.Launcher.ShellRun", ["Test", "Test"]) def test_close_app(): - assert api.close_app() == {"method": "Flow.Launcher.CloseApp", "parameters": ()} + check_output(api.close_app, "Flow.Launcher.CloseApp", []) def test_hide_app(): - assert api.hide_app() == {"method": "Flow.Launcher.HideApp", "parameters": ()} + check_output(api.hide_app, "Flow.Launcher.HideApp", []) def test_show_app(): - assert api.show_app() == {"method": "Flow.Launcher.ShowApp", "parameters": ()} + check_output(api.show_app, "Flow.Launcher.ShowApp", []) def test_show_msg(): - assert api.show_msg("Test", "Test", "Test") == {"method": "Flow.Launcher.ShowMsg", "parameters": ("Test", "Test", "Test")} + check_output(api.show_msg, "Flow.Launcher.ShowMsg", ["Test", "Test", "Test"]) def test_open_setting_dialog(): - assert api.open_setting_dialog() == {"method": "Flow.Launcher.OpenSettingDialog", "parameters": ()} + check_output(api.open_setting_dialog, "Flow.Launcher.OpenSettingDialog", []) def test_start_loading_bar(): - assert api.start_loading_bar() == {"method": "Flow.Launcher.StartLoadingBar", "parameters": ()} + check_output(api.start_loading_bar, "Flow.Launcher.StartLoadingBar", []) def test_stop_loading_bar(): - assert api.stop_loading_bar() == {"method": "Flow.Launcher.StopLoadingBar", "parameters": ()} + check_output(api.stop_loading_bar, "Flow.Launcher.StopLoadingBar", []) def test_reload_plugins(): - assert api.reload_plugins() == {"method": "Flow.Launcher.ReloadPlugins", "parameters": ()} + check_output(api.reload_plugins, "Flow.Launcher.ReloadPlugins", []) def test_copy_to_clipboard(): - assert api.copy_to_clipboard("Test", False, True) == {"method": "Flow.Launcher.CopyToClipboard", "parameters": ("Test", False, True)} + check_output(api.copy_to_clipboard, "Flow.Launcher.CopyToClipboard", ["Test", False, True]) def test_open_directory(): - assert api.open_directory("Test", "Test") == {"method": "Flow.Launcher.OpenDirectory", "parameters": ("Test", "Test")} + check_output(api.open_directory, "Flow.Launcher.OpenDirectory", ["Test", "Test"]) def test_open_url(): - assert api.open_url("Test", False) == {"method": "Flow.Launcher.OpenUrl", "parameters": ("Test", False)} + check_output(api.open_url, "Flow.Launcher.OpenUrl", ["Test", False]) def test_open_uri(): - assert api.open_uri("Test") == {"method": "Flow.Launcher.OpenAppUri", "parameters": ("Test",)} + check_output(api.open_uri, "Flow.Launcher.OpenUri", ["Test"]) diff --git a/tests/test_id_generation.py b/tests/test_id_generation.py new file mode 100644 index 0000000..cb98d4a --- /dev/null +++ b/tests/test_id_generation.py @@ -0,0 +1,7 @@ +from pyflowlauncher.jsonrpc import id_generation + + +def test_increment_int(): + id_gen = id_generation.incremental_int(1) + assert next(id_gen) == 1 + assert next(id_gen) == 2 diff --git a/tests/test_jsonrpc.py b/tests/test_jsonrpc.py deleted file mode 100644 index f9e183b..0000000 --- a/tests/test_jsonrpc.py +++ /dev/null @@ -1,30 +0,0 @@ -import sys -import pytest - -from pyflowlauncher.jsonrpc import JsonRPCClient - - -@pytest.fixture -def capture_stdout(monkeypatch): - buffer = {"stdout": "", "write_calls": 0} - - def fake_writes(s): - buffer["stdout"] += s - buffer["write_calls"] += 1 - - monkeypatch.setattr(sys.stdout, "write", fake_writes) - return buffer - - -def test_send(capture_stdout): - jsonrpc = JsonRPCClient() - jsonrpc.send({"method": "Test", "parameters": []}) - - assert capture_stdout["stdout"] == '{"method": "Test", "parameters": []}' - - -def test_recieve(monkeypatch): - jsonrpc = JsonRPCClient() - - monkeypatch.setattr(sys, "argv", ["test.py", '{"method": "Test", "parameters": []}']) - assert jsonrpc.recieve() == {"method": "Test", "parameters": []} diff --git a/tests/test_jsonrpc_client.py b/tests/test_jsonrpc_client.py new file mode 100644 index 0000000..9f72f4c --- /dev/null +++ b/tests/test_jsonrpc_client.py @@ -0,0 +1,17 @@ +from pyflowlauncher.jsonrpc import client + + +def test_create_request(): + assert client.create_request("Test", ["Test", False], id=1) == { + "method": "Test", "parameters": ["Test", False], "id": 1, "jsonrpc": "2.0"} + + +def test_request_from_string(): + assert client.request_from_string( + "Test", ["Test", False], id=1) == '{"jsonrpc": "2.0", "method": "Test", "parameters": ["Test", false], "id": 1}' + + +def test_send_request(capsys): + client.send_request(client.create_request("Test", ["Test", False], id=1)) + captured = capsys.readouterr() + assert captured.out == '{"jsonrpc": "2.0", "method": "Test", "parameters": ["Test", false], "id": 1}\n' diff --git a/tests/test_jsonrpc_server.py b/tests/test_jsonrpc_server.py new file mode 100644 index 0000000..06b6840 --- /dev/null +++ b/tests/test_jsonrpc_server.py @@ -0,0 +1,23 @@ +from pyflowlauncher.jsonrpc import server + + +def test_parse_request(): + assert server.parse_request('{"jsonrpc": "2.0", "method": "Test", "params": ["Test", false], "id": 1}') == { + "jsonrpc": "2.0", + "method": "Test", + "params": ["Test", False], + "id": 1, + } + + +def test_create_response(): + assert server.create_response("Test", 1) == { + "jsonrpc": "2.0", + "result": "Test", + "id": 1, + "SettingsChange": None, + } + + +def test_response(): + assert server.response("Test", 1) == '{"jsonrpc": "2.0", "result": "Test", "id": 1, "SettingsChange": null}' From d5db3ec0d191c7528d162e4cdd9282151d50b0c8 Mon Sep 17 00:00:00 2001 From: Garulf <535299+Garulf@users.noreply.github.com> Date: Mon, 19 Feb 2024 06:16:22 -0500 Subject: [PATCH 3/3] Update response --- pyflowlauncher/jsonrpc/models.py | 14 ++++++++++---- pyflowlauncher/jsonrpc/server.py | 17 ++++++++++++----- pyflowlauncher/plugin.py | 4 ++-- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/pyflowlauncher/jsonrpc/models.py b/pyflowlauncher/jsonrpc/models.py index 4d4033c..562944e 100644 --- a/pyflowlauncher/jsonrpc/models.py +++ b/pyflowlauncher/jsonrpc/models.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional if sys.version_info < (3, 11): from typing_extensions import NotRequired, TypedDict @@ -20,16 +20,22 @@ class BaseJsonRPCRequest(TypedDict): class JsonRPCRequest(BaseJsonRPCRequest): """Flow Launcher JsonRPC Request""" dontHideAfterAction: NotRequired[bool] - settings: NotRequired[dict] + settings: NotRequired[Dict] class BaseJsonRPCResult(TypedDict): """Standard JsonRPC Result""" - id: NotRequired[int] + id: NotRequired[Optional[int]] jsonrpc: str result: Any class JsonRPCResult(BaseJsonRPCResult): """Flow Launcher JsonRPC Result""" - SettingsChange: NotRequired[Optional[dict]] + SettingsChange: NotRequired[Optional[Dict]] + + +class PartialJsonRPCResult(TypedDict): + """Flow Launcher JsonRPC Result""" + SettingsChange: NotRequired[Optional[Dict]] + result: NotRequired[Dict] diff --git a/pyflowlauncher/jsonrpc/server.py b/pyflowlauncher/jsonrpc/server.py index 1c05602..9d2a487 100644 --- a/pyflowlauncher/jsonrpc/server.py +++ b/pyflowlauncher/jsonrpc/server.py @@ -3,7 +3,7 @@ import json from typing import Any, Dict, Optional -from .models import JsonRPCRequest, JsonRPCResult +from .models import JsonRPCRequest, JsonRPCResult, PartialJsonRPCResult from . import JSONRPC_VER, ids @@ -15,14 +15,21 @@ def parse_request(message: str) -> JsonRPCRequest: return request -def create_response(result: Any, id: int, SettingsChange: Optional[Dict] = None) -> JsonRPCResult: +def create_response(result: Any, SettingsChange: Optional[Dict] = None, id: Optional[int] = None) -> JsonRPCResult: return { "jsonrpc": JSONRPC_VER, "result": result, + "SettingsChange": SettingsChange, "id": id, - "SettingsChange": SettingsChange } -def response(result: Any, id: int, SettingsChange: Optional[Dict] = None) -> str: - return json.dumps(create_response(result, id, SettingsChange)) +def response_string(result: Any, id: Optional[int] = None, SettingsChange: Optional[Dict] = None) -> str: + return json.dumps(create_response(result, SettingsChange, id)) + + +def response(result: Any, SettingsChange: Optional[Dict] = None) -> PartialJsonRPCResult: + return { + "result": result, + "SettingsChange": SettingsChange + } diff --git a/pyflowlauncher/plugin.py b/pyflowlauncher/plugin.py index 9532bb5..f0353f2 100644 --- a/pyflowlauncher/plugin.py +++ b/pyflowlauncher/plugin.py @@ -62,8 +62,8 @@ def action(self, method: Method, parameters: Optional[Iterable] = None) -> JsonR async def run_async(self) -> None: request = server.parse_request(sys.argv[1]) - feedback = await self._event_handler.trigger_event(request["method"], *request["parameters"]) - print(server.response(feedback, request["id"])) + response = await self._event_handler.trigger_event(request["method"], *request["parameters"]) + print(server.response_string(**response, id=request["id"])) def run(self) -> None: if sys.version_info >= (3, 10, 0):