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

docs, dev-requirements: upgrade sphinx and try to fix some type issues with documentation #2496

Merged
merged 5 commits into from
Oct 26, 2023
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
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__
Exirel marked this conversation as resolved.
Show resolved Hide resolved

# 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
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
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.

"""
Exirel marked this conversation as resolved.
Show resolved Hide resolved

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."""
11 changes: 3 additions & 8 deletions sopel/tools/memories.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@

from collections import defaultdict
import threading
from typing import TYPE_CHECKING
from typing import Optional

from .identifiers import Identifier

if TYPE_CHECKING:
from typing import Callable, Optional

IdentifierFactory = Callable[[str], Identifier]
from .identifiers import Identifier, IdentifierFactory


class SopelMemory(dict):
Expand Down Expand Up @@ -100,7 +95,7 @@
return result


class SopelIdentifierMemory(SopelMemory):

Check warning

Code scanning / CodeQL

`__eq__` not overridden when adding attributes Warning

The class 'SopelIdentifierMemory' does not override
'__eq__'
, but adds the new attribute
make_identifier
.
"""Special Sopel memory that stores ``Identifier`` as key.

This is a convenient subclass of :class:`SopelMemory` that always casts its
Expand Down Expand Up @@ -156,7 +151,7 @@
identifier_factory: IdentifierFactory = Identifier,
) -> None:
super().__init__(*args)
self.make_identifier = identifier_factory
self.make_identifier: IdentifierFactory = identifier_factory
"""A factory to transform keys into identifiers."""

def _make_key(self, key: Optional[str]) -> Optional[Identifier]:
Expand Down
43 changes: 19 additions & 24 deletions sopel/tools/target.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"""User and channel objects used in state tracking."""
from __future__ import annotations

import functools
from typing import Any, Callable, Optional, TYPE_CHECKING, Union
from typing import Any, Optional, TYPE_CHECKING, Union

from sopel import privileges
from sopel.tools import identifiers, memories
from sopel.tools import memories
from sopel.tools.identifiers import Identifier, IdentifierFactory

if TYPE_CHECKING:
from datetime import datetime


IdentifierFactory = Callable[[str], identifiers.Identifier]
import datetime


@functools.total_ordering
Expand All @@ -28,12 +27,12 @@ class User:

def __init__(
self,
nick: identifiers.Identifier,
nick: Identifier,
user: Optional[str],
host: Optional[str],
) -> None:
assert isinstance(nick, identifiers.Identifier)
self.nick: identifiers.Identifier = nick
assert isinstance(nick, Identifier)
self.nick: Identifier = nick
"""The user's nickname."""
self.user: Optional[str] = user
"""The user's local username.
Expand All @@ -53,7 +52,7 @@ def __init__(
Will be ``None`` if Sopel has not yet received complete user
information from the IRC server.
"""
self.channels: dict[identifiers.Identifier, 'Channel'] = {}
self.channels: dict[Identifier, 'Channel'] = {}
"""The channels the user is in.

This maps channel name :class:`~sopel.tools.identifiers.Identifier`\\s
Expand Down Expand Up @@ -123,11 +122,11 @@ class Channel:

def __init__(
self,
name: identifiers.Identifier,
identifier_factory: IdentifierFactory = identifiers.Identifier,
name: Identifier,
identifier_factory: IdentifierFactory = Identifier,
) -> None:
assert isinstance(name, identifiers.Identifier)
self.name: identifiers.Identifier = name
assert isinstance(name, Identifier)
self.name: Identifier = name
"""The name of the channel."""

self.make_identifier: IdentifierFactory = identifier_factory
Expand All @@ -139,7 +138,7 @@ def __init__(
"""

self.users: dict[
identifiers.Identifier,
Identifier,
User,
] = memories.SopelIdentifierMemory(
identifier_factory=self.make_identifier,
Expand All @@ -150,7 +149,7 @@ def __init__(
:class:`User` objects.
"""
self.privileges: dict[
identifiers.Identifier,
Identifier,
int,
] = memories.SopelIdentifierMemory(
identifier_factory=self.make_identifier,
Expand All @@ -177,17 +176,17 @@ def __init__(
does not automatically populate all modes and lists.
"""

self.last_who: Optional[datetime] = None
self.last_who: Optional[datetime.datetime] = None
"""The last time a WHO was requested for the channel."""

self.join_time: Optional[datetime] = None
self.join_time: Optional[datetime.datetime] = None
"""The time the server acknowledged our JOIN message.

Based on server-reported time if the ``server-time`` IRCv3 capability
is available, otherwise the time Sopel received it.
"""

def clear_user(self, nick: identifiers.Identifier) -> None:
def clear_user(self, nick: Identifier) -> None:
"""Remove ``nick`` from this channel.

:param nick: the nickname of the user to remove
Expand Down Expand Up @@ -426,11 +425,7 @@ def is_voiced(self, nick: str) -> bool:
identifier = self.make_identifier(nick)
return bool(self.privileges.get(identifier, 0) & privileges.VOICE)

def rename_user(
self,
old: identifiers.Identifier,
new: identifiers.Identifier,
) -> None:
def rename_user(self, old: Identifier, new: Identifier) -> None:
"""Rename a user.

:param old: the user's old nickname
Expand Down
Loading