Skip to content

Commit

Permalink
Release 8.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
dgw committed Nov 16, 2024
1 parent 6ccd615 commit abe42ec
Show file tree
Hide file tree
Showing 32 changed files with 703 additions and 309 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
46 changes: 46 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,52 @@ This file is used to auto-generate the "Changelog" section of Sopel's website.
When adding new entries, follow the style guide in NEWS.spec.md to avoid
causing problems with the site build.

Changes between 8.0.0 and 8.0.1
===============================

Plugin changes
--------------

* find:
* Fixed double-bold formatting [[#2589][]]
* Support escaping backslashes [[#2589][]]

Core changes
------------

* Use distribution name to query version of entry-point plugins [[#2594][]]
* Added plugin version number in `sopel-plugins show` output [[#2638][]]
* Fixed loading folder-style plugins with relative imports [[#2633][]]
* Fixed rate-limiting behavior for rules without a rate limit [[#2629][]]
* `config.types.ChoiceAttribute` logs invalid values for debugging [[#2624][]]
* Also remove null (`\x00`) in `irc.utils.safe()` function [[#2620][]]

Housekeeping changes
--------------------

* Document advanced tip about arbitrarily scheduling code [[#2617][]]
* Include `versionadded` notes for more methods in `irc.AbstractBot` [[#2642][]]
* Start moving from `typing.Optional` to the `| None` convention [[#2642][]]
* Minor updates to keep up with type-checking ecosystem [[#2614][], [#2628][]]
* Start checking for incomplete type annotations [[#2616][]]
* Added tests to `find` plugin [[#2589][]]
* Fixed slowdown in `@plugin.example` tests with `repeat` enabled [[#2630][]]

[#2589]: https://github.com/sopel-irc/sopel/pull/2589
[#2594]: https://github.com/sopel-irc/sopel/pull/2594
[#2614]: https://github.com/sopel-irc/sopel/pull/2614
[#2616]: https://github.com/sopel-irc/sopel/pull/2616
[#2617]: https://github.com/sopel-irc/sopel/pull/2617
[#2620]: https://github.com/sopel-irc/sopel/pull/2620
[#2624]: https://github.com/sopel-irc/sopel/pull/2624
[#2628]: https://github.com/sopel-irc/sopel/pull/2628
[#2629]: https://github.com/sopel-irc/sopel/pull/2629
[#2630]: https://github.com/sopel-irc/sopel/pull/2630
[#2633]: https://github.com/sopel-irc/sopel/pull/2633
[#2638]: https://github.com/sopel-irc/sopel/pull/2638
[#2642]: https://github.com/sopel-irc/sopel/pull/2642


Changes between 7.1.9 and 8.0.0
===============================

Expand Down
39 changes: 39 additions & 0 deletions docs/source/plugin/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,45 @@ If something is not in here, feel free to ask about it on our IRC channel, or
maybe open an issue with the solution if you devise one yourself.


Running a function on a schedule
================================

Sopel provides the :func:`@plugin.interval <sopel.plugin.interval>` decorator
to run plugin callables periodically, but plugin developers semi-frequently ask
how to run a function at the same time every day/week.

Integrating this kind of feature into Sopel's plugin API is trickier than one
might think, and it's actually simpler to have plugins just use a library like
`schedule`__ directly::

import schedule

from sopel import plugin


def scheduled_message(bot):
bot.say("This is the scheduled message.", "#channelname")


def setup(bot):
# schedule the message at midnight every day
schedule.every().day.at('00:00').do(scheduled_message, bot=bot)


@plugin.interval(60)
def run_schedule(bot):
schedule.run_pending()

As long as the ``bot`` is passed as an argument, the scheduled function can
access config settings or any other attributes/properties it needs.

Multiple plugins all setting up their own checks with ``interval`` naturally
creates *some* overhead, but it shouldn't be significant compared to all the
other things happening inside a Sopel bot with numerous plugins.

.. __: https://pypi.org/project/schedule/


Restricting commands to certain channels
========================================

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespaces = false

[project]
name = "sopel"
version = "8.0.0"
version = "8.0.1"
description = "Simple and extensible IRC bot"
maintainers = [
{ name="dgw" },
Expand Down
84 changes: 41 additions & 43 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
from types import MappingProxyType
from typing import (
Any,
Callable,
Optional,
Sequence,
TYPE_CHECKING,
TypeVar,
Union,
Expand All @@ -36,6 +38,8 @@

if TYPE_CHECKING:
from collections.abc import Iterable, Mapping

from sopel.plugins.handlers import AbstractPluginHandler
from sopel.trigger import PreTrigger


Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -391,45 +394,49 @@ 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)
self.register_jobs(jobs)
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):
Expand Down Expand Up @@ -595,30 +602,26 @@ def rate_limit_info(
if trigger.admin or rule.is_unblockable():
return False, None

nick = trigger.nick
is_channel = trigger.sender and not trigger.sender.is_nick()
channel = trigger.sender if is_channel else None

at_time = trigger.time

user_metrics = rule.get_user_metrics(trigger.nick)
channel_metrics = rule.get_channel_metrics(channel)
global_metrics = rule.get_global_metrics()

if user_metrics.is_limited(at_time - rule.user_rate_limit):
if rule.is_user_rate_limited(nick, at_time):
template = rule.user_rate_template
rate_limit_type = "user"
rate_limit = rule.user_rate_limit
metrics = user_metrics
elif is_channel and channel_metrics.is_limited(at_time - rule.channel_rate_limit):
metrics = rule.get_user_metrics(nick)
elif channel and rule.is_channel_rate_limited(channel, at_time):
template = rule.channel_rate_template
rate_limit_type = "channel"
rate_limit = rule.channel_rate_limit
metrics = channel_metrics
elif global_metrics.is_limited(at_time - rule.global_rate_limit):
metrics = rule.get_channel_metrics(channel)
elif rule.is_global_rate_limited(at_time):
template = rule.global_rate_template
rate_limit_type = "global"
rate_limit = rule.global_rate_limit
metrics = global_metrics
metrics = rule.get_global_metrics()
else:
return False, None

Expand Down Expand Up @@ -993,12 +996,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::
Expand All @@ -1011,14 +1013,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::
Expand All @@ -1030,13 +1030,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:
Expand All @@ -1056,7 +1054,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:
Expand All @@ -1071,7 +1069,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:
Expand Down
3 changes: 1 addition & 2 deletions sopel/builtins/calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion sopel/builtins/dice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading

0 comments on commit abe42ec

Please sign in to comment.