From 602abeffd091f6d7b33e25453e5cdfba2d65e77b Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 1 Sep 2024 14:12:34 +0200 Subject: [PATCH 1/9] tools, calc: fix incomplete type defs --- sopel/builtins/calc.py | 3 +-- sopel/tools/calculation.py | 20 ++++++++++++++------ sopel/tools/identifiers.py | 11 ++++------- sopel/tools/memories.py | 25 ++++++++++++++----------- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/sopel/builtins/calc.py b/sopel/builtins/calc.py index 50816e657..95791d5f7 100644 --- a/sopel/builtins/calc.py +++ b/sopel/builtins/calc.py @@ -42,8 +42,7 @@ def c(bot, trigger): # Account for the silly non-Anglophones and their silly radix point. eqn = trigger.group(2).replace(',', '.') try: - result = eval_equation(eqn) - result = "{:.10g}".format(result) + result = "{:.10g}".format(eval_equation(eqn)) except eval_equation.Error as err: bot.reply("Can't process expression: {}".format(str(err))) return diff --git a/sopel/tools/calculation.py b/sopel/tools/calculation.py index a1e85b653..96646048d 100644 --- a/sopel/tools/calculation.py +++ b/sopel/tools/calculation.py @@ -42,7 +42,11 @@ def __init__( self.binary_ops = bin_ops or {} self.unary_ops = unary_ops or {} - def __call__(self, expression_str: str, timeout: float = 5.0): + def __call__( + self, + expression_str: str, + timeout: float = 5.0, + ) -> int | float: """Evaluate a Python expression and return the result. :param expression_str: the expression to evaluate @@ -56,7 +60,7 @@ def __call__(self, expression_str: str, timeout: float = 5.0): ast_expression = ast.parse(expression_str, mode='eval') return self._eval_node(ast_expression.body, time.time() + timeout) - def _eval_node(self, node: ast.AST, timeout: float): + def _eval_node(self, node: ast.AST, timeout: float) -> int | float: """Recursively evaluate the given :class:`ast.Node `. :param node: the AST node to evaluate @@ -116,7 +120,7 @@ def _eval_node(self, node: ast.AST, timeout: float): ) -def guarded_mul(left: float, right: float): +def guarded_mul(left: int | float, right: int | float) -> int | float: """Multiply two values, guarding against overly large inputs. :param left: the left operand @@ -139,7 +143,7 @@ def guarded_mul(left: float, right: float): return operator.mul(left, right) -def pow_complexity(num: int, exp: int): +def pow_complexity(num: int, exp: int) -> float: """Estimate the worst case time :func:`pow` takes to calculate. :param num: base @@ -205,7 +209,7 @@ def pow_complexity(num: int, exp: int): return exp ** 1.590 * num.bit_length() ** 1.73 / 36864057619.3 -def guarded_pow(num: float, exp: float): +def guarded_pow(num: int | float, exp: int | float) -> int | float: """Raise a number to a power, guarding against overly large inputs. :param num: base @@ -255,7 +259,11 @@ def __init__(self): unary_ops=self.__unary_ops ) - def __call__(self, expression_str: str, timeout: float = 5.0): + def __call__( + self, + expression_str: str, + timeout: float = 5.0, + ) -> int | float: result = ExpressionEvaluator.__call__(self, expression_str, timeout) # This wrapper is here so additional sanity checks could be done diff --git a/sopel/tools/identifiers.py b/sopel/tools/identifiers.py index 6b0937a99..9297be111 100644 --- a/sopel/tools/identifiers.py +++ b/sopel/tools/identifiers.py @@ -97,9 +97,8 @@ def rfc1459_strict_lower(text: str) -> str: class Identifier(str): """A ``str`` subclass which acts appropriately for IRC identifiers. - :param str identifier: IRC identifier + :param identifier: IRC identifier :param casemapping: a casemapping function (optional keyword argument) - :type casemapping: Callable[[:class:`str`], :class:`str`] When used as normal ``str`` objects, case will be preserved. However, when comparing two Identifier objects, or comparing an Identifier @@ -162,12 +161,11 @@ def lower(self) -> str: return self.casemapping(self) @staticmethod - def _lower(identifier: str): + def _lower(identifier: str) -> str: """Convert an identifier to lowercase per :rfc:`2812`. - :param str identifier: the identifier (nickname or channel) to convert + :param identifier: the identifier (nickname or channel) to convert :return: RFC 2812-compliant lowercase version of ``identifier`` - :rtype: str :meta public: @@ -186,12 +184,11 @@ def _lower(identifier: str): return rfc1459_lower(identifier) @staticmethod - def _lower_swapped(identifier: str): + def _lower_swapped(identifier: str) -> str: """Backward-compatible version of :meth:`_lower`. :param identifier: the identifier (nickname or channel) to convert :return: RFC 2812-non-compliant lowercase version of ``identifier`` - :rtype: str This is what the old :meth:`_lower` function did before Sopel 7.0. It maps ``{}``, ``[]``, ``|``, ``\\``, ``^``, and ``~`` incorrectly. diff --git a/sopel/tools/memories.py b/sopel/tools/memories.py index 1fb2b0b2a..390df2455 100644 --- a/sopel/tools/memories.py +++ b/sopel/tools/memories.py @@ -8,7 +8,7 @@ from collections import defaultdict import threading -from typing import Any, Optional, TYPE_CHECKING, Union +from typing import Any, TYPE_CHECKING, Union from typing_extensions import override @@ -18,7 +18,10 @@ from collections.abc import Iterable, Mapping from typing import Tuple - MemoryConstructorInput = Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] + MemoryConstructorInput = Union[ + Mapping[str, Any], + Iterable[Tuple[str, Any]], + ] class _NO_DEFAULT: @@ -176,7 +179,7 @@ def setup(bot): """ def __init__( self, - *args, + *args: MemoryConstructorInput, identifier_factory: IdentifierFactory = Identifier, ) -> None: if len(args) > 1: @@ -193,7 +196,7 @@ def __init__( else: super().__init__() - def _make_key(self, key: Optional[str]) -> Optional[Identifier]: + def _make_key(self, key: str | None) -> Identifier | None: if key is None: return None return self.make_identifier(key) @@ -221,19 +224,19 @@ def _convert_keys( # return converted input data return ((self.make_identifier(k), v) for k, v in data) - def __getitem__(self, key: Optional[str]): + def __getitem__(self, key: str | None) -> Any: return super().__getitem__(self._make_key(key)) - def __contains__(self, key): + def __contains__(self, key: Any) -> Any: return super().__contains__(self._make_key(key)) - def __setitem__(self, key: Optional[str], value): + def __setitem__(self, key: str | None, value: Any) -> Any: super().__setitem__(self._make_key(key), value) - def setdefault(self, key: str, default=None): + def setdefault(self, key: str, default: Any = None) -> Any: return super().setdefault(self._make_key(key), default) - def __delitem__(self, key: str): + def __delitem__(self, key: str) -> None: super().__delitem__(self._make_key(key)) def copy(self): @@ -243,7 +246,7 @@ def copy(self): """ return type(self)(self, identifier_factory=self.make_identifier) - def get(self, key: str, default=_NO_DEFAULT): + def get(self, key: str, default: Any = _NO_DEFAULT) -> Any: """Get the value of ``key`` from this ``SopelIdentifierMemory``. Takes an optional ``default`` value, just like :meth:`dict.get`. @@ -252,7 +255,7 @@ def get(self, key: str, default=_NO_DEFAULT): return super().get(self._make_key(key)) return super().get(self._make_key(key), default) - def pop(self, key: str, default=_NO_DEFAULT): + def pop(self, key: str, default: Any = _NO_DEFAULT) -> Any: """Pop the value of ``key`` from this ``SopelIdentifierMemory``. Takes an optional ``default`` value, just like :meth:`dict.pop`. From 7973a297f91ae7a86562ae2f597a2f4c3a0d7bcb Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 1 Sep 2024 14:19:23 +0200 Subject: [PATCH 2/9] irc: fix incomplete type defs --- sopel/irc/__init__.py | 34 +++++++++++++--------------------- sopel/irc/backends.py | 6 +++--- sopel/irc/modes.py | 2 +- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/sopel/irc/__init__.py b/sopel/irc/__init__.py index e2a3b1fa8..7658c4ac7 100644 --- a/sopel/irc/__init__.py +++ b/sopel/irc/__init__.py @@ -201,18 +201,13 @@ def server_capabilities(self) -> dict[str, Optional[str]]: @property def isupport(self) -> ISupport: - """Features advertised by the server. - - :type: :class:`~.isupport.ISupport` instance - """ + """Features advertised by the server.""" return self._isupport @property def myinfo(self) -> MyInfo: """Server/network information. - :type: :class:`~.utils.MyInfo` instance - .. versionadded:: 7.0 """ if self._myinfo is None: @@ -342,8 +337,6 @@ def get_irc_backend( """Set up the IRC backend based on the bot's settings. :return: the initialized IRC backend object - :rtype: an object implementing the interface of - :class:`~sopel.irc.abstract_backends.AbstractIRCBackend` """ timeout = int(self.settings.core.timeout) ping_interval = int(self.settings.core.timeout_ping_interval) @@ -369,8 +362,8 @@ def get_irc_backend( def run(self, host: str, port: int = 6667) -> None: """Connect to IRC server and run the bot forever. - :param str host: the IRC server hostname - :param int port: the IRC server port + :param host: the IRC server hostname + :param port: the IRC server port """ source_address = ((self.settings.core.bind_host, 0) if self.settings.core.bind_host else None) @@ -412,7 +405,7 @@ def on_connect(self) -> None: def on_message(self, message: str) -> None: """Handle an incoming IRC message. - :param str message: the received raw IRC message + :param message: the received raw IRC message """ if self.backend is None: raise RuntimeError(ERR_BACKEND_NOT_INITIALIZED) @@ -443,7 +436,7 @@ def on_message(self, message: str) -> None: def on_message_sent(self, raw: str) -> None: """Handle any message sent through the connection. - :param str raw: raw text message sent through the connection + :param raw: raw text message sent through the connection When a message is sent through the IRC connection, the bot will log the raw message. If necessary, it will also simulate the @@ -525,7 +518,7 @@ def rebuild_nick(self) -> None: def change_current_nick(self, new_nick: str) -> None: """Change the current nick without configuration modification. - :param str new_nick: new nick to be used by the bot + :param new_nick: new nick to be used by the bot """ if self.backend is None: raise RuntimeError(ERR_BACKEND_NOT_INITIALIZED) @@ -548,11 +541,10 @@ def _shutdown(self) -> None: # Features @abc.abstractmethod - def dispatch(self, pretrigger: trigger.PreTrigger): + def dispatch(self, pretrigger: trigger.PreTrigger) -> None: """Handle running the appropriate callables for an incoming message. :param pretrigger: Sopel PreTrigger object - :type pretrigger: :class:`sopel.trigger.PreTrigger` .. important:: This method **MUST** be implemented by concrete subclasses. @@ -561,8 +553,8 @@ def dispatch(self, pretrigger: trigger.PreTrigger): def log_raw(self, line: str, prefix: str) -> None: """Log raw line to the raw log. - :param str line: the raw line - :param str prefix: additional information to prepend to the log line + :param line: the raw line + :param prefix: additional information to prepend to the log line The ``prefix`` is usually either ``>>`` for an outgoing ``line`` or ``<<`` for a received one. @@ -611,8 +603,8 @@ def write(self, args: Iterable[str], text: Optional[str] = None) -> None: def action(self, text: str, dest: str) -> None: """Send a CTCP ACTION PRIVMSG to a user or channel. - :param str text: the text to send in the CTCP ACTION - :param str dest: the destination of the CTCP ACTION + :param text: the text to send in the CTCP ACTION + :param dest: the destination of the CTCP ACTION The same loop detection and length restrictions apply as with :func:`say`, though automatic message splitting is not available. @@ -622,8 +614,8 @@ def action(self, text: str, dest: str) -> None: def join(self, channel: str, password: Optional[str] = None) -> None: """Join a ``channel``. - :param str channel: the channel to join - :param str password: an optional channel password + :param channel: the channel to join + :param password: an optional channel password If ``channel`` contains a space, and no ``password`` is given, the space is assumed to split the argument into the channel to join and its diff --git a/sopel/irc/backends.py b/sopel/irc/backends.py index 5c5bf9e9d..afa0a80b9 100644 --- a/sopel/irc/backends.py +++ b/sopel/irc/backends.py @@ -20,7 +20,7 @@ import socket import ssl import threading -from typing import Optional, TYPE_CHECKING +from typing import Any, Optional, TYPE_CHECKING from .abstract_backends import AbstractIRCBackend @@ -158,7 +158,7 @@ def __init__( ca_certs: Optional[str] = None, ssl_ciphers: Optional[list[str]] = None, ssl_minimum_version: ssl.TLSVersion = ssl.TLSVersion.TLSv1_2, - **kwargs, + **kwargs: Any, ): super().__init__(bot) # connection parameters @@ -379,7 +379,7 @@ def get_connection_kwargs(self) -> dict: } async def _connect_to_server( - self, **connection_kwargs + self, **connection_kwargs: Any, ) -> tuple[ Optional[asyncio.StreamReader], Optional[asyncio.StreamWriter], diff --git a/sopel/irc/modes.py b/sopel/irc/modes.py index 694eb0d45..fb1ec74ac 100755 --- a/sopel/irc/modes.py +++ b/sopel/irc/modes.py @@ -100,7 +100,7 @@ class ModeException(Exception): class ModeTypeUnknown(ModeException): """Exception when a mode's type is unknown or cannot be determined.""" - def __init__(self, mode) -> None: + def __init__(self, mode: str) -> None: super().__init__('Unknown type for mode %s' % mode) From 737beac634b10b43cb9f4e77c0888d34202d9727 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 1 Sep 2024 14:24:08 +0200 Subject: [PATCH 3/9] lifecycle: fix incomplete type defs --- sopel/lifecycle.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sopel/lifecycle.py b/sopel/lifecycle.py index fbd897bad..27f6565d9 100644 --- a/sopel/lifecycle.py +++ b/sopel/lifecycle.py @@ -13,7 +13,7 @@ import inspect import logging import traceback -from typing import Callable, Optional +from typing import Callable from packaging.version import parse as parse_version @@ -21,13 +21,13 @@ def deprecated( - reason: Optional[str] = None, - version: Optional[str] = None, - removed_in: Optional[str] = None, - warning_in: Optional[str] = None, + reason: str | Callable | None = None, + version: str | None = None, + removed_in: str | None = None, + warning_in: str | None = None, stack_frame: int = -1, - func: Optional[Callable] = None, -): + func: Callable | None = None, +) -> Callable: """Decorator to mark deprecated functions in Sopel's API :param reason: optional text added to the deprecation warning From 7f253da784a2d4076f1276acbf2e042c4dd81418 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 1 Sep 2024 14:30:31 +0200 Subject: [PATCH 4/9] db: fix incomplete type defs --- sopel/db.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sopel/db.py b/sopel/db.py index 8deaa8035..101300351 100644 --- a/sopel/db.py +++ b/sopel/db.py @@ -34,6 +34,8 @@ if typing.TYPE_CHECKING: from collections.abc import Iterable + from sopel.config import Config + LOGGER = logging.getLogger(__name__) @@ -142,7 +144,7 @@ class SopelDB: def __init__( self, - config, + config: Config, identifier_factory: IdentifierFactory = Identifier, ) -> None: self.make_identifier: IdentifierFactory = identifier_factory @@ -628,7 +630,7 @@ def forget_nick_group(self, nick: str) -> None: def delete_nick_group(self, nick: str) -> None: # pragma: nocover self.forget_nick_group(nick) - def merge_nick_groups(self, first_nick: str, second_nick: str): + def merge_nick_groups(self, first_nick: str, second_nick: str) -> None: """Merge two nick groups. :param first_nick: one nick in the first group to merge @@ -788,7 +790,7 @@ def get_channel_value( channel: str, key: str, default: typing.Optional[typing.Any] = None, - ): + ) -> typing.Any: """Get a value from the key-value store for ``channel``. :param channel: the channel whose values to access @@ -980,8 +982,8 @@ def get_nick_or_channel_value( self, name: str, key: str, - default=None - ) -> typing.Optional[typing.Any]: + default: typing.Any | None = None + ) -> typing.Any | None: """Get a value from the key-value store for ``name``. :param name: nick or channel whose values to access From 8fe9603b3b6b93dfe54ed7d7871dc3d861a31a48 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 1 Sep 2024 15:09:53 +0200 Subject: [PATCH 5/9] bot, plugins: fix incomplete type defs --- sopel/bot.py | 66 ++++++++++++++++++++------------------- sopel/plugins/handlers.py | 7 +++++ sopel/plugins/rules.py | 46 +++++++++++---------------- 3 files changed, 59 insertions(+), 60 deletions(-) diff --git a/sopel/bot.py b/sopel/bot.py index cc908f247..abe2f506e 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -17,7 +17,9 @@ from types import MappingProxyType from typing import ( Any, + Callable, Optional, + Sequence, TYPE_CHECKING, TypeVar, Union, @@ -36,6 +38,8 @@ if TYPE_CHECKING: from collections.abc import Iterable, Mapping + + from sopel.plugins.handlers import AbstractPluginHandler from sopel.trigger import PreTrigger @@ -182,7 +186,6 @@ def hostmask(self) -> Optional[str]: :return: the bot's current hostmask if the bot is connected and in a least one channel; ``None`` otherwise - :rtype: Optional[str] """ if not self.users or self.nick not in self.users: # bot must be connected and in at least one channel @@ -198,11 +201,11 @@ def plugins(self) -> Mapping[str, plugins.handlers.AbstractPluginHandler]: """ return MappingProxyType(self._plugins) - def has_channel_privilege(self, channel, privilege) -> bool: + def has_channel_privilege(self, channel: str, privilege: int) -> bool: """Tell if the bot has a ``privilege`` level or above in a ``channel``. - :param str channel: a channel the bot is in - :param int privilege: privilege level to check + :param channel: a channel the bot is in + :param privilege: privilege level to check :raise ValueError: when the channel is unknown This method checks the bot's privilege level in a channel, i.e. if it @@ -339,10 +342,10 @@ def post_setup(self) -> None: # plugins management - def reload_plugin(self, name) -> None: + def reload_plugin(self, name: str) -> None: """Reload a plugin. - :param str name: name of the plugin to reload + :param name: name of the plugin to reload :raise plugins.exceptions.PluginNotRegistered: when there is no ``name`` plugin registered @@ -391,22 +394,24 @@ def reload_plugins(self) -> None: # TODO: deprecate both add_plugin and remove_plugin; see #2425 - def add_plugin(self, plugin, callables, jobs, shutdowns, urls) -> None: + def add_plugin( + self, + plugin: AbstractPluginHandler, + callables: Sequence[Callable], + jobs: Sequence[Callable], + shutdowns: Sequence[Callable], + urls: Sequence[Callable], + ) -> None: """Add a loaded plugin to the bot's registry. :param plugin: loaded plugin to add - :type plugin: :class:`sopel.plugins.handlers.AbstractPluginHandler` :param callables: an iterable of callables from the ``plugin`` - :type callables: :term:`iterable` :param jobs: an iterable of functions from the ``plugin`` that are periodically invoked - :type jobs: :term:`iterable` :param shutdowns: an iterable of functions from the ``plugin`` that should be called on shutdown - :type shutdowns: :term:`iterable` :param urls: an iterable of functions from the ``plugin`` to call when matched against a URL - :type urls: :term:`iterable` """ self._plugins[plugin.name] = plugin self.register_callables(callables) @@ -414,22 +419,24 @@ def add_plugin(self, plugin, callables, jobs, shutdowns, urls) -> None: self.register_shutdowns(shutdowns) self.register_urls(urls) - def remove_plugin(self, plugin, callables, jobs, shutdowns, urls) -> None: + def remove_plugin( + self, + plugin: AbstractPluginHandler, + callables: Sequence[Callable], + jobs: Sequence[Callable], + shutdowns: Sequence[Callable], + urls: Sequence[Callable], + ) -> None: """Remove a loaded plugin from the bot's registry. :param plugin: loaded plugin to remove - :type plugin: :class:`sopel.plugins.handlers.AbstractPluginHandler` :param callables: an iterable of callables from the ``plugin`` - :type callables: :term:`iterable` :param jobs: an iterable of functions from the ``plugin`` that are periodically invoked - :type jobs: :term:`iterable` :param shutdowns: an iterable of functions from the ``plugin`` that should be called on shutdown - :type shutdowns: :term:`iterable` :param urls: an iterable of functions from the ``plugin`` to call when matched against a URL - :type urls: :term:`iterable` """ name = plugin.name if not self.has_plugin(name): @@ -993,12 +1000,11 @@ def on_scheduler_error( self, scheduler: plugin_jobs.Scheduler, exc: BaseException, - ): + ) -> None: """Called when the Job Scheduler fails. :param scheduler: the job scheduler that errored - :type scheduler: :class:`sopel.plugins.jobs.Scheduler` - :param Exception exc: the raised exception + :param exc: the raised exception .. seealso:: @@ -1011,14 +1017,12 @@ def on_job_error( scheduler: plugin_jobs.Scheduler, job: tools_jobs.Job, exc: BaseException, - ): + ) -> None: """Called when a job from the Job Scheduler fails. :param scheduler: the job scheduler responsible for the errored ``job`` - :type scheduler: :class:`sopel.plugins.jobs.Scheduler` :param job: the Job that errored - :type job: :class:`sopel.tools.jobs.Job` - :param Exception exc: the raised exception + :param exc: the raised exception .. seealso:: @@ -1030,13 +1034,11 @@ def error( self, trigger: Optional[Trigger] = None, exception: Optional[BaseException] = None, - ): + ) -> None: """Called internally when a plugin causes an error. - :param trigger: the ``Trigger``\\ing line (if available) - :type trigger: :class:`sopel.trigger.Trigger` - :param Exception exception: the exception raised by the error (if - available) + :param trigger: the IRC line that caused the error (if available) + :param exception: the exception raised by the error (if available) """ message = 'Unexpected error' if exception: @@ -1056,7 +1058,7 @@ def error( def _host_blocked(self, host: str) -> bool: """Check if a hostname is blocked. - :param str host: the hostname to check + :param host: the hostname to check """ bad_masks = self.config.core.host_blocks for bad_mask in bad_masks: @@ -1071,7 +1073,7 @@ def _host_blocked(self, host: str) -> bool: def _nick_blocked(self, nick: str) -> bool: """Check if a nickname is blocked. - :param str nick: the nickname to check + :param nick: the nickname to check """ bad_nicks = self.config.core.nick_blocks for bad_nick in bad_nicks: diff --git a/sopel/plugins/handlers.py b/sopel/plugins/handlers.py index 8a07cb3ad..a9e3bd7dd 100644 --- a/sopel/plugins/handlers.py +++ b/sopel/plugins/handlers.py @@ -97,6 +97,13 @@ class AbstractPluginHandler(abc.ABC): on shutdown (either upon exiting Sopel or unloading that plugin). """ + name: str + """Plugin identifier. + + The name of a plugin identifies this plugin: when Sopel loads a plugin, + it will store its information under that identifier. + """ + @abc.abstractmethod def load(self): """Load the plugin. diff --git a/sopel/plugins/rules.py b/sopel/plugins/rules.py index 76832b837..36aee06eb 100644 --- a/sopel/plugins/rules.py +++ b/sopel/plugins/rules.py @@ -26,6 +26,7 @@ import threading from typing import ( Any, + Callable, Optional, Type, TYPE_CHECKING, @@ -39,7 +40,11 @@ if TYPE_CHECKING: from collections.abc import Generator, Iterable + + from sopel.bot import Sopel + from sopel.config import Config from sopel.tools.identifiers import Identifier + from sopel.trigger import PreTrigger __all__ = [ @@ -541,14 +546,16 @@ class AbstractRule(abc.ABC): """ @classmethod @abc.abstractmethod - def from_callable(cls: Type[TypedRule], settings, handler) -> TypedRule: + def from_callable( + cls: Type[TypedRule], + settings: Config, + handler: Callable, + ) -> TypedRule: """Instantiate a rule object from ``settings`` and ``handler``. :param settings: Sopel's settings - :type settings: :class:`sopel.config.Config` - :param callable handler: a function-based rule handler + :param handler: a function-based rule handler :return: an instance of this class created from the ``handler`` - :rtype: :class:`AbstractRule` Sopel's function-based rule handlers are simple callables, decorated with :mod:`sopel.plugin`'s decorators to add attributes, such as rate @@ -580,8 +587,6 @@ def priority_scale(self): def get_plugin_name(self) -> str: """Get the rule's plugin name. - :rtype: str - The rule's plugin name will be used in various places to select, register, unregister, and manipulate the rule based on its plugin, which is referenced by its name. @@ -591,8 +596,6 @@ def get_plugin_name(self) -> str: def get_rule_label(self) -> str: """Get the rule's label. - :rtype: str - A rule can have a label, which can identify the rule by string, the same way a plugin can be identified by its name. This label can be used to select, register, unregister, and manipulate the rule based on its @@ -603,8 +606,6 @@ def get_rule_label(self) -> str: def get_usages(self) -> tuple: """Get the rule's usage examples. - :rtype: tuple - A rule can have usage examples, i.e. a list of examples showing how the rule can be used, or in what context it can be triggered. """ @@ -613,8 +614,6 @@ def get_usages(self) -> tuple: def get_test_parameters(self) -> tuple: """Get parameters for automated tests. - :rtype: tuple - A rule can have automated tests attached to it, and this method must return the test parameters: @@ -633,8 +632,6 @@ def get_test_parameters(self) -> tuple: def get_doc(self) -> str: """Get the rule's documentation. - :rtype: str - A rule's documentation is a short text that can be displayed to a user on IRC upon asking for help about this rule. The equivalent of Python docstrings, but for IRC rules. @@ -644,8 +641,6 @@ def get_doc(self) -> str: def get_priority(self) -> str: """Get the rule's priority. - :rtype: str - A rule can have a priority, based on the three pre-defined priorities used by Sopel: ``PRIORITY_HIGH``, ``PRIORITY_MEDIUM``, and ``PRIORITY_LOW``. @@ -662,8 +657,6 @@ def get_priority(self) -> str: def get_output_prefix(self) -> str: """Get the rule's output prefix. - :rtype: str - .. seealso:: See the :class:`sopel.bot.SopelWrapper` class for more information @@ -671,13 +664,11 @@ def get_output_prefix(self) -> str: """ @abc.abstractmethod - def match(self, bot, pretrigger) -> Iterable: + def match(self, bot: Sopel, pretrigger: PreTrigger) -> Iterable: """Match a pretrigger according to the rule. :param bot: Sopel instance - :type bot: :class:`sopel.bot.Sopel` :param pretrigger: line to match - :type pretrigger: :class:`sopel.trigger.PreTrigger` This method must return a list of `match objects`__. @@ -685,12 +676,11 @@ def match(self, bot, pretrigger) -> Iterable: """ @abc.abstractmethod - def match_event(self, event) -> bool: + def match_event(self, event: str) -> bool: """Tell if the rule matches this ``event``. - :param str event: potential matching event + :param event: potential matching event :return: ``True`` when ``event`` matches the rule, ``False`` otherwise - :rtype: bool """ @abc.abstractmethod @@ -845,7 +835,7 @@ def global_rate_template(self) -> Optional[str]: """ @abc.abstractmethod - def parse(self, text) -> Generator: + def parse(self, text: str) -> Generator: """Parse ``text`` and yield matches. :param str text: text to parse by the rule @@ -1046,7 +1036,7 @@ def __init__(self, self._handler = handler # filters - self._events = events or ['PRIVMSG'] + self._events: list[str] = events or ['PRIVMSG'] self._ctcp = ctcp or [] self._allow_bots = bool(allow_bots) self._allow_echo = bool(allow_echo) @@ -1171,10 +1161,10 @@ def parse(self, text): if result: yield result - def match_event(self, event) -> bool: + def match_event(self, event: str | None) -> bool: return bool(event and event in self._events) - def match_ctcp(self, command: Optional[str]) -> bool: + def match_ctcp(self, command: str | None) -> bool: if not self._ctcp: return True From 5912d7092bdd1a010bfc2124c1757dc0a3910784 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 1 Sep 2024 15:11:29 +0200 Subject: [PATCH 6/9] coretasks: fix incomplete type defs --- sopel/coretasks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sopel/coretasks.py b/sopel/coretasks.py index f10ebdbe2..93b11dbd2 100644 --- a/sopel/coretasks.py +++ b/sopel/coretasks.py @@ -161,7 +161,7 @@ def _handle_sasl_capability( CAP_SASL = plugin.capability('sasl', handler=_handle_sasl_capability) -def setup(bot: Sopel): +def setup(bot: Sopel) -> None: """Set up the coretasks plugin. The setup phase is used to activate the throttle feature to prevent a flood @@ -1261,7 +1261,7 @@ def _make_sasl_plain_token(account, password): @plugin.thread(False) @plugin.unblockable @plugin.priority('medium') -def sasl_success(bot: SopelWrapper, trigger: Trigger): +def sasl_success(bot: SopelWrapper, trigger: Trigger) -> None: """Resume capability negotiation on successful SASL auth.""" LOGGER.info("Successful SASL Auth.") bot.resume_capability_negotiation(CAP_SASL.cap_req, 'coretasks') @@ -1514,7 +1514,7 @@ def _record_who( away: Optional[bool] = None, is_bot: Optional[bool] = None, modes: Optional[str] = None, -): +) -> None: nick = bot.make_identifier(nick) channel = bot.make_identifier(channel) if nick not in bot.users: From 2f00d4c5710b042e1e607e3ae671e08c1d9d10d1 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 1 Sep 2024 15:26:00 +0200 Subject: [PATCH 7/9] builtins: fix incomplete type defs --- sopel/builtins/dice.py | 2 +- sopel/builtins/safety.py | 16 ++++++++-------- sopel/builtins/units.py | 30 +++++++++++++++++++++--------- sopel/builtins/url.py | 22 +++++++++++++--------- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/sopel/builtins/dice.py b/sopel/builtins/dice.py index 40f0bfde7..bdfefd550 100644 --- a/sopel/builtins/dice.py +++ b/sopel/builtins/dice.py @@ -244,7 +244,7 @@ def _roll_dice(dice_match: re.Match[str]) -> DicePouch: @plugin.example(".roll 2d10+3", user_help=True) @plugin.example(".roll 1d6", user_help=True) @plugin.output_prefix('[dice] ') -def roll(bot: SopelWrapper, trigger: Trigger): +def roll(bot: SopelWrapper, trigger: Trigger) -> None: """Rolls dice and reports the result. The dice roll follows this format: XdY[vZ][+N][#COMMENT] diff --git a/sopel/builtins/safety.py b/sopel/builtins/safety.py index 0522f54bf..bf47e7d6f 100644 --- a/sopel/builtins/safety.py +++ b/sopel/builtins/safety.py @@ -56,7 +56,7 @@ class SafetySection(types.StaticSection): """Optional hosts-file formatted domain blocklist to use instead of StevenBlack's.""" -def configure(settings: Config): +def configure(settings: Config) -> None: """ | name | example | purpose | | ---- | ------- | ------- | @@ -90,7 +90,7 @@ def configure(settings: Config): ) -def setup(bot: Sopel): +def setup(bot: Sopel) -> None: bot.settings.define_section("safety", SafetySection) if bot.settings.safety.default_mode is None: @@ -166,7 +166,7 @@ def download_domain_list(bot: Sopel, path: str) -> bool: return True -def update_local_cache(bot: Sopel, init: bool = False): +def update_local_cache(bot: Sopel, init: bool = False) -> None: """Download the current malware domain list and load it into memory. :param init: Load the file even if it's unchanged @@ -202,7 +202,7 @@ def update_local_cache(bot: Sopel, init: bool = False): bot.memory[SAFETY_CACHE_LOCAL_KEY] = unsafe_domains -def shutdown(bot: Sopel): +def shutdown(bot: Sopel) -> None: bot.memory.pop(SAFETY_CACHE_KEY, None) bot.memory.pop(SAFETY_CACHE_LOCAL_KEY, None) bot.memory.pop(SAFETY_CACHE_LOCK_KEY, None) @@ -211,7 +211,7 @@ def shutdown(bot: Sopel): @plugin.rule(r'(?u).*(https?://\S+).*') @plugin.priority('high') @plugin.output_prefix(PLUGIN_OUTPUT_PREFIX) -def url_handler(bot: SopelWrapper, trigger: Trigger): +def url_handler(bot: SopelWrapper, trigger: Trigger) -> None: """Checks for malicious URLs.""" mode = bot.db.get_channel_value( trigger.sender, @@ -365,7 +365,7 @@ def virustotal_lookup( @plugin.example(".virustotal https://malware.wicar.org/") @plugin.example(".virustotal hxxps://malware.wicar.org/") @plugin.output_prefix("[safety][VirusTotal] ") -def vt_command(bot: SopelWrapper, trigger: Trigger): +def vt_command(bot: SopelWrapper, trigger: Trigger) -> None: """Look up VT results on demand.""" if not bot.settings.safety.vt_api_key: bot.reply("Sorry, I don't have a VirusTotal API key configured.") @@ -421,7 +421,7 @@ def vt_command(bot: SopelWrapper, trigger: Trigger): @plugin.command('safety') @plugin.example(".safety on") @plugin.output_prefix(PLUGIN_OUTPUT_PREFIX) -def toggle_safety(bot: SopelWrapper, trigger: Trigger): +def toggle_safety(bot: SopelWrapper, trigger: Trigger) -> None: """Set safety setting for channel.""" if not trigger.admin and bot.channels[trigger.sender].privileges[trigger.nick] < plugin.OP: bot.reply('Only channel operators can change safety settings') @@ -455,7 +455,7 @@ def toggle_safety(bot: SopelWrapper, trigger: Trigger): # Clean the cache every day # Code above also calls this if there are too many cache entries @plugin.interval(24 * 60 * 60) -def _clean_cache(bot: Sopel): +def _clean_cache(bot: Sopel) -> None: """Cleans up old entries in URL safety cache.""" update_local_cache(bot) diff --git a/sopel/builtins/units.py b/sopel/builtins/units.py index 02316339a..31b61e634 100644 --- a/sopel/builtins/units.py +++ b/sopel/builtins/units.py @@ -9,10 +9,16 @@ from __future__ import annotations import re +from typing import Pattern, TYPE_CHECKING from sopel import plugin +if TYPE_CHECKING: + from sopel.bot import SopelWrapper + from sopel.trigger import Trigger + + PLUGIN_OUTPUT_PREFIX = '[units] ' find_temp = re.compile(r'(-?[0-9]*\.?[0-9]*)[ °]*(K|C|F)', re.IGNORECASE) @@ -20,23 +26,23 @@ find_mass = re.compile(r'([0-9]*\.?[0-9]*)[ ]*(lb|lbm|pound[s]?|ounce|oz|(?:kilo|)gram(?:me|)[s]?|[k]?g)', re.IGNORECASE) -def f_to_c(temp): +def f_to_c(temp: float) -> float: return (float(temp) - 32) * 5 / 9 -def c_to_k(temp): +def c_to_k(temp: float) -> float: return temp + 273.15 -def c_to_f(temp): +def c_to_f(temp: float) -> float: return (9.0 / 5.0 * temp + 32) -def k_to_c(temp): +def k_to_c(temp: float) -> float: return temp - 273.15 -def _extract_source(pattern, trigger) -> tuple[str, ...]: +def _extract_source(pattern: Pattern, trigger: Trigger) -> tuple[str, ...]: match = pattern.match(trigger.group(2)) if match: return match.groups() @@ -49,7 +55,7 @@ def _extract_source(pattern, trigger) -> tuple[str, ...]: @plugin.example('.temp 100C', '100.00°C = 212.00°F = 373.15K') @plugin.example('.temp 100K', '-173.15°C = -279.67°F = 100.00K') @plugin.output_prefix(PLUGIN_OUTPUT_PREFIX) -def temperature(bot, trigger): +def temperature(bot: SopelWrapper, trigger: Trigger) -> int | None: """Convert temperatures""" try: source = _extract_source(find_temp, trigger) @@ -71,7 +77,7 @@ def temperature(bot, trigger): if kelvin <= 0: bot.reply("Physically impossible temperature.") - return + return None bot.say("{:.2f}°C = {:.2f}°F = {:.2f}K".format( celsius, @@ -79,6 +85,8 @@ def temperature(bot, trigger): kelvin, )) + return None + @plugin.command('length', 'distance') @plugin.example('.distance 3m', '3.00m = 9 feet, 10.11 inches') @@ -92,7 +100,7 @@ def temperature(bot, trigger): @plugin.example('.length 3 au', '448793612.10km = 278867421.71 miles') @plugin.example('.length 3 parsec', '92570329129020.20km = 57520535754731.61 miles') @plugin.output_prefix(PLUGIN_OUTPUT_PREFIX) -def distance(bot, trigger): +def distance(bot: SopelWrapper, trigger: Trigger) -> int | None: """Convert distances""" try: source = _extract_source(find_length, trigger) @@ -160,10 +168,12 @@ def distance(bot, trigger): bot.say('{} = {}'.format(metric_part, stupid_part)) + return None + @plugin.command('weight', 'mass') @plugin.output_prefix(PLUGIN_OUTPUT_PREFIX) -def mass(bot, trigger): +def mass(bot: SopelWrapper, trigger: Trigger) -> int | None: """Convert mass""" try: source = _extract_source(find_mass, trigger) @@ -199,3 +209,5 @@ def mass(bot, trigger): stupid_part = '{:.2f} oz'.format(ounce) bot.say('{} = {}'.format(metric_part, stupid_part)) + + return None diff --git a/sopel/builtins/url.py b/sopel/builtins/url.py index 452b59793..66864d2e3 100644 --- a/sopel/builtins/url.py +++ b/sopel/builtins/url.py @@ -85,7 +85,7 @@ class UrlSection(types.StaticSection): """ -def configure(config: Config): +def configure(config: Config) -> None: """ | name | example | purpose | | ---- | ------- | ------- | @@ -120,7 +120,7 @@ def configure(config: Config): ) -def setup(bot: Sopel): +def setup(bot: Sopel) -> None: bot.config.define_section('url', UrlSection) if bot.config.url.exclude: @@ -149,7 +149,7 @@ def setup(bot: Sopel): bot.memory['shortened_urls'] = tools.SopelMemory() -def shutdown(bot: Sopel): +def shutdown(bot: Sopel) -> None: # Unset `url_exclude` and `last_seen_url`, but not `shortened_urls`; # clearing `shortened_urls` will increase API calls. Leaving it in memory # should not lead to unexpected behavior. @@ -160,7 +160,7 @@ def shutdown(bot: Sopel): pass -def _user_can_change_excludes(bot: SopelWrapper, trigger: Trigger): +def _user_can_change_excludes(bot: SopelWrapper, trigger: Trigger) -> bool: if trigger.admin: return True @@ -178,7 +178,7 @@ def _user_can_change_excludes(bot: SopelWrapper, trigger: Trigger): @plugin.example('.urlpexclude example\\.com/\\w+', user_help=True) @plugin.example('.urlexclude example.com/path', user_help=True) @plugin.output_prefix('[url] ') -def url_ban(bot: SopelWrapper, trigger: Trigger): +def url_ban(bot: SopelWrapper, trigger: Trigger) -> None: """Exclude a URL from auto title. Use ``urlpexclude`` to exclude a pattern instead of a URL. @@ -229,7 +229,7 @@ def url_ban(bot: SopelWrapper, trigger: Trigger): @plugin.example('.urlpallow example\\.com/\\w+', user_help=True) @plugin.example('.urlallow example.com/path', user_help=True) @plugin.output_prefix('[url] ') -def url_unban(bot: SopelWrapper, trigger: Trigger): +def url_unban(bot: SopelWrapper, trigger: Trigger) -> None: """Allow a URL for auto title. Use ``urlpallow`` to allow a pattern instead of a URL. @@ -282,7 +282,7 @@ def url_unban(bot: SopelWrapper, trigger: Trigger): 'Google | www.google.com', online=True, vcr=True) @plugin.output_prefix('[url] ') -def title_command(bot: SopelWrapper, trigger: Trigger): +def title_command(bot: SopelWrapper, trigger: Trigger) -> None: """ Show the title or URL information for the given URL, or the last URL seen in this channel. @@ -322,7 +322,7 @@ def title_command(bot: SopelWrapper, trigger: Trigger): @plugin.rule(r'(?u).*(https?://\S+).*') @plugin.output_prefix('[url] ') -def title_auto(bot: SopelWrapper, trigger: Trigger): +def title_auto(bot: SopelWrapper, trigger: Trigger) -> None: """ Automatically show titles for URLs. For shortened URLs/redirects, find where the URL redirects to and show the title for that. @@ -444,7 +444,11 @@ def process_urls( yield URLInfo(url, title, final_hostname, tinyurl, False) -def check_callbacks(bot: SopelWrapper, url: str, use_excludes: bool = True) -> bool: +def check_callbacks( + bot: SopelWrapper, + url: str, + use_excludes: bool = True, +) -> bool: """Check if ``url`` is excluded or matches any URL callback patterns. :param bot: Sopel instance From 9dcca3531a2fb91e82593f21f880b3a9c28709c6 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sun, 1 Sep 2024 15:28:54 +0200 Subject: [PATCH 8/9] mypy: add --disallow-incomplete-defs option --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7578bd058..98e76cd01 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ lint-style: flake8 sopel/ test/ lint-type: - mypy --check-untyped-defs sopel + mypy --check-untyped-defs --disallow-incomplete-defs sopel .PHONY: test test_norecord test_novcr vcr_rerecord test: From 5cf930d5cb748a81de9bd951c86301f07516bced Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Fri, 18 Oct 2024 18:01:29 +0200 Subject: [PATCH 9/9] tools: minor improvements of type annotations --- sopel/tools/calculation.py | 8 ++++---- sopel/tools/memories.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sopel/tools/calculation.py b/sopel/tools/calculation.py index 96646048d..b32068705 100644 --- a/sopel/tools/calculation.py +++ b/sopel/tools/calculation.py @@ -60,7 +60,7 @@ def __call__( ast_expression = ast.parse(expression_str, mode='eval') return self._eval_node(ast_expression.body, time.time() + timeout) - def _eval_node(self, node: ast.AST, timeout: float) -> int | float: + def _eval_node(self, node: ast.AST, timeout: float) -> float: """Recursively evaluate the given :class:`ast.Node `. :param node: the AST node to evaluate @@ -120,7 +120,7 @@ def _eval_node(self, node: ast.AST, timeout: float) -> int | float: ) -def guarded_mul(left: int | float, right: int | float) -> int | float: +def guarded_mul(left: float, right: float) -> float: """Multiply two values, guarding against overly large inputs. :param left: the left operand @@ -209,7 +209,7 @@ def pow_complexity(num: int, exp: int) -> float: return exp ** 1.590 * num.bit_length() ** 1.73 / 36864057619.3 -def guarded_pow(num: int | float, exp: int | float) -> int | float: +def guarded_pow(num: float, exp: float) -> float: """Raise a number to a power, guarding against overly large inputs. :param num: base @@ -263,7 +263,7 @@ def __call__( self, expression_str: str, timeout: float = 5.0, - ) -> int | float: + ) -> float: result = ExpressionEvaluator.__call__(self, expression_str, timeout) # This wrapper is here so additional sanity checks could be done diff --git a/sopel/tools/memories.py b/sopel/tools/memories.py index 390df2455..c23f53e6a 100644 --- a/sopel/tools/memories.py +++ b/sopel/tools/memories.py @@ -230,7 +230,7 @@ def __getitem__(self, key: str | None) -> Any: def __contains__(self, key: Any) -> Any: return super().__contains__(self._make_key(key)) - def __setitem__(self, key: str | None, value: Any) -> Any: + def __setitem__(self, key: str | None, value: Any) -> None: super().__setitem__(self._make_key(key), value) def setdefault(self, key: str, default: Any = None) -> Any: