Skip to content

Commit

Permalink
Merge pull request #2496 from Exirel/docs-upgrade-and-type-fix-or-so-…
Browse files Browse the repository at this point in the history
…i-wish

docs, dev-requirements: upgrade sphinx and try to fix some type issues with documentation
  • Loading branch information
dgw authored Oct 26, 2023
2 parents a91ed84 + 73cf3a3 commit 8a60980
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 89 deletions.
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
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.
"""

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 @@ -150,7 +145,7 @@ def __init__(
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

0 comments on commit 8a60980

Please sign in to comment.