From 3ca366fea9810ea7806bf506307ffd51f4af180f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 5 Oct 2023 22:22:10 +0300 Subject: [PATCH] Blacken and isort code, add pre-commit and CI linting --- .github/workflows/python-lint.yml | 24 ++++++++++++++ .pre-commit-config.yaml | 19 +++++++++++ pyproject.toml | 11 +++++++ reactbot/bot.py | 28 +++++++++------- reactbot/config.py | 54 ++++++++++++++++++------------- reactbot/rule.py | 9 +++--- reactbot/simplepattern.py | 13 ++++---- reactbot/template.py | 19 +++++++---- 8 files changed, 125 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/python-lint.yml create mode 100644 .pre-commit-config.yaml create mode 100644 pyproject.toml diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml new file mode 100644 index 0000000..ea32a95 --- /dev/null +++ b/.github/workflows/python-lint.yml @@ -0,0 +1,24 @@ +name: Python lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - uses: isort/isort-action@master + with: + sortPaths: "./reactbot" + - uses: psf/black@stable + with: + src: "./reactbot" + - name: pre-commit + run: | + pip install pre-commit + pre-commit run -av trailing-whitespace + pre-commit run -av end-of-file-fixer + pre-commit run -av check-added-large-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..91bcb81 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,19 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + exclude_types: [markdown] + - id: end-of-file-fixer + - id: check-added-large-files + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + language_version: python3 + files: ^rss/.*\.pyi?$ + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + files: ^rss/.*\.pyi?$ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3e608c9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.isort] +profile = "black" +force_to_top = "typing" +from_first = true +combine_as_imports = true +known_first_party = ["mautrix", "maubot"] +line_length = 99 + +[tool.black] +line-length = 99 +target-version = ["py38"] diff --git a/reactbot/bot.py b/reactbot/bot.py index 5868dda..a31fa31 100644 --- a/reactbot/bot.py +++ b/reactbot/bot.py @@ -13,16 +13,15 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Type, Tuple, Dict +from typing import Dict, Tuple, Type import time from attr import dataclass -from mautrix.types import EventType, MessageType, UserID, RoomID -from mautrix.util.config import BaseProxyConfig - -from maubot import Plugin, MessageEvent +from maubot import MessageEvent, Plugin from maubot.handlers import event +from mautrix.types import EventType, MessageType, RoomID, UserID +from mautrix.util.config import BaseProxyConfig from .config import Config, ConfigError @@ -73,12 +72,15 @@ class ReactBot(Plugin): fi.max = self.config["antispam.room.max"] fi.delay = self.config["antispam.room.delay"] - def _make_flood_info(self, for_type: str) -> 'FloodInfo': - return FloodInfo(max=self.config[f"antispam.{for_type}.max"], - delay=self.config[f"antispam.{for_type}.delay"], - count=0, last_message=0) + def _make_flood_info(self, for_type: str) -> "FloodInfo": + return FloodInfo( + max=self.config[f"antispam.{for_type}.max"], + delay=self.config[f"antispam.{for_type}.delay"], + count=0, + last_message=0, + ) - def _get_flood_info(self, flood_map: dict, key: str, for_type: str) -> 'FloodInfo': + def _get_flood_info(self, flood_map: dict, key: str, for_type: str) -> "FloodInfo": try: return flood_map[key] except KeyError: @@ -86,8 +88,10 @@ class ReactBot(Plugin): return fi def is_flood(self, evt: MessageEvent) -> bool: - return (self._get_flood_info(self.user_flood, evt.sender, "user").bump() - or self._get_flood_info(self.room_flood, evt.room_id, "room").bump()) + return ( + self._get_flood_info(self.user_flood, evt.sender, "user").bump() + or self._get_flood_info(self.room_flood, evt.room_id, "room").bump() + ) @event.on(EventType.ROOM_MESSAGE) async def event_handler(self, evt: MessageEvent) -> None: diff --git a/reactbot/config.py b/reactbot/config.py index 83e2283..b264a6b 100644 --- a/reactbot/config.py +++ b/reactbot/config.py @@ -13,18 +13,18 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import List, Union, Dict, Any +from typing import Any, Dict, List, Union import re from jinja2 import Template as JinjaStringTemplate from jinja2.nativetypes import NativeTemplate as JinjaNativeTemplate -from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper from mautrix.types import EventType +from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper +from .rule import RPattern, Rule from .simplepattern import SimplePattern from .template import Template -from .rule import Rule, RPattern InputPattern = Union[str, Dict[str, str]] @@ -49,28 +49,32 @@ class Config(BaseProxyConfig): self.rules = {} self.default_flags = self._get_flags(self["default_flags"]) - self.templates = {name: self._make_template(name, tpl) - for name, tpl in self["templates"].items()} - self.rules = {name: self._make_rule(name, rule) - for name, rule in self["rules"].items()} + self.templates = { + name: self._make_template(name, tpl) for name, tpl in self["templates"].items() + } + self.rules = {name: self._make_rule(name, rule) for name, rule in self["rules"].items()} def _make_rule(self, name: str, rule: Dict[str, Any]) -> Rule: try: - return Rule(rooms=set(rule.get("rooms", [])), - not_rooms=set(rule.get("not_rooms", [])), - matches=self._compile_all(rule["matches"]), - not_matches=self._compile_all(rule.get("not_matches", [])), - type=EventType.find(rule["type"]) if "type" in rule else None, - template=self.templates[rule["template"]], - variables=self._parse_variables(rule)) + return Rule( + rooms=set(rule.get("rooms", [])), + not_rooms=set(rule.get("not_rooms", [])), + matches=self._compile_all(rule["matches"]), + not_matches=self._compile_all(rule.get("not_matches", [])), + type=EventType.find(rule["type"]) if "type" in rule else None, + template=self.templates[rule["template"]], + variables=self._parse_variables(rule), + ) except Exception as e: raise ConfigError(f"Failed to load {name}") from e def _make_template(self, name: str, tpl: Dict[str, Any]) -> Template: try: - return Template(type=EventType.find(tpl.get("type", "m.room.message")), - variables=self._parse_variables(tpl), - content=self._parse_content(tpl.get("content", None))).init() + return Template( + type=EventType.find(tpl.get("type", "m.room.message")), + variables=self._parse_variables(tpl), + content=self._parse_content(tpl.get("content", None)), + ).init() except Exception as e: raise ConfigError(f"Failed to load {name}") from e @@ -93,13 +97,19 @@ class Config(BaseProxyConfig): @staticmethod def _parse_variables(data: Dict[str, Any]) -> Dict[str, Any]: - return {name: (JinjaNativeTemplate(var_tpl) - if isinstance(var_tpl, str) and var_tpl.startswith("{{") - else var_tpl) - for name, var_tpl in data.get("variables", {}).items()} + return { + name: ( + JinjaNativeTemplate(var_tpl) + if isinstance(var_tpl, str) and var_tpl.startswith("{{") + else var_tpl + ) + for name, var_tpl in data.get("variables", {}).items() + } @staticmethod - def _parse_content(content: Union[Dict[str, Any], str]) -> Union[Dict[str, Any], JinjaStringTemplate]: + def _parse_content( + content: Union[Dict[str, Any], str] + ) -> Union[Dict[str, Any], JinjaStringTemplate]: if not content: return {} elif isinstance(content, str): diff --git a/reactbot/rule.py b/reactbot/rule.py index 51a8bee..07e714d 100644 --- a/reactbot/rule.py +++ b/reactbot/rule.py @@ -13,16 +13,15 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Optional, Match, Dict, List, Set, Union, Pattern, Any +from typing import Any, Dict, List, Match, Optional, Pattern, Set, Union from attr import dataclass -from mautrix.types import RoomID, EventType - from maubot import MessageEvent +from mautrix.types import EventType, RoomID -from .template import Template, OmitValue from .simplepattern import SimplePattern +from .template import OmitValue, Template RPattern = Union[Pattern, SimplePattern] @@ -59,7 +58,7 @@ class Rule: async def execute(self, evt: MessageEvent, match: Match) -> None: extra_vars = { "0": match.group(0), - **{str(i+1): val for i, val in enumerate(match.groups())}, + **{str(i + 1): val for i, val in enumerate(match.groups())}, **match.groupdict(), } content = self.template.execute(evt=evt, rule_vars=self.variables, extra_vars=extra_vars) diff --git a/reactbot/simplepattern.py b/reactbot/simplepattern.py index f40d7ce..e2ed6a3 100644 --- a/reactbot/simplepattern.py +++ b/reactbot/simplepattern.py @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Callable, List, Dict, Optional, NamedTuple +from typing import Callable, Dict, List, NamedTuple, Optional import re @@ -68,21 +68,22 @@ class SimplePattern: return SimpleMatch(self.pattern) @staticmethod - def compile(pattern: str, flags: re.RegexFlag = re.RegexFlag(0), force_raw: bool = False - ) -> Optional['SimplePattern']: + def compile( + pattern: str, flags: re.RegexFlag = re.RegexFlag(0), force_raw: bool = False + ) -> Optional["SimplePattern"]: ignorecase = flags == re.IGNORECASE s_pattern = pattern.lower() if ignorecase else pattern esc = "" if not force_raw: esc = re.escape(pattern) first, last = pattern[0], pattern[-1] - if first == '^' and last == '$' and (force_raw or esc == f"\\^{pattern[1:-1]}\\$"): + if first == "^" and last == "$" and (force_raw or esc == f"\\^{pattern[1:-1]}\\$"): s_pattern = s_pattern[1:-1] func = matcher_equals - elif first == '^' and (force_raw or esc == f"\\^{pattern[1:]}"): + elif first == "^" and (force_raw or esc == f"\\^{pattern[1:]}"): s_pattern = s_pattern[1:] func = matcher_startswith - elif last == '$' and (force_raw or esc == f"{pattern[:-1]}\\$"): + elif last == "$" and (force_raw or esc == f"{pattern[:-1]}\\$"): s_pattern = s_pattern[:-1] func = matcher_endswith elif force_raw or esc == pattern: diff --git a/reactbot/template.py b/reactbot/template.py index f150a59..8ab96a5 100644 --- a/reactbot/template.py +++ b/reactbot/template.py @@ -13,17 +13,17 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Union, Dict, List, Tuple, Any +from typing import Any, Dict, List, Tuple, Union from itertools import chain -import json import copy +import json import re from attr import dataclass from jinja2 import Template as JinjaStringTemplate from jinja2.nativetypes import Template as JinjaNativeTemplate -from mautrix.types import EventType, Event +from mautrix.types import Event, EventType class Key(str): @@ -48,7 +48,7 @@ class Template: _variable_locations: List[Tuple[Index, ...]] = None - def init(self) -> 'Template': + def init(self) -> "Template": self._variable_locations = [] self._map_variable_locations((), self.content) return self @@ -80,13 +80,18 @@ class Template: return variables[full_var_match.group(1)] return variable_regex.sub(lambda match: str(variables[match.group(1)]), tpl) - def execute(self, evt: Event, rule_vars: Dict[str, Any], extra_vars: Dict[str, str] - ) -> Dict[str, Any]: + def execute( + self, evt: Event, rule_vars: Dict[str, Any], extra_vars: Dict[str, str] + ) -> Dict[str, Any]: variables = extra_vars for name, template in chain(rule_vars.items(), self.variables.items()): if isinstance(template, JinjaNativeTemplate): rendered_var = template.render(event=evt, variables=variables, **global_vars) - if not isinstance(rendered_var, (str, int, list, tuple, dict, bool)) and rendered_var is not None and rendered_var is not OmitValue: + if ( + not isinstance(rendered_var, (str, int, list, tuple, dict, bool)) + and rendered_var is not None + and rendered_var is not OmitValue + ): rendered_var = str(rendered_var) variables[name] = rendered_var else: