Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mypy: add --disallow-incomplete-defs option #2616

Merged
merged 9 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
66 changes: 34 additions & 32 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,
Exirel marked this conversation as resolved.
Show resolved Hide resolved
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:
Exirel marked this conversation as resolved.
Show resolved Hide resolved
"""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 @@ -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::

Expand All @@ -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::

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
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
16 changes: 8 additions & 8 deletions sopel/builtins/safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| ---- | ------- | ------- |
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 21 additions & 9 deletions sopel/builtins/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,40 @@
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)
find_length = re.compile(r'([0-9]*\.?[0-9]*)[ ]*(mile[s]?|mi|inch|in|foot|feet|ft|yard[s]?|yd|(?:milli|centi|kilo|)meter[s]?|[mkc]?m|ly|light-year[s]?|au|astronomical unit[s]?|parsec[s]?|pc)', re.IGNORECASE)
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()
Expand All @@ -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)
Expand All @@ -71,14 +77,16 @@ 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,
fahrenheit,
kelvin,
))

return None
SnoopJ marked this conversation as resolved.
Show resolved Hide resolved


@plugin.command('length', 'distance')
@plugin.example('.distance 3m', '3.00m = 9 feet, 10.11 inches')
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -199,3 +209,5 @@ def mass(bot, trigger):
stupid_part = '{:.2f} oz'.format(ounce)

bot.say('{} = {}'.format(metric_part, stupid_part))

return None
dgw marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading