aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/docs.yaml42
-rw-r--r--CHANGELOG.md88
-rw-r--r--README.md4
-rw-r--r--botcore/_bot.py6
-rw-r--r--docs/README.md48
-rw-r--r--docs/_static/changelog.css11
-rw-r--r--docs/_static/changelog.js41
-rw-r--r--docs/_templates/base.html12
-rw-r--r--docs/_templates/sidebar/navigation.html46
-rw-r--r--docs/changelog.rst125
-rw-r--r--docs/conf.py89
-rw-r--r--docs/index.rst5
-rw-r--r--docs/pages/index_redirect.html19
-rw-r--r--docs/pages/versions.html23
-rw-r--r--docs/utils.py119
-rw-r--r--poetry.lock64
-rw-r--r--pyproject.toml9
17 files changed, 612 insertions, 139 deletions
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index ad10f75f..5254d524 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -13,8 +13,9 @@ concurrency:
jobs:
- build-deploy:
- name: Build & Deploy Docs
+ latest-build:
+ # We only need to verify that the docs build with no warnings here
+ name: Build Latest Docs Version
runs-on: ubuntu-latest
steps:
@@ -31,13 +32,42 @@ jobs:
# https://github.com/bitprophet/releases/pull/82
- run: pip install six
- - name: Generate AutoDoc References
- run: sphinx-apidoc -o docs/output botcore -feM
-
- name: Generate HTML Site
run: sphinx-build -nW -j auto -b html docs docs/build
- - name: Uplaod Build Artifact
+ - name: Upload Build Artifact
+ uses: actions/upload-artifact@v2
+ with:
+ name: docs-latest
+ path: docs/build/*
+
+ build-deploy:
+ name: Build & Deploy Docs
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0 # We need to check out the entire repository to find all tags
+
+ - name: Install Python Dependencies
+ uses: HassanAbouelela/actions/setup-python@setup-python_v1.1.0
+ with:
+ dev: true
+ python_version: 3.9
+ install_args: "--extras async-rediscache"
+
+ # Undeclared dependency for `releases`... whoops
+ # https://github.com/bitprophet/releases/pull/82
+ - run: pip install six
+
+ - name: Build All Doc Versions
+ run: sphinx-multiversion docs docs/build -n -j auto
+ env:
+ BUILD_DOCS_FOR_HEAD: ${{ github.event_name == 'pull_request' }}
+ BRANCH_NAME: ${{ github.head_ref }}
+
+ - name: Upload Build Artifact
uses: actions/upload-artifact@v2
with:
name: docs
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index e6c217f4..00000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,88 +0,0 @@
-# Changelog
-
-## 7.1.0 24th May 2022
-- Feature: Bump Discord.py to [`4cbe8f5`](https://github.com/Rapptz/discord.py/tree/4cbe8f58e16f6a76371ce45a69e0832130d6d24f)
- - This fixes a bug with permission resolution when dealing with timed out members.
-
-## 7.0.0 10th May 2022
-- Fix: Capture all characters up to a whitespace in the Discord Invite regex.
-- Breaking: Discord invite regex no longer returns a URL safe result, refer to documentation for safely handling it.
-
-## 6.4.0 26th April 2022
-- Feature: Bump discord.py to `[5a06fa5](https://github.com/Rapptz/discord.py/tree/5a06fa5f3e28d2b7191722e1a84c541560008aea)`
- - Notably, one of the commits in this bump dynamically extends the timeout of `Guild.chunk()` based on the number or members, so it should actually work on our guild now.
-
-## 6.3.2 25th April 2022
-- Fix: Actually use `statsd_url` when it gets passed to `BotBase`.
-
-## 6.3.1 21st April 2022
-- Fix: Correct version number in pyproject.toml
-
-## 6.3.0 21st April 2022
-- Feature: Don't load modules starting with `_`
-
-## 6.2.0 21st April 2022
-- Feature: Load each cog in it's own task to avoid a single cog crashing entire load process.
-
-## 6.1.0 20th April 2022
-- Feature: Add `unqualify` to the `botcore.utils` namespace for use in bots that manipulate extensions
-
-## 6.0.0 19th April 2022
-- Breaking: Bump discord.py to [987235d5649e7c2b1a927637bab6547244ecb2cf](https://github.com/Rapptz/discord.py/tree/987235d5649e7c2b1a927637bab6547244ecb2cf)
- - This reverts a change to help command behaviour that broke our custom pagination
- - This also adds basic forum channel support to discord.py
-
-## 5.0.4 18th April 2022
-- Feature: Allow passing an `api_client` to `BotBase`.`__init__` to specify the `botcore.site_api.APIClient` instance to use.
-
-## 5.0.3 18th April 2022
-- Fix: Reconnect to redis session on setup if it is closed.
-
-## 5.0.2 5th April 2022
-- Fix: Create a dummy `AsyncstatsdClient` before connecting to real url, in case a connection cannot be made on init.
-- Fix: Move the creation of the `asyncio.Event`, `BotBase._guild_available` to within the setup hook, to avoid a deprecation notice.
-
-## 5.0.1 2nd April 2022
-- Fix: Move creation of BotBase's `aiohttp.AsyncResolver` to the async setup hook, to avoid deprecation notice
-
-## 5.0.0 2nd April 2022
-- Breaking: Remove public extensions util
-- Feature: Add `BotBase`, a `discord.ext.commands.Bot` sub-class, which abstracts a lot of logic shared between our bots
-- Feature: Add async statsd client
-- Dependencies: Bump Discord.py to latest alpha commit
-
-## 4.0.0 14th March 2022
-- Breaking: Migrate back to Discord.py 2.0
-
-## 3.0.1 5th March 2022
-- Fix: Setup log tracing when `botcore.utils.logging` is imported so that it can be used within botcore functions.
-
-## 3.0.0 3rd March 2022
- - Breaking: Move `apply_monkey_patches()` directly to `botcore.utils` namespace
-
-## 2.1.0 24th February 2022
- - Feature: Port the Site API wrapper from the bot repo.
-
-## 2.0.0 22nd February 2022
-- Breaking: Moved regex to `botcore.utils` namespace
-- Feature: Migrate from discord.py 2.0a0 to disnake.
-- Feature: Add common monkey patches.
-- Feature: Port many common utilities from our bots
- - caching
- - channel
- - extensions
- - loggers
- - members
- - scheduling
-- Support: Added intersphinx to docs.
-
-## 1.2.0 9th January 2022
-- Feature: Add code block detection regex.
-
-## 1.1.0 2nd December 2021
-- Support: Add autogenerated docs.
-- Feature: Add a regex utility.
-
-
-## 1.0.0 17th November 2021
-- Support: Add the core package, poetry, and linting CI.
diff --git a/README.md b/README.md
index c6f83a08..427ab10b 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
-# bot-core
+# bot-core ![Version]
+
+[Version]: https://img.shields.io/github/v/tag/python-discord/bot-core?label=latest&logo=version
diff --git a/botcore/_bot.py b/botcore/_bot.py
index a36dba15..e9eba5c5 100644
--- a/botcore/_bot.py
+++ b/botcore/_bot.py
@@ -17,8 +17,10 @@ from botcore.utils.logging import get_logger
try:
from async_rediscache import RedisSession
+ REDIS_AVAILABLE = True
except ImportError:
- RedisSession = discord.utils._MissingSentinel
+ RedisSession = None
+ REDIS_AVAILABLE = False
log = get_logger()
@@ -70,7 +72,7 @@ class BotBase(commands.Bot):
self.api_client = api_client
self.statsd_url = statsd_url
- if redis_session and RedisSession == discord.utils._MissingSentinel:
+ if redis_session and not REDIS_AVAILABLE:
warnings.warn("redis_session kwarg passed, but async-rediscache not installed!")
elif redis_session:
self.redis_session = redis_session
diff --git a/docs/README.md b/docs/README.md
index fa719292..89124d5e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -3,6 +3,8 @@ Meta information about this project's documentation.
Table of contents:
- [Building the docs](#Building)
+- [Docs Layout](#Layout)
+- [Building all versions](#Versions)
- [Writing docstrings](#Docstrings)
- [Writing a changelog](#Changelog)
@@ -15,11 +17,45 @@ poetry run task docs
The output will be in the [`/docs/build`](.) directory.
-Additionally, there are two helper tasks: `apidoc` and `builddoc`.
-`apidoc` is responsible for calling autodoc and generating docs from docstrings.
-`builddoc` generates the HTML site, and should be called after apidoc.
-Neither of these two tasks needs to be manually called, as the `docs` task calls both.
+## Layout
+The docs folder has a few moving components, here's a brief description of each:
+
+- `_static`: This folder includes extra resources that are copied blindly by sphinx into the result
+ making it perfect for resources such as custom CSS or JS.
+- `_templates` & `pages`: Both are considered HTML templates, and passed to Sphinx as `templates_path`.
+ The difference between them is that `_templates` is only used to provide templates and overrides that
+ are used by other pages, while `pages` are full-blown independent pages that will be included in the
+ result. Files in `pages` are passed to Sphinx as `html_additional_pages`, and will maintain the same
+ structure as the folder. That is to say, a file such as `pages/a/b.html` will create `https://bot-core/a/b.html`.
+- `changelog.rst`: This contains a list of all the project's changes. Please refer to [Changelog](#Changelog)
+ below for more info.
+- `index.rst`: The main content for the project's homepage.
+- `conf.py`: Configuration for Sphinx. This includes things such as the project name, version,
+ plugins and their configuration.
+- `utils.py`: Helpful function used by `conf.py` to properly create the docs.
+- `netliy_build.py`: Script which downloads the build output in netlify. Refer to [Static Netlify Build](#static-builds)
+
+
+## Versions
+The project supports building all different versions at once using [sphinx-multiversion][multiversion]
+after version `v7.1.0`. You can run the following command to achieve that:
+
+```shell
+poetry run sphinx_multiversion -v docs docs/build -n -j auto -n
+```
+
+This will build all tags, as well as the main branch. To build branches besides the main one
+(such as the one you are currently working on), set the `BUILD_DOCS_FOR_HEAD` environment variable
+to True.
+
+When using multi-version, keep the following in mind:
+1. This command will not fail on warnings, unlike the docs task. Make sure that passes first
+ before using this one.
+2. Make sure to clear the build directory before running this script to avoid conflicts.
+
+
+[multiversion]: https://holzhaus.github.io/sphinx-multiversion/master/index.html
## Docstrings
@@ -38,11 +74,11 @@ Refer to the [sphinx documentation][docstring-sections] for more information.
Each change requires an entry in the [Changelog](./changelog.rst).
Refer to the [Releases][releases] documentation for more information on the exact format and content of entries
-You can use [this site][releases] to get the PR number you'll use for your entry.
+You can use [this site][next] to get the PR number that'll be assigned for your entry.
-[next]: https://ichard26.github.io/next-pr-number/?owner=python-discord&name=bot-core
[releases]: https://releases.readthedocs.io/en/latest/concepts.html
+[next]: https://ichard26.github.io/next-pr-number/?owner=python-discord&name=bot-core
## Static Builds
We deploy our docs to netlify to power static previews on PRs.
diff --git a/docs/_static/changelog.css b/docs/_static/changelog.css
new file mode 100644
index 00000000..343792a1
--- /dev/null
+++ b/docs/_static/changelog.css
@@ -0,0 +1,11 @@
+[data-theme='dark'] #changelog .dark,
+[data-theme='light'] #changelog .light,
+[data-theme='auto'] #changelog .light {
+ display: inline;
+}
+
+[data-theme='dark'] #changelog .light,
+[data-theme='light'] #changelog .dark,
+[data-theme='auto'] #changelog .dark {
+ display: none;
+}
diff --git a/docs/_static/changelog.js b/docs/_static/changelog.js
new file mode 100644
index 00000000..94834eaa
--- /dev/null
+++ b/docs/_static/changelog.js
@@ -0,0 +1,41 @@
+/** Update the changelog colors in dark mode */
+function changelog_color_main() {
+ const changelog = document.getElementById("changelog");
+
+ function updateEntryColor(span) {
+ const lightColorSpan = span;
+ const darkColorSpan = lightColorSpan.cloneNode(true);
+
+ lightColorSpan.parentElement.insertBefore(darkColorSpan, lightColorSpan);
+
+ lightColorSpan.classList.add("light");
+ darkColorSpan.classList.add("dark");
+
+ let color;
+ switch (darkColorSpan.textContent) {
+ case "Feature":
+ color = "#5BF38E";
+ break;
+ case "Support":
+ color = "#55A5E7";
+ break;
+ case "Bug":
+ color = "#E14F4F";
+ break;
+ default:
+ color = lightColorSpan.style.color;
+ }
+
+ darkColorSpan.style["color"] = color;
+ }
+
+ const TYPES = ["Feature", "Bug", "Support", "Breaking"];
+
+ if (changelog !== null) {
+ Array.from(changelog.getElementsByTagName("span"))
+ .filter(value => TYPES.includes(value.textContent))
+ .forEach(updateEntryColor)
+ }
+}
+
+changelog_color_main();
diff --git a/docs/_templates/base.html b/docs/_templates/base.html
new file mode 100644
index 00000000..541dbd0b
--- /dev/null
+++ b/docs/_templates/base.html
@@ -0,0 +1,12 @@
+{% extends "furo/base.html" %}
+
+{# Make sure the project name uses the correct version #}
+{% if versions %}
+ {% if current_version == latest_version %}
+ {% set docstitle = "Latest (" + current_version.version + ")" %}
+ {% else %}
+ {% set docstitle = current_version.name %}
+ {% endif %}
+
+ {% set docstitle = project + " " + docstitle %}
+{% endif %}
diff --git a/docs/_templates/sidebar/navigation.html b/docs/_templates/sidebar/navigation.html
new file mode 100644
index 00000000..02239887
--- /dev/null
+++ b/docs/_templates/sidebar/navigation.html
@@ -0,0 +1,46 @@
+<div class="sidebar-tree">
+ {{ furo_navigation_tree }}
+
+ {# Include a version navigation menu in the side bar #}
+ {% if versions %}
+ <ul>
+ <li class="toctree-l1 has-children {{ "current-page" if pagename == "versions" }}">
+ {# The following block is taken from furo's generated sidebar dropdown #}
+ <a class="reference internal" href="{{ pathto("versions") }}">Versions</a>
+ <input {{ "checked" if pagename == "versions" }} class="toctree-checkbox" id="toctree-checkbox-versions" name="toctree-checkbox-versions" role="switch" type="checkbox">
+ <label for="toctree-checkbox-versions">
+ <div class="visually-hidden">Toggle child pages in navigation</div>
+ <i class="icon"><svg><use href="#svg-arrow-right"></use></svg></i>
+ </label>
+ {# End copied block #}
+
+ <ul>
+ {% for version in versions | reverse %}
+ <li class="toctree-l2 {{ "current-page" if version == current_version }}">
+ <a class="version_link reference internal" href="{{ version.url }}">{{ version.name }}</a>
+ </li>
+ {% endfor %}
+
+ <script>
+ // Make sure we keep any hyperlinked resources when switching version
+ function updateHash() {
+ for (let tag of document.getElementsByClassName("version_link")) {
+ // Extract the original URL
+ let destination = tag.getAttribute("href");
+ if (destination.indexOf("#") !== -1) {
+ destination = destination.slice(0, destination.indexOf("#"));
+ }
+
+ // Update the url with the current hash
+ tag.setAttribute("href", destination + document.location.hash);
+ }
+ }
+
+ updateHash();
+ addEventListener("hashchange", _ => { updateHash() });
+ </script>
+ </ul>
+ </li>
+ </ul>
+ {% endif %}
+</div>
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 00000000..e666a266
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1,125 @@
+.. See docs for details on formatting your entries
+ https://releases.readthedocs.io/en/latest/concepts.html
+
+Changelog
+=========
+
+- :support:`79` Add `sphinx-multiversion <https://pypi.org/project/sphinx-multiversion/>`_ to make available older doc versions.
+- :support:`79` Restore on-site changelog.
+
+
+- :release:`7.1.0 <24th May 2022>`
+- :feature:`78` Bump Discord.py to :literal-url:`4cbe8f5 <https://github.com/Rapptz/discord.py/tree/4cbe8f58e16f6a76371ce45a69e0832130d6d24f>`:
+
+ - This fixes a bug with permission resolution when dealing with timed out members.
+
+
+- :release:`7.0.0 <10th May 2022>`
+- :bug:`75 major` Capture all characters up to a whitespace in the Discord Invite regex.
+- :breaking:`75` Discord invite regex no longer returns a URL safe result, refer to documentation for safely handling it.
+
+
+- :release:`6.4.0 <26th April 2022>`
+- :feature:`72` Bump discord.py to :literal-url:`5a06fa5 <https://github.com/Rapptz/discord.py/tree/5a06fa5f3e28d2b7191722e1a84c541560008aea>`:
+
+ - Notably, one of the commits in this bump dynamically extends the timeout of ``Guild.chunk()`` based on the number or members, so it should actually work on our guild now.
+
+
+- :release:`6.3.2 <25th April 2022>`
+- :bug:`69` Actually use ``statsd_url`` when it gets passed to ``BotBase``.
+
+
+- :release:`6.3.1 <21st April 2022>`
+- :bug:`68` Correct version number in pyproject.toml
+
+
+- :release:`6.3.0 <21st April 2022>`
+- :feature:`-` (Committed directly to main) Don't load modules starting with ``_``
+
+
+- :release:`6.2.0 <21st April 2022>`
+- :feature:`66` Load each cog in it's own task to avoid a single cog crashing entire load process.
+
+
+- :release:`6.1.0 <20th April 2022>`
+- :feature:`65` Add ``unqualify`` to the ``botcore.utils`` namespace for use in bots that manipulate extensions.
+
+
+- :release:`6.0.0 <19th April 2022>`
+- :breaking:`64` Bump discord.py to :literal-url:`987235d <https://github.com/Rapptz/discord.py/tree/987235d5649e7c2b1a927637bab6547244ecb2cf>`:
+
+ - This reverts a change to help command behaviour that broke our custom pagination
+ - This also adds basic forum channel support to discord.py
+
+
+- :release:`5.0.4 <18th April 2022>` 63
+
+ ..
+ Feature 63 Needs to be explicitly included above because it was improperly released within a bugfix version
+ instead of a minor release
+
+- :feature:`63` Allow passing an ``api_client`` to ``BotBase.__init__`` to specify the ``botcore.site_api.APIClient`` instance to use.
+
+
+- :release:`5.0.3 <18th April 2022>`
+- :bug:`61` Reconnect to redis session on setup if it is closed.
+
+
+- :release:`5.0.2 <5th April 2022>`
+- :bug:`56` Create a dummy ``AsyncstatsdClient`` before connecting to real url, in case a connection cannot be made on init.
+- :bug:`56` Move the creation of the ``asyncio.Event``, ``BotBase._guild_available`` to within the setup hook, to avoid a deprecation notice.
+
+
+- :release:`5.0.1 <2nd April 2022>`
+- :bug:`54` Move creation of BotBase's ``aiohttp.AsyncResolver`` to the async setup hook, to avoid deprecation notice
+
+
+- :release:`5.0.0 <2nd April 2022>`
+- :breaking:`42` Remove public extensions util.
+- :feature:`42` Add ``BotBase``, a ``discord.ext.commands.Bot`` sub-class, which abstracts a lot of logic shared between our bots.
+- :feature:`42` Add async statsd client.
+- :support:`42` Bump Discord.py to latest alpha commit.
+
+
+- :release:`4.0.0 <14th March 2022>`
+- :breaking:`39` Migrate back to Discord.py 2.0.
+
+
+- :release:`3.0.1 <5th March 2022>`
+- :bug:`37` Setup log tracing when ``botcore.utils.logging`` is imported so that it can be used within botcore functions.
+
+
+- :release:`3.0.0 <3rd March 2022>`
+- :breaking:`35` Move ``apply_monkey_patches()`` directly to `botcore.utils` namespace.
+
+
+- :release:`2.1.0 <24th February 2022>`
+- :feature:`34` Port the Site API wrapper from the bot repo.
+
+
+- :release:`2.0.0 <22nd February 2022>`
+- :breaking:`35` Moved regex to ``botcore.utils`` namespace
+- :breaking:`32` Migrate from discord.py 2.0a0 to disnake.
+- :feature:`32` Add common monkey patches.
+- :feature:`29` Port many common utilities from our bots:
+
+ - caching
+ - channel
+ - extensions
+ - loggers
+ - members
+ - scheduling
+- :support:`2` Added intersphinx to docs.
+
+
+- :release:`1.2.0 <9th January 2022>`
+- :feature:`12` Code block detection regex.
+
+
+- :release:`1.1.0 <2nd December 2021>`
+- :support:`2` Autogenerated docs.
+- :feature:`2` Regex utility.
+
+
+- :release:`1.0.0 <17th November 2021>`
+- :feature:`1` Core package, poetry, and linting CI.
diff --git a/docs/conf.py b/docs/conf.py
index 8459f3fb..9aaa8eff 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -3,13 +3,18 @@
import functools
import os.path
+import shutil
import sys
from pathlib import Path
import git
+import releases
+import sphinx.util.logging
import tomli
from sphinx.application import Sphinx
+logger = sphinx.util.logging.getLogger(__name__)
+
# Handle the path not being set correctly in actions.
sys.path.insert(0, os.path.abspath('..'))
@@ -21,10 +26,9 @@ project = "Bot Core"
copyright = "2021, Python Discord"
author = "Python Discord"
-PROJECT_ROOT = Path(__file__).parent.parent
REPO_LINK = "https://github.com/python-discord/bot-core"
-SOURCE_FILE_LINK = f"{REPO_LINK}/blob/{git.Repo(PROJECT_ROOT).commit().hexsha}"
+PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT.absolute()))
# The full version, including alpha/beta/rc tags
@@ -46,12 +50,14 @@ extensions = [
"sphinx.ext.todo",
"sphinx.ext.napoleon",
"sphinx_autodoc_typehints",
+ "releases",
"sphinx.ext.linkcode",
"sphinx.ext.githubpages",
+ "sphinx_multiversion",
]
# Add any paths that contain templates here, relative to this directory.
-templates_path = ["_templates"]
+templates_path = ["_templates", "pages"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@@ -76,18 +82,24 @@ html_theme_options = {
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
+
+# Html files under pages/ are rendered separately and added to the final build
+html_additional_pages = {
+ file.removesuffix(".html"): file
+ for file in utils.get_recursive_file_uris(Path("pages"), "*.html")
+}
+
html_title = f"{project} v{version}"
html_short_title = project
html_logo = "https://raw.githubusercontent.com/python-discord/branding/main/logos/logo_full/logo_full.min.svg"
html_favicon = html_logo
-html_css_files = [
- "index.css",
- "logo.css",
-]
+static = Path("_static")
+html_css_files = utils.get_recursive_file_uris(static, "*.css")
+html_js_files = utils.get_recursive_file_uris(static, "*.js")
-utils.cleanup()
+utils.build_api_doc()
def skip(*args) -> bool:
@@ -102,9 +114,32 @@ def skip(*args) -> bool:
return would_skip
+def post_build(_: Sphinx, exception: Exception) -> None:
+ """Clean up and process files after the build has finished."""
+ if exception:
+ # Don't accidentally supress exceptions
+ raise exception from None
+
+ build_folder = PROJECT_ROOT / "docs" / "build"
+ main_build = build_folder / "main"
+
+ if main_build.exists() and not (build_folder / "index.html").exists():
+ # We don't have an index in the root folder, add a redirect
+ shutil.copy((main_build / "index_redirect.html"), (build_folder / "index.html"))
+ shutil.copytree((main_build / "_static"), (build_folder / "_static"), dirs_exist_ok=True)
+ (build_folder / ".nojekyll").touch(exist_ok=True)
+
+
def setup(app: Sphinx) -> None:
"""Add extra hook-based autodoc configuration."""
app.connect("autodoc-skip-member", skip)
+ app.connect("build-finished", post_build)
+ app.add_role("literal-url", utils.emphasized_url)
+
+ # Add a `breaking` role to the changelog
+ releases.ISSUE_TYPES["breaking"] = "F50F10" # This is the hex for a light red color
+ releases.reorder_release_entries = utils.reorder_release_entries
+ app.add_role("breaking", releases.issues_role)
ignored_modules = [
@@ -147,4 +182,40 @@ intersphinx_mapping = {
# -- Options for the linkcode extension --------------------------------------
-linkcode_resolve = functools.partial(utils.linkcode_resolve, SOURCE_FILE_LINK)
+linkcode_resolve = functools.partial(utils.linkcode_resolve, REPO_LINK)
+
+
+# -- Options for releases extension ------------------------------------------
+releases_github_path = REPO_LINK.removeprefix("https://github.com/")
+releases_release_uri = f"{REPO_LINK}/releases/tag/v%s"
+
+
+def _releases_setup(app: Sphinx) -> dict:
+ """Wrap the default setup of releases to declare it as parallel-read safe."""
+ _original_releases_setup(app)
+ return {"parallel_read_safe": True}
+
+
+_original_releases_setup = releases.setup
+releases.setup = _releases_setup
+
+
+# -- Options for the multiversion extension ----------------------------------
+# Filter out older versions, and don't build branches other than main
+# unless `BUILD_DOCS_FOR_HEAD` env variable is True.
+smv_remote_whitelist = "origin"
+smv_latest_version = "main"
+
+smv_branch_whitelist = "main"
+if os.getenv("BUILD_DOCS_FOR_HEAD", "False").lower() == "true":
+ if not (branch := os.getenv("BRANCH_NAME")):
+ try:
+ branch = git.Repo(PROJECT_ROOT).active_branch.name
+ except git.InvalidGitRepositoryError:
+ pass
+
+ if branch:
+ logger.info(f"Adding branch {branch} to build whitelist.")
+ smv_branch_whitelist = f"main|{branch}"
+
+smv_tag_whitelist = r"v(?!([0-6]\.)|(7\.[0-1]\.0))" # Don't include any versions prior to v7.1.1
diff --git a/docs/index.rst b/docs/index.rst
index 98dcd611..0a375b90 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,8 +15,9 @@ Reference
.. toctree::
:caption: Other:
+ :hidden:
- Changelog <https://github.com/python-discord/bot-core/blob/main/CHANGELOG.md>
+ changelog
Extras
@@ -25,4 +26,4 @@ Extras
* :ref:`genindex`
* :ref:`search`
* :repo-file:`Information <docs/README.md>`
-* :repo-file:`Changelog <CHANGELOG.md>`
+* :doc:`changelog`
diff --git a/docs/pages/index_redirect.html b/docs/pages/index_redirect.html
new file mode 100644
index 00000000..3745c62c
--- /dev/null
+++ b/docs/pages/index_redirect.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+{% set pagename = master_doc %}
+
+{# Redirect users to the actual docs page #}
+{% block site_meta %}
+ {# Make sure this is loaded as early as possible #}
+ <script>window.location.replace("./main/index.html")</script>
+ {{ super() }}
+{% endblock %}
+
+{# Show some text in-case the redirection fails #}
+{% block body %}
+ {{ super() }}
+ <div style="display: flex; text-align: center; justify-content: center; align-items: center; height: 100%;">
+ <h2><a href="./main/index.html">
+ Please click here if you were not redirected to the latest build.
+ </a></h2>
+ </div>
+{% endblock %}
diff --git a/docs/pages/versions.html b/docs/pages/versions.html
new file mode 100644
index 00000000..f4564dbf
--- /dev/null
+++ b/docs/pages/versions.html
@@ -0,0 +1,23 @@
+{% extends "page.html" %}
+{% set title = "Versions" %}
+
+{% block content -%}
+ {% if versions %}
+ <h1>Versions</h1>
+ <dl>
+ <dt>Documentation is available for the following versions:</dt>
+ <dd><ul>
+ {# List all avaialble versions #}
+ {% for version in versions | reverse %}
+ <li>
+ <a class="reference internal" href="{{ version.url }}">{{ version.name }}</a>
+ {{ "(current)" if version == current_version }}
+ {{ "- latest" if version == latest_version }}
+ </li>
+ {% endfor %}
+ </ul></dd>
+ </dl>
+ {% else %}
+ <h1>No version information available!</h1>
+ {% endif %}
+{%- endblock %}
diff --git a/docs/utils.py b/docs/utils.py
index 9116c130..bb8074ba 100644
--- a/docs/utils.py
+++ b/docs/utils.py
@@ -1,15 +1,31 @@
"""Utilities used in generating docs."""
import ast
-import importlib
+import importlib.util
import inspect
+import os
+import subprocess
import typing
from pathlib import Path
-PROJECT_ROOT = Path(__file__).parent.parent
+import docutils.nodes
+import docutils.parsers.rst.states
+import git
+import releases
+import sphinx.util.logging
+logger = sphinx.util.logging.getLogger(__name__)
-def linkcode_resolve(source_url: str, domain: str, info: dict[str, str]) -> typing.Optional[str]:
+
+def get_build_root() -> Path:
+ """Get the project root folder for the current build."""
+ root = Path.cwd()
+ if root.name == "docs":
+ root = root.parent
+ return root
+
+
+def linkcode_resolve(repo_link: str, domain: str, info: dict[str, str]) -> typing.Optional[str]:
"""
Function called by linkcode to get the URL for a given resource.
@@ -21,7 +37,25 @@ def linkcode_resolve(source_url: str, domain: str, info: dict[str, str]) -> typi
symbol_name = info["fullname"]
- module = importlib.import_module(info["module"])
+ build_root = get_build_root()
+
+ # Import the package to find files
+ origin = build_root / info["module"].replace(".", "/")
+ search_locations = []
+
+ if origin.is_dir():
+ search_locations.append(origin.absolute().as_posix())
+ origin = origin / "__init__.py"
+ else:
+ origin = Path(origin.absolute().as_posix() + ".py")
+ if not origin.exists():
+ raise Exception(f"Could not find `{info['module']}` as a package or file.")
+
+ # We can't use a normal import (importlib.import_module), because the module can conflict with another copy
+ # in multiversion builds. We load the module from the file location instead
+ spec = importlib.util.spec_from_file_location(info["module"], origin, submodule_search_locations=search_locations)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
symbol = [module]
for name in symbol_name.split("."):
@@ -58,9 +92,15 @@ def linkcode_resolve(source_url: str, domain: str, info: dict[str, str]) -> typi
start += offset
end += offset
- file = Path(inspect.getfile(module)).relative_to(PROJECT_ROOT).as_posix()
+ file = Path(inspect.getfile(module)).relative_to(build_root).as_posix()
+
+ try:
+ sha = git.Repo(build_root).commit().hexsha
+ except git.InvalidGitRepositoryError:
+ # We are building a historical version, no git data available
+ sha = build_root.name
- url = f"{source_url}/{file}#L{start}"
+ url = f"{repo_link}/blob/{sha}/{file}#L{start}"
if end != start:
url += f"-L{end}"
@@ -71,7 +111,7 @@ def cleanup() -> None:
"""Remove unneeded autogenerated doc files, and clean up others."""
included = __get_included()
- for file in (PROJECT_ROOT / "docs" / "output").iterdir():
+ for file in (get_build_root() / "docs" / "output").iterdir():
if file.name in ("botcore.rst", "botcore.exts.rst", "botcore.utils.rst") and file.name in included:
content = file.read_text(encoding="utf-8").splitlines(keepends=True)
@@ -92,7 +132,6 @@ def cleanup() -> None:
else:
# These are files that have not been explicitly included in the docs via __all__
- print("Deleted file", file.name)
file.unlink()
continue
@@ -101,6 +140,24 @@ def cleanup() -> None:
file.write_text(content, encoding="utf-8")
+def build_api_doc() -> None:
+ """Generate auto-module directives using apidoc."""
+ cmd = os.getenv("APIDOC_COMMAND") or "sphinx-apidoc -o docs/output botcore -feM"
+ cmd = cmd.split()
+
+ build_root = get_build_root()
+ output_folder = build_root / cmd[cmd.index("-o") + 1]
+
+ if output_folder.exists():
+ logger.info(f"Skipping api-doc for {output_folder.as_posix()} as it already exists.")
+ return
+
+ result = subprocess.run(cmd, cwd=build_root, stdout=subprocess.PIPE, check=True, env=os.environ)
+ logger.debug("api-doc Output:\n" + result.stdout.decode(encoding="utf-8") + "\n")
+
+ cleanup()
+
+
def __get_included() -> set[str]:
"""Get a list of files that should be included in the final build."""
@@ -108,7 +165,7 @@ def __get_included() -> set[str]:
try:
module = importlib.import_module(module_name)
except ModuleNotFoundError:
- return {}
+ return set()
_modules = {module.__name__ + ".rst"}
if hasattr(module, "__all__"):
@@ -118,3 +175,47 @@ def __get_included() -> set[str]:
return _modules
return get_all_from_module("botcore")
+
+
+def reorder_release_entries(release_list: list[releases.Release]) -> None:
+ """
+ Sort `releases` based on `release.type`.
+
+ This is meant to be used as an override for `releases.reorder_release_entries` to support
+ custom types.
+ """
+ order = {"breaking": 0, "feature": 1, "bug": 2, "support": 3}
+ for release in release_list:
+ release["entries"].sort(key=lambda entry: order[entry.type])
+
+
+def emphasized_url(
+ name: str, rawtext: str, text: str, lineno: int, inliner: docutils.parsers.rst.states.Inliner, *__
+) -> tuple[list, list]:
+ """
+ Sphinx role to add hyperlinked literals.
+
+ ReST: :literal-url:`Google <https://google.com>`
+ Markdown equivalent: [`Google`](https://google.com)
+
+ Refer to https://docutils.sourceforge.io/docs/howto/rst-roles.html for details on the input and output.
+ """
+ arguments = text.rsplit(maxsplit=1)
+ if len(arguments) != 2:
+ message = inliner.reporter.error(
+ f"`{name}` expects a message and a URL, formatted as: :{name}:`message <url>`",
+ line=lineno
+ )
+ problem = inliner.problematic(text, rawtext, message)
+ return [problem], [message]
+
+ message, url = arguments
+ url: str = url[1:-1] # Remove the angled brackets off the start and end
+
+ literal = docutils.nodes.literal(rawtext, message)
+ return [docutils.nodes.reference(rawtext, "", literal, refuri=url)], []
+
+
+def get_recursive_file_uris(folder: Path, match_pattern: str) -> list[str]:
+ """Get the URI of any file relative to folder which matches the `match_pattern` regex."""
+ return [file.relative_to(folder).as_posix() for file in folder.rglob(match_pattern)]
diff --git a/poetry.lock b/poetry.lock
index a6b2612e..18c91773 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -235,22 +235,21 @@ testing = ["pre-commit"]
[[package]]
name = "fakeredis"
-version = "1.7.5"
+version = "1.8"
description = "Fake implementation of redis API for testing purposes."
category = "main"
optional = true
-python-versions = ">=3.7"
+python-versions = ">=3.7,<4.0"
[package.dependencies]
-lupa = {version = "*", optional = true, markers = "extra == \"lua\""}
-packaging = "*"
+lupa = {version = ">=1.13,<2.0", optional = true, markers = "extra == \"lua\""}
redis = "<=4.3.1"
-six = ">=1.12"
-sortedcontainers = "*"
+six = ">=1.16.0,<2.0.0"
+sortedcontainers = ">=2.4.0,<3.0.0"
[package.extras]
-aioredis = ["aioredis"]
-lua = ["lupa"]
+aioredis = ["aioredis (>=2.0.1,<3.0.0)"]
+lua = ["lupa (>=1.13,<2.0)"]
[[package]]
name = "filelock"
@@ -769,6 +768,18 @@ hiredis = ["hiredis (>=1.0.0)"]
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"]
[[package]]
+name = "releases"
+version = "1.6.3"
+description = "A Sphinx extension for changelog manipulation"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+semantic-version = "<2.7"
+sphinx = ">=1.3"
+
+[[package]]
name = "requests"
version = "2.27.1"
description = "Python HTTP for Humans."
@@ -787,6 +798,14 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
+name = "semantic-version"
+version = "2.6.0"
+description = "A library implementing the 'SemVer' scheme."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
@@ -874,6 +893,17 @@ testing = ["covdefaults (>=2.2)", "coverage (>=6.3)", "diff-cover (>=6.4)", "npt
type_comments = ["typed-ast (>=1.5.2)"]
[[package]]
+name = "sphinx-multiversion"
+version = "0.2.4"
+description = "Add support for multiple versions to sphinx"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+sphinx = ">=2.1"
+
+[[package]]
name = "sphinxcontrib-applehelp"
version = "1.0.2"
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
@@ -1059,7 +1089,7 @@ async-rediscache = ["async-rediscache"]
[metadata]
lock-version = "1.1"
python-versions = "3.9.*"
-content-hash = "437a2dc7523efe2e6031432a5935451611ba427a9314bb0287f1a9cbb4466d77"
+content-hash = "c59b21365534625a127a8bfaffd41a704714c3241817f4d71ad86b68602cf930"
[metadata.files]
aiohttp = [
@@ -1249,8 +1279,8 @@ execnet = [
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
]
fakeredis = [
- {file = "fakeredis-1.7.5-py3-none-any.whl", hash = "sha256:c4ca2be686e7e7637756ccc7dcad8472a5e4866b065431107d7a4b7a250d4e6f"},
- {file = "fakeredis-1.7.5.tar.gz", hash = "sha256:49375c630981dd4045d9a92e2709fcd4476c91f927e0228493eefa625e705133"},
+ {file = "fakeredis-1.8-py3-none-any.whl", hash = "sha256:65dcd78c0cd29d17daccce9f58698f6ab61ad7a404eab373fcad2b76fe8db03d"},
+ {file = "fakeredis-1.8.tar.gz", hash = "sha256:cbf8d74ae06672d40b2fa88b9ee4f1d6efd56b06b2e7f0be2c639647f00643f1"},
]
filelock = [
{file = "filelock-3.7.0-py3-none-any.whl", hash = "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6"},
@@ -1712,10 +1742,18 @@ redis = [
{file = "redis-4.3.1-py3-none-any.whl", hash = "sha256:84316970995a7adb907a56754d2b92d88fc2d252963dc5ac34c88f0f1a22c25d"},
{file = "redis-4.3.1.tar.gz", hash = "sha256:94b617b4cd296e94991146f66fc5559756fbefe9493604f0312e4d3298ac63e9"},
]
+releases = [
+ {file = "releases-1.6.3-py2.py3-none-any.whl", hash = "sha256:cb3435ba372a6807433800fbe473760cfa781171513f670f3c4b76983ac80f18"},
+ {file = "releases-1.6.3.tar.gz", hash = "sha256:555ae4c97a671a420281c1c782e9236be25157b449fdf20b4c4b293fe93db2f1"},
+]
requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
]
+semantic-version = [
+ {file = "semantic_version-2.6.0-py3-none-any.whl", hash = "sha256:2d06ab7372034bcb8b54f2205370f4aa0643c133b7e6dbd129c5200b83ab394b"},
+ {file = "semantic_version-2.6.0.tar.gz", hash = "sha256:2a4328680073e9b243667b201119772aefc5fc63ae32398d6afafff07c4f54c0"},
+]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
@@ -1744,6 +1782,10 @@ sphinx-autodoc-typehints = [
{file = "sphinx_autodoc_typehints-1.18.1-py3-none-any.whl", hash = "sha256:f8f5bb7c13a9a71537dc2be2eb3b9e28a9711e2454df63587005eacf6fbac453"},
{file = "sphinx_autodoc_typehints-1.18.1.tar.gz", hash = "sha256:07631c5f0c6641e5ba27143494aefc657e029bed3982138d659250e617f6f929"},
]
+sphinx-multiversion = [
+ {file = "sphinx-multiversion-0.2.4.tar.gz", hash = "sha256:5cd1ca9ecb5eed63cb8d6ce5e9c438ca13af4fa98e7eb6f376be541dd4990bcb"},
+ {file = "sphinx_multiversion-0.2.4-py3-none-any.whl", hash = "sha256:dec29f2a5890ad68157a790112edc0eb63140e70f9df0a363743c6258fbeb478"},
+]
sphinxcontrib-applehelp = [
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
diff --git a/pyproject.toml b/pyproject.toml
index 6c5d3c37..4290417f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "bot-core"
-version = "7.1.0"
+version = "7.1.1"
description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community."
authors = ["Python Discord <[email protected]>"]
license = "MIT"
@@ -43,14 +43,13 @@ Sphinx = "4.5.0"
GitPython = "3.1.27"
sphinx-autodoc-typehints = "1.18.1"
furo = "2022.4.7"
-
+releases = "1.6.3"
+sphinx-multiversion = "0.2.4"
[tool.taskipy.tasks]
lint = "pre-commit run --all-files"
precommit = "pre-commit install"
-apidoc = "sphinx-apidoc -o docs/output botcore -feM"
-builddoc = "sphinx-build -nW -j auto -b html docs docs/build"
-docs = "task apidoc && task builddoc"
+docs = "sphinx-build -nW -j auto -b html docs docs/build"
test = "pytest -n 8 --ff"
retest = "pytest -n 8 --lf"