1
0
mirror of https://github.com/weechat/weechat.git synced 2026-06-12 14:14:48 +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
+1 -4
View File
@@ -18,7 +18,6 @@ env:
curl
devscripts
equivs
flake8
gem2deb
guile-3.0-dev
lcov
@@ -40,8 +39,6 @@ env:
php-dev
pipx
pkgconf
pylint
python3-bandit
python3-dev
ruby-pygments.rb
shellcheck
@@ -158,7 +155,7 @@ jobs:
sudo apt-get --yes --no-install-recommends install ${{ env.WEECHAT_DEPS_UBUNTU }}
# uninstall php imagick as is causes a crash when loading php plugin (see #2009)
sudo apt-get --yes purge php8.3-imagick
pipx install msgcheck schemathesis
pipx install msgcheck ruff schemathesis
- name: Check gettext files
run: msgcheck po/*.po
+15
View File
@@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2025 Sébastien Helleu <flashcode@flashtux.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
line-length = 120
[lint]
select = ["ALL"]
ignore = [
"D203", # incorrect-blank-line-before-class
"D213", # multi-line-summary-second-line
]
[lint.pylint]
max-args = 10
+4 -4
View File
@@ -7,6 +7,8 @@
# DO NOT EDIT BY HAND!
#
# ruff: noqa: A002,D400,D205,D301,D415,E501,PIE790,PYI021,PYI048,UP006,UP007,UP035
from typing import Dict, Union
WEECHAT_RC_OK: int = 0
@@ -55,8 +57,7 @@ WEECHAT_HOOK_SIGNAL_POINTER: str = "pointer"
def register(name: str, author: str, version: str, license: str, description: str, shutdown_function: str, charset: str) -> int:
"""`register in WeeChat plugin API reference <https://weechat.org/doc/weechat/api/#_register>`_
"""
"""`register in WeeChat plugin API reference <https://weechat.org/doc/weechat/api/#_register>`_"""
...
@@ -1981,8 +1982,7 @@ def window_get_integer(window: str, property: str) -> int:
def window_get_string(window: str, property: str) -> str:
"""`window_get_string in WeeChat plugin API reference <https://weechat.org/doc/weechat/api/#_window_get_string>`_
"""
"""`window_get_string in WeeChat plugin API reference <https://weechat.org/doc/weechat/api/#_window_get_string>`_"""
...
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
+30 -52
View File
@@ -18,7 +18,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""
"""Check Curl symbols defined in WeeChat source code.
Check if Curl symbols defined in src/core/core-url.c are matching symbols
defined in Curl (introduced/deprecated/last versions), using this file:
https://github.com/curl/curl/blob/master/docs/libcurl/symbols-in-versions.
@@ -41,16 +42,15 @@ Or with Curl repository cloned locally:
This script requires Python 3.7+.
"""
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, TextIO, Tuple
# ruff: noqa: COM812,T201
import io
import re
import sys
from dataclasses import dataclass
from pathlib import Path
SRC_PATH = (
Path(__file__).resolve().parent.parent / "src" / "core" / "core-url.c"
)
SRC_PATH = Path(__file__).resolve().parent.parent / "src" / "core" / "core-url.c"
# NOTE: keep version in sync with CMakeLists.txt
CURL_MIN_VERSION_STR = "7.68.0"
@@ -72,13 +72,10 @@ WEECHAT_CURL_MIN_MAX_VERSION_RE = (
r"(?P<str_max_version>[0-9][0-9.]+) \*/"
)
WEECHAT_ENDIF_RE = r"#endif"
WEECHAT_CURL_CONSTANT_RE = (
r" URL_DEF_CONST\((?P<prefix>[A-Z0-9_]+), (?P<name>[A-Z0-9_]+)\),"
)
WEECHAT_CURL_OPTION_RE = (
r" URL_DEF_OPTION\((?P<name>[A-Z0-9_]+), .*\),"
)
WEECHAT_CURL_CONSTANT_RE = r" URL_DEF_CONST\((?P<prefix>[A-Z0-9_]+), (?P<name>[A-Z0-9_]+)\),"
WEECHAT_CURL_OPTION_RE = r" URL_DEF_OPTION\((?P<name>[A-Z0-9_]+), .*\),"
CURL_SYMBOL_RE = r"[A-Z][A-Z0-9_]"
CURL_VERSION_ITEMS = 3
@dataclass
@@ -92,8 +89,7 @@ class WeechatCurlSymbol:
def curl_version_to_int(version: str) -> int:
"""
Convert Curl version as string to integer.
"""Convert Curl version as string to integer.
:param version: version as string (eg: "7.87.0")
:return: version as integer, eg: 481024 (== 0x075700, 0x57 == 87)
@@ -102,7 +98,7 @@ def curl_version_to_int(version: str) -> int:
return 0
result = 0
items = version.split(".")
if len(items) < 3:
while len(items) < CURL_VERSION_ITEMS:
items.append("0")
factor = 0
for item in reversed(items):
@@ -112,8 +108,7 @@ def curl_version_to_int(version: str) -> int:
def curl_version_to_str(version: int) -> str:
"""
Convert Curl version as integer to string.
"""Convert Curl version as integer to string.
:param version: version as integer, eg: 481024 (0x075700)
:return: version as string, eg: "7.87.0"
@@ -127,15 +122,14 @@ def curl_version_to_str(version: int) -> str:
return result.rstrip(".")
def get_curl_symbols(symbols_file: TextIO) -> Dict[str, Tuple[int, int]]:
"""
Parse file docs/libcurl/symbols-in-versions from Curl repository.
def get_curl_symbols(symbols_file: io.TextIOBase) -> dict[str, tuple[int, int]]:
"""Parse file docs/libcurl/symbols-in-versions from Curl repository.
:param symbols_file: file with Curl symbols
:return: Curl symbols as dict: {name: (version_min, version_max)}
"""
curl_symbol_pattern = re.compile(CURL_SYMBOL_RE)
symbols: Dict[str, Tuple[int, int]] = {}
symbols: dict[str, tuple[int, int]] = {}
if symbols_file.isatty():
return symbols
for line in symbols_file:
@@ -150,10 +144,8 @@ def get_curl_symbols(symbols_file: TextIO) -> Dict[str, Tuple[int, int]]:
return symbols
def check_req_symbols(symbols: List[WeechatCurlSymbol]) -> int:
"""
Checks the symbols' min/max version, relative to minimum Curl version that
we require.
def check_req_symbols(symbols: list[WeechatCurlSymbol]) -> int:
"""Check the symbols' min/max version, relative to min Curl version required.
:return: errors
"""
@@ -177,13 +169,11 @@ def check_req_symbols(symbols: List[WeechatCurlSymbol]) -> int:
return errors
def get_weechat_curl_symbols() -> Tuple[List[WeechatCurlSymbol], int]:
"""
Parse Curl symbols declared in src/core/core-url.c.
def get_weechat_curl_symbols() -> tuple[list[WeechatCurlSymbol], int]: # noqa: C901,PLR0915
"""Parse Curl symbols declared in src/core/core-url.c.
:return: tuple (list_symbols, errors)
"""
# pylint: disable=too-many-locals,too-many-statements
min_version_pattern = re.compile(WEECHAT_CURL_MIN_VERSION_RE)
max_version_pattern = re.compile(WEECHAT_CURL_MAX_VERSION_RE)
min_max_version_pattern = re.compile(WEECHAT_CURL_MIN_MAX_VERSION_RE)
@@ -192,10 +182,10 @@ def get_weechat_curl_symbols() -> Tuple[List[WeechatCurlSymbol], int]:
option_pattern = re.compile(WEECHAT_CURL_OPTION_RE)
v_min: int = 0
v_max: int = 0
symbols: List[WeechatCurlSymbol] = []
symbols: list[WeechatCurlSymbol] = []
errors: int = 0
line_no: int = 0
with open(SRC_PATH, encoding="utf-8") as src_file:
with Path(SRC_PATH).open(encoding="utf-8") as src_file:
for line in src_file:
line_no += 1
# min Curl version
@@ -207,9 +197,7 @@ def get_weechat_curl_symbols() -> Tuple[List[WeechatCurlSymbol], int]:
comment_min = curl_version_to_int(str_min_vers)
if v_min != comment_min:
print(
f"{SRC_PATH}:{line_no}: min version not matching "
f"the comment: "
f"{hex_min_vers} != {str_min_vers}"
f"{SRC_PATH}:{line_no}: min version not matching the comment: {hex_min_vers} != {str_min_vers}"
)
errors += 1
continue
@@ -222,9 +210,7 @@ def get_weechat_curl_symbols() -> Tuple[List[WeechatCurlSymbol], int]:
comment_max = curl_version_to_int(str_max_vers)
if v_max != comment_max:
print(
f"{SRC_PATH}:{line_no}: max version not matching "
f"the comment: "
f"{hex_max_vers} != {str_max_vers}"
f"{SRC_PATH}:{line_no}: max version not matching the comment: {hex_max_vers} != {str_max_vers}"
)
errors += 1
continue
@@ -240,16 +226,12 @@ def get_weechat_curl_symbols() -> Tuple[List[WeechatCurlSymbol], int]:
comment_max = curl_version_to_int(str_max_vers)
if v_min != comment_min:
print(
f"{SRC_PATH}:{line_no}: min version not matching "
f"the comment: "
f"{hex_min_vers} != {str_min_vers}"
f"{SRC_PATH}:{line_no}: min version not matching the comment: {hex_min_vers} != {str_min_vers}"
)
errors += 1
if v_max != comment_max:
print(
f"{SRC_PATH}:{line_no}: max version not matching "
f"the comment: "
f"{hex_max_vers} != {str_max_vers}"
f"{SRC_PATH}:{line_no}: max version not matching the comment: {hex_max_vers} != {str_max_vers}"
)
errors += 1
continue
@@ -276,11 +258,10 @@ def get_weechat_curl_symbols() -> Tuple[List[WeechatCurlSymbol], int]:
def check_symbols(
weechat_curl_symbols: List[WeechatCurlSymbol],
curl_symbols: Dict[str, Tuple[int, int]],
weechat_curl_symbols: list[WeechatCurlSymbol],
curl_symbols: dict[str, tuple[int, int]],
) -> int:
"""
Check that symbols declared are matching Curl symbols.
"""Check that symbols declared are matching Curl symbols.
:param weechat_curl_symbols: list of Curl symbols in WeeChat
:param curl_symbols: list of all Curl symbols
@@ -291,10 +272,7 @@ def check_symbols(
for symbol in weechat_curl_symbols:
curl_symbol = curl_symbols.get(symbol.name)
if not curl_symbol:
print(
f"{SRC_PATH}:{symbol.line_no}: symbol {symbol.name} "
f"not found in Curl"
)
print(f"{SRC_PATH}:{symbol.line_no}: symbol {symbol.name} not found in Curl")
errors += 1
continue
if curl_symbol[0] > req_curl and symbol.min_curl != curl_symbol[0]:
+2 -4
View File
@@ -23,7 +23,7 @@
#
# Check shell and Python scripts in WeeChat git repository using these tools:
# - shell scripts: shellcheck
# - Python scripts: flake8 + pylint + bandit
# - Python scripts: ruff
#
set -o errexit
@@ -45,7 +45,5 @@ done
# check Python scripts
for script in ${python_scripts}; do
flake8 --max-line-length=100 --builtins=_ "${root_dir}/$script"
pylint --additional-builtins=_ "${root_dir}/$script"
bandit "${root_dir}/$script"
ruff check "${root_dir}/$script"
done
+22 -28
View File
@@ -19,16 +19,16 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""
Generate Python stub: API constants and functions, with type annotations.
"""Generate Python stub with API constants and functions.
This script requires Python 3.6+.
This script requires Python 3.10+.
"""
from pathlib import Path
from textwrap import indent
# ruff: noqa: T201
import re
from pathlib import Path
from textwrap import indent
DOC_DIR = Path(__file__).resolve().parent.parent / "doc" / "en"
SRC_DIR = Path(__file__).resolve().parent.parent / "src"
@@ -43,12 +43,12 @@ STUB_HEADER = """\
# DO NOT EDIT BY HAND!
#
# ruff: noqa: A002,D400,D205,D301,D415,E501,PIE790,PYI021,PYI048,UP006,UP007,UP035
from typing import Dict, Union
"""
CONSTANT_RE = (
r"WEECHAT_SCRIPT_CONST_(?P<type>(INT|STR))\((?P<constant>WEECHAT_[A-Z0-9_]+)\)"
)
CONSTANT_RE = r"WEECHAT_SCRIPT_CONST_(?P<type>(INT|STR))\((?P<constant>WEECHAT_[A-Z0-9_]+)\)"
FUNCTION_RE = r"""\[source,python\]
----
@@ -60,40 +60,34 @@ def (?P<function>\w+)(?P<args>[^)]*)(?P<return>\) -> [^:]+:) \.\.\.(?P<example>.
def print_stub_constants() -> None:
"""Print constants, extracted from the plugin-script.c."""
constant_pattern = re.compile(CONSTANT_RE)
with open(
SRC_DIR / "plugins" / "plugin-script.c", encoding="utf-8"
) as plugin_script_file, open(
SRC_DIR / "plugins" / "weechat-plugin.h", encoding="utf-8"
) as plugin_public_header_file:
plugin_script_c = Path(SRC_DIR / "plugins" / "plugin-script.c")
weechat_plugin_h = Path(SRC_DIR / "plugins" / "weechat-plugin.h")
with (
plugin_script_c.open(encoding="utf-8") as plugin_script_file,
weechat_plugin_h.open(encoding="utf-8") as plugin_public_header_file,
):
plugin_script = plugin_script_file.read()
plugin_public_header = plugin_public_header_file.read()
for match in constant_pattern.finditer(plugin_script):
value_re = rf'^#define {match["constant"]} +(?P<value>[\w"-]+)$'
value_match = re.search(value_re, plugin_public_header, re.MULTILINE)
value = f' = {value_match["value"]}' if value_match else ""
print(f'{match["constant"]}: {match["type"].lower()}{value}')
value = f" = {value_match['value']}" if value_match else ""
print(f"{match['constant']}: {match['type'].lower()}{value}")
def print_stub_functions() -> None:
"""Print function prototypes, extracted from the Plugin API reference."""
function_pattern = re.compile(FUNCTION_RE, re.DOTALL)
with open(DOC_DIR / "weechat_plugin_api.en.adoc",
encoding="utf-8") as api_doc_file:
with Path(DOC_DIR / "weechat_plugin_api.en.adoc").open(encoding="utf-8") as api_doc_file:
api_doc = api_doc_file.read()
for match in function_pattern.finditer(api_doc):
url = f'https://weechat.org/doc/weechat/api/#_{match["function"]}'
example = (
f'\n ::\n\n{indent(match["example"].lstrip(), " " * 8)}'
if match["example"]
else ""
)
print(
f"""\n
url = f"https://weechat.org/doc/weechat/api/#_{match['function']}"
example = f"\n ::\n\n{indent(match['example'].lstrip(), ' ' * 8)}\n " if match["example"] else ""
str_func = f"""\n
def {match["function"]}{match["args"]}{match["return"]}
\"""`{match["function"]} in WeeChat plugin API reference <{url}>`_{example}
\"""
\"""`{match["function"]} in WeeChat plugin API reference <{url}>`_{example}\"""
..."""
)
print(str_func)
def stub_api() -> None: