Skip to content

Commit

Permalink
Merge branch 'master' into feature/python3.12-support
Browse files Browse the repository at this point in the history
  • Loading branch information
SnoopJ committed Oct 27, 2023
2 parents 515ba60 + 8a60980 commit b68b351
Show file tree
Hide file tree
Showing 23 changed files with 964 additions and 267 deletions.
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ for installing from the latest source below.
Latest source
=============
First, either clone the repository with ``git clone
https://github.com/sopel-irc/sopel.git`` or download a tarball `from GitHub
<https://github.com/sopel-irc/sopel/releases/latest>`_.
https://github.com/sopel-irc/sopel.git`` or download a `source archive from
GitHub <https://github.com/sopel-irc/sopel/archive/refs/heads/master.zip>`_.

Note: Sopel requires Python 3.8+ to run.

Expand Down
8 changes: 3 additions & 5 deletions contrib/tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
package_root = .
min_version = 4.3.3
envlist =
py{37,38,39,310,311,312}-qa
py{38,39,310,311,312}-qa
skip_missing_interpreters = true
ignore_base_python_conflict = true
labels =
lint = py{37,38,39,310,311,312}-lint
test = py{37,38,39,310,311,312}-test
lint = py{38,39,310,311,312}-lint
test = py{38,39,310,311,312}-test


[testenv]
Expand All @@ -16,14 +16,12 @@ package = sdist
allowlist_externals =
make
envname =
py37: py37
py38: py38
py39: py39
py310: py310
py311: py311
py312: py312
envdir =
py37: {toxinidir}/.tox/py37
py38: {toxinidir}/.tox/py38
py39: {toxinidir}/.tox/py39
py310: {toxinidir}/.tox/py310
Expand Down
5 changes: 3 additions & 2 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ flake8-future-import
flake8-import-order
flake8-type-checking; python_version >= '3.8'
# Sphinx theme
furo==2022.4.7
furo==2023.9.10
pytest~=7.1.0
pytest-vcr~=1.0.2
requests-mock~=1.9.3
sphinx>=4,<5
sphinx>=7.1.0,<8; python_version <= '3.8'
sphinx>=7.2.0,<8; python_version > '3.8'
# specify exact autoprogram version because the new (in 2021) maintainer
# showed that they will indeed make major changes in patch versions
sphinxcontrib-autoprogram==0.1.8
Expand Down
23 changes: 18 additions & 5 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
# serve to show the default.

from datetime import date
import sys, os
parentdir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.sys.path.insert(0,parentdir)

from sopel import __version__

# If extensions (or modules to document with autodoc) are in another directory,
Expand All @@ -24,7 +22,7 @@
# -- General configuration -----------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = '4.0'
needs_sphinx = '7.1' # todo: upgrade when Py3.8 reaches EOL

# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
Expand Down Expand Up @@ -111,7 +109,22 @@
pygments_dark_style = 'monokai'

# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
modindex_common_prefix = ['sopel.']

# If a signature’s length in characters exceeds the number set, each parameter
# within the signature will be displayed on an individual logical line.
maximum_signature_line_length = 80


# -- Options for autodoc -------------------------------------------------------

autodoc_type_aliases = {
'Casemapping': 'sopel.tools.identifiers.Casemapping',
'IdentifierFactory': 'sopel.tools.identifiers.IdentifierFactory',
'ModeTuple': 'sopel.irc.modes.ModeTuple',
'ModeDetails': 'sopel.irc.modes.ModeDetails',
'PrivilegeDetails': 'sopel.irc.modes.PrivilegeDetails',
}


# -- Options for HTML output ---------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Documentation
plugin
package
tests
genindex
modindex

.. toctree::
:caption: Donate
Expand Down
18 changes: 6 additions & 12 deletions docs/source/package/plugins/rules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,10 @@ sopel.plugins.rules
:members:
:undoc-members:

.. class:: TypedRule

A :class:`~typing.TypeVar` bound to :class:`AbstractRule`. When used in
the :meth:`AbstractRule.from_callable` class method, it means the return
value must be an instance of the class used to call that method and not a
different subclass of ``AbstractRule``.

.. versionadded:: 8.0

This ``TypeVar`` was added as part of a goal to start type-checking
Sopel and is not used at runtime.
.. autoclass:: TypedRule
:members:
:undoc-members:

.. TODO remove when sphinx-autodoc can manage TypeVar properly.
.. autoclass:: RuleMetrics
:members:
:undoc-members:
4 changes: 4 additions & 0 deletions docs/source/plugin/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ handler to run after the capability is acknowledged or denied by the server::
.. autoclass:: sopel.plugin.CapabilityNegotiation
:members:

.. autoclass:: sopel.plugin.CapabilityHandler
:members:
:special-members: __call__


Working with capabilities
-------------------------
Expand Down
8 changes: 8 additions & 0 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,14 @@ class SopelWrapper:
their ``bot`` argument. It acts as a proxy, providing the ``trigger``'s
``sender`` (source channel or private message) as the default
``destination`` argument for overridden methods.
.. deprecated:: 8.0
``SopelWrapper`` is being replaced with a ``contextvars`` based
alternative. For more information, see `#2460`__.
.. __: https://github.com/sopel-irc/sopel/issues/2460
"""
def __init__(self, sopel, trigger, output_prefix=''):
if not output_prefix:
Expand Down
5 changes: 2 additions & 3 deletions sopel/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,13 @@
from sqlalchemy.sql import delete, func, select, update

from sopel.lifecycle import deprecated
from sopel.tools.identifiers import Identifier
from sopel.tools.identifiers import Identifier, IdentifierFactory

if typing.TYPE_CHECKING:
from collections.abc import Iterable


LOGGER = logging.getLogger(__name__)
IdentifierFactory = typing.Callable[[str], Identifier]


def _deserialize(value):
Expand Down Expand Up @@ -146,7 +145,7 @@ def __init__(
config,
identifier_factory: IdentifierFactory = Identifier,
) -> None:
self.make_identifier = identifier_factory
self.make_identifier: IdentifierFactory = identifier_factory

if config.core.db_url is not None:
self.url = make_url(config.core.db_url)
Expand Down
6 changes: 4 additions & 2 deletions sopel/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,10 @@ def decorator(func):

@functools.wraps(func)
def deprecated_func(*args, **kwargs):
if not (warning_in and
parse_version(warning_in) >= parse_version(__version__)):
warn_ver = warning_in and parse_version(warning_in).release
this_ver = parse_version(__version__).release

if not (warn_ver and warn_ver >= this_ver):
original_frame = inspect.stack()[-stack_frame]
mod = inspect.getmodule(original_frame[0])
module_name = None
Expand Down
4 changes: 2 additions & 2 deletions sopel/modules/currency.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

PLUGIN_OUTPUT_PREFIX = '[currency] '
FIAT_PROVIDERS = {
'exchangerate.host': 'https://api.exchangerate.host/latest?base=EUR',
'open.er-api.com': 'https://open.er-api.com/v6/latest/EUR',
'fixer.io': '//data.fixer.io/api/latest?base=EUR&access_key={}',
}
CRYPTO_URL = 'https://api.coingecko.com/api/v3/exchange_rates'
Expand All @@ -43,7 +43,7 @@ class CurrencySection(types.StaticSection):
fiat_provider = types.ChoiceAttribute(
'fiat_provider',
list(FIAT_PROVIDERS.keys()),
default='exchangerate.host',
default='open.er-api.com',
)
"""Which data provider to use (some of which require no API key)"""
fixer_io_key = types.ValidatedAttribute('fixer_io_key', default=None)
Expand Down
14 changes: 12 additions & 2 deletions sopel/modules/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ def search(bot, trigger):


@plugin.command('suggest')
@plugin.example('.suggest wikip', 'wikipedia', online=True, vcr=True)
@plugin.example('.suggest', '.suggest what?')
@plugin.example('.suggest sopel irc', 'sopel irc', online=True, vcr=True)
@plugin.example('.suggest wikip', 'wikipedia', online=True, vcr=True, user_help=True)
@plugin.example('.suggest lkashdfiauwgaef', 'Sorry, no result.', online=True, vcr=True)
@plugin.output_prefix(PLUGIN_OUTPUT_PREFIX)
def suggest(bot, trigger):
Expand All @@ -213,9 +214,18 @@ def suggest(bot, trigger):
answer = xmltodict.parse(response.text)['toplevel']

try:
answer = answer['CompleteSuggestion'][0]['suggestion']['@data']
answer = answer['CompleteSuggestion']

try:
answer = answer[0]
except KeyError:
# only one suggestion; don't need to extract first item
pass

answer = answer['suggestion']['@data']
except TypeError:
answer = None

if answer:
bot.say(answer)
else:
Expand Down
74 changes: 50 additions & 24 deletions sopel/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
Callable,
Optional,
Pattern,
Tuple,
Protocol,
TYPE_CHECKING,
Union,
)
Expand Down Expand Up @@ -121,30 +121,15 @@ class CapabilityNegotiation(enum.Enum):
"""


if TYPE_CHECKING:
CapabilityHandler = Callable[
[Tuple[str, ...], SopelWrapper, bool],
CapabilityNegotiation,
]


class capability:
"""Decorate a function to request a capability and handle the result.
class CapabilityHandler(Protocol):
""":class:`~typing.Protocol` definition for capability handler.
:param name: name of the capability to negotiate with the server; this
positional argument can be used multiple times to form a
single ``CAP REQ``
:param handler: optional keyword argument, acknowledgement handler
When a plugin requests a capability, it can define a callback handler for
that request using :class:`capability` as a decorator. That handler will be
called upon Sopel receiving either an ``ACK`` (capability enabled) or a
``NAK`` (capability denied) CAP message.
The Client Capability Negotiation is a feature of IRCv3 that exposes a
mechanism for a server to advertise a list of features and for clients to
request them when they are available.
This decorator will register a capability request, allowing the bot to
request capabilities if they are available. You can request more than one
at a time, which will make for one single request.
The handler must follow this interface::
Example::
from sopel import plugin
from sopel.bot import SopelWrapper
Expand All @@ -168,7 +153,48 @@ def capability_handler(
# always return if Sopel can send "CAP END" (DONE)
# or if the plugin must notify the bot for that later (CONTINUE)
return CapabilityNegotiation.DONE
return plugin.CapabilityNegotiation.DONE
.. note::
This protocol class should be used for type checking and documentation
purposes only.
"""
def __call__(
self,
cap_req: tuple[str, ...],
bot: SopelWrapper,
acknowledged: bool,
) -> CapabilityNegotiation:
"""A capability handler must be a callable with this signature.
:param cap_req: the capability request, as a tuple of string
:param bot: the bot instance
:param acknowledged: that flag that tells if the capability is enabled
or denied
:return: the return value indicates if the capability negotiation is
complete for this request or not
"""


class capability:
"""Decorate a function to request a capability and handle the result.
:param name: name of the capability to negotiate with the server; this
positional argument can be used multiple times to form a
single ``CAP REQ``
:param handler: optional keyword argument, acknowledgement handler
The Client Capability Negotiation is a feature of IRCv3 that exposes a
mechanism for a server to advertise a list of features and for clients to
request them when they are available.
This decorator will register a capability request, allowing the bot to
request capabilities if they are available. You can request more than one
at a time, which will make for one single request.
The handler must follow the :class:`CapabilityHandler` protocol.
.. note::
Expand Down
12 changes: 12 additions & 0 deletions sopel/plugins/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@
]

TypedRule = TypeVar('TypedRule', bound='AbstractRule')
"""A :class:`~typing.TypeVar` bound to :class:`AbstractRule`.
When used in the :meth:`AbstractRule.from_callable` class method, it means the
return value must be an instance of the class used to call that method and not
a different subclass of ``AbstractRule``.
.. versionadded:: 8.0
This ``TypeVar`` was added as part of a goal to start type-checking
Sopel and is not used at runtime.
"""

LOGGER = logging.getLogger(__name__)

Expand Down
5 changes: 5 additions & 0 deletions sopel/tools/identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from typing import Callable

Casemapping = Callable[[str], str]
"""Type definition of a casemapping callable."""

ASCII_TABLE = str.maketrans(string.ascii_uppercase, string.ascii_lowercase)
RFC1459_TABLE = str.maketrans(
Expand Down Expand Up @@ -278,3 +279,7 @@ def is_nick(self) -> bool:
"""
return bool(self) and not self.startswith(self.chantypes)


IdentifierFactory = Callable[[str], Identifier]
"""Type definition of an identifier factory."""
Loading

0 comments on commit b68b351

Please sign in to comment.