diff --git a/codemods/README.md b/codemods/README.md new file mode 100644 index 000000000..87af91224 --- /dev/null +++ b/codemods/README.md @@ -0,0 +1,11 @@ +## Code autogenerator + +This folder is used to run python scripts which can autogenerate code used for adding features of +new Bot API updates. + +## Requirements + +Requires Python 3.10 and higher, and the package `libcst`. + +## Usage + diff --git a/codemods/__init__.py b/codemods/__init__.py new file mode 100644 index 000000000..1383cc461 --- /dev/null +++ b/codemods/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. diff --git a/codemods/add_new_method.py b/codemods/add_new_method.py new file mode 100644 index 000000000..1383cc461 --- /dev/null +++ b/codemods/add_new_method.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. diff --git a/codemods/add_new_parameter.py b/codemods/add_new_parameter.py new file mode 100644 index 000000000..32886ad81 --- /dev/null +++ b/codemods/add_new_parameter.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import sys +from pathlib import Path + +import libcst as cst +from libcst.display import dump + +sys.path.insert(0, str(Path.cwd().absolute())) + +from tests.test_official.scraper import TelegramParameter + + +class BotVisitor(cst.CSTVisitor): + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: + if node.name.value == "send_message": + print("returning true for ", node.name.value) + return True + return False + + def visit_Arg(self, node: cst.Arg) -> None: + print(node.value.value) + + +class BotTransformer(cst.CSTTransformer): + def __init__(self, methods: dict[str, TelegramParameter]) -> None: + self.methods = methods + self.stack: list[tuple[str, ...]] = [] + + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: + self.stack.append((node.name.value,)) + return node.name.value in self.methods + + def leave_FunctionDef( + self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef + ) -> cst.FunctionDef: + method_name = self.stack.pop() + if original_node.name.value not in self.methods: + return original_node + print(dump(updated_node)) + + # get which method we are in + method_name = method_name[0] + tg_param = self.methods.pop(method_name) + # Let's add our parameter now at the last position: + + # if the arg is required, we will add it to the end anyway (backward compat) and have a + # type hint of Optional[]. + annot = cst.Annotation( + annotation=cst.Subscript( + value=cst.Name(value="Optional"), + slice=[ + cst.SubscriptElement( + slice=cst.Index(value=cst.Name(value=tg_param.param_type)) + ) + ], + ) + ) + new_param = cst.Param( + name=cst.Name(tg_param.param_name), + annotation=annot, + default=cst.Name(value="None"), + comma=original_node.params.params[-1].comma, + whitespace_after_param=original_node.params.params[-1].whitespace_after_param, + ) + new_params = (*updated_node.params.params, new_param) + return updated_node.with_changes( + params=updated_node.params.with_changes(params=new_params) + ) + + +def add_param_to_bot_method(method_name: str, param: TelegramParameter) -> None: + """Add a parameter to a method in the Bot class. + + Args: + method_name (str): The name of the method. + param (TelegramParameter): The parameter to add. + """ + # All ast editing is done in place + bot_file = Path("telegram/_bot.py") + with bot_file.open() as file: + source = cst.parse_module(file.read()) + # s = dump(source) + mod_tree = source.visit(BotTransformer({method_name: param})) + code = mod_tree.code + + with bot_file.open("w") as file: + file.write(code) + + +if __name__ == "__main__": + add_param_to_bot_method("send_message", TelegramParameter("effect_id", "str", False, "desc")) + # failures = parse_failures() + # missing_method_params = failures[0] + # for method_name, param in missing_method_params.items(): + # print("Adding parameter", param.param_name, "to method", method_name) + # add_param_to_bot_method(method_name, param) + # break diff --git a/codemods/collector.py b/codemods/collector.py new file mode 100644 index 000000000..9e79e4502 --- /dev/null +++ b/codemods/collector.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +"""This file determines which methods/parameters need to be added to the API. It does so by +running test_official.py. +""" + +import os +import re +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path.cwd().absolute())) + +os.environ["TEST_OFFICIAL"] = "true" + +from functools import cache + +from helpers import to_camel_case + +from tests.test_official.scraper import TelegramParameter +from tests.test_official.test_official import classes, methods + + +def run_test_official() -> list: + """Run test_official.py and gather which errors occured.""" + + try: + output = subprocess.run( + ["pytest", "tests/test_official/test_official.py", "-q", "--no-header", "--tb=line"], + capture_output=True, + check=True, + text=True, + ) + except subprocess.CalledProcessError as e: # if test_official.py fails (expected) + output = e.output + else: + output = output.stdout + + # truncate part before ===failures=== + str_output = output[output.find("====") :] + failures: list[str] = str_output.split("\n")[1:-2] + return failures + + +def get_telegram_parameter( + param_name: str, method_name: str | None = None, class_name: str | None = None +) -> TelegramParameter: + """Get a TelegramParameter object from the scraper based on the method and parameter name. + + Args: + method_name (str): The name of the method. + param_name (str): The name of the parameter. + + Returns: + TelegramParameter: The TelegramParameter object. + """ + + if method_name is not None: + for method in methods: + if method.method_name == to_camel_case(method_name): + for param in method.method_parameters: + if param.param_name == param_name: + return param + elif class_name is not None: + for cls in classes: + if cls.class_name == class_name: + for param in cls.class_parameters: + if param.param_name == param_name: + return param + else: + raise ValueError("Either method_name or class_name must be provided.") + + raise ValueError(f"Param {param_name} not found in method {method_name} or class {class_name}") + + +@cache +def parse_failures() -> tuple[dict[str, TelegramParameter], dict[str, TelegramParameter]]: + """Parse the output of run_test_official() to determine which methods/parameters need to be + added to the API. + + Returns: + list[TelegramParameter]: A list of parameters that need to be added to the API. + """ + + failures = run_test_official() + + # regex patterns + param_missing_str = "AssertionError: Parameter ([a-z_]+) not found in ([a-z_]+)" + attribute_missing_str = "AssertionError: Attribute ([a-z_]+) not found in ([a-zA-Z0-9]+)" + + missing_params_in_methods = {} # {method_name: TelegramParameter} + missing_attrs_in_classes = {} # {class_name: TelegramParameter} + + for failure in failures: + # We will only count missing parameters/attributes for now: + + # missing parameter + if match := re.search(param_missing_str, failure): + param_name = match.group(1) + method_name = match.group(2) + tg_param = get_telegram_parameter(param_name, method_name=method_name) + missing_params_in_methods[method_name] = tg_param + + # missing attribute + elif match := re.search(attribute_missing_str, failure): + attr_name = match.group(1) + class_name = match.group(2) + tg_param = get_telegram_parameter(attr_name, class_name=class_name) + missing_attrs_in_classes[class_name] = tg_param + + else: + print(f"Unknown failure: {failure}") + + return missing_params_in_methods, missing_attrs_in_classes + + +# run_test_official() diff --git a/codemods/helpers.py b/codemods/helpers.py new file mode 100644 index 000000000..0e0021c29 --- /dev/null +++ b/codemods/helpers.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + + +def to_camel_case(snake_str: str) -> str: + """Convert a snake_case string to a CamelCase string. + + Args: + snake_str (str): The snake_case string. + + Returns: + str: The CamelCase string. + """ + + components = snake_str.split("_") + return components[0] + "".join(x.title() for x in components[1:]) diff --git a/codemods/requirements-script.txt b/codemods/requirements-script.txt new file mode 100644 index 000000000..51390d430 --- /dev/null +++ b/codemods/requirements-script.txt @@ -0,0 +1 @@ +libcst \ No newline at end of file diff --git a/codemods/tests/__init__.py b/codemods/tests/__init__.py new file mode 100644 index 000000000..1383cc461 --- /dev/null +++ b/codemods/tests/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/].