From e80438132269a927e07d06d15a8c15d250a34678 Mon Sep 17 00:00:00 2001 From: Moosems <95927277+Moosems@users.noreply.github.com> Date: Fri, 6 Sep 2024 21:18:49 -0600 Subject: [PATCH] Prepare Release Candidate 0 --- README.md | 2 +- collegamento/client_server/client.py | 37 ++++++++++++------- collegamento/client_server/server.py | 4 +-- collegamento/client_server/test_func.py | 2 -- collegamento/client_server/utils.py | 19 +++++----- collegamento/files_variant.py | 47 ++++++++++--------------- docs/source/classes.rst | 16 ++++++++- docs/source/conf.py | 2 +- docs/source/variables.rst | 7 ++++ setup.py | 2 +- tests/test_file_variant.py | 2 ++ 11 files changed, 83 insertions(+), 57 deletions(-) delete mode 100644 collegamento/client_server/test_func.py diff --git a/README.md b/README.md index e4e4357..3fa87b6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

collegamento v0.2.0

+

collegamento v0.3.0-rc0

A tool that makes it much easier to make offload work when asyncio isn't an option. diff --git a/collegamento/client_server/client.py b/collegamento/client_server/client.py index 56b6251..f41b4ce 100644 --- a/collegamento/client_server/client.py +++ b/collegamento/client_server/client.py @@ -1,13 +1,16 @@ -# TODO: make sure everything is type hinted while removing redundancy """Defines the Client and Server class which provides a convenient and easy to use IPC interface. >>> def test(*args): ... return >>> c = Client({"test": (test, True)}) >>> c.request({"command": "test"}) +>>> c.request({"command": "test"}) >>> from time import sleep >>> sleep(1) ->>> assert c.get_response("test") is not None +>>> res = c.get_response("test") +>>> res +[{...}, {...}] +>>> assert len(res) == 2 >>> c.kill_IPC() """ @@ -20,7 +23,9 @@ USER_FUNCTION, CollegamentoError, Request, + RequestQueueType, Response, + ResponseQueueType, ) @@ -47,13 +52,13 @@ def __init__( The most common input is commands and id_max. server_type is only really useful for wrappers.""" self.all_ids: list[int] = [] - self.id_max = id_max + self.id_max: int = id_max # int corresponds to str and str to int = int -> str & str -> int self.current_ids: dict[str | int, int | str] = {} self.newest_responses: dict[str, list[Response]] = {} - self.server_type = server_type + self.server_type: type = server_type self.commands: dict[str, tuple[USER_FUNCTION, bool]] = {} @@ -67,8 +72,8 @@ def __init__( self.commands[command] = func self.newest_responses[command] = [] - self.request_queue: Queue - self.response_queue: Queue + self.request_queue: RequestQueueType + self.response_queue: ResponseQueueType self.main_process: Process self.create_server() @@ -81,6 +86,9 @@ def create_server(self): # so it doesn't try to access queues that no longer exist self.main_process.terminate() + self.check_responses() + self.all_ids = [] # The remaining ones will never have been finished + self.request_queue = Queue() self.response_queue = Queue() self.main_process = Process( @@ -99,7 +107,7 @@ def create_message_id(self) -> int: # In cases where there are many many requests being sent it may be faster to choose a # random id than to iterate through the list of id's and find an unclaimed one - id = randint(1, self.id_max) # 0 is reserved for the empty case + id: int = randint(1, self.id_max) # 0 is reserved for the empty case while id in self.all_ids: id = randint(1, self.id_max) self.all_ids.append(id) @@ -136,13 +144,13 @@ def request(self, command: str, **kwargs) -> None: def parse_response(self, res: Response) -> None: """Parses main process output and discards useless responses - internal API""" - id = res["id"] + id: int = res["id"] self.all_ids.remove(id) if "command" not in res: return - command = res["command"] + command: str = res["command"] if command == "add-command": return @@ -177,7 +185,9 @@ def get_response(self, command: str) -> Response | list[Response] | None: # If we know that the command doesn't allow multiple requests don't give a list if not self.commands[command][1]: - return response[0] + return response[ + 0 + ] # Will only ever be one but we know its at index 0 return response @@ -198,8 +208,11 @@ def add_command( "type": "request", "command": "add-command", } - command_tuple = (command, multiple_requests) - final_request.update({"name": name, "function": command_tuple}) # type: ignore + command_tuple: tuple[USER_FUNCTION, bool] = ( + command, + multiple_requests, + ) + final_request.update(**{"name": name, "function": command_tuple}) self.request_queue.put(final_request) self.commands[name] = command_tuple diff --git a/collegamento/client_server/server.py b/collegamento/client_server/server.py index 021333a..a7bff29 100644 --- a/collegamento/client_server/server.py +++ b/collegamento/client_server/server.py @@ -69,7 +69,7 @@ def parse_line(self, message: Request) -> None: self.simple_id_response(id) return - command: str = message["command"] # type: ignore + command: str = message["command"] if command == "add-command": request_name: str = message["name"] # type: ignore @@ -91,7 +91,7 @@ def parse_line(self, message: Request) -> None: self.newest_requests[command].append(message) def cancel_old_ids(self) -> None: - accepted_ids = [ + accepted_ids: list[int] = [ request["id"] for request_list in list(self.newest_requests.values()) for request in request_list diff --git a/collegamento/client_server/test_func.py b/collegamento/client_server/test_func.py deleted file mode 100644 index 6ff32af..0000000 --- a/collegamento/client_server/test_func.py +++ /dev/null @@ -1,2 +0,0 @@ -def foo(server, request): - print("Foo called", request["id"]) diff --git a/collegamento/client_server/utils.py b/collegamento/client_server/utils.py index af92aea..6ac15ea 100644 --- a/collegamento/client_server/utils.py +++ b/collegamento/client_server/utils.py @@ -4,6 +4,7 @@ from beartype.typing import Callable +# TODO: Move away from TypedDict class Message(TypedDict): """Base class for messages in and out of the server""" @@ -25,19 +26,21 @@ class Response(Message): result: NotRequired[Any] +class CollegamentoError(Exception): ... # I don't like the boilerplate either + + USER_FUNCTION = Callable[["Server", Request], Any] # type: ignore COMMANDS_MAPPING = dict[ str, USER_FUNCTION | tuple[USER_FUNCTION, bool] ] # if bool is true the command allows multiple requests + +ResponseQueueType = GenericQueueClass +RequestQueueType = GenericQueueClass + +# "If this is CPython < 3.12. We are now in the No Man's Land +# of Typing. In this case, avoid subscripting "GenericQueue". Ugh." +# - @leycec, maintainer of the amazing @beartype if TYPE_CHECKING: ResponseQueueType = GenericQueueClass[Response] RequestQueueType = GenericQueueClass[Request] -# Else, this is CPython < 3.12. We are now in the No Man's Land -# of Typing. In this case, avoid subscripting "GenericQueue". Ugh. -else: - ResponseQueueType = GenericQueueClass - RequestQueueType = GenericQueueClass - - -class CollegamentoError(Exception): ... # I don't like the boilerplate either diff --git a/collegamento/files_variant.py b/collegamento/files_variant.py index fa95dcf..835cff2 100644 --- a/collegamento/files_variant.py +++ b/collegamento/files_variant.py @@ -22,9 +22,10 @@ def update_files(server: "FileServer", request: Request) -> None: if request["remove"]: # type: ignore server.files.pop(file) - else: - contents: str = request["contents"] # type: ignore - server.files[file] = contents + return + + contents: str = request["contents"] # type: ignore + server.files[file] = contents class FileClient(Client): @@ -47,18 +48,15 @@ def create_server(self) -> None: super().create_server() - files_copy = self.files.copy() - self.files = {} - for file, data in files_copy.items(): + for file, data in self.files.items(): self.update_file(file, data) def request(self, command: str, **kwargs) -> None: - if "file" in kwargs: - file = kwargs["file"] - if file not in self.files: - raise CollegamentoError( - f"File {file} not in files! Files are {self.files.keys()}" - ) + file: str | None = kwargs.get("file") + if file and file not in self.files: + raise CollegamentoError( + f"File {file} not in files! Files are {self.files.keys()}" + ) super().request(command, **kwargs) @@ -67,14 +65,12 @@ def update_file(self, file: str, current_state: str) -> None: self.files[file] = current_state - file_notification: dict = { - "command": "FileNotification", - "file": file, - "remove": False, - "contents": self.files[file], - } - - super().request(**file_notification) + super().request( + "FileNotification", + file=file, + remove=False, + contents=self.files[file], + ) def remove_file(self, file: str) -> None: """Removes a file from the main_server - external API""" @@ -83,13 +79,7 @@ def remove_file(self, file: str) -> None: f"Cannot remove file {file} as file is not in file database!" ) - file_notification: dict = { - "command": "FileNotification", - "file": file, - "remove": True, - } - - super().request(**file_notification) + super().request("FileNotification", file=file, remove=True) class FileServer(Server): @@ -112,7 +102,6 @@ def __init__( def handle_request(self, request: Request) -> None: if "file" in request and request["command"] != "FileNotification": - file = request["file"] - request["file"] = self.files[file] + request["file"] = self.files[request["file"]] super().handle_request(request) diff --git a/docs/source/classes.rst b/docs/source/classes.rst index f4354de..9805fa7 100644 --- a/docs/source/classes.rst +++ b/docs/source/classes.rst @@ -14,7 +14,7 @@ The ``CollegamentoError`` class is a simple error class for ``Collegamento``. ``Request`` *********** -The ``Request`` class is a TypedDict meant to provide a framework for items given to functions used by the IPC. It *will* almsot always contain extra items regardless of the fact that that's not supposed to happen (just add ``# type: ignore`` to the end of the line to shut up the langserver). The data provided will not be typed checked to make sure its proper. The responsibility of data rests on the user. +The ``Request`` class is a TypedDict meant to provide a framework for items given to functions used by the IPC. It *will* almsot always contain extra items regardless of the fact that they aren't outlined in the ``TypedDict`` (just add ``# type: ignore`` to the end of the line to shut up the langserver). The data provided will not be typed checked to make sure its proper. The responsibility of data rests on the user. .. _Response Overview: @@ -71,3 +71,17 @@ This class also has some changed functionality. When you make a ``.request()`` a ************** The ``FileServer`` is a backend piece of code made visible for commands that can be given to a ``FileClient``. See my explanation on :ref:`Server Overview` + +.. _RequestQueueType Overview: + +``RequestQueueType`` +********************* + +A subtype hint for `multiprocessing.Queue[Request]` that is meant to be used for the ``Request`` class that defaults to `multiprocessing.Queue` if the python version is less than 3.12. + +.. _ResponseQueueType Overview: + +``ResponseQueueType`` +********************** + +A subtype hint for `multiprocessing.Queue[Response]` that is meant to be used for the ``Response`` class that defaults to `multiprocessing.Queue` if the python version is less than 3.12. diff --git a/docs/source/conf.py b/docs/source/conf.py index 42b4788..e6f1017 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,7 @@ project = "collegamento" copyright = "2024, Moosems" author = "Moosems" -release = "v0.2.0" +release = "v0.3.0-rc0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/source/variables.rst b/docs/source/variables.rst index 6fee7de..b47f705 100644 --- a/docs/source/variables.rst +++ b/docs/source/variables.rst @@ -8,3 +8,10 @@ Variables ***************** ``USER_FUNCTION`` is a type variable that simply states that any function that matches this type takes in a :ref:`Server Overview` and :ref:`Request Overview` class (positionally) and can return anything it pleases (though this will never be used). + +.. _COMMANDS_MAPPING Overview: + +``COMMANDS_MAPPING`` +******************** + +``COMMANDS_MAPPING`` is a type variable that states that any dictionary that matches this type has keys that are strings and values that are either functions that match the :ref:`USER_FUNCTION Overview` type or a tuple with that function and a boolean that indicates whether the function can have multiple :ref:``Request Overview``'s. diff --git a/setup.py b/setup.py index 4e7734f..f659c86 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="collegamento", - version="0.2.0", + version="0.3.0-rc0", description="Collegamento provides an easy to use Client/Server IPC backend", author="Moosems", author_email="moosems.j@gmail.com", diff --git a/tests/test_file_variant.py b/tests/test_file_variant.py index d6abc09..8b9f05f 100644 --- a/tests/test_file_variant.py +++ b/tests/test_file_variant.py @@ -17,6 +17,8 @@ def test_file_variants(): context.update_file("test", "test contents") context.update_file("test2", "test contents2") + sleep(1) + context.create_server() context.request("test") sleep(1)