diff --git a/.copier-answers.yml b/.copier-answers.yml
index e3b6e940..62b9e3c8 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
-_commit: 0.11.3
+_commit: 0.15.7
_src_path: gh:pawamoy/copier-pdm.git
author_email: pawamoy@pm.me
author_fullname: Timothée Mazzucotelli
@@ -8,13 +8,13 @@ copyright_date: '2019'
copyright_holder: Timothée Mazzucotelli
copyright_holder_email: pawamoy@pm.me
copyright_license: ISC License
+insiders: true
project_description: Automatic documentation from sources, for MkDocs.
project_name: mkdocstrings
-python_package_command_line_name: mkdocstrings
+python_package_command_line_name: ''
python_package_distribution_name: mkdocstrings
python_package_import_name: mkdocstrings
repository_name: mkdocstrings
repository_namespace: mkdocstrings
repository_provider: github.com
-use_precommit: false
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index cf5764f4..01e293ac 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,4 +1,4 @@
-github:
-- pawamoy
+github: pawamoy
+ko_fi: pawamoy
custom:
- https://www.paypal.me/pawamoy
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ef882e3c..2b5ed158 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,10 +32,10 @@ jobs:
python-version: "3.8"
- name: Resolving dependencies
- run: pdm lock -v
+ run: pdm lock -v --no-cross-platform -G ci-quality
- name: Install dependencies
- run: pdm install -G duty -G docs -G quality -G typing -G security
+ run: pdm install -G ci-quality
- name: Check if the documentation builds correctly
run: pdm run duty check-docs
@@ -49,9 +49,13 @@ jobs:
- name: Check for vulnerabilities in dependencies
run: pdm run duty check-dependencies
+ - name: Check for breaking changes in the API
+ run: pdm run duty check-api
+
tests:
strategy:
+ max-parallel: 4
matrix:
os:
- ubuntu-latest
@@ -76,10 +80,10 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Resolving dependencies
- run: pdm lock -v
+ run: pdm lock -v --no-cross-platform -G ci-tests
- name: Install dependencies
- run: pdm install --no-editable -G duty -G tests -G docs
+ run: pdm install --no-editable -G ci-tests
- name: Run the test suite
run: pdm run duty test
diff --git a/.github/workflows/dists.yml b/.github/workflows/dists.yml
new file mode 100644
index 00000000..41833b63
--- /dev/null
+++ b/.github/workflows/dists.yml
@@ -0,0 +1,30 @@
+name: dists
+
+on: push
+permissions:
+ contents: write
+
+jobs:
+ build:
+ name: Build dists
+ runs-on: ubuntu-latest
+ if: github.repository_owner == 'pawamoy-insiders'
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Setup Python
+ uses: actions/setup-python@v3
+ - name: Install build
+ run: python -m pip install build
+ - name: Build dists
+ run: python -m build
+ - name: Upload dists artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: mkdocstrings-insiders
+ path: ./dist/*
+ - name: Create release and upload assets
+ uses: softprops/action-gh-release@v1
+ if: startsWith(github.ref, 'refs/tags/')
+ with:
+ files: ./dist/*
diff --git a/.gitignore b/.gitignore
index f6a13b06..97dc958b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,7 +11,10 @@ pip-wheel-metadata/
.python-version
site/
pdm.lock
-.pdm.toml
+pdm.toml
+.pdm-plugins/
+.pdm-python
__pypackages__/
.mypy_cache/
.venv/
+.cache/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ea7668c..bbeb8ce6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [0.22.0](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.22.0) - 2023-05-25
+
+[Compare with 0.21.2](https://github.com/mkdocstrings/mkdocstrings/compare/0.21.2...0.22.0)
+
+### Features
+
+- Allow extensions to add templates ([cf0af05](https://github.com/mkdocstrings/mkdocstrings/commit/cf0af059eb89240eba0437de417c124389e2f20e) by Timothée Mazzucotelli). [PR #569](https://github.com/mkdocstrings/mkdocstrings/pull/569)
+
+### Code Refactoring
+
+- Report inventory loading errors ([2c05d78](https://github.com/mkdocstrings/mkdocstrings/commit/2c05d7854b87251e26c1a2e1810b85702ff110f3) by Timothée Mazzucotelli). Co-authored-by: Oleh Prypin
+
## [0.21.2](https://github.com/mkdocstrings/mkdocstrings/releases/tag/0.21.2) - 2023-04-06
[Compare with 0.21.1](https://github.com/mkdocstrings/mkdocstrings/compare/0.21.1...0.21.2)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d9f8e89f..4ecc0fa0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -61,7 +61,7 @@ As usual:
1. run `make check` to check everything (fix any warning)
1. run `make test` to run the tests (fix any issue)
1. if you updated the documentation or the project dependencies:
- 1. run `make docs-serve`
+ 1. run `make docs`
1. go to http://localhost:8000 and check that everything looks good
1. follow our [commit message convention](#commit-message-convention)
diff --git a/Makefile b/Makefile
index b034ffff..b71d86ce 100644
--- a/Makefile
+++ b/Makefile
@@ -5,19 +5,18 @@ DUTY = $(shell [ -n "${VIRTUAL_ENV}" ] || echo pdm run) duty
args = $(foreach a,$($(subst -,_,$1)_args),$(if $(value $a),$a="$($a)"))
check_quality_args = files
-docs_serve_args = host port
+docs_args = host port
release_args = version
test_args = match
BASIC_DUTIES = \
changelog \
+ check-api \
check-dependencies \
clean \
coverage \
docs \
docs-deploy \
- docs-regen \
- docs-serve \
format \
release
@@ -33,7 +32,7 @@ help:
.PHONY: lock
lock:
- @pdm lock
+ @pdm lock --dev
.PHONY: setup
setup:
@@ -42,7 +41,7 @@ setup:
.PHONY: check
check:
@pdm multirun duty check-quality check-types check-docs
- @$(DUTY) check-dependencies
+ @$(DUTY) check-dependencies check-api
.PHONY: $(BASIC_DUTIES)
$(BASIC_DUTIES):
diff --git a/config/coverage.ini b/config/coverage.ini
index fde9d55a..1bcf0935 100644
--- a/config/coverage.ini
+++ b/config/coverage.ini
@@ -17,6 +17,7 @@ omit =
src/*/__init__.py
src/*/__main__.py
tests/__init__.py
+ tests/fixtures/*.py
exclude_lines =
pragma: no cover
if TYPE_CHECKING
diff --git a/config/ruff.toml b/config/ruff.toml
index 62f45f64..53824875 100644
--- a/config/ruff.toml
+++ b/config/ruff.toml
@@ -63,6 +63,7 @@ ignore = [
"D105", # Missing docstring in magic method
"D417", # Missing argument description in the docstring
"E501", # Line too long
+ "ERA001", # Commented out code
"G004", # Logging statement uses f-string
"INP001", # File is part of an implicit namespace package
"PLR0911", # Too many return statements
diff --git a/docs/.overrides/main.html b/docs/.overrides/main.html
new file mode 100644
index 00000000..cf8adeb7
--- /dev/null
+++ b/docs/.overrides/main.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+
+{% block announce %}
+
+ Sponsorship
+ is now available!
+
+ {% include ".icons/octicons/heart-fill-16.svg" %}
+ —
+
+ For updates follow @pawamoy on
+
+
+ {% include ".icons/fontawesome/brands/mastodon.svg" %}
+
+ Fosstodon
+
+{% endblock %}
diff --git a/docs/css/insiders.css b/docs/css/insiders.css
new file mode 100644
index 00000000..81dbd756
--- /dev/null
+++ b/docs/css/insiders.css
@@ -0,0 +1,98 @@
+@keyframes heart {
+
+ 0%,
+ 40%,
+ 80%,
+ 100% {
+ transform: scale(1);
+ }
+
+ 20%,
+ 60% {
+ transform: scale(1.15);
+ }
+}
+
+@keyframes vibrate {
+ 0%, 2%, 4%, 6%, 8%, 10%, 12%, 14%, 16%, 18% {
+ -webkit-transform: translate3d(-2px, 0, 0);
+ transform: translate3d(-2px, 0, 0);
+ }
+ 1%, 3%, 5%, 7%, 9%, 11%, 13%, 15%, 17%, 19% {
+ -webkit-transform: translate3d(2px, 0, 0);
+ transform: translate3d(2px, 0, 0);
+ }
+ 20%, 100% {
+ -webkit-transform: translate3d(0, 0, 0);
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+.heart {
+ color: #e91e63;
+}
+
+.pulse {
+ animation: heart 1000ms infinite;
+}
+
+.vibrate {
+ animation: vibrate 2000ms infinite;
+}
+
+.new-feature svg {
+ fill: var(--md-accent-fg-color) !important;
+}
+
+a.insiders {
+ color: #e91e63;
+}
+
+.sponsorship-list {
+ width: 100%;
+}
+
+.sponsorship-item {
+ float: left;
+ border-radius: 100%;
+ display: block;
+ height: 1.6rem;
+ margin: .2rem;
+ overflow: hidden;
+ width: 1.6rem;
+}
+
+.sponsorship-item:focus, .sponsorship-item:hover {
+ transform: scale(1.1);
+}
+
+.sponsorship-item img {
+ filter: grayscale(100%) opacity(75%);
+ height: auto;
+ width: 100%;
+}
+
+.sponsorship-item:focus img, .sponsorship-item:hover img {
+ filter: grayscale(0);
+}
+
+.sponsorship-item.private {
+ background: var(--md-default-fg-color--lightest);
+ color: var(--md-default-fg-color);
+ font-size: .6rem;
+ font-weight: 700;
+ line-height: 1.6rem;
+ text-align: center;
+}
+
+.mastodon {
+ color: #897ff8;
+ border-radius: 100%;
+ box-shadow: inset 0 0 0 .05rem currentcolor;
+ display: inline-block;
+ height: 1.2rem !important;
+ padding: .25rem;
+ transition: all .25s;
+ vertical-align: bottom !important;
+ width: 1.2rem;
+}
\ No newline at end of file
diff --git a/docs/css/mkdocstrings.css b/docs/css/mkdocstrings.css
index f269d975..af758331 100644
--- a/docs/css/mkdocstrings.css
+++ b/docs/css/mkdocstrings.css
@@ -4,7 +4,30 @@ div.doc-contents:not(.first) {
border-left: .05rem solid var(--md-typeset-table-color);
}
+/* Mark external links as such. */
+a.autorefs-external::after {
+ /* https://primer.style/octicons/arrow-up-right-24 */
+ background-image: url('data:image/svg+xml, ');
+ content: ' ';
+
+ display: inline-block;
+ vertical-align: middle;
+ position: relative;
+ bottom: 0.1em;
+ margin-left: 0.2em;
+ margin-right: 0.1em;
+
+ height: 0.7em;
+ width: 0.7em;
+ border-radius: 100%;
+ background-color: var(--md-typeset-a-color);
+}
+
+a.autorefs-external:hover::after {
+ background-color: var(--md-accent-fg-color);
+}
+
/* Avoid breaking parameters name, etc. in table cells. */
td code {
word-break: normal !important;
-}
+}
\ No newline at end of file
diff --git a/docs/insiders/changelog.md b/docs/insiders/changelog.md
new file mode 100644
index 00000000..0f438566
--- /dev/null
+++ b/docs/insiders/changelog.md
@@ -0,0 +1,3 @@
+# Changelog
+
+## mkdocstrings Insiders
diff --git a/docs/insiders/goals.yml b/docs/insiders/goals.yml
new file mode 100644
index 00000000..896b9240
--- /dev/null
+++ b/docs/insiders/goals.yml
@@ -0,0 +1 @@
+goals: {}
diff --git a/docs/insiders/index.md b/docs/insiders/index.md
new file mode 100644
index 00000000..0f5b0de9
--- /dev/null
+++ b/docs/insiders/index.md
@@ -0,0 +1,222 @@
+# Insiders
+
+*mkdocstrings* follows the **sponsorware** release strategy, which means
+that new features are first exclusively released to sponsors as part of
+[Insiders][insiders]. Read on to learn [what sponsorships achieve][sponsorship],
+[how to become a sponsor][sponsors] to get access to Insiders,
+and [what's in it for you][features]!
+
+## What is Insiders?
+
+*mkdocstrings Insiders* is a private fork of *mkdocstrings*, hosted as
+a private GitHub repository. Almost[^1] [all new features][features]
+are developed as part of this fork, which means that they are immediately
+available to all eligible sponsors, as they are made collaborators of this
+repository.
+
+ [^1]:
+ In general, every new feature is first exclusively released to sponsors, but
+ sometimes upstream dependencies enhance
+ existing features that must be supported by *mkdocstrings*.
+
+Every feature is tied to a [funding goal][funding] in monthly subscriptions. When a
+funding goal is hit, the features that are tied to it are merged back into
+*mkdocstrings* and released for general availability, making them available
+to all users. Bugfixes are always released in tandem.
+
+Sponsorships start as low as [**$10 a month**][sponsors].[^2]
+
+ [^2]:
+ Note that $10 a month is the minimum amount to become eligible for
+ Insiders. While GitHub Sponsors also allows to sponsor lower amounts or
+ one-time amounts, those can't be granted access to Insiders due to
+ technical reasons. Such contributions are still very much welcome as
+ they help ensuring the project's sustainability.
+
+
+## What sponsorships achieve
+
+Sponsorships make this project sustainable, as they buy the maintainers of this
+project time – a very scarce resource – which is spent on the development of new
+features, bug fixing, stability improvement, issue triage and general support.
+The biggest bottleneck in Open Source is time.[^3]
+
+ [^3]:
+ Making an Open Source project sustainable is exceptionally hard: maintainers
+ burn out, projects are abandoned. That's not great and very unpredictable.
+ The sponsorware model ensures that if you decide to use *mkdocstrings*,
+ you can be sure that bugs are fixed quickly and new features are added
+ regularly.
+
+
+
+## What's in it for me?
+
+```python exec="1" session="insiders"
+data_source = [
+ "docs/insiders/goals.yml",
+ ("mkdocstrings-python", "https://mkdocstrings.github.io/python/", "insiders/goals.yml"),
+]
+```
+
+```python exec="1" session="insiders"
+--8<-- "scripts/insiders.py"
+```
+
+```python exec="1" session="insiders"
+print(f"""The moment you become a sponsor , you'll get **immediate
+access to {len(completed_features)} additional features** that you can start using right away, and
+which are currently exclusively available to sponsors:\n""")
+
+for feature in completed_features:
+ feature.render(badge=True)
+```
+
+## How to become a sponsor
+
+Thanks for your interest in sponsoring! In order to become an eligible sponsor
+with your GitHub account, visit [pawamoy's sponsor profile][github sponsor profile],
+and complete a sponsorship of **$10 a month or more**.
+You can use your individual or organization GitHub account for sponsoring.
+
+**Important**: If you're sponsoring **[@pawamoy][github sponsor profile]**
+through a GitHub organization, please send a short email
+to pawamoy@pm.me with the name of your
+organization and the GitHub account of the individual
+that should be added as a collaborator.[^4]
+
+You can cancel your sponsorship anytime.[^5]
+
+ [^4]:
+ It's currently not possible to grant access to each member of an
+ organization, as GitHub only allows for adding users. Thus, after
+ sponsoring, please send an email to pawamoy@pm.me, stating which
+ account should become a collaborator of the Insiders repository. We're
+ working on a solution which will make access to organizations much simpler.
+ To ensure that access is not tied to a particular individual GitHub account,
+ create a bot account (i.e. a GitHub account that is not tied to a specific
+ individual), and use this account for the sponsoring. After being added to
+ the list of collaborators, the bot account can create a private fork of the
+ private Insiders GitHub repository, and grant access to all members of the
+ organizations.
+
+ [^5]:
+ If you cancel your sponsorship, GitHub schedules a cancellation request
+ which will become effective at the end of the billing cycle. This means
+ that even though you cancel your sponsorship, you will keep your access to
+ Insiders as long as your cancellation isn't effective. All charges are
+ processed by GitHub through Stripe. As we don't receive any information
+ regarding your payment, and GitHub doesn't offer refunds, sponsorships are
+ non-refundable.
+
+```python exec="1" session="insiders"
+print_join_sponsors_button()
+```
+
+
+
+```python exec="1" session="insiders"
+print_sponsors()
+```
+
+
+
+
+ If you sponsor publicly, you're automatically added here with a link to
+ your profile and avatar to show your support for *mkdocstrings*.
+ Alternatively, if you wish to keep your sponsorship private, you'll be a
+ silent +1. You can select visibility during checkout and change it
+ afterwards.
+
+
+## Funding
+
+```python exec="1" session="insiders"
+print(f"Current funding is at **$ {human_readable_amount(current_funding)} a month**.")
+```
+
+### Goals
+
+The following section lists all funding goals. Each goal contains a list of
+features prefixed with a checkmark symbol, denoting whether a feature is
+:octicons-check-circle-fill-24:{ style="color: #00e676" } already available or
+:octicons-check-circle-fill-24:{ style="color: var(--md-default-fg-color--lightest)" } planned,
+but not yet implemented. When the funding goal is hit,
+the features are released for general availability.
+
+```python exec="1" session="insiders" idprefix=""
+for goal in goals.values():
+ if not goal.complete:
+ goal.render()
+```
+
+
+
+## Frequently asked questions
+
+### Compatibility
+
+> We're building an open source project and want to allow outside collaborators
+to use *mkdocstrings* locally without having access to Insiders.
+Is this still possible?
+
+Yes. Insiders is compatible with *mkdocstrings*. Almost all new features
+and configuration options are either backward-compatible or implemented behind
+feature flags. Most Insiders features enhance the overall experience,
+though while these features add value for the users of your project, they
+shouldn't be necessary for previewing when making changes to content.
+
+### Payment
+
+> We don't want to pay for sponsorship every month. Are there any other options?
+
+Yes. You can sponsor on a yearly basis by [switching your GitHub account to a
+yearly billing cycle][billing cycle]. If for some reason you cannot do that, you
+could also create a dedicated GitHub account with a yearly billing cycle, which
+you only use for sponsoring (some sponsors already do that).
+
+If you have any problems or further questions, please reach out to pawamoy@pm.me.
+
+### Terms
+
+> Are we allowed to use Insiders under the same terms and conditions as
+*mkdocstrings*?
+
+Yes. Whether you're an individual or a company, you may use *mkdocstrings
+Insiders* precisely under the same terms as *mkdocstrings*, which are given
+by the [ISC License][license]. However, we kindly ask you to respect our
+**fair use policy**:
+
+- Please **don't distribute the source code** of Insiders. You may freely use
+ it for public, private or commercial projects, privately fork or mirror it,
+ but please don't make the source code public, as it would counteract the
+ sponsorware strategy.
+
+- If you cancel your subscription, you're automatically removed as a
+ collaborator and will miss out on all future updates of Insiders. However, you
+ may **use the latest version** that's available to you **as long as you like**.
+ Just remember that [GitHub deletes private forks][private forks].
+
+[insiders]: #what-is-insiders
+[sponsorship]: #what-sponsorships-achieve
+[sponsors]: #how-to-become-a-sponsor
+[features]: #whats-in-it-for-me
+[funding]: #funding
+[goals completed]: #goals-completed
+[github sponsor profile]: https://github.com/sponsors/pawamoy
+[billing cycle]: https://docs.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/changing-the-duration-of-your-billing-cycle
+[license]: ../license/
+[private forks]: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/removing-a-collaborator-from-a-personal-repository
diff --git a/docs/insiders/installation.md b/docs/insiders/installation.md
new file mode 100644
index 00000000..9852182b
--- /dev/null
+++ b/docs/insiders/installation.md
@@ -0,0 +1,190 @@
+---
+title: Getting started with Insiders
+---
+
+# Getting started with Insiders
+
+*mkdocstrings Insiders* is a compatible drop-in replacement for *mkdocstrings*,
+and can be installed similarly using `pip` or `git`.
+Note that in order to access the Insiders repository,
+you need to [become an eligible sponsor] of @pawamoy on GitHub.
+
+ [become an eligible sponsor]: index.md#how-to-become-a-sponsor
+
+## Installation
+
+### with pip (ssh/https)
+
+*mkdocstrings Insiders* can be installed with `pip` [using SSH][using ssh]:
+
+```bash
+pip install git+ssh://git@github.com/pawamoy-insiders/mkdocstrings.git
+```
+
+ [using ssh]: https://docs.github.com/en/authentication/connecting-to-github-with-ssh
+
+Or using HTTPS:
+
+```bash
+pip install git+https://${GH_TOKEN}@github.com/pawamoy-insiders/mkdocstrings.git
+```
+
+>? NOTE: **How to get a GitHub personal access token**
+> The `GH_TOKEN` environment variable is a GitHub token.
+> It can be obtained by creating a [personal access token] for
+> your GitHub account. It will give you access to the Insiders repository,
+> programmatically, from the command line or GitHub Actions workflows:
+>
+> 1. Go to https://github.com/settings/tokens
+> 2. Click on [Generate a new token]
+> 3. Enter a name and select the [`repo`][scopes] scope
+> 4. Generate the token and store it in a safe place
+>
+> [personal access token]: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token
+> [Generate a new token]: https://github.com/settings/tokens/new
+> [scopes]: https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes
+>
+> Note that the personal access
+> token must be kept secret at all times, as it allows the owner to access your
+> private repositories.
+
+### with pip (self-hosted)
+
+Self-hosting the Insiders package makes it possible to depend on *mkdocstrings* normally,
+while transparently downloading and installing the Insiders version locally.
+It means that you can specify your dependencies normally, and your contributors without access
+to Insiders will get the public version, while you get the Insiders version on your machine.
+
+WARNING: **Limitation**
+With this method, there is no way to force the installation of an Insiders version
+rather than a public version. If there is a public version that is more recent
+than your self-hosted Insiders version, the public version will take precedence.
+Remember to regularly update your self-hosted versions by uploading latest distributions.
+
+You can build the distributions for Insiders yourself, by cloning the repository
+and using [build] to build the distributions,
+or you can download them from our [GitHub Releases].
+You can upload these distributions to a private PyPI-like registry
+([Artifactory], [Google Cloud], [pypiserver], etc.)
+with [Twine]:
+
+ [build]: https://pypi.org/project/build/
+ [Artifactory]: https://jfrog.com/help/r/jfrog-artifactory-documentation/pypi-repositories
+ [Google Cloud]: https://cloud.google.com/artifact-registry/docs/python
+ [pypiserver]: https://pypi.org/project/pypiserver/
+ [Github Releases]: https://github.com/pawamoy-insiders/mkdocstrings/releases
+ [Twine]: https://pypi.org/project/twine/
+
+```bash
+# download distributions in ~/dists, then upload with:
+twine upload --repository-url https://your-private-index.com ~/dists/*
+```
+
+You might also need to provide a username and password/token to authenticate against the registry.
+Please check [Twine's documentation][twine docs].
+
+ [twine docs]: https://twine.readthedocs.io/en/stable/
+
+You can then configure pip (or other tools) to look for packages into your package index.
+For example, with pip:
+
+```bash
+pip config set global.extra-index-url https://your-private-index.com/simple
+```
+
+Note that the URL might differ depending on whether your are uploading a package (with Twine)
+or installing a package (with pip), and depending on the registry you are using (Artifactory, Google Cloud, etc.).
+Please check the documentation of your registry to learn how to configure your environment.
+
+**We kindly ask that you do not upload the distributions to public registries,
+as it is against our [Terms of use](../#terms).**
+
+>? TIP: **Full example with `pypiserver`**
+> In this example we use [pypiserver] to serve a local PyPI index.
+>
+> ```bash
+> pip install --user pypiserver
+> # or pipx install pypiserver
+>
+> # create a packages directory
+> mkdir -p ~/.local/pypiserver/packages
+>
+> # run the pypi server without authentication
+> pypi-server run -p 8080 -a . -P . ~/.local/pypiserver/packages &
+> ```
+>
+> We can configure the credentials to access the server in [`~/.pypirc`][pypirc]:
+>
+> [pypirc]: https://packaging.python.org/en/latest/specifications/pypirc/
+>
+> ```ini title=".pypirc"
+> [distutils]
+> index-servers =
+> local
+>
+> [local]
+> repository: http://localhost:8080
+> username:
+> password:
+> ```
+>
+> We then clone the Insiders repository, build distributions and upload them to our local server:
+>
+> ```bash
+> # clone the repository
+> git clone git@github.com:pawamoy-insiders/mkdocstrings
+> cd mkdocstrings
+>
+> # install build
+> pip install --user build
+> # or pipx install build
+>
+> # checkout latest tag
+> git checkout $(git describe --tags --abbrev=0)
+>
+> # build the distributions
+> pyproject-build
+>
+> # upload them to our local server
+> twine upload -r local dist/* --skip-existing
+> ```
+>
+> Finally, we configure pip, and for example [PDM][pdm], to use our local index to find packages:
+>
+> ```bash
+> pip config set global.extra-index-url http://localhost:8080/simple
+> pdm config pypi.extra.url http://localhost:8080/simple
+> ```
+>
+> [pdm]: https://pdm.fming.dev/latest/
+>
+> Now when running `pip install mkdocstrings`,
+> or resolving dependencies with PDM,
+> both tools will look into our local index and find the Insiders version.
+> **Remember to update your local index regularly!**
+
+### with git
+
+Of course, you can use *mkdocstrings Insiders* directly from `git`:
+
+```
+git clone git@github.com:pawamoy-insiders/mkdocstrings
+```
+
+When cloning from `git`, the package must be installed:
+
+```
+pip install -e mkdocstrings
+```
+
+## Upgrading
+
+When upgrading Insiders, you should always check the version of *mkdocstrings*
+which makes up the first part of the version qualifier. For example, a version like
+`8.x.x.4.x.x` means that Insiders `4.x.x` is currently based on `8.x.x`.
+
+If the major version increased, it's a good idea to consult the [changelog]
+and go through the steps to ensure your configuration is up to date and
+all necessary changes have been made.
+
+ [changelog]: ./changelog.md
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 1b360ffb..4d2b074e 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -48,7 +48,7 @@ when it should be `[Section][pytkdocs.parsers.docstrings.Section]`.
## Some objects are not rendered (they do not appear in the generated docs)
- Make sure the configuration options of the handler are correct.
- Check the documentation for [Handlers](handlers/overview.md) to see the available options for each handler.
+ Check the documentation for [Handlers](usage/handlers.md) to see the available options for each handler.
- Also make sure your documentation in your source code is formatted correctly.
For Python code, check the [supported docstring styles](https://mkdocstrings.github.io/python/usage/#supported-docstrings-styles) page.
- Re-run the Mkdocs command with `-v`, and carefully read any traceback.
@@ -116,13 +116,13 @@ use this workaround.
Please open an ticket on the [bugtracker][bugtracker] with a detailed
explanation and screenshots of the bad-looking parts.
-Note that you can always [customize the look](theming.md) of *mkdocstrings* blocks -- through both HTML and CSS.
+Note that you can always [customize the look](usage/theming.md) of *mkdocstrings* blocks -- through both HTML and CSS.
## Warning: could not find cross-reference target
TIP: **New in version 0.15.**
Cross-linking used to include any Markdown heading, but now it's only for *mkdocstrings* identifiers by default.
-See [Cross-references to any Markdown heading](usage.md#cross-references-to-any-markdown-heading) to opt back in.
+See [Cross-references to any Markdown heading](usage/index.md#cross-references-to-any-markdown-heading) to opt back in.
Make sure the referenced object is properly rendered: verify your configuration options.
diff --git a/docs/handlers/overview.md b/docs/usage/handlers.md
similarity index 80%
rename from docs/handlers/overview.md
rename to docs/usage/handlers.md
index 779f7c81..e381090e 100644
--- a/docs/handlers/overview.md
+++ b/docs/usage/handlers.md
@@ -5,8 +5,8 @@ A handler is what makes it possible to collect and render documentation for a pa
## Available handlers
- Crystal
+- Python
- Python (Legacy)
-- Python (Experimental)
## About the Python handlers
@@ -192,7 +192,7 @@ is written for inspiration.
### Templates
Your handler's implementation should normally be backed by templates, which go
-to the directory `mkdocstrings_handlers/custom_handler/templates/some_theme`.
+to the directory `mkdocstrings_handlers/custom_handler/templates/some_theme`
(`custom_handler` here should be replaced with the actual name of your handler,
and `some_theme` should be the name of an actual MkDocs theme that you support,
e.g. `material`).
@@ -258,3 +258,79 @@ plugins:
some_config_option: "b"
other_config_option: 1
```
+
+## Handler extensions
+
+*mkdocstrings* provides a way for third-party packages
+to extend or alter the behavior of handlers.
+For example, an extension of the Python handler
+could add specific support for another Python library.
+
+NOTE: This feature is intended for developers.
+If you are a user and want to customize how objects are rendered,
+see [Theming / Customization](../theming/#customization).
+
+Such extensions can register additional template folders
+that will be used when rendering collected data.
+Extensions are responsible for synchronizing
+with the handler itself so that it uses the additional templates.
+
+An extension is a Python package
+that defines an entry-point for a specific handler:
+
+```toml title="pyproject.toml"
+[project.entry-points."mkdocstrings.python.templates"] # (1)!
+extension-name = "extension_package:get_templates_path" # (2)!
+```
+
+1. Replace `python` by the name of the handler you want to add templates to.
+1. Replace `extension-name` by any name you want,
+ and replace `extension_package:get_templates_path`
+ by the actual module path and function name in your package.
+
+This entry-point assumes that the extension provides
+a `get_templates_path` function directly under the `extension_package` package:
+
+```tree
+pyproject.toml
+extension_package/
+ __init__.py
+ templates/
+```
+
+```python title="extension_package/__init__.py"
+from pathlib import Path
+
+
+def get_templates_path() -> Path:
+ return Path(__file__).parent / "templates"
+```
+
+This function doesn't accept any argument
+and returns the path ([`pathlib.Path`][] or [`str`][])
+to a directory containing templates.
+The directory must contain one subfolder
+for each supported theme, even if empty
+(see "fallback theme" in [custom handlers templates](#templates_1)).
+For example:
+
+```tree
+pyproject.toml
+extension_package/
+ __init__.py
+ templates/
+ material/
+ readthedocs/
+ mkdocs/
+```
+
+*mkdocstrings* will add the folders corresponding to the user-selected theme,
+and to the handler's defined fallback theme, as usual.
+
+The names of the extension templates
+must not overlap with the handler's original templates.
+
+The extension is then responsible, in collaboration with its target handler,
+for mutating the collected data in order to instruct the handler
+to use one of the extension template when rendering particular objects.
+See each handler's docs to see if they support extensions, and how.
diff --git a/docs/usage.md b/docs/usage/index.md
similarity index 99%
rename from docs/usage.md
rename to docs/usage/index.md
index 0969164c..3318c053 100644
--- a/docs/usage.md
+++ b/docs/usage/index.md
@@ -142,7 +142,7 @@ The above is equivalent to:
```
Some handlers accept additional global configuration.
-Check the documentation for your handler of interest in [Handlers](handlers/overview.md).
+Check the documentation for your handler of interest in [Handlers](handlers.md).
## Cross-references
diff --git a/docs/theming.md b/docs/usage/theming.md
similarity index 100%
rename from docs/theming.md
rename to docs/usage/theming.md
diff --git a/duties.py b/duties.py
index 01d632ae..77410349 100644
--- a/duties.py
+++ b/duties.py
@@ -5,18 +5,23 @@
import os
import sys
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
from duty import duty
from duty.callables import black, blacken_docs, coverage, lazy, mkdocs, mypy, pytest, ruff, safety
+if sys.version_info < (3, 8):
+ from importlib_metadata import version as pkgversion
+else:
+ from importlib.metadata import version as pkgversion
+
+
if TYPE_CHECKING:
from duty.context import Context
PY_SRC_PATHS = (Path(_) for _ in ("src", "tests", "duties.py", "scripts"))
PY_SRC_LIST = tuple(str(_) for _ in PY_SRC_PATHS)
PY_SRC = " ".join(PY_SRC_LIST)
-TESTING = os.environ.get("TESTING", "0") in {"1", "true"}
CI = os.environ.get("CI", "0") in {"1", "true", "yes", ""}
WINDOWS = os.name == "nt"
PTY = not WINDOWS and not CI
@@ -30,6 +35,34 @@ def pyprefix(title: str) -> str: # noqa: D103
return title
+def merge(d1: Any, d2: Any) -> Any: # noqa: D103
+ basic_types = (int, float, str, bool, complex)
+ if isinstance(d1, dict) and isinstance(d2, dict):
+ for key, value in d2.items():
+ if key in d1:
+ if isinstance(d1[key], basic_types):
+ d1[key] = value
+ else:
+ d1[key] = merge(d1[key], value)
+ else:
+ d1[key] = value
+ return d1
+ if isinstance(d1, list) and isinstance(d2, list):
+ return d1 + d2
+ return d2
+
+
+def mkdocs_config() -> str: # noqa: D103
+ from mkdocs import utils
+
+ # patch YAML loader to merge arrays
+ utils.merge = merge
+
+ if "+insiders" in pkgversion("mkdocs-material"):
+ return "mkdocs.insiders.yml"
+ return "mkdocs.yml"
+
+
@duty
def changelog(ctx: Context) -> None:
"""Update the changelog in-place with latest commits.
@@ -39,7 +72,7 @@ def changelog(ctx: Context) -> None:
"""
from git_changelog.cli import build_and_render
- git_changelog = lazy("git_changelog")(build_and_render)
+ git_changelog = lazy(build_and_render, name="git_changelog")
ctx.run(
git_changelog(
repository=".",
@@ -48,7 +81,7 @@ def changelog(ctx: Context) -> None:
template="keepachangelog",
parse_trailers=True,
parse_refs=False,
- sections=("build", "deps", "feat", "fix", "refactor"),
+ sections=["build", "deps", "feat", "fix", "refactor"],
bump_latest=True,
in_place=True,
),
@@ -56,7 +89,7 @@ def changelog(ctx: Context) -> None:
)
-@duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies"])
+@duty(pre=["check_quality", "check_types", "check_docs", "check_dependencies", "check-api"])
def check(ctx: Context) -> None: # noqa: ARG001
"""Check it all!
@@ -104,7 +137,7 @@ def check_docs(ctx: Context) -> None:
"""
Path("htmlcov").mkdir(parents=True, exist_ok=True)
Path("htmlcov/index.html").touch(exist_ok=True)
- ctx.run(mkdocs.build(strict=True), title=pyprefix("Building documentation"))
+ ctx.run(mkdocs.build(strict=True, config_file=mkdocs_config()), title=pyprefix("Building documentation"))
@duty
@@ -121,6 +154,23 @@ def check_types(ctx: Context) -> None:
)
+@duty
+def check_api(ctx: Context) -> None:
+ """Check for API breaking changes.
+
+ Parameters:
+ ctx: The context instance (passed automatically).
+ """
+ from griffe.cli import check as g_check
+
+ griffe_check = lazy(g_check, name="griffe.check")
+ ctx.run(
+ griffe_check("mkdocstrings", search_paths=["src"]),
+ title="Checking for API breaking changes",
+ nofail=True,
+ )
+
+
@duty(silent=True)
def clean(ctx: Context) -> None:
"""Delete temporary files.
@@ -142,17 +192,7 @@ def clean(ctx: Context) -> None:
@duty
-def docs(ctx: Context) -> None:
- """Build the documentation locally.
-
- Parameters:
- ctx: The context instance (passed automatically).
- """
- ctx.run(mkdocs.build, title="Building documentation")
-
-
-@duty
-def docs_serve(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None:
+def docs(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None:
"""Serve the documentation (localhost:8000).
Parameters:
@@ -161,7 +201,7 @@ def docs_serve(ctx: Context, host: str = "127.0.0.1", port: int = 8000) -> None:
port: The port to serve the docs on.
"""
ctx.run(
- mkdocs.serve(dev_addr=f"{host}:{port}"),
+ mkdocs.serve(dev_addr=f"{host}:{port}", config_file=mkdocs_config()),
title="Serving documentation",
capture=False,
)
@@ -174,8 +214,23 @@ def docs_deploy(ctx: Context) -> None:
Parameters:
ctx: The context instance (passed automatically).
"""
- ctx.run("git remote add org-pages git@github.com:mkdocstrings/mkdocstrings.github.io", silent=True, nofail=True)
- ctx.run(mkdocs.gh_deploy(remote_name="org-pages", force=True), title="Deploying documentation")
+ os.environ["DEPLOY"] = "true"
+ config_file = mkdocs_config()
+ if config_file == "mkdocs.yml":
+ ctx.run(lambda: False, title="Not deploying docs without Material for MkDocs Insiders!")
+ origin = ctx.run("git config --get remote.origin.url", silent=True)
+ if "pawamoy-insiders/mkdocstrings" in origin:
+ ctx.run("git remote add org-pages git@github.com:mkdocstrings/mkdocstrings.github.io", silent=True, nofail=True)
+ ctx.run(
+ mkdocs.gh_deploy(config_file=config_file, remote_name="org-pages", force=True),
+ title="Deploying documentation",
+ )
+ else:
+ ctx.run(
+ lambda: False,
+ title="Not deploying docs from public repository (do that from insiders instead!)",
+ nofail=True,
+ )
@duty
@@ -197,7 +252,7 @@ def format(ctx: Context) -> None:
)
-@duty
+@duty(post=["docs-deploy"])
def release(ctx: Context, version: str) -> None:
"""Release a new Python package.
@@ -205,15 +260,19 @@ def release(ctx: Context, version: str) -> None:
ctx: The context instance (passed automatically).
version: The new version number to use.
"""
+ origin = ctx.run("git config --get remote.origin.url", silent=True)
+ if "pawamoy-insiders/mkdocstrings" in origin:
+ ctx.run(
+ lambda: False,
+ title="Not releasing from insiders repository (do that from public repo instead!)",
+ )
ctx.run("git add pyproject.toml CHANGELOG.md", title="Staging files", pty=PTY)
ctx.run(["git", "commit", "-m", f"chore: Prepare release {version}"], title="Committing changes", pty=PTY)
ctx.run(f"git tag {version}", title="Tagging commit", pty=PTY)
- if not TESTING:
- ctx.run("git push", title="Pushing commits", pty=False)
- ctx.run("git push --tags", title="Pushing tags", pty=False)
- ctx.run("pdm build", title="Building dist/wheel", pty=PTY)
- ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY)
- docs_deploy.run()
+ ctx.run("git push", title="Pushing commits", pty=False)
+ ctx.run("git push --tags", title="Pushing tags", pty=False)
+ ctx.run("pdm build", title="Building dist/wheel", pty=PTY)
+ ctx.run("twine upload --skip-existing dist/*", title="Publishing version", pty=PTY)
@duty(silent=True, aliases=["coverage"])
diff --git a/mkdocs.insiders.yml b/mkdocs.insiders.yml
new file mode 100644
index 00000000..a93edcc3
--- /dev/null
+++ b/mkdocs.insiders.yml
@@ -0,0 +1,5 @@
+INHERIT: mkdocs.yml
+
+# waiting for https://github.com/squidfunk/mkdocs-material/issues/5446
+# plugins:
+# - typeset
diff --git a/mkdocs.yml b/mkdocs.yml
index 191526d4..f544387c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -5,7 +5,8 @@ repo_url: "https://github.com/mkdocstrings/mkdocstrings"
edit_uri: "blob/master/docs/"
repo_name: "mkdocstrings/mkdocstrings"
site_dir: "site"
-watch: [README.md, CONTRIBUTING.md, CHANGELOG.md, src/mkdocstrings]
+watch: [mkdocs.yml, README.md, CONTRIBUTING.md, CHANGELOG.md, src/mkdocstrings]
+copyright: Copyright © 2019 Timothée Mazzucotelli
nav:
- Home:
@@ -14,34 +15,54 @@ nav:
- Credits: credits.md
- License: license.md
- Usage:
- - usage.md
- - Theming: theming.md
- - Handlers:
- - handlers/overview.md
+ - usage/index.md
+ - Theming: usage/theming.md
+ - Handlers: usage/handlers.md
+ - All handlers:
- Crystal: https://mkdocstrings.github.io/crystal/
+ - Python: https://mkdocstrings.github.io/python/
- Python (Legacy): https://mkdocstrings.github.io/python-legacy/
- - Python (Experimental): https://mkdocstrings.github.io/python/
- - Recipes: recipes.md
- - Troubleshooting: troubleshooting.md
+ - Guides:
+ - Recipes: recipes.md
+ - Troubleshooting: troubleshooting.md
# defer to gen-files + literate-nav
- Code Reference: reference/
- Development:
- Contributing: contributing.md
- Code of Conduct: code_of_conduct.md
- Coverage report: coverage.md
+- Insiders:
+ - insiders/index.md
+ - Getting started:
+ - Installation: insiders/installation.md
+ - Changelog: insiders/changelog.md
- Author's website: https://pawamoy.github.io/
theme:
name: material
logo: logo.svg
+ custom_dir: docs/.overrides
features:
+ - announce.dismiss
+ - content.action.edit
+ - content.action.view
- content.code.annotate
+ - content.code.copy
+ - content.tooltips
+ - navigation.footer
+ - navigation.indexes
+ - navigation.sections
- navigation.tabs
- navigation.tabs.sticky
- navigation.top
- search.highlight
- search.suggest
+ - toc.follow
palette:
+ - media: "(prefers-color-scheme)"
+ toggle:
+ icon: material/brightness-auto
+ name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: teal
@@ -55,25 +76,34 @@ theme:
accent: lime
toggle:
icon: material/weather-night
- name: Switch to light mode
+ name: Switch to system preference
extra_css:
- css/style.css
- css/material.css
- css/mkdocstrings.css
+- css/insiders.css
markdown_extensions:
+- attr_list
- admonition
- callouts
+- footnotes
+- pymdownx.emoji:
+ emoji_index: !!python/name:materialx.emoji.twemoji
+ emoji_generator: !!python/name:materialx.emoji.to_svg
- pymdownx.details
-- pymdownx.emoji
- pymdownx.magiclink
- pymdownx.snippets:
check_paths: true
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
-- pymdownx.tasklist
+ slugify: !!python/object/apply:pymdownx.slugs.slugify
+ kwds:
+ case: lower
+- pymdownx.tasklist:
+ custom_checkbox: true
- toc:
permalink: "¤"
@@ -83,11 +113,9 @@ plugins:
- gen-files:
scripts:
- scripts/gen_ref_nav.py
- - scripts/gen_redirects.py
- literate-nav:
nav_file: SUMMARY.txt
- coverage
-- section-index
- mkdocstrings:
handlers:
python:
@@ -100,6 +128,15 @@ plugins:
merge_init_into_class: true
docstring_options:
ignore_init_summary: true
+- git-committers:
+ enabled: !ENV [DEPLOY, false]
+ repository: mkdocstrings/mkdocstrings
+- redirects:
+ redirect_maps:
+ theming.md: usage/theming.md
+ handlers/overview.md: usage/handlers.md
+- minify:
+ minify_html: !ENV [DEPLOY, false]
extra:
social:
@@ -109,3 +146,7 @@ extra:
link: https://fosstodon.org/@pawamoy
- icon: fontawesome/brands/twitter
link: https://twitter.com/pawamoy
+ - icon: fontawesome/brands/gitter
+ link: https://gitter.im/mkdocstrings/community
+ - icon: fontawesome/brands/python
+ link: https://pypi.org/project/mkdocstrings/
diff --git a/pyproject.toml b/pyproject.toml
index 07665246..a36f6d25 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,12 @@
[build-system]
-requires = ["pdm-pep517"]
-build-backend = "pdm.pep517.api"
+requires = ["pdm-backend"]
+build-backend = "pdm.backend"
[project]
name = "mkdocstrings"
description = "Automatic documentation from sources, for MkDocs."
authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}]
-license = "ISC"
+license = {text = "ISC"}
readme = "README.md"
requires-python = ">=3.7"
keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc", "documentation"]
@@ -35,6 +35,7 @@ dependencies = [
"mkdocs>=1.2",
"mkdocs-autorefs>=0.3.1",
"pymdown-extensions>=6.3",
+ "importlib-metadata>=4.6; python_version < '3.10'",
"typing-extensions>=4.1; python_version < '3.10'",
]
@@ -51,13 +52,16 @@ Repository = "https://github.com/mkdocstrings/mkdocstrings"
Issues = "https://github.com/mkdocstrings/mkdocstrings/issues"
Discussions = "https://github.com/mkdocstrings/mkdocstrings/discussions"
Gitter = "https://gitter.im/mkdocstrings/community"
-Funding = "https://github.com/sponsors/mkdocstrings"
+Funding = "https://github.com/sponsors/pawamoy"
[project.entry-points."mkdocs.plugins"]
mkdocstrings = "mkdocstrings.plugin:MkdocstringsPlugin"
[tool.pdm]
version = {source = "scm"}
+plugins = [
+ "pdm-multirun",
+]
[tool.pdm.build]
package-dir = "src"
@@ -65,18 +69,22 @@ includes = ["src/mkdocstrings"]
editable-backend = "editables"
[tool.pdm.dev-dependencies]
-duty = ["duty>=0.8"]
+duty = ["duty>=0.10"]
+ci-quality = ["mkdocstrings[duty,docs,quality,typing,security]"]
+ci-tests = ["mkdocstrings[duty,docs,tests]"]
docs = [
"black>=23.1",
+ "markdown-callouts>=0.2",
+ "markdown-exec>=0.5",
"mkdocs>=1.3",
"mkdocs-coverage>=0.2",
"mkdocs-gen-files>=0.3",
+ "mkdocs-git-committers-plugin-2>=1.1",
"mkdocs-literate-nav>=0.4",
"mkdocs-material>=7.3",
- "mkdocs-section-index>=0.3",
+ "mkdocs-minify-plugin>=0.6.4",
+ "mkdocs-redirects>=1.2.0",
"mkdocstrings-python>=0.5.1",
- "markdown-callouts>=0.2",
- "markdown-exec>=0.5",
"toml>=0.10",
]
maintain = [
@@ -100,7 +108,9 @@ typing = [
"mypy>=0.911",
"types-docutils",
"types-markdown>=3.3",
- "types-pyyaml",
+ "types-pyyaml>=6.0",
"types-toml>=0.10",
]
-security = ["safety>=2"]
+security = [
+ "safety>=2",
+]
diff --git a/scripts/gen_credits.py b/scripts/gen_credits.py
index 7f59f8f9..85ac9041 100644
--- a/scripts/gen_credits.py
+++ b/scripts/gen_credits.py
@@ -53,6 +53,8 @@ def _get_deps(base_deps: Mapping[str, Mapping[str, str]]) -> dict[str, dict[str,
for dep in base_deps:
parsed = regex.match(dep).groupdict() # type: ignore[union-attr]
dep_name = parsed["dist"].lower()
+ if dep_name not in lock_pkgs:
+ continue
deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]}
again = True
@@ -63,7 +65,7 @@ def _get_deps(base_deps: Mapping[str, Mapping[str, str]]) -> dict[str, dict[str,
for pkg_dependency in lock_pkgs[pkg_name].get("dependencies", []):
parsed = regex.match(pkg_dependency).groupdict() # type: ignore[union-attr]
dep_name = parsed["dist"].lower()
- if dep_name not in deps and dep_name != project["name"]:
+ if dep_name in lock_pkgs and dep_name not in deps and dep_name != project["name"]:
deps[dep_name] = {"license": _get_license(dep_name), **parsed, **lock_pkgs[dep_name]}
again = True
@@ -87,7 +89,7 @@ def _render_credits() -> str:
}
template_text = dedent(
"""
- These projects were used to build `{{ project_name }}`. **Thank you!**
+ These projects were used to build *{{ project_name }}*. **Thank you!**
[`python`](https://www.python.org/) |
[`pdm`](https://pdm.fming.dev/) |
diff --git a/scripts/gen_redirects.py b/scripts/gen_redirects.py
deleted file mode 100644
index f35cce9c..00000000
--- a/scripts/gen_redirects.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Generate redirection pages for autorefs reference."""
-
-import mkdocs_gen_files
-
-redirect_map = {
- "reference/autorefs/references.md": "https://mkdocstrings.github.io/autorefs/reference/mkdocs_autorefs/references/",
- "reference/autorefs/plugin.md": "https://mkdocstrings.github.io/autorefs/reference/mkdocs_autorefs/plugin/",
-}
-
-redirect_template = """
-
-Redirecting...
-"""
-
-for page, link in redirect_map.items():
- with mkdocs_gen_files.open(page, "w") as fd:
- print(redirect_template.format(link=link), file=fd)
diff --git a/scripts/insiders.py b/scripts/insiders.py
new file mode 100644
index 00000000..0d23a45a
--- /dev/null
+++ b/scripts/insiders.py
@@ -0,0 +1,216 @@
+"""Functions related to Insiders funding goals."""
+
+from __future__ import annotations
+
+import json
+import logging
+import posixpath
+from dataclasses import dataclass
+from datetime import date, datetime, timedelta
+from itertools import chain
+from pathlib import Path
+from textwrap import dedent
+from typing import Iterable, cast
+from urllib.error import HTTPError
+from urllib.parse import urljoin
+from urllib.request import urlopen
+
+import yaml
+
+logger = logging.getLogger(f"mkdocs.logs.{__name__}")
+
+
+def human_readable_amount(amount: int) -> str: # noqa: D103
+ str_amount = str(amount)
+ if len(str_amount) >= 4: # noqa: PLR2004
+ return f"{str_amount[:len(str_amount)-3]},{str_amount[-3:]}"
+ return str_amount
+
+
+@dataclass
+class Project:
+ """Class representing an Insiders project."""
+
+ name: str
+ url: str
+
+
+@dataclass
+class Feature:
+ """Class representing an Insiders feature."""
+
+ name: str
+ ref: str
+ since: date | None
+ project: Project | None
+
+ def url(self, rel_base: str = "..") -> str: # noqa: D102
+ if self.project:
+ rel_base = self.project.url
+ return posixpath.join(rel_base, self.ref.lstrip("/"))
+
+ def render(self, rel_base: str = "..", *, badge: bool = False) -> None: # noqa: D102
+ new = ""
+ if badge:
+ recent = self.since and date.today() - self.since <= timedelta(days=60) # noqa: DTZ011
+ if recent:
+ ft_date = self.since.strftime("%B %d, %Y") # type: ignore[union-attr]
+ new = f' :material-alert-decagram:{{ .new-feature .vibrate title="Added on {ft_date}" }}'
+ project = f"[{self.project.name}]({self.project.url}) — " if self.project else ""
+ print(f"- [{'x' if self.since else ' '}] {project}[{self.name}]({self.url(rel_base)}){new}")
+
+
+@dataclass
+class Goal:
+ """Class representing an Insiders goal."""
+
+ name: str
+ amount: int
+ features: list[Feature]
+ complete: bool = False
+
+ @property
+ def human_readable_amount(self) -> str: # noqa: D102
+ return human_readable_amount(self.amount)
+
+ def render(self, rel_base: str = "..") -> None: # noqa: D102
+ print(f"#### $ {self.human_readable_amount} — {self.name}\n")
+ for feature in self.features:
+ feature.render(rel_base)
+ print("")
+
+
+def load_goals(data: str, funding: int = 0, project: Project | None = None) -> dict[int, Goal]:
+ """Load goals from JSON data.
+
+ Parameters:
+ data: The JSON data.
+ funding: The current total funding, per month.
+ origin: The origin of the data (URL).
+
+ Returns:
+ A dictionaries of goals, keys being their target monthly amount.
+ """
+ goals_data = yaml.safe_load(data)["goals"]
+ return {
+ amount: Goal(
+ name=goal_data["name"],
+ amount=amount,
+ complete=funding >= amount,
+ features=[
+ Feature(
+ name=feature_data["name"],
+ ref=feature_data["ref"],
+ since=feature_data["since"]
+ and datetime.strptime(feature_data["since"], "%Y/%m/%d").date(), # noqa: DTZ007
+ project=project,
+ )
+ for feature_data in goal_data["features"]
+ ],
+ )
+ for amount, goal_data in goals_data.items()
+ }
+
+
+def _load_goals_from_disk(path: str, funding: int = 0) -> dict[int, Goal]:
+ try:
+ data = Path(path).read_text()
+ except OSError as error:
+ raise RuntimeError(f"Could not load data from disk: {path}") from error
+ return load_goals(data, funding)
+
+
+def _load_goals_from_url(source_data: tuple[str, str, str], funding: int = 0) -> dict[int, Goal]:
+ project_name, project_url, data_fragment = source_data
+ data_url = urljoin(project_url, data_fragment)
+ try:
+ with urlopen(data_url) as response: # noqa: S310
+ data = response.read()
+ except HTTPError as error:
+ raise RuntimeError(f"Could not load data from network: {data_url}") from error
+ return load_goals(data, funding, project=Project(name=project_name, url=project_url))
+
+
+def _load_goals(source: str | tuple[str, str, str], funding: int = 0) -> dict[int, Goal]:
+ if isinstance(source, str):
+ return _load_goals_from_disk(source, funding)
+ return _load_goals_from_url(source, funding)
+
+
+def funding_goals(source: str | list[str | tuple[str, str, str]], funding: int = 0) -> dict[int, Goal]:
+ """Load funding goals from a given data source.
+
+ Parameters:
+ source: The data source (local file path or URL).
+ funding: The current total funding, per month.
+
+ Returns:
+ A dictionaries of goals, keys being their target monthly amount.
+ """
+ if isinstance(source, str):
+ return _load_goals_from_disk(source, funding)
+ goals = {}
+ for src in source:
+ source_goals = _load_goals(src)
+ for amount, goal in source_goals.items():
+ if amount not in goals:
+ goals[amount] = goal
+ else:
+ goals[amount].features.extend(goal.features)
+ return goals
+
+
+def feature_list(goals: Iterable[Goal]) -> list[Feature]:
+ """Extract feature list from funding goals.
+
+ Parameters:
+ goals: A list of funding goals.
+
+ Returns:
+ A list of features.
+ """
+ return list(chain.from_iterable(goal.features for goal in goals))
+
+
+def load_json(url: str) -> str | list | dict: # noqa: D103
+ with urlopen(url) as response: # noqa: S310
+ return json.loads(response.read().decode())
+
+
+data_source = globals()["data_source"]
+sponsor_url = "https://github.com/sponsors/pawamoy"
+data_url = "https://raw.githubusercontent.com/pawamoy/sponsors/main"
+numbers: dict[str, int] = load_json(f"{data_url}/numbers.json") # type: ignore[assignment]
+sponsors: list[dict] = load_json(f"{data_url}/sponsors.json") # type: ignore[assignment]
+current_funding = numbers["total"]
+sponsors_count = numbers["count"]
+goals = funding_goals(data_source, funding=current_funding)
+all_features = feature_list(goals.values())
+completed_features = sorted(
+ (ft for ft in all_features if ft.since),
+ key=lambda ft: cast(date, ft.since),
+ reverse=True,
+)
+
+
+def print_join_sponsors_button() -> None: # noqa: D103
+ btn_classes = "{ .md-button .md-button--primary }"
+ print(
+ dedent(
+ f"""
+ [:octicons-heart-fill-24:{{ .pulse }}
+ Join our {sponsors_count} awesome sponsors]({sponsor_url}){btn_classes}
+ """,
+ ),
+ )
+
+
+def print_sponsors() -> None: # noqa: D103
+ private_sponsors_count = sponsors_count - len(sponsors)
+ for sponsor in sponsors:
+ print(
+ f"""""",
+ )
+ if private_sponsors_count:
+ print(f"""""")
diff --git a/scripts/setup.sh b/scripts/setup.sh
index 9e7ab1ff..f6c90de5 100755
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -10,11 +10,11 @@ if ! command -v pdm &>/dev/null; then
pipx install pdm
fi
if ! pdm self list 2>/dev/null | grep -q pdm-multirun; then
- pipx inject pdm pdm-multirun
+ pdm install --plugins
fi
if [ -n "${PYTHON_VERSIONS}" ]; then
- pdm multirun -vi ${PYTHON_VERSIONS// /,} pdm install
+ pdm multirun -vi ${PYTHON_VERSIONS// /,} pdm install --dev
else
- pdm install
+ pdm install --dev
fi
diff --git a/src/mkdocstrings/handlers/base.py b/src/mkdocstrings/handlers/base.py
index 51efd775..b6ecd4fa 100644
--- a/src/mkdocstrings/handlers/base.py
+++ b/src/mkdocstrings/handlers/base.py
@@ -11,6 +11,7 @@
from __future__ import annotations
import importlib
+import sys
import warnings
from contextlib import suppress
from pathlib import Path
@@ -31,6 +32,12 @@
from mkdocstrings.inventory import Inventory
from mkdocstrings.loggers import get_template_logger
+# TODO: remove once support for Python 3.9 is dropped
+if sys.version_info < (3, 10):
+ from importlib_metadata import entry_points
+else:
+ from importlib.metadata import entry_points
+
CollectorItem = Any
@@ -93,12 +100,23 @@ def __init__(self, handler: str, theme: str, custom_templates: str | None = None
self._theme = theme
self._custom_templates = custom_templates
+ # add selected theme templates
themes_dir = self.get_templates_dir(handler)
paths.append(themes_dir / theme)
+ # add extended theme templates
+ extended_templates_dirs = self.get_extended_templates_dirs(handler)
+ for templates_dir in extended_templates_dirs:
+ paths.append(templates_dir / theme)
+
+ # add fallback theme templates
if self.fallback_theme and self.fallback_theme != theme:
paths.append(themes_dir / self.fallback_theme)
+ # add fallback theme of extended templates
+ for templates_dir in extended_templates_dirs:
+ paths.append(templates_dir / self.fallback_theme)
+
for path in paths:
css_path = path / "style.css"
if css_path.is_file():
@@ -179,6 +197,18 @@ def get_templates_dir(self, handler: str) -> Path:
raise FileNotFoundError(f"Can't find 'templates' folder for handler '{handler}'")
+ def get_extended_templates_dirs(self, handler: str) -> list[Path]:
+ """Load template extensions for the given handler, return their templates directories.
+
+ Arguments:
+ handler: The name of the handler to get the extended templates directory of.
+
+ Returns:
+ The extensions templates directories.
+ """
+ discovered_extensions = entry_points(group=f"mkdocstrings.{handler}.templates")
+ return [extension.load()() for extension in discovered_extensions]
+
def get_anchors(self, data: CollectorItem) -> tuple[str, ...] | set[str]:
"""Return the possible identifiers (HTML anchors) for a collected item.
diff --git a/src/mkdocstrings/loggers.py b/src/mkdocstrings/loggers.py
index 5e3f42bb..63502474 100644
--- a/src/mkdocstrings/loggers.py
+++ b/src/mkdocstrings/loggers.py
@@ -7,8 +7,6 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, MutableMapping, Sequence
-from mkdocs.utils import warning_filter
-
try:
from jinja2 import pass_context
except ImportError: # TODO: remove once Jinja2 < 3.1 is dropped
@@ -135,7 +133,6 @@ def get_logger(name: str) -> LoggerAdapter:
A logger configured to work well in MkDocs.
"""
logger = logging.getLogger(f"mkdocs.plugins.{name}")
- logger.addFilter(warning_filter)
return LoggerAdapter(name.split(".", 1)[0], logger)
diff --git a/src/mkdocstrings/plugin.py b/src/mkdocstrings/plugin.py
index 5233cf4f..4c902935 100644
--- a/src/mkdocstrings/plugin.py
+++ b/src/mkdocstrings/plugin.py
@@ -14,7 +14,6 @@
from __future__ import annotations
-import collections
import functools
import gzip
import os
@@ -226,16 +225,17 @@ def on_config(self, config: Config, **kwargs: Any) -> Config: # noqa: ARG002
config["extra_css"].insert(0, self.css_filename) # So that it has lower priority than user files.
- self._inv_futures = []
+ self._inv_futures = {}
if to_import:
inv_loader = futures.ThreadPoolExecutor(4)
for handler_name, import_item in to_import:
+ loader = self.get_handler(handler_name).load_inventory
future = inv_loader.submit(
self._load_inventory, # type: ignore[misc]
- self.get_handler(handler_name).load_inventory,
+ loader,
**import_item,
)
- self._inv_futures.append(future)
+ self._inv_futures[future] = (loader, import_item)
inv_loader.shutdown(wait=False)
if self.config["watch"]:
@@ -286,9 +286,18 @@ def on_env(self, env: Environment, config: Config, *args: Any, **kwargs: Any) ->
if self._inv_futures:
log.debug(f"Waiting for {len(self._inv_futures)} inventory download(s)")
futures.wait(self._inv_futures, timeout=30)
- for page, identifier in collections.ChainMap(*(fut.result() for fut in self._inv_futures)).items():
+ results = {}
+ # Reversed order so that pages from first futures take precedence:
+ for fut in reversed(list(self._inv_futures)):
+ try:
+ results.update(fut.result())
+ except Exception as error: # noqa: BLE001
+ loader, import_item = self._inv_futures[fut]
+ loader_name = loader.__func__.__qualname__
+ log.error(f"Couldn't load inventory {import_item} through {loader_name}: {error}") # noqa: TRY400
+ for page, identifier in results.items():
config["plugins"]["autorefs"].register_url(page, identifier)
- self._inv_futures = []
+ self._inv_futures = {}
def on_post_build(
self,
diff --git a/tests/test_handlers.py b/tests/test_handlers.py
index a0b3be3e..777173e8 100644
--- a/tests/test_handlers.py
+++ b/tests/test_handlers.py
@@ -2,10 +2,18 @@
from __future__ import annotations
+from contextlib import suppress
+from pathlib import Path
+from typing import TYPE_CHECKING
+
import pytest
+from jinja2.exceptions import TemplateNotFound
from markdown import Markdown
-from mkdocstrings.handlers.base import Highlighter
+from mkdocstrings.handlers.base import BaseRenderer, Highlighter
+
+if TYPE_CHECKING:
+ from mkdocstrings.plugin import MkdocstringsPlugin
@pytest.mark.parametrize("extension_name", ["codehilite", "pymdownx.highlight"])
@@ -43,3 +51,53 @@ def test_highlighter_basic(extension_name: str | None, inline: bool) -> None:
actual = hl.highlight("import foo", language="python", inline=inline)
assert "import" in actual
assert "import foo" not in actual # Highlighting has split it up.
+
+
+@pytest.fixture(name="extended_templates")
+def fixture_extended_templates(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: # noqa: D103
+ monkeypatch.setattr(BaseRenderer, "get_extended_templates_dirs", lambda self, handler: [tmp_path])
+ return tmp_path
+
+
+def test_extended_templates(extended_templates: Path, plugin: MkdocstringsPlugin) -> None:
+ """Test the extended templates functionality.
+
+ Parameters:
+ extended_templates: Temporary folder.
+ plugin: Instance of our plugin.
+ """
+ handler = plugin._handlers.get_handler("python") # type: ignore[union-attr]
+
+ # assert mocked method added temp path to loader
+ search_paths = handler.env.loader.searchpath # type: ignore[union-attr]
+ assert any(str(extended_templates) in path for path in search_paths)
+
+ # assert "new" template is not found
+ for path in search_paths:
+ # TODO: use missing_ok=True once support for Python 3.7 is dropped
+ with suppress(FileNotFoundError):
+ Path(path).joinpath("new.html").unlink()
+ with pytest.raises(expected_exception=TemplateNotFound):
+ handler.env.get_template("new.html")
+
+ # check precedence: base theme, base fallback theme, extended theme, extended fallback theme
+ # start with last one and go back up
+ handler.env.cache = None
+
+ extended_fallback_theme = extended_templates.joinpath(handler.fallback_theme)
+ extended_fallback_theme.mkdir()
+ extended_fallback_theme.joinpath("new.html").write_text("extended fallback new")
+ assert handler.env.get_template("new.html").render() == "extended fallback new"
+
+ extended_theme = extended_templates.joinpath("mkdocs")
+ extended_theme.mkdir()
+ extended_theme.joinpath("new.html").write_text("extended new")
+ assert handler.env.get_template("new.html").render() == "extended new"
+
+ base_fallback_theme = Path(search_paths[1])
+ base_fallback_theme.joinpath("new.html").write_text("base fallback new")
+ assert handler.env.get_template("new.html").render() == "base fallback new"
+
+ base_theme = Path(search_paths[0])
+ base_theme.joinpath("new.html").write_text("base new")
+ assert handler.env.get_template("new.html").render() == "base new"