1
0
mirror of https://github.com/weechat/weechat.git synced 2026-06-25 04:16:38 +02:00

ci: switch from bandit/flake8/pylint to ruff in CI for Python scripts

This commit is contained in:
Sébastien Helleu
2025-12-07 09:22:34 +01:00
parent a2a71b4d33
commit 9e814860ae
9 changed files with 1419 additions and 1302 deletions
File diff suppressed because it is too large Load Diff
+206 -203
View File
@@ -18,9 +18,9 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""
Scripts generator for WeeChat: build source of scripts in all languages to
test the scripting API.
"""Test script generator for WeeChat.
Build source of scripts in all languages to test the scripting API.
This script can be run in WeeChat or as a standalone script
(during automatic tests, it is loaded as a WeeChat script).
@@ -30,36 +30,33 @@ It uses the following scripts:
- testapi.py: the WeeChat scripting API tests
"""
# pylint: disable=wrong-import-order,wrong-import-position
# pylint: disable=useless-object-inheritance
# pylint: disable=consider-using-f-string
# pylint: disable=super-with-arguments
# pylint: disable=consider-using-with
# pylint: disable=unspecified-encoding
# ruff: noqa: T201,UP006,UP007,UP035
from __future__ import print_function
import argparse
import ast
from datetime import datetime
import datetime
import inspect
from io import StringIO
import io
import os
import sys
import traceback
from pathlib import Path
from typing import Tuple, Type, Union
sys.dont_write_bytecode = True
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(SCRIPT_DIR)
SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.append(str(SCRIPT_DIR))
from unparse import ( # noqa: E402
UnparsePython,
UnparsePerl,
UnparseRuby,
UnparseLua,
UnparseTcl,
Unparse,
UnparseGuile,
UnparseJavaScript,
UnparseLua,
UnparsePerl,
UnparsePhp,
UnparsePython,
UnparseRuby,
UnparseTcl,
)
RUNNING_IN_WEECHAT = True
@@ -69,315 +66,328 @@ except ImportError:
RUNNING_IN_WEECHAT = False
SCRIPT_NAME = 'testapigen'
SCRIPT_AUTHOR = 'Sébastien Helleu <flashcode@flashtux.org>'
SCRIPT_VERSION = '0.1'
SCRIPT_LICENSE = 'GPL3'
SCRIPT_DESC = 'Generate scripting API test scripts'
SCRIPT_NAME = "testapigen"
SCRIPT_AUTHOR = "Sébastien Helleu <flashcode@flashtux.org>"
SCRIPT_VERSION = "0.1"
SCRIPT_LICENSE = "GPL3"
SCRIPT_DESC = "Generate scripting API test scripts"
SCRIPT_COMMAND = 'testapigen'
SCRIPT_COMMAND = "testapigen"
class WeechatScript(object): # pylint: disable=too-many-instance-attributes
"""
A generic WeeChat script.
class WeechatScript:
"""A generic WeeChat script.
This class must NOT be instantiated directly, use subclasses instead:
PythonScript, PerlScript, ...
"""
def __init__(self, unparse_class, tree, source_script, output_dir,
language, extension, comment_char='#',
weechat_module='weechat'):
# pylint: disable=too-many-arguments
def __init__(
self,
unparse_class: Type[Unparse],
tree: ast.AST,
source_script: str,
output_dir: str,
language: str,
extension: str,
comment_char: str = "#",
weechat_module: str = "weechat",
) -> None:
"""Initialize a WeeChat script generator."""
self.unparse_class = unparse_class
self.tree = tree
self.source_script = os.path.realpath(source_script)
self.output_dir = os.path.realpath(output_dir)
self.language = language
self.extension = extension
self.script_name = 'weechat_testapi.%s' % extension
self.script_path = os.path.join(self.output_dir, self.script_name)
self.script_name = f"weechat_testapi.{extension}"
self.script_path = Path(self.output_dir) / self.script_name
self.comment_char = comment_char
self.weechat_module = weechat_module
self.update_tree()
def comment(self, string):
def comment(self, string: str) -> str:
"""Get a commented line."""
return '%s %s' % (self.comment_char, string)
return f"{self.comment_char} {string}"
def update_tree(self):
def update_tree(self) -> None:
"""Make changes in AST tree."""
functions = {
'prnt': 'print',
'prnt_date_tags': 'print_date_tags',
'prnt_datetime_tags': 'print_datetime_tags',
'prnt_y': 'print_y',
'prnt_y_date_tags': 'print_y_date_tags',
'prnt_y_datetime_tags': 'print_y_datetime_tags',
"prnt": "print",
"prnt_date_tags": "print_date_tags",
"prnt_datetime_tags": "print_datetime_tags",
"prnt_y": "print_y",
"prnt_y_date_tags": "print_y_date_tags",
"prnt_y_datetime_tags": "print_y_datetime_tags",
}
tests_count = 0
for node in ast.walk(self.tree):
# rename some API functions
if self.language != 'python' and \
isinstance(node, ast.Call) and \
isinstance(node.func, ast.Attribute) and \
node.func.value.id == 'weechat':
if (
self.language != "python"
and isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.value.id == "weechat"
):
node.func.attr = functions.get(node.func.attr, node.func.attr)
# count number of tests
if isinstance(node, ast.Call) and \
isinstance(node.func, ast.Name) and \
node.func.id == 'check':
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "check":
tests_count += 1
# replace script variables in string values
variables = {
'{SCRIPT_SOURCE}': self.source_script,
'{SCRIPT_NAME}': self.script_name,
'{SCRIPT_PATH}': self.script_path,
'{SCRIPT_AUTHOR}': 'Sebastien Helleu',
'{SCRIPT_VERSION}': '1.0',
'{SCRIPT_LICENSE}': 'GPL3',
'{SCRIPT_DESCRIPTION}': ('%s scripting API test' %
self.language.capitalize()),
'{SCRIPT_LANGUAGE}': self.language,
'{SCRIPT_TESTS}': str(tests_count),
"{SCRIPT_SOURCE}": self.source_script,
"{SCRIPT_NAME}": self.script_name,
"{SCRIPT_PATH}": str(self.script_path),
"{SCRIPT_AUTHOR}": "Sebastien Helleu",
"{SCRIPT_VERSION}": "1.0",
"{SCRIPT_LICENSE}": "GPL3",
"{SCRIPT_DESCRIPTION}": f"{self.language.capitalize()} scripting API test",
"{SCRIPT_LANGUAGE}": self.language,
"{SCRIPT_TESTS}": str(tests_count),
}
# replace variables
for node in ast.walk(self.tree):
if isinstance(node, ast.Constant) and node.value in variables:
node.value = variables[node.value]
def write_header(self, output):
def write_header(self, output: io.TextIOBase) -> None:
"""Generate script header (just comments by default)."""
comments = (
'',
'%s -- WeeChat %s scripting API testing' % (
self.script_name, self.language.capitalize()),
'',
'WeeChat script automatically generated by testapigen.py.',
'DO NOT EDIT BY HAND!',
'',
'Date: %s' % datetime.now(),
'',
"",
f"{self.script_name} -- WeeChat {self.language.capitalize()} scripting API testing",
"",
"WeeChat script automatically generated by testapigen.py.",
"DO NOT EDIT BY HAND!",
"",
f"Date: {datetime.datetime.now(tz=datetime.timezone.utc)}",
"",
)
for line in comments:
output.write(self.comment(line).rstrip() + '\n')
output.writelines(f"{self.comment(line)}\n" for line in comments)
def write(self):
def write(self) -> None:
"""Write script on disk."""
print('Writing script %s... ' % self.script_path, end='')
with open(self.script_path, 'w') as output:
print(f"Writing script {self.script_path}... ", end="")
with self.script_path.open("w") as output:
self.write_header(output)
self.unparse_class(output).add(self.tree)
output.write('\n')
output.write("\n")
self.write_footer(output)
print('OK')
print("OK")
def write_footer(self, output):
def write_footer(self, output: io.TextIOBase) -> None:
"""Write footer (nothing by default)."""
pass # pylint: disable=unnecessary-pass
class WeechatPythonScript(WeechatScript):
"""A WeeChat script written in Python."""
def __init__(self, tree, source_script, output_dir):
super(WeechatPythonScript, self).__init__(
UnparsePython, tree, source_script, output_dir, 'python', 'py')
def __init__(self, tree: ast.AST, source_script: str, output_dir: str) -> None:
"""Initialize Python script."""
super().__init__(UnparsePython, tree, source_script, output_dir, "python", "py")
def write_header(self, output):
super(WeechatPythonScript, self).write_header(output)
output.write('\n'
'import weechat')
def write_header(self, output: io.TextIOBase) -> None:
"""Write header of Python script."""
super().write_header(output)
output.write("\nimport weechat")
def write_footer(self, output):
output.write('\n'
'\n'
'if __name__ == "__main__":\n'
' weechat_init()\n')
def write_footer(self, output: io.TextIOBase) -> None:
"""Write footer of Python script."""
super().write_footer(output)
output.write('\n\nif __name__ == "__main__":\n weechat_init()\n')
class WeechatPerlScript(WeechatScript):
"""A WeeChat script written in Perl."""
def __init__(self, tree, source_script, output_dir):
super(WeechatPerlScript, self).__init__(
UnparsePerl, tree, source_script, output_dir, 'perl', 'pl')
def __init__(self, tree: ast.AST, source_script: str, output_dir: str) -> None:
"""Initialize Perl script."""
super().__init__(UnparsePerl, tree, source_script, output_dir, "perl", "pl")
def write_footer(self, output):
output.write('\n'
'weechat_init();\n')
def write_footer(self, output: io.TextIOBase) -> None:
"""Write footer of Perl script."""
super().write_footer(output)
output.write("\nweechat_init();\n")
class WeechatRubyScript(WeechatScript):
"""A WeeChat script written in Ruby."""
def __init__(self, tree, source_script, output_dir):
super(WeechatRubyScript, self).__init__(
UnparseRuby, tree, source_script, output_dir, 'ruby', 'rb')
def __init__(self, tree: ast.AST, source_script: str, output_dir: str) -> None:
"""Initialize Ruby script."""
super().__init__(UnparseRuby, tree, source_script, output_dir, "ruby", "rb")
def update_tree(self):
super(WeechatRubyScript, self).update_tree()
def update_tree(self) -> None:
"""Make changes in AST tree of Ruby script."""
super().update_tree()
for node in ast.walk(self.tree):
if isinstance(node, ast.Attribute) and \
node.value.id == 'weechat':
node.value.id = 'Weechat'
if isinstance(node, ast.Call) \
and isinstance(node.func, ast.Attribute) \
and node.func.attr == 'config_new_option':
if isinstance(node, ast.Attribute) and node.value.id == "weechat":
node.value.id = "Weechat"
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr == "config_new_option"
):
node.args = [*node.args[:11], ast.List(node.args[11:])]
class WeechatLuaScript(WeechatScript):
"""A WeeChat script written in Lua."""
def __init__(self, tree, source_script, output_dir):
super(WeechatLuaScript, self).__init__(
UnparseLua, tree, source_script, output_dir, 'lua', 'lua',
comment_char='--')
def __init__(self, tree: ast.AST, source_script: str, output_dir: str) -> None:
"""Initialize Lua script."""
super().__init__(UnparseLua, tree, source_script, output_dir, "lua", "lua", comment_char="--")
def write_footer(self, output):
output.write('\n'
'weechat_init()\n')
def write_footer(self, output: io.TextIOBase) -> None:
"""Write footer of Lua script."""
super().write_footer(output)
output.write("\nweechat_init()\n")
class WeechatTclScript(WeechatScript):
"""A WeeChat script written in Tcl."""
def __init__(self, tree, source_script, output_dir):
super(WeechatTclScript, self).__init__(
UnparseTcl, tree, source_script, output_dir, 'tcl', 'tcl')
def __init__(self, tree: ast.AST, source_script: str, output_dir: str) -> None:
"""Initialize Tcl script."""
super().__init__(UnparseTcl, tree, source_script, output_dir, "tcl", "tcl")
def write_footer(self, output):
output.write('\n'
'weechat_init\n')
def write_footer(self, output: io.TextIOBase) -> None:
"""Write footer of Tcl script."""
super().write_footer(output)
output.write("\nweechat_init\n")
class WeechatGuileScript(WeechatScript):
"""A WeeChat script written in Guile (Scheme)."""
def __init__(self, tree, source_script, output_dir):
super(WeechatGuileScript, self).__init__(
UnparseGuile, tree, source_script, output_dir, 'guile', 'scm',
comment_char=';')
def __init__(self, tree: ast.AST, source_script: str, output_dir: str) -> None:
"""Initialize Guile script."""
super().__init__(UnparseGuile, tree, source_script, output_dir, "guile", "scm", comment_char=";")
def update_tree(self):
super(WeechatGuileScript, self).update_tree()
def update_tree(self) -> None:
"""Make changes in AST tree of Guile script."""
super().update_tree()
functions_with_list = (
'config_new_section',
'config_new_option',
"config_new_section",
"config_new_option",
)
for node in ast.walk(self.tree):
if isinstance(node, ast.Call) \
and isinstance(node.func, ast.Attribute) \
and node.func.attr in functions_with_list:
node.args = [ast.Call('list', node.args)]
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and node.func.attr in functions_with_list
):
node.args = [ast.Call("list", node.args)]
def write_footer(self, output):
output.write('\n'
'(weechat_init)\n')
def write_footer(self, output: io.TextIOBase) -> None:
"""Write footer of Guile script."""
super().write_footer(output)
output.write("\n(weechat_init)\n")
class WeechatJavaScriptScript(WeechatScript):
"""A WeeChat script written in JavaScript."""
def __init__(self, tree, source_script, output_dir):
super(WeechatJavaScriptScript, self).__init__(
UnparseJavaScript, tree, source_script, output_dir,
'javascript', 'js', comment_char='//')
def __init__(self, tree: ast.AST, source_script: str, output_dir: str) -> None:
"""Initialize JavaScript script."""
super().__init__(UnparseJavaScript, tree, source_script, output_dir, "javascript", "js", comment_char="//")
def write_footer(self, output):
output.write('\n'
'weechat_init()\n')
def write_footer(self, output: io.TextIOBase) -> None:
"""Writer footer of JavaScript script."""
super().write_footer(output)
output.write("\nweechat_init()\n")
class WeechatPhpScript(WeechatScript):
"""A WeeChat script written in PHP."""
def __init__(self, tree, source_script, output_dir):
super(WeechatPhpScript, self).__init__(
UnparsePhp, tree, source_script, output_dir, 'php', 'php',
comment_char='//')
def __init__(self, tree: ast.AST, source_script: str, output_dir: str) -> None:
"""Initialize PHP script."""
super().__init__(UnparsePhp, tree, source_script, output_dir, "php", "php", comment_char="//")
def write_header(self, output):
output.write('<?php\n')
super(WeechatPhpScript, self).write_header(output)
def write_header(self, output: io.TextIOBase) -> None:
"""Writer header of PHP script."""
output.write("<?php\n")
super().write_header(output)
def write_footer(self, output: io.TextIOBase) -> None:
"""Write footer of PHP script."""
super().write_footer(output)
output.write("\nweechat_init();\n")
def write_footer(self, output):
output.write('\n'
'weechat_init();\n')
# ============================================================================
def update_nodes(tree):
"""
Update the tests AST tree (in-place):
def update_nodes(tree: ast.AST) -> None:
"""Update the tests AST tree (in-place).
Actions performed:
1. add a print message in each test_* function
2. add arguments in calls to check() function
"""
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and \
node.name.startswith('test_'):
if isinstance(node, ast.FunctionDef) and node.name.startswith("test_"):
# add a print at the beginning of each test function
node.body.insert(
0, ast.parse('weechat.prnt("", " > %s");' % node.name))
elif isinstance(node, ast.Call) and \
isinstance(node.func, ast.Name) and \
node.func.id == 'check':
node.body.insert(0, ast.parse(f'weechat.prnt("", " > {node.name}");'))
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "check":
# add two arguments in the call to "check" function:
# 1. the string representation of the test
# 2. the line number in source (as string)
# for example if this test is on line 50:
# check(weechat.test() == 123)
# >>> check(weechat.test() == 123)
# it becomes:
# check(weechat.test() == 123, 'weechat.test() == 123', '50')
output = StringIO()
# >>> check(weechat.test() == 123, 'weechat.test() == 123', '50')
output = io.StringIO()
unparsed = UnparsePython(output=output)
unparsed.add(node.args[0])
node.args.append(ast.Constant(output.getvalue()))
node.args.append(ast.Constant(str(node.func.lineno)))
def get_tests(path):
def get_tests(path: str) -> ast.AST:
"""Parse the source with tests and return the AST node."""
test_script = open(path).read()
tests = ast.parse(test_script)
update_nodes(tests)
return tests
with Path(path).open() as f:
test_script = f.read()
tests = ast.parse(test_script)
update_nodes(tests)
return tests
def generate_scripts(source_script, output_dir):
def generate_scripts(source_script: str, output_dir: str) -> Tuple[int, Union[str, None]]:
"""Generate scripts in all languages to test the API."""
ret_code = 0
error = None
try:
for name, obj in inspect.getmembers(sys.modules[__name__]):
if inspect.isclass(obj) and name != 'WeechatScript' and \
name.startswith('Weechat') and name.endswith('Script'):
if (
inspect.isclass(obj)
and name != "WeechatScript"
and name.startswith("Weechat")
and name.endswith("Script")
):
tests = get_tests(source_script)
obj(tests, source_script, output_dir).write()
except Exception as exc: # pylint: disable=broad-except
except Exception as exc: # noqa: BLE001
ret_code = 1
error = 'ERROR: %s\n\n%s' % (str(exc), traceback.format_exc())
error = f"ERROR: {exc}\n\n{traceback.format_exc()}"
return ret_code, error
def testapigen_cmd_cb(data, buf, args):
def testapigen_cmd_cb(data: str, buf: str, args: str) -> int: # noqa: ARG001
"""Callback for WeeChat command /testapigen."""
def print_error(msg):
def print_error(msg: str) -> None:
"""Print an error message on core buffer."""
weechat.prnt('', '%s%s' % (weechat.prefix('error'), msg))
weechat.prnt("", f"{weechat.prefix('error')}{msg}")
try:
source_script, output_dir = args.split()
except ValueError:
print_error('ERROR: invalid arguments for /testapigen')
print_error("ERROR: invalid arguments for /testapigen")
return weechat.WEECHAT_RC_OK
if not weechat.mkdir_parents(output_dir, 0o755):
print_error('ERROR: invalid directory: %s' % output_dir)
print_error("ERROR: invalid directory: {output_dir}")
return weechat.WEECHAT_RC_OK
ret_code, error = generate_scripts(source_script, output_dir)
if error:
@@ -385,23 +395,16 @@ def testapigen_cmd_cb(data, buf, args):
return weechat.WEECHAT_RC_OK if ret_code == 0 else weechat.WEECHAT_RC_ERROR
def get_parser_args():
def get_parser_args() -> argparse.Namespace:
"""Get parser arguments."""
parser = argparse.ArgumentParser(
description=('Generate WeeChat scripts in all languages '
'to test the API.'))
parser.add_argument(
'script',
help='the path to Python script with tests')
parser.add_argument(
'-o', '--output-dir',
default='.',
help='output directory (defaults to current directory)')
parser = argparse.ArgumentParser(description=("Generate WeeChat scripts in all languages to test the API."))
parser.add_argument("script", help="the path to Python script with tests")
parser.add_argument("-o", "--output-dir", default=".", help="output directory (defaults to current directory)")
return parser.parse_args()
def main():
"""Main function (when script is not loaded in WeeChat)."""
def main() -> None:
"""Generate all scripts (when script is not loaded in WeeChat)."""
args = get_parser_args()
ret_code, error = generate_scripts(args.script, args.output_dir)
if error:
@@ -409,17 +412,17 @@ def main():
sys.exit(ret_code)
if __name__ == '__main__':
if __name__ == "__main__":
if RUNNING_IN_WEECHAT:
weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION,
SCRIPT_LICENSE, SCRIPT_DESC, '', '')
weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "")
weechat.hook_command(
SCRIPT_COMMAND,
'Generate scripting API test scripts',
'source_script output_dir',
'source_script: path to source script (testapi.py)\n'
' output_dir: output directory for scripts',
'',
'testapigen_cmd_cb', '')
"Generate scripting API test scripts",
"source_script output_dir",
"source_script: path to source script (testapi.py)\n output_dir: output directory for scripts",
"",
"testapigen_cmd_cb",
"",
)
else:
main()
File diff suppressed because it is too large Load Diff