8000 Initial PoC for code autogeneration by harshil21 · Pull Request #4284 · python-telegram-bot/python-telegram-bot · GitHub
[go: up one dir, main page]

Skip to content

Initial PoC for code autogeneration #4284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions codemods/README.md
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 10 in codemods/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

codemods/README.md#L10

[no-consecutive-blank-lines] Remove 1 line after node

18 changes: 18 additions & 0 deletions codemods/__init__.py
Original file line number Diff line number Diff line change
@@ -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 <devs@python-telegram-bot.org>
#
# 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/].
18 changes: 18 additions & 0 deletions codemods/add_new_method.py
Original file line number Diff line number Diff line change
@@ -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 <devs@python-telegram-bot.org>
#
# 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/].
115 changes: 115 additions & 0 deletions codemods/add_new_parameter.py
10000
Original file line number Diff line number Diff line change
@@ -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 <devs@python-telegram-bot.org>
#
# 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[<type>].
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
135 changes: 135 additions & 0 deletions codemods/collector.py
Original file line number Diff line number Diff line change
@@ -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 <devs@python-telegram-bot.org>
#
# 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

Check warning on line 26 in codemods/collector.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

codemods/collector.py#L26

Consider possible security implications associated with the subprocess module.
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(

Check warning on line 46 in codemods/collector.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

codemods/collector.py#L46

Starting a process with a partial executable path

Check warning on line 46 in codemods/collector.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

codemods/collector.py#L46

subprocess call - check for execution of untrusted input.
["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 Telegra 1E0A mParameter 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()
32 changes: 32 additions & 0 deletions codemods/helpers.py
Original file line number Diff line number Diff line change
@@ -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 <devs@python-telegram-bot.org>
#
# 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:])
1 change: 1 addition & 0 deletions codemods/requirements-script.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
libcst
18 changes: 18 additions & 0 deletions codemods/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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 <devs@python-telegram-bot.org>
#
# 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/].
0