diff --git a/.readthedocs.yml b/.readthedocs.yml index 1e48672de0..be03096e5b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,13 +6,13 @@ formats: build: os: ubuntu-24.04 tools: - python: "3.9" + python: "3.11" jobs: create_environment: - asdf plugin add uv - asdf install uv latest - asdf global uv latest - - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --no-default-groups --extra docs + - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv sync --no-default-groups --group docs install: - "true" diff --git a/docs/.ruff.toml b/docs/.ruff.toml new file mode 100644 index 0000000000..41bf35e5d3 --- /dev/null +++ b/docs/.ruff.toml @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: MIT + +extend = "../pyproject.toml" +src = ["../"] + +target-version = "py311" diff --git a/docs/_static/scorer.js b/docs/_static/scorer.js index f3bc8ce584..51647bfea2 100644 --- a/docs/_static/scorer.js +++ b/docs/_static/scorer.js @@ -17,6 +17,7 @@ function __score(haystack, regex) { } let subLength = match[0].length; let start = match.index; + // longer (and later) submatches get a higher score penalty return (subLength * 1000 + start) / 1000.0; } @@ -38,23 +39,29 @@ function __setPattern() { Scorer = { // Implement the following function to further tweak the score for each result - // The function takes a result array [filename, title, anchor, descr, score] + // The function takes a result array [docname, title, anchor, descr, score, filename, kind] // and returns the new score. score: (result) => { - // only inflate the score of things that are actual API reference things - const [, title, , , score,] = result; + const [, title, , , score, , kind] = result; if (queryBeingDone === undefined) { __setPattern(); } + // penalize text matches a little bit, sphinx scores pages that have a matching subtitle + // the same as pages that actually have the search term as the title, for some reason + if (kind === "text") return score - 1; + + // only inflate the score of things that are actual API reference things if (pattern !== null && title.startsWith('disnake.')) { - let _score = __score(title, pattern); - if (_score === Number.MAX_VALUE) { + const penalty = __score(title, pattern); + if (penalty === Number.MAX_VALUE) { return score; } - let newScore = 100 + queryBeingDone.length - _score; - // console.log(`${title}: ${score} -> ${newScore} (${_score})`); + // calculate new score on top of title score; we want to rank *all* API results + // right below matching pages, and have pages with only a fulltext match appear last + const newScore = Scorer.title - (penalty / 1000); + // console.log(`${title}: ${score} -> ${newScore} (${penalty})`); return newScore; } return score; diff --git a/docs/_static/style.css b/docs/_static/style.css index fb9bece8b5..1ce8443812 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -200,13 +200,14 @@ body { } /* all URLs only show underline on hover */ -a { +/* (also overrides sphinx's :visited highlight) */ +a, a:visited { text-decoration: none; color: var(--link-text); transition: color 0.3s; } -a:hover { +a:hover, a:hover:visited { text-decoration: underline; color: var(--link-hover-text); transition: color 0.05s; @@ -236,12 +237,12 @@ header > nav { line-height: 1em; /* It defaults to 1.2, pushing some icons out of vertical alignment */ } -header > nav a { +header > nav a, header > nav a:visited { color: var(--nav-link-text); margin: 0 0.5em; } -header > nav a:hover { +header > nav a:hover, header > nav a:hover:visited { color: var(--nav-link-hover-text); text-decoration: none; } @@ -286,7 +287,7 @@ footer { z-index: 5; } -footer a { +footer a, footer a:visited { text-decoration: underline; color: var(--footer-link); } diff --git a/docs/_templates/api_redirect.js_t b/docs/_templates/api_redirect.js.jinja similarity index 88% rename from docs/_templates/api_redirect.js_t rename to docs/_templates/api_redirect.js.jinja index ee4b68b64e..a07980dbc1 100644 --- a/docs/_templates/api_redirect.js_t +++ b/docs/_templates/api_redirect.js.jinja @@ -9,8 +9,8 @@ const redirects_map = {{ redirect_data }}; if (!url.pathname.endsWith("/api.html")) return; - // URL_ROOT is relative to `url`, and points to e.g. `/en/latest/` - const root = new URL(DOCUMENTATION_OPTIONS.URL_ROOT, url); + // content_root is relative to `url`, and points to e.g. `/en/latest/` + const root = new URL(document.documentElement.dataset.content_root, url); if (!root.pathname.endsWith("/")) root.pathname += "/"; const targetPath = redirects_map[url.hash.slice(1)]; diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 47bd68988d..3e1c767ac6 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -1,7 +1,7 @@ - + @@ -21,7 +21,6 @@ {%- block scripts %} - {% if ("/" + pagename).endswith("/api") %} diff --git a/docs/conf.py b/docs/conf.py index 935e922039..e1290fb679 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,12 +14,15 @@ import importlib.util import inspect +import logging import os import re import subprocess # noqa: TID251 import sys -from typing import Any, Dict, Optional +import warnings +from typing import Any +import sphinx.deprecation from sphinx.application import Sphinx # If extensions (or modules to document with autodoc) are in another directory, @@ -90,7 +93,9 @@ templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = ".rst" +source_suffix = { + ".rst": "restructuredtext", +} # The encoding of source files. # source_encoding = 'utf-8-sig' @@ -208,7 +213,7 @@ def git(*args: str) -> str: _disnake_module_path = os.path.dirname(_spec.origin) -def linkcode_resolve(domain: str, info: Dict[str, Any]) -> Optional[str]: +def linkcode_resolve(domain: str, info: dict[str, Any]) -> str | None: if domain != "py": return None @@ -506,3 +511,19 @@ def setup(app: Sphinx) -> None: import disnake del disnake.Embed.Empty # type: ignore + + warnings.filterwarnings( + "ignore", + category=sphinx.deprecation.RemovedInSphinx90Warning, + module="hoverxref.extension", + ) + + # silence somewhat verbose `Writing evaluated template result to ...` log + logging.getLogger("sphinx.sphinx.util.fileutil").addFilter( + lambda r: getattr(r, "subtype", None) != "template_evaluation" + ) + + # `document is referenced in multiple toctrees:` is fine and expected + logging.getLogger("sphinx.sphinx.environment").addFilter( + lambda r: getattr(r, "subtype", None) != "multiple_toc_parents" + ) diff --git a/docs/extensions/attributetable.py b/docs/extensions/attributetable.py index 030343ea2d..48fe343c4f 100644 --- a/docs/extensions/attributetable.py +++ b/docs/extensions/attributetable.py @@ -5,7 +5,7 @@ import inspect import re from collections import defaultdict -from typing import TYPE_CHECKING, ClassVar, DefaultDict, Dict, List, NamedTuple, Optional, Tuple +from typing import TYPE_CHECKING, ClassVar, NamedTuple from docutils import nodes from sphinx import addnodes @@ -104,7 +104,7 @@ class PyAttributeTable(SphinxDirective): final_argument_whitespace = False option_spec: ClassVar[OptionSpec] = {} - def parse_name(self, content: str) -> Tuple[str, Optional[str]]: + def parse_name(self, content: str) -> tuple[str, str | None]: match = _name_parser_regex.match(content) path, name = match.groups() if match else (None, None) if path: @@ -119,7 +119,7 @@ def parse_name(self, content: str) -> Tuple[str, Optional[str]]: return modulename, name - def run(self) -> List[nodes.Node]: + def run(self) -> list[nodes.Node]: """If you're curious on the HTML this is meant to generate:
@@ -156,10 +156,10 @@ def run(self) -> List[nodes.Node]: return [node] -def build_lookup_table(env: BuildEnvironment) -> Dict[str, List[str]]: +def build_lookup_table(env: BuildEnvironment) -> dict[str, list[str]]: # Given an environment, load up a lookup table of # full-class-name: objects - result: DefaultDict[str, List[str]] = defaultdict(list) + result: defaultdict[str, list[str]] = defaultdict(list) domain = env.domains["py"] ignored = { @@ -182,7 +182,7 @@ def build_lookup_table(env: BuildEnvironment) -> Dict[str, List[str]]: class TableElement(NamedTuple): fullname: str label: str - badge: Optional[attributetablebadge] + badge: attributetablebadge | None def process_attributetable(app: Sphinx, doctree: nodes.document, docname: str) -> None: @@ -211,12 +211,12 @@ def process_attributetable(app: Sphinx, doctree: nodes.document, docname: str) - def get_class_results( - lookup: Dict[str, List[str]], modulename: str, name: str, fullname: str -) -> Dict[str, List[TableElement]]: + lookup: dict[str, list[str]], modulename: str, name: str, fullname: str +) -> dict[str, list[TableElement]]: module = importlib.import_module(modulename) cls = getattr(module, name) - groups: Dict[str, List[TableElement]] = { + groups: dict[str, list[TableElement]] = { _("Attributes"): [], _("Methods"): [], } @@ -265,7 +265,7 @@ def get_class_results( return groups -def class_results_to_node(key: str, elements: List[TableElement]) -> attributetablecolumn: +def class_results_to_node(key: str, elements: list[TableElement]) -> attributetablecolumn: title = attributetabletitle(key, key) ul = nodes.bullet_list("") ul["classes"].append("py-attribute-table-list") diff --git a/docs/extensions/builder.py b/docs/extensions/builder.py index 6eee763436..7aa4bf03eb 100644 --- a/docs/extensions/builder.py +++ b/docs/extensions/builder.py @@ -7,7 +7,7 @@ from docutils import nodes from sphinx.environment.adapters.indexentries import IndexEntries -from sphinxext.opengraph.descriptionparser import DescriptionParser +from sphinxext.opengraph._description_parser import DescriptionParser if TYPE_CHECKING: from sphinx.application import Sphinx diff --git a/docs/extensions/collapse.py b/docs/extensions/collapse.py index a37bdd3780..037d0b7e2b 100644 --- a/docs/extensions/collapse.py +++ b/docs/extensions/collapse.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT from __future__ import annotations -from typing import TYPE_CHECKING, ClassVar, List +from typing import TYPE_CHECKING, ClassVar from docutils import nodes from docutils.parsers.rst import Directive, directives @@ -36,7 +36,7 @@ class CollapseDirective(Directive): option_spec: ClassVar[OptionSpec] = {"open": directives.flag} - def run(self) -> List[collapse]: + def run(self) -> list[collapse]: self.assert_has_content() node = collapse( "\n".join(self.content), diff --git a/docs/extensions/exception_hierarchy.py b/docs/extensions/exception_hierarchy.py index 49ed16f539..586059e9be 100644 --- a/docs/extensions/exception_hierarchy.py +++ b/docs/extensions/exception_hierarchy.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT from __future__ import annotations -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from docutils import nodes from docutils.parsers.rst import Directive @@ -28,7 +28,7 @@ def depart_exception_hierarchy_node(self: HTMLTranslator, node: nodes.Element) - class ExceptionHierarchyDirective(Directive): has_content = True - def run(self) -> List[exception_hierarchy]: + def run(self) -> list[exception_hierarchy]: self.assert_has_content() node = exception_hierarchy("\n".join(self.content)) self.state.nested_parse(self.content, self.content_offset, node) diff --git a/docs/extensions/fulltoc.py b/docs/extensions/fulltoc.py index 62499afd4a..8923f7b424 100644 --- a/docs/extensions/fulltoc.py +++ b/docs/extensions/fulltoc.py @@ -29,7 +29,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, cast +from typing import TYPE_CHECKING, cast from docutils import nodes from sphinx import addnodes @@ -112,7 +112,7 @@ def build_full_toctree( """ env: BuildEnvironment = builder.env doctree = env.get_doctree(index) - toctrees: List[nodes.Element] = [] + toctrees: list[nodes.Element] = [] for toctreenode in doctree.traverse(addnodes.toctree): toctree = env.resolve_toctree( docname, diff --git a/docs/extensions/redirects.py b/docs/extensions/redirects.py index 46114dd743..631e5796ea 100644 --- a/docs/extensions/redirects.py +++ b/docs/extensions/redirects.py @@ -3,7 +3,7 @@ import json from pathlib import Path -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING from sphinx.application import Sphinx from sphinx.util.fileutil import copy_asset_file @@ -11,13 +11,13 @@ if TYPE_CHECKING: from ._types import SphinxExtensionMeta -SCRIPT_PATH = "_templates/api_redirect.js_t" +SCRIPT_PATH = "_templates/api_redirect.js.jinja" -def collect_redirects(app: Sphinx) -> Dict[str, str]: +def collect_redirects(app: Sphinx) -> dict[str, str]: # mapping of html node id (i.e., thing after "#" in URLs) to the correct page name # e.g, api.html#disnake.Thread => api/channels.html - mapping: Dict[str, str] = {} + mapping: dict[str, str] = {} # see https://www.sphinx-doc.org/en/master/extdev/domainapi.html#sphinx.domains.Domain.get_objects domain = app.env.domains["py"] @@ -27,7 +27,7 @@ def collect_redirects(app: Sphinx) -> Dict[str, str]: return mapping -def copy_redirect_script(app: Sphinx, exception: Exception) -> None: +def copy_redirect_script(app: Sphinx, exception: Exception | None) -> None: if app.builder.format != "html" or exception: return @@ -41,6 +41,7 @@ def copy_redirect_script(app: Sphinx, exception: Exception) -> None: SCRIPT_PATH, str(Path(app.outdir, "_static", "api_redirect.js")), context=context, + force=True, ) diff --git a/docs/extensions/resourcelinks.py b/docs/extensions/resourcelinks.py index d93f6f2715..4acfba3a29 100644 --- a/docs/extensions/resourcelinks.py +++ b/docs/extensions/resourcelinks.py @@ -4,7 +4,8 @@ # Licensed under BSD. from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any from docutils import nodes, utils from sphinx.util.nodes import split_explicit_title @@ -18,16 +19,16 @@ from ._types import SphinxExtensionMeta -def make_link_role(resource_links: Dict[str, str]) -> RoleFunction: +def make_link_role(resource_links: dict[str, str]) -> RoleFunction: def role( typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner, - options: Optional[Dict[str, Any]] = None, - content: Optional[List[str]] = None, - ) -> Tuple[List[Node], List[system_message]]: + options: dict[str, Any] | None = None, + content: Sequence[str] | None = None, + ) -> tuple[list[Node], list[system_message]]: text = utils.unescape(text) has_explicit_title, title, key = split_explicit_title(text) full_url = resource_links[key] diff --git a/noxfile.py b/noxfile.py index 33d2512934..8fa587d574 100755 --- a/noxfile.py +++ b/noxfile.py @@ -89,8 +89,9 @@ def __post_init__(self) -> None: # docs and pyright ExecutionGroup( sessions=("docs", "pyright"), + python="3.11", pyright_paths=("docs",), - extras=("docs",), + groups=("docs",), ), # codemodding and pyright ExecutionGroup( @@ -229,6 +230,8 @@ def docs(session: nox.Session) -> None: "sphinx-autobuild", "--ignore", "_build", + "--re-ignore", + "__pycache__", "--watch", "../disnake", "--watch", @@ -474,7 +477,7 @@ def dev(session: nox.Session) -> None: """ session.run("uv", "lock", external=True) session.run("uv", "venv", "--clear", external=True) - session.run("uv", "sync", "--all-extras", "--all-groups", external=True) + session.run("uv", "sync", "--all-extras", external=True) session.run("uv", "run", "prek", "install", "--overwrite", external=True) diff --git a/pyproject.toml b/pyproject.toml index 28e6b509e0..5a1c40d033 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,16 +55,6 @@ voice = [ "PyNaCl>=1.5.0,<1.6", 'audioop-lts>=0.2.1; python_version >= "3.13"' ] -docs = [ - "sphinx==7.0.1", - "sphinxcontrib-trio~=1.1.2", - "sphinx-hoverxref==1.3.0", - "sphinx-autobuild~=2021.3", - "sphinxcontrib-towncrier==0.3.2a0", - "towncrier==23.6.0", - "sphinx-notfound-page==0.8.3", - "sphinxext-opengraph==0.9.1", -] [dependency-groups] dev = [ @@ -90,7 +80,17 @@ tools = [ { include-group = "ruff" }, ] changelog = [ - "towncrier==23.6.0", + "towncrier==25.8.0", +] +docs = [ + "sphinx==8.2.3", + "sphinx-autobuild~=2025.8.25", + "sphinx-hoverxref==1.4.2", + "sphinx-notfound-page==1.1.0", + "sphinxcontrib-towncrier==0.5.0a0", + "sphinxcontrib-trio~=1.1.2", + "sphinxext-opengraph==0.13.0", + { include-group = "changelog" } ] codemod = [ # run codemods on the repository (mostly automated typing) @@ -123,6 +123,9 @@ include = ["disnake*"] [tool.uv] required-version = ">=0.8.4" +[tool.uv.dependency-groups] +docs = { requires-python = ">=3.11" } + [tool.ruff] line-length = 100