diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 20:21:34 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 20:21:34 +0000 |
commit | fe1438b06234f8e5ecd4caa7eedfeec585b6f77c (patch) | |
tree | 5c2a9ff683189a61e0855ca3f24df319e7e03b7f /scripts | |
parent | Initial commit. (diff) | |
download | pygls-fe1438b06234f8e5ecd4caa7eedfeec585b6f77c.tar.xz pygls-fe1438b06234f8e5ecd4caa7eedfeec585b6f77c.zip |
Adding upstream version 1.3.0.upstream/1.3.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'scripts')
-rw-r--r-- | scripts/check_client_is_uptodate.py | 20 | ||||
-rw-r--r-- | scripts/generate_client.py | 207 | ||||
-rw-r--r-- | scripts/generate_contributors_md.py | 48 |
3 files changed, 275 insertions, 0 deletions
diff --git a/scripts/check_client_is_uptodate.py b/scripts/check_client_is_uptodate.py new file mode 100644 index 0000000..1c5b1be --- /dev/null +++ b/scripts/check_client_is_uptodate.py @@ -0,0 +1,20 @@ +import sys +import subprocess + +AUTOGENERATED_CLIENT_FILE = "pygls/lsp/client.py" + +subprocess.run(["poe", "generate_client"]) + +result = subprocess.run( + ["git", "diff", "--exit-code", AUTOGENERATED_CLIENT_FILE], stdout=subprocess.DEVNULL +) + +if result.returncode == 0: + print("✅ Pygls client is up to date") +else: + print( + "🔴 Pygls client not uptodate\n" + "1. Generate with: `poetry run poe generate_client`\n" + "2. Commit" + ) + sys.exit(result.returncode) diff --git a/scripts/generate_client.py b/scripts/generate_client.py new file mode 100644 index 0000000..6aab8f2 --- /dev/null +++ b/scripts/generate_client.py @@ -0,0 +1,207 @@ +"""Script to automatically generate a lanaguge client from `lsprotocol` type definitons +""" +import argparse +import inspect +import pathlib +import re +import sys +import textwrap +from typing import Optional +from typing import Set +from typing import Tuple +from typing import Type + +from lsprotocol._hooks import _resolve_forward_references +from lsprotocol.types import METHOD_TO_TYPES +from lsprotocol.types import message_direction + +cli = argparse.ArgumentParser( + description="generate language client from lsprotocol types." +) +cli.add_argument("-o", "--output", default=None) + + +def write_imports(imports: Set[Tuple[str, str]]) -> str: + lines = [] + + for import_ in sorted(list(imports), key=lambda i: (i[0], i[1])): + if isinstance(import_, tuple): + mod, name = import_ + lines.append(f"from {mod} import {name}") + continue + + lines.append(f"import {import_}") + + return "\n".join(lines) + + +def to_snake_case(string: str) -> str: + return "".join(f"_{c.lower()}" if c.isupper() else c for c in string) + + +def write_notification( + method: str, + request: Type, + params: Optional[Type], + imports: Set[Tuple[str, str]], +) -> str: + python_name = to_snake_case(method).replace("/", "_").replace("$_", "") + + if params is None: + param_name = "None" + param_mod = "" + else: + param_mod, param_name = params.__module__, params.__name__ + param_mod = param_mod.replace("lsprotocol.types", "types") + "." + + return "\n".join( + [ + f"def {python_name}(self, params: {param_mod}{param_name}) -> None:", + f' """Send a :lsp:`{method}` notification.', + "", + textwrap.indent(inspect.getdoc(request) or "", " "), + ' """', + " if self.stopped:", + ' raise RuntimeError("Client has been stopped.")', + "", + f' self.protocol.notify("{method}", params)', + "", + ] + ) + + +def get_response_type(response: Type, imports: Set[Tuple[str, str]]) -> str: + # Find the response type. + result_field = [f for f in response.__attrs_attrs__ if f.name == "result"][0] + result = re.sub(r"<class '([\w.]+)'>", r"\1", str(result_field.type)) + result = re.sub(r"ForwardRef\('([\w.]+)'\)", r"lsprotocol.types.\1", result) + result = result.replace("NoneType", "None") + + # Replace any typing imports with their short name. + for match in re.finditer(r"typing.([\w]+)", result): + imports.add(("typing", match.group(1))) + + result = result.replace("lsprotocol.types.", "types.") + result = result.replace("typing.", "") + + return result + + +def write_method( + method: str, + request: Type, + params: Optional[Type], + response: Type, + imports: Set[Tuple[str, str]], +) -> str: + python_name = to_snake_case(method).replace("/", "_").replace("$_", "") + + if params is None: + param_name = "None" + param_mod = "" + else: + param_mod, param_name = params.__module__, params.__name__ + param_mod = param_mod.replace("lsprotocol.types", "types") + "." + + result_type = get_response_type(response, imports) + + return "\n".join( + [ + f"def {python_name}(", + " self,", + f" params: {param_mod}{param_name},", + f" callback: Optional[Callable[[{result_type}], None]] = None,", + ") -> Future:", + f' """Make a :lsp:`{method}` request.', + "", + textwrap.indent(inspect.getdoc(request) or "", " "), + ' """', + " if self.stopped:", + ' raise RuntimeError("Client has been stopped.")', + "", + f' return self.protocol.send_request("{method}", params, callback)', + "", + f"async def {python_name}_async(", + " self,", + f" params: {param_mod}{param_name},", + f") -> {result_type}:", + f' """Make a :lsp:`{method}` request.', + "", + textwrap.indent(inspect.getdoc(request) or "", " "), + ' """', + " if self.stopped:", + ' raise RuntimeError("Client has been stopped.")', + "", + f' return await self.protocol.send_request_async("{method}", params)', + "", + ] + ) + + +def generate_client() -> str: + methods = [] + imports = { + ("concurrent.futures", "Future"), + ("lsprotocol", "types"), + ("pygls.protocol", "LanguageServerProtocol"), + ("pygls.protocol", "default_converter"), + ("pygls.client", "JsonRPCClient"), + ("typing", "Callable"), + ("typing", "Optional"), + } + + for method_name, types in METHOD_TO_TYPES.items(): + # Skip any requests that come from the server. + if message_direction(method_name) == "serverToClient": + continue + + request, response, params, _ = types + + if response is None: + method = write_notification(method_name, request, params, imports) + else: + method = write_method(method_name, request, params, response, imports) + + methods.append(textwrap.indent(method, " ")) + + code = [ + "# GENERATED FROM scripts/gen-client.py -- DO NOT EDIT", + "# flake8: noqa", + write_imports(imports), + "", + "", + "class BaseLanguageClient(JsonRPCClient):", + "", + " def __init__(", + " self,", + " name: str,", + " version: str,", + " protocol_cls=LanguageServerProtocol,", + " converter_factory=default_converter,", + " **kwargs,", + " ):", + " self.name = name", + " self.version = version", + " super().__init__(protocol_cls, converter_factory, **kwargs)", + "", + *methods, + ] + return "\n".join(code) + + +def main(): + args = cli.parse_args() + + # Make sure all the type annotations in lsprotocol are resolved correctly. + _resolve_forward_references() + client = generate_client() + + if args.output is None: + sys.stdout.write(client) + else: + output = pathlib.Path(args.output) + output.write_text(client) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_contributors_md.py b/scripts/generate_contributors_md.py new file mode 100644 index 0000000..655181a --- /dev/null +++ b/scripts/generate_contributors_md.py @@ -0,0 +1,48 @@ +""" +Example JSON object: +{ + "login": "danixeee", + "id": 16227576, + "node_id": "MDQ6VXNlcjE2MjI3NTc2", + "avatar_url": "https://avatars.githubusercontent.com/u/16227576?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/danixeee", + "html_url": "https://github.com/danixeee", + "followers_url": "https://api.github.com/users/danixeee/followers", + "following_url": "https://api.github.com/users/danixeee/following{/other_user}", + "gists_url": "https://api.github.com/users/danixeee/gists{/gist_id}", + "starred_url": "https://api.github.com/users/danixeee/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/danixeee/subscriptions", + "organizations_url": "https://api.github.com/users/danixeee/orgs", + "repos_url": "https://api.github.com/users/danixeee/repos", + "events_url": "https://api.github.com/users/danixeee/events{/privacy}", + "received_events_url": "https://api.github.com/users/danixeee/received_events", + "type": "User", + "site_admin": false, + "contributions": 321 +} +""" + +import requests + +PYGLS_CONTRIBUTORS_JSON_URL = ( + "https://api.github.com/repos/openlawlibrary/pygls/contributors" +) +CONTRIBUTORS_FILE = "CONTRIBUTORS.md" + +response = requests.get(PYGLS_CONTRIBUTORS_JSON_URL) +contributors = sorted(response.json(), key=lambda d: d["login"].lower()) + +contents = "# Contributors (contributions)\n" + +for contributor in contributors: + name = contributor["login"] + contributions = contributor["contributions"] + url = contributor["html_url"] + contents += f"* [{name}]({url}) ({contributions})\n" + +file = open(CONTRIBUTORS_FILE, "w") +n = file.write(contents) +file.close() + +print("✅ CONTRIBUTORS.md updated") |