Skip to content

Commit

Permalink
Use collegamento
Browse files Browse the repository at this point in the history
  • Loading branch information
Moosems committed Aug 1, 2024
1 parent 15f0bae commit 30d4323
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 455 deletions.
3 changes: 2 additions & 1 deletion salve/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

beartype_this_package()

from collegamento import Response # noqa: F401, E402

from .ipc import IPC # noqa: F401, E402
from .misc import ( # noqa: F401, E402
AUTOCOMPLETE,
Expand All @@ -11,6 +13,5 @@
HIGHLIGHT,
LINKS_AND_CHARS,
REPLACEMENTS,
Response,
)
from .server_functions import is_unicode_letter # noqa: F401, E402
227 changes: 43 additions & 184 deletions salve/ipc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,114 +3,52 @@
from pathlib import Path
from random import randint

from collegamento import USER_FUNCTION, FileClient

from .misc import (
AUTOCOMPLETE,
COMMAND,
COMMANDS,
DEFINITION,
EDITORCONFIG,
Notification,
Request,
RequestQueueType,
Response,
ResponseQueueType,
HIGHLIGHT,
LINKS_AND_CHARS,
REPLACEMENTS,
)
from .wrappers import (
editorconfig_request_wrapper,
find_autocompletions_request_wrapper,
get_definition_request_wrapper,
get_highlights_request_wrapper,
get_replacements_request_wrapper,
get_special_tokens_request_wrapper,
)
from .server import Server

# from collegamento import FileClient



class IPC:
class IPC(FileClient):
"""The IPC class is used to talk to the server and run commands. The public API includes the following methods:
- IPC.request()
- IPC.cancel_request()
- IPC.update_file()
- IPC.remove_file()
- IPC.kill_IPC()
"""

def __init__(self, id_max: int = 15_000) -> None:
self.all_ids: list[int] = []
self.id_max = id_max
self.current_ids: dict[str, int] = {}
self.newest_responses: dict[str, Response | None] = {}
for command in COMMANDS:
self.current_ids[command] = 0
self.newest_responses[command] = None

self.files: dict[str, str] = {}

self.logger: Logger = getLogger("IPC")
self.logger.info("Creating server")
self.response_queue: ResponseQueueType = Queue()
self.requests_queue: RequestQueueType = Queue()
self.main_server: Process
self.create_server()
self.logger.info("Initialization is complete")

def create_server(self) -> None:
"""Creates the main_server through a subprocess - internal API"""
freeze_support()
server_logger = getLogger("Server")
self.main_server = Process(
target=Server,
args=(self.response_queue, self.requests_queue, server_logger),
daemon=True,
def __init__(self, id_max: int = 15000) -> None:
super().__init__(
id_max=id_max,
commands={
AUTOCOMPLETE: find_autocompletions_request_wrapper,
REPLACEMENTS: get_replacements_request_wrapper,
HIGHLIGHT: get_highlights_request_wrapper,
EDITORCONFIG: editorconfig_request_wrapper,
DEFINITION: get_definition_request_wrapper,
LINKS_AND_CHARS: get_special_tokens_request_wrapper,
},
)
self.main_server.start()
self.logger.info("Server created")

self.logger.info("Copying files to server")
files_copy = self.files.copy()
self.files = {}
for file, data in files_copy.items():
self.update_file(file, data)
self.logger.debug("Finished copying files to server")

def create_message(self, type: str, **kwargs) -> None:
"""Creates a Message based on the args and kwawrgs provided. Highly flexible. - internal API"""
self.logger.info("Creating message for server")
id = 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)

self.logger.debug("ID for message created")
if not self.main_server.is_alive():
self.logger.critical(
"Server was killed at some point, creating server"
)
self.create_server()

match type:
case "request":
self.logger.info("Creating request for server")
command = kwargs.get("command", "")
self.current_ids[command] = id
request: Request = {
"id": id,
"type": type,
"command": command,
"file": "",
}
request.update(**kwargs)
self.logger.debug(f"Request created: {request}")
self.requests_queue.put(request)
self.logger.info("Message sent")
case "notification":
self.logger.info("Creating notification for server")
notification: Notification = {
"id": id,
"type": type,
"remove": False,
"file": "",
"contents": "",
}
notification.update(**kwargs)
self.logger.debug(f"Notification created: {notification}")
self.requests_queue.put(notification)
self.logger.info("Message sent")

def request(
# Pyright likes to complain and say this won't work but it actually does
# TODO: Use plum or custom multiple dispatch (make it a new project for salve organization)
def request( # type: ignore
self,
command: COMMAND,
file: str = "",
Expand All @@ -136,96 +74,17 @@ def request(
raise Exception(f"File {file} does not exist in system!")

self.logger.debug("Sending info to create_message()")
self.create_message(
type="request",
command=command,
file=file,
expected_keywords=expected_keywords,
current_word=current_word,
language=language,
text_range=text_range,
file_path=file_path,
definition_starters=definition_starters,
request: dict = {
"command": command,
"expected_keywords": expected_keywords,
"current_word": current_word,
"language": language,
"text_range": text_range,
"file_path": file_path,
"definition_starters": definition_starters,
}
if file:
request.update({"file": file})
super().request(
request
)

def cancel_request(self, command: str) -> None:
"""Cancels a request of type command - external API"""
if command not in COMMANDS:
self.logger.exception(
f"Cannot cancel command {command}, valid commands are {COMMANDS}"
)
raise Exception(
f"Cannot cancel command {command}, valid commands are {COMMANDS}"
)

self.logger.info(f"Cancelled command: {command}")
self.current_ids[command] = 0

def parse_response(self, res: Response) -> None:
"""Parses main_server output line and discards useless responses - internal API"""
self.logger.debug("Parsing server response")
id = res["id"]
self.all_ids.remove(id)

if "command" not in res:
self.logger.info("Response was notification response")
return

command = res["command"]
if id != self.current_ids[command]:
self.logger.info("Response is from old request")
return

self.logger.info(f"Response is useful for command type: {command}")
self.current_ids[command] = 0
self.newest_responses[command] = res

def check_responses(self) -> None:
"""Checks all main_server output by calling IPC.parse_line() on each response - internal API"""
self.logger.debug("Checking responses")
while not self.response_queue.empty():
self.parse_response(self.response_queue.get())

def get_response(self, command: str) -> Response | None:
"""Checks responses and returns the current response of type command if it has been returned - external API"""
self.logger.info(f"Getting response for type: {command}")
if command not in COMMANDS:
self.logger.exception(
f"Cannot get response of command {command}, valid commands are {COMMANDS}"
)
raise Exception(
f"Cannot get response of command {command}, valid commands are {COMMANDS}"
)

self.check_responses()
response: Response | None = self.newest_responses[command]
self.newest_responses[command] = None
self.logger.info("Response retrieved")
return response

def update_file(self, file: str, current_state: str) -> None:
"""Updates files in the system - external API"""

self.logger.info(f"Updating file: {file}")
self.files[file] = current_state

self.logger.debug("Notifying server of file update")
self.create_message("notification", file=file, contents=current_state)

def remove_file(self, file: str) -> None:
"""Removes a file from the main_server - external API"""
if file not in list(self.files.keys()):
self.logger.exception(
f"Cannot remove file {file} as file is not in file database!"
)
raise Exception(
f"Cannot remove file {file} as file is not in file database!"
)

self.logger.info("Notifying server of file deletion")
self.create_message("notification", remove=True, file=file)

def kill_IPC(self) -> None:
"""Kills the main_server when salve_ipc's services are no longer required - external API"""
self.logger.info("Killing server")
self.main_server.kill()
54 changes: 0 additions & 54 deletions salve/misc.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
from multiprocessing.queues import Queue as GenericQueueClass
from pathlib import Path
from typing import TYPE_CHECKING, NotRequired, TypedDict

from token_tools import Token

COMMANDS: list[str] = [
"autocomplete",
"replacements",
Expand All @@ -20,51 +14,3 @@
EDITORCONFIG: COMMAND = COMMANDS[3]
DEFINITION: COMMAND = COMMANDS[4]
LINKS_AND_CHARS: COMMAND = COMMANDS[5]


class Message(TypedDict):
"""Base class for messages in and out of the server"""

id: int
type: str # Can be "request", "response", "notification"


class Request(Message):
"""Request results/output from the server with command specific input"""

command: str # Can only be commands in COMMANDS
file: str
expected_keywords: NotRequired[list[str]] # autocomplete, replacements
current_word: NotRequired[str] # autocomplete, replacements, definition
language: NotRequired[str] # highlight
text_range: NotRequired[tuple[int, int]] # highlight, links_and_chars
file_path: NotRequired[Path | str] # editorconfig
definition_starters: NotRequired[
list[tuple[str, str]]
] # definition (list of regexes)


class Notification(Message):
"""Notifies the server to add/update/remove a file for usage in fulfilling commands"""

file: str
remove: bool
contents: NotRequired[str]


class Response(Message):
"""Server responses to requests and notifications"""

cancelled: bool
command: NotRequired[str]
result: NotRequired[list[str | Token] | dict[str, str] | Token]


if TYPE_CHECKING:
ResponseQueueType = GenericQueueClass[Response]
RequestQueueType = GenericQueueClass[Request | Notification]
# 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
Loading

0 comments on commit 30d4323

Please sign in to comment.