diff --git a/.flake8 b/.flake8
deleted file mode 100644
index e14b09a0543..00000000000
--- a/.flake8
+++ /dev/null
@@ -1,38 +0,0 @@
-[flake8]
-max-line-length = 95
-ignore =
- E116,
- E203,
- E241,
- E251,
- E501,
- E741,
- W503,
- W504,
- I101,
- SIM102,
- SIM103,
- SIM105,
- SIM114,
- SIM115,
- SIM117,
- SIM223,
- SIM401,
- SIM907,
- SIM910,
-exclude =
- .git,
- .tox,
- .venv,
- venv,
- node_modules/*,
- tests/roots/*,
- build/*,
- doc/_build/*,
- sphinx/search/*,
- doc/usage/extensions/example*.py,
-per-file-ignores =
- doc/conf.py:W605
- sphinx/events.py:E704,
- tests/test_extensions/ext_napoleon_pep526_data_google.py:MLL001,
- tests/test_extensions/ext_napoleon_pep526_data_numpy.py:MLL001,
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000000..d0f6ad06464
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,65 @@
+# Binary data types
+*.gif binary
+*.jpg binary
+*.mo binary
+*.pdf binary
+*.png binary
+*.zip binary
+
+# Unix-style line endings
+[attr]unix text eol=lf
+
+*.conf unix
+*.css unix
+*.cls unix
+*.csv unix
+*.dot unix
+*.html unix
+*.inc unix
+*.ini unix
+*.jinja unix
+*.js unix
+*.md unix
+*.mjs unix
+*.py unix
+*.rst unix
+*.sty unix
+*.tex unix
+*.toml unix
+*.txt unix
+*.svg unix
+*.xml unix
+*.yml unix
+
+# CRLF files
+[attr]dos text eol=crlf
+
+*.bat dos
+*.bat.jinja dos
+*.stp dos
+tests/roots/test-pycode/cp_1251_coded.py dos
+
+# Language aware diff headers
+*.c diff=cpp
+*.h diff=cpp
+*.css diff=css
+*.html diff=html
+*.md diff=markdown
+*.py diff=python
+# *.rst diff=reStructuredText
+*.tex diff=tex
+
+# Non UTF-8 encodings
+tests/roots/test-pycode/cp_1251_coded.py working-tree-encoding=windows-1251
+
+# Generated files
+# https://github.com/github/linguist/blob/master/docs/overrides.md
+#
+# To always hide generated files in local diffs, mark them as binary:
+# $ git config diff.generated.binary true
+#
+[attr]generated linguist-generated=true diff=generated
+
+tests/js/fixtures/**/*.js generated
+sphinx/search/minified-js/*.js generated
+sphinx/themes/bizstyle/static/css3-mediaqueries.js generated
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 226532b79bc..b4fdfdf70b9 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -5,5 +5,5 @@ contact_links:
url: https://stackoverflow.com/questions/tagged/python-sphinx
about: For Q&A purpose, please use Stackoverflow with the tag python-sphinx
- name: Discussion
- url: https://groups.google.com/forum/#!forum/sphinx-users
- about: For general discussion, please use sphinx-users mailing list.
+ url: https://github.com/sphinx-doc/sphinx/discussions
+ about: For general discussion, please use GitHub Discussions.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 2b0c121c5e4..5df17861b08 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,19 +1,33 @@
-Subject:
+
-### Feature or Bugfix
-
-- Feature
-- Bugfix
-- Refactoring
-### Purpose
--
--
+## Purpose
-### Detail
--
--
+
+
+## References
+
+
+
+- <...>
+- <...>
+- <...>
diff --git a/.github/workflows/builddoc.yml b/.github/workflows/builddoc.yml
index e3347a7b535..7f8471deecb 100644
--- a/.github/workflows/builddoc.yml
+++ b/.github/workflows/builddoc.yml
@@ -22,6 +22,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -29,11 +31,10 @@ jobs:
- name: Install graphviz
run: sudo apt-get install --no-install-recommends --yes graphviz
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install .[docs]
- name: Render the documentation
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index 4252d8f4338..878b4237bce 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -28,22 +28,21 @@ jobs:
id-token: write # for PyPI trusted publishing
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install build dependencies (pypa/build, twine)
run: |
uv pip install build "twine>=5.1"
- # resolution fails without betterproto and protobuf-specs
- uv pip install "pypi-attestations~=0.0.12" "sigstore-protobuf-specs==0.3.2" "betterproto==2.0.0b6"
- name: Build distribution
run: python -m build
@@ -62,11 +61,15 @@ jobs:
show-summary: "true"
- name: Convert attestations to PEP 740
- # workflow_ref example: sphinx-doc/sphinx/.github/workflows/create-release.yml@refs/heads/master
run: >
- python utils/convert_attestations.py
- "${{ steps.attest.outputs.bundle-path }}"
- "https://github.com/${{ github.workflow_ref }}"
+ uv run utils/convert_attestations.py
+ "$BUNDLE_PATH"
+ "$SIGNER_IDENTITY"
+ env:
+ BUNDLE_PATH: "${{ steps.attest.outputs.bundle-path }}"
+ # workflow_ref example: sphinx-doc/sphinx/.github/workflows/create-release.yml@refs/heads/master
+ # this forms the "signer identity" for the attestations
+ SIGNER_IDENTITY: "https://github.com/${{ github.workflow_ref }}"
- name: Inspect PEP 740 attestations
run: |
@@ -75,8 +78,10 @@ jobs:
- name: Prepare attestation bundles for uploading
run: |
mkdir -p /tmp/attestation-bundles
- cp "${{ steps.attest.outputs.bundle-path }}" /tmp/attestation-bundles/
+ cp "$BUNDLE_PATH" /tmp/attestation-bundles/
cp dist/*.publish.attestation /tmp/attestation-bundles/
+ env:
+ BUNDLE_PATH: "${{ steps.attest.outputs.bundle-path }}"
- name: Upload attestation bundles
uses: actions/upload-artifact@v4
@@ -97,7 +102,7 @@ jobs:
headers: {Authorization: `bearer ${oidc_request_token}`},
});
const oidc_token = (await oidc_resp.json()).value;
-
+
// exchange the OIDC token for an API token
const mint_resp = await fetch('https://pypi.org/_/oidc/github/mint-token', {
method: 'post',
@@ -127,6 +132,8 @@ jobs:
contents: write # for softprops/action-gh-release to create GitHub release
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Get release version
id: get_version
uses: actions/github-script@v7
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 96a7ef718bd..c8444c6a14f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -24,60 +24,41 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Get Ruff version from pyproject.toml
run: |
RUFF_VERSION=$(awk -F'[="]' '/\[project\.optional-dependencies\]/ {p=1} /ruff/ {if (p) print $4}' pyproject.toml)
echo "RUFF_VERSION=$RUFF_VERSION" >> $GITHUB_ENV
- - name: Install Ruff
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- --write-out "%{stderr}Downloaded: %{url}\n"
- "https://astral.sh/ruff/$RUFF_VERSION/install.sh"
- | sh
+ - name: Install Ruff ${{ env.RUFF_VERSION }}
+ uses: astral-sh/ruff-action@v3
+ with:
+ args: --version
+ version: ${{ env.RUFF_VERSION }}
- name: Lint with Ruff
- run: ruff check . --output-format github
+ run: ruff check --output-format=github
- name: Format with Ruff
- run: ruff format . --diff
-
- flake8:
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v4
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3"
- - name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
- - name: Install dependencies
- run: uv pip install --upgrade "flake8>=6.0"
- - name: Lint with flake8
- run: flake8 .
+ run: ruff format --diff
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install ".[lint,test]"
- name: Type check with mypy
@@ -88,16 +69,17 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install ".[lint,test]"
- name: Type check with pyright
@@ -108,16 +90,17 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install --upgrade sphinx-lint
- name: Lint documentation with sphinx-lint
@@ -128,16 +111,17 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3"
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install --upgrade twine build
- name: Lint with twine
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index d86f4b36282..e9d58e4896a 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -2,19 +2,57 @@ name: Lock old threads
on:
schedule:
+ # Run at midnight daily
- cron: "0 0 * * *"
-
-permissions:
- issues: write
- pull-requests: write
+ workflow_dispatch:
jobs:
action:
- if: github.repository_owner == 'sphinx-doc'
runs-on: ubuntu-latest
+ if: github.repository_owner == 'sphinx-doc'
+ permissions:
+ # to lock issues and PRs
+ issues: write
+ pull-requests: write
steps:
- - uses: dessant/lock-threads@v3
- with:
- github-token: ${{ github.token }}
- issue-inactive-days: "30"
- pr-inactive-days: "30"
+ - uses: actions/github-script@v7
+ with:
+ retries: 3
+ # language=JavaScript
+ script: |
+ const _FOUR_WEEKS_MILLISECONDS = 28 * 24 * 60 * 60 * 1000;
+ const _FOUR_WEEKS_DATE = new Date(Date.now() - _FOUR_WEEKS_MILLISECONDS);
+ const FOUR_WEEKS_AGO = `${_FOUR_WEEKS_DATE.toISOString().substring(0, 10)}T00:00:00Z`;
+ const OWNER = context.repo.owner;
+ const REPO = context.repo.repo;
+
+ try {
+ for (const thread_type of ["issue", "pr"]) {
+ core.debug(`Finding ${thread_type}s to lock`);
+ const query = thread_type === "issue"
+ ? `repo:${OWNER}/${REPO} updated:<${FOUR_WEEKS_AGO} is:closed is:unlocked is:issue`
+ : `repo:${OWNER}/${REPO} updated:<${FOUR_WEEKS_AGO} is:closed is:unlocked is:pr`;
+ core.debug(`Using query '${query}'`);
+ // https://octokit.github.io/rest.js/v21/#search-issues-and-pull-requests
+ const {data: {items: results}} = await github.rest.search.issuesAndPullRequests({
+ q: query,
+ order: "desc",
+ sort: "updated",
+ per_page: 100,
+ });
+ for (const item of results) {
+ if (item.locked) continue;
+ const thread_num = item.number;
+ core.debug(`Locking #${thread_num} (${thread_type})`);
+ // https://octokit.github.io/rest.js/v21/#issues-lock
+ await github.rest.issues.lock({
+ owner: OWNER,
+ repo: REPO,
+ issue_number: thread_num,
+ lock_reason: "resolved",
+ });
+ }
+ }
+ } catch (err) {
+ core.setFailed(err.message);
+ }
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index aa3a27753b2..3c76b1405cb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -47,6 +47,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v5
with:
@@ -56,11 +58,10 @@ jobs:
- name: Install graphviz
run: sudo apt-get install --no-install-recommends --yes graphviz
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install .[test]
- name: Install Docutils ${{ matrix.docutils }}
@@ -85,6 +86,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python }} (deadsnakes)
uses: deadsnakes/action@v3.2.0
with:
@@ -117,6 +120,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python }} (deadsnakes)
uses: deadsnakes/action@v3.2.0
with:
@@ -130,9 +135,6 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install .[test]
- # markupsafe._speedups has not declared that it can run safely without the GIL
- - name: Remove markupsafe._speedups
- run: rm -rf "$(python -c 'from markupsafe._speedups import __file__ as f; print(f)')"
- name: Test with pytest
run: python -m pytest -vv --durations 25
env:
@@ -150,6 +152,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python ${{ matrix.python }} (deadsnakes)
uses: deadsnakes/action@v3.2.0
with:
@@ -164,9 +168,6 @@ jobs:
python -m pip install --upgrade pip
sed -i 's/flit_core>=3.7/flit_core @ git+https:\/\/github.com\/pypa\/flit.git#subdirectory=flit_core/' pyproject.toml
python -m pip install .[test]
- # markupsafe._speedups has not declared that it can run safely without the GIL
- - name: Remove markupsafe._speedups
- run: rm -rf "$(python -c 'from markupsafe._speedups import __file__ as f; print(f)')"
- name: Test with pytest
run: python -m pytest -vv --durations 25
env:
@@ -179,6 +180,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -188,9 +191,10 @@ jobs:
- name: Install graphviz
run: choco install --no-progress graphviz
- name: Install uv
- run: >
- Invoke-WebRequest -Uri "https://astral.sh/uv/install.ps1"
- | Invoke-Expression
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install .[test]
- name: Test with pytest
@@ -211,6 +215,8 @@ jobs:
wget --no-verbose https://github.com/w3c/epubcheck/releases/download/v${EPUBCHECK_VERSION}/epubcheck-${EPUBCHECK_VERSION}.zip
unzip epubcheck-${EPUBCHECK_VERSION}.zip
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -220,11 +226,10 @@ jobs:
- name: Install graphviz
run: sudo apt-get install --no-install-recommends --yes graphviz
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install .[test]
- name: Install Docutils' HEAD
@@ -243,6 +248,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -252,11 +259,10 @@ jobs:
- name: Install graphviz
run: sudo apt-get install --no-install-recommends --yes graphviz
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: |
uv pip install .[test] --resolution lowest-direct
@@ -275,6 +281,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -282,11 +290,10 @@ jobs:
- name: Check Python version
run: python --version --version
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install .[test]
- name: Test with pytest
@@ -303,6 +310,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -312,11 +321,10 @@ jobs:
- name: Install graphviz
run: sudo apt-get install --no-install-recommends --yes graphviz
- name: Install uv
- run: >
- curl --no-progress-meter --location --fail
- --proto '=https' --tlsv1.2
- "https://astral.sh/uv/install.sh"
- | sh
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
run: uv pip install .[test] pytest-cov
- name: Test with pytest
@@ -324,4 +332,4 @@ jobs:
env:
VIRTUALENV_SYSTEM_SITE_PACKAGES: "1"
- name: codecov
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v5
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 29359f90958..84727288fde 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -34,6 +34,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Use Node.js ${{ env.node-version }}
uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/transifex.yml b/.github/workflows/transifex.yml
index 78e74efcf5a..9f4698ead52 100644
--- a/.github/workflows/transifex.yml
+++ b/.github/workflows/transifex.yml
@@ -16,6 +16,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -25,8 +27,13 @@ jobs:
mkdir -p /tmp/tx_cli && cd $_
curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash
shell: bash
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
- run: pip install --upgrade babel jinja2
+ run: uv pip install --upgrade babel jinja2
- name: Extract translations from source code
run: python utils/babel_runner.py extract
- name: Push translations to transifex.com
@@ -45,6 +52,8 @@ jobs:
steps:
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -54,8 +63,13 @@ jobs:
mkdir -p /tmp/tx_cli && cd $_
curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash
shell: bash
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: latest
+ enable-cache: false
- name: Install dependencies
- run: pip install --upgrade babel jinja2
+ run: uv pip install --upgrade babel jinja2
- name: Extract translations from source code
run: python utils/babel_runner.py extract
- name: Pull translations from transifex.com
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 4e7f5f9f3b9..550ac42f47e 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,4 +1,6 @@
version: 2
+sphinx:
+ configuration: doc/conf.py
build:
os: ubuntu-22.04
diff --git a/.ruff.toml b/.ruff.toml
index e34aed8f70a..f82928eca65 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -3,16 +3,23 @@ line-length = 88
output-format = "full"
extend-exclude = [
- "tests/roots/*",
- "tests/js/roots/*",
"build/*",
"doc/_build/*",
- "doc/usage/extensions/example*.py",
+ "tests/roots/test-directive-code/target.py", # Tests break if formatted
+ "tests/roots/test-pycode/cp_1251_coded.py", # Not UTF-8
]
+[format]
+preview = true
+quote-style = "single"
+
[lint]
preview = true
ignore = [
+ # flake8-builtins
+ "A001", # Variable `{name}` is shadowing a Python builtin
+ "A002", # Function argument `{name}` is shadowing a Python builtin
+ "A005", # Module `{name}` is shadowing a Python builtin module
# flake8-annotations
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `{name}`
# flake8-unused-arguments ('ARG')
@@ -20,25 +27,60 @@ ignore = [
"ARG002", # Unused method argument: `{name}`
"ARG003", # Unused class method argument: `{name}`
"ARG005", # Unused lambda argument: `{name}`
- # flake8-commas ('COM')
+ # flake8-blind-except
+ "BLE001", # Do not catch blind exception: `{name}`
+ # flake8-commas
"COM812", # Trailing comma missing
+ # flake8-copyright
+ "CPY001", # Missing copyright notice at top of file
+ # pydocstyle ('D')
+ "D100", # Missing docstring in public module
+ "D101", # Missing docstring in public class
+ "D102", # Missing docstring in public method
+ "D103", # Missing docstring in public function
+ "D104", # Missing docstring in public package
+ "D105", # Missing docstring in magic method
+ "D107", # Missing docstring in `__init__`
+ "D200", # One-line docstring should fit on one line
+ "D205", # 1 blank line required between summary line and description
+ "D400", # First line should end with a period
+ "D401", # First line of docstring should be in imperative mood: "{first_line}"
+ # pydoclint
+ "DOC201", # `return` is not documented in docstring
+ "DOC402", # `yield` is not documented in docstring
+ "DOC501", # Raised exception `{id}` missing from docstring
# pycodestyle
"E741", # Ambiguous variable name: `{name}`
+ # eradicate
+ "ERA001", # Found commented-out code
# pyflakes
"F841", # Local variable `{name}` is assigned to but never used
- # flake8-logging-format
- "G003", # Logging statement uses `+`
+ # flake8-boolean-trap
+ "FBT001", # Boolean-typed positional argument in function definition
+ "FBT002", # Boolean default positional argument in function definition
+ "FBT003", # Boolean positional value in function call
+ # flake8-fixme
+ "FIX001", # Line contains FIXME, consider resolving the issue
+ "FIX002", # Line contains TODO, consider resolving the issue
+ "FIX003", # Line contains XXX, consider resolving the issue
+ "FIX004", # Line contains HACK, consider resolving the issue
# refurb
"FURB101", # `open` and `read` should be replaced by `Path(...).read_text(...)`
"FURB103", # `open` and `write` should be replaced by `Path(...).write_text(...)`
+ # flake8-implicit-str-concat
+ # pep8-naming
+ "N801", # Class name `{name}` should use CapWords convention
+ "N802", # Function name `{name}` should be lowercase
+ "N803", # Argument name `{name}` should be lowercase
+ "N806", # Variable `{name}` in function should be lowercase
+ "N818", # Exception name `{name}` should be named with an Error suffix
# perflint
"PERF203", # `try`-`except` within a loop incurs performance overhead
- # flake8-pie ('PIE')
+ # flake8-pie
"PIE790", # Unnecessary `pass` statement
- # pylint ('PLC')
+ # pylint
"PLC0415", # `import` should be at the top-level of a file
"PLC2701", # Private name import `{name}` from external module `{module}`
- # pylint ('PLR')
"PLR0904", # Too many public methods ({methods} > {max_methods})
"PLR0911", # Too many return statements ({returns} > {max_returns})
"PLR0912", # Too many branches ({branches} > {max_branches})
@@ -52,8 +94,22 @@ ignore = [
"PLR5501", # Use `elif` instead of `else` then `if`, to reduce indentation
"PLR6104", # Use `{operator}` to perform an augmented assignment directly
"PLR6301", # Method `{method_name}` could be a function, class method, or static method
- # pylint ('PLW')
"PLW2901", # Outer {outer_kind} variable `{name}` overwritten by inner {inner_kind} target
+ # flake8-use-pathlib
+ "PTH100", # `os.path.abspath()` should be replaced by `Path.resolve()`
+ "PTH110", # `os.path.exists()` should be replaced by `Path.exists()`
+ "PTH113", # `os.path.isfile()` should be replaced by `Path.is_file()`
+ "PTH118", # `os.{module}.join()` should be replaced by `Path` with `/` operator
+ "PTH119", # `os.path.basename()` should be replaced by `Path.name`
+ "PTH120", # `os.path.dirname()` should be replaced by `Path.parent`
+ "PTH122", # `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent`
+ "PTH123", # `open()` should be replaced by `Path.open()`
+ # flake8-pyi ('PYI')
+ "PYI025", # Use `from collections.abc import Set as AbstractSet` to avoid confusion with the `set` builtin
+ # flake8-return
+ "RET504", # Unnecessary assignment to `{name}` before `return` statement
+ "RET505", # Unnecessary `{branch}` after `return` statement
+ "RET506", # Unnecessary `{branch}` after `raise` statement
# flake8-bandit ('S')
"S101", # Use of `assert` detected
"S110", # `try`-`except`-`pass` detected, consider logging the exception
@@ -63,17 +119,34 @@ ignore = [
# flake8-simplify
"SIM102", # Use a single `if` statement instead of nested `if` statements
"SIM108", # Use ternary operator `{contents}` instead of `if`-`else`-block
+ # flake8-self
+ "SLF001", # Private member accessed: `{access}`
+ # flake8-todos ('TD')
+ "TD001", # Invalid TODO tag: `{tag}`
+ "TD002", # Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...`
+ "TD003", # Missing issue link on the line following this TODO
+ "TD004", # Missing colon in TODO
+ # tryceratops
+ "TRY002", # Create your own exception
+ "TRY300", # Consider moving this statement to an `else` block
# pyupgrade
"UP031", # Use format specifiers instead of percent format
"UP032", # Use f-string instead of `format` call
+ # Ruff-specific rules ('RUF')
+ "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
+ "RUF021", # Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
+ "RUF022", # `__all__` is not sorted
+ "RUF023", # `{}.__slots__` is not sorted
+ "RUF027", # Possible f-string without an `f` prefix
+ "RUF039", # First argument to {call} is not raw string
+ "RUF052", # Local dummy variable `{}` is accessed
]
external = [ # Whitelist for RUF100 unknown code warnings
- "E704",
"SIM113",
]
select = [
# flake8-builtins ('A')
- # NOT YET USED
+ "A",
# airflow ('AIR')
# Airflow is not used in Sphinx
# flake8-annotations ('ANN')
@@ -85,7 +158,7 @@ select = [
# flake8-bugbear ('B')
"B",
# flake8-blind-except ('BLE')
- # NOT YET USED
+ "BLE",
# flake8-comprehensions ('C4')
"C4",
# mccabe ('C90')
@@ -93,53 +166,15 @@ select = [
# flake8-commas ('COM')
"COM", # Trailing comma prohibited
# flake8-copyright ('CPY')
- # NOT YET USED
+ "CPY",
# pydocstyle ('D')
-# "D100", # Missing docstring in public module
-# "D101", # Missing docstring in public class
-# "D102", # Missing docstring in public method
-# "D103", # Missing docstring in public function
-# "D104", # Missing docstring in public package
-# "D105", # Missing docstring in magic method
- "D106", # Missing docstring in public nested class
-# "D107", # Missing docstring in `__init__`
-# "D200", # One-line docstring should fit on one line
- "D201", # No blank lines allowed before function docstring (found {num_lines})
- "D202", # No blank lines allowed after function docstring (found {num_lines})
- "D204", # 1 blank line required after class docstring
-# "D205", # 1 blank line required between summary line and description
- "D206", # Docstring should be indented with spaces, not tabs
- "D207", # Docstring is under-indented
- "D208", # Docstring is over-indented
- "D209", # Multi-line docstring closing quotes should be on a separate line
- "D210", # No whitespaces allowed surrounding docstring text
- "D211", # No blank lines allowed before class docstring
-# "D212", # Multi-line docstring summary should start at the first line
-# "D213", # Multi-line docstring summary should start at the second line
-# "D214", # Section is over-indented ("{name}")
-# "D215", # Section underline is over-indented ("{name}")
- "D300", # Use triple double quotes `"""`
- "D301", # Use `r"""` if any backslashes in a docstring
-# "D400", # First line should end with a period
-# "D401", # First line of docstring should be in imperative mood: "{first_line}"
- "D402", # First line should not be the function's signature
- "D403", # First word of the first line should be capitalized: `{}` -> `{}`
-# "D404", # First word of the docstring should not be "This"
- "D405", # Section name should be properly capitalized ("{name}")
-# "D406", # Section name should end with a newline ("{name}")
-# "D407", # Missing dashed underline after section ("{name}")
- "D408", # Section underline should be in the line following the section's name ("{name}")
- "D409", # Section underline should match the length of its name ("{name}")
- "D410", # Missing blank line after section ("{name}")
- "D411", # Missing blank line before section ("{name}")
-# "D412", # No blank lines allowed between a section header and its content ("{name}")
-# "D413", # Missing blank line after last section ("{name}")
- "D414", # Section has no content ("{name}")
-# "D415", # First line should end with a period, question mark, or exclamation point
- "D416", # Section name should end with a colon ("{name}")
+ "D",
+ "D212", # Multi-line docstring summary should start at the first line
+ # "D404", # First word of the docstring should not be "This"
+ # "D415", # First line should end with a period, question mark, or exclamation point
"D417", # Missing argument description in the docstring for `{definition}`: `{name}`
- "D418", # Function decorated with `@overload` shouldn't contain a docstring
- "D419", # Docstring is empty
+ # pydoclint ('DOC')
+ "DOC",
# flake8-django ('DJ')
# Django is not used in Sphinx
# flake8-datetimez ('DTZ')
@@ -149,7 +184,7 @@ select = [
# flake8-errmsg ('EM')
"EM",
# eradicate ('ERA')
- # NOT YET USED
+ "ERA",
# flake8-executable ('EXE')
"EXE",
# pyflakes ('F')
@@ -159,9 +194,9 @@ select = [
# flake8-fastapi ('FAST')
# FastAPI is not used in Sphinx
# flake8-boolean-trap ('FBT')
- # NOT YET USED
+ "FBT",
# flake8-fixme ('FIX')
- # NOT YET USED
+ "FIX",
# flynt ('FLY')
"FLY",
# refurb ('FURB')
@@ -171,17 +206,17 @@ select = [
# isort ('I')
"I",
# flake8-import-conventions ('ICN')
- "ICN", # flake8-import-conventions
+ "ICN",
# flake8-no-pep420 ('INP')
"INP",
# flake8-gettext ('INT')
"INT",
# flake8-implicit-str-concat ('ISC')
- # NOT YET USED
+ "ISC",
# flake8-logging ('LOG')
"LOG",
# pep8-naming ('N')
- # NOT YET USED
+ "N",
# numpy-specific rules ('NPY')
# Numpy is not used in Sphinx
# pandas-vet ('PD')
@@ -197,88 +232,39 @@ select = [
# flake8-pytest-style ('PT')
"PT",
# flake8-use-pathlib ('PTH')
- # NOT YET USED
+ "PTH",
# flake8-pyi ('PYI')
- # Stub files are not used in Sphinx
+ "PYI",
# flake8-quotes ('Q')
-# "Q000", # Double quotes found but single quotes preferred
-# "Q001", # Single quote multiline found but double quotes preferred
- "Q002", # Single quote docstring found but double quotes preferred
- "Q003", # Change outer quotes to avoid escaping inner quotes
- "Q004", # Unnecessary escape on inner quote character
+ "Q",
# flake8-return ('RET')
- "RET501", # Do not explicitly `return None` in function if it is the only possible return value
- "RET502", # Do not implicitly `return None` in function able to return non-`None` value
-# "RET503", # Missing explicit `return` at the end of function able to return non-`None` value
-# "RET504", # Unnecessary assignment to `{name}` before `return` statement
-# "RET505", # Unnecessary `{branch}` after `return` statement
-# "RET506", # Unnecessary `{branch}` after `raise` statement
- "RET507", # Unnecessary `{branch}` after `continue` statement
- "RET508", # Unnecessary `{branch}` after `break` statement
+ "RET",
# flake8-raise ('RSE')
"RSE",
- # ruff-specific rules ('RUF')
-# "RUF001", # String contains ambiguous {}. Did you mean {}?
- "RUF002", # Docstring contains ambiguous {}. Did you mean {}?
-# "RUF003", # Comment contains ambiguous {}. Did you mean {}?
- "RUF005", # Consider `{expression}` instead of concatenation
- "RUF006", # Store a reference to the return value of `{expr}.{method}`
- "RUF007", # Prefer `itertools.pairwise()` over `zip()` when iterating over successive pairs
- "RUF008", # Do not use mutable default values for dataclass attributes
- "RUF009", # Do not perform function call `{name}` in dataclass defaults
- "RUF010", # Use explicit conversion flag
-# "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
- "RUF013", # PEP 484 prohibits implicit `Optional`
-# "RUF015", # Prefer `next({iterable})` over single element slice
- "RUF016", # Slice in indexed access to type `{value_type}` uses type `{index_type}` instead of an integer
- "RUF017", # Avoid quadratic list summation
- "RUF018", # Avoid assignment expressions in `assert` statements
- "RUF019", # Unnecessary key check before dictionary access
- "RUF020", # `{never_like} | T` is equivalent to `T`
-# "RUF021", # Parenthesize `a and b` expressions when chaining `and` and `or` together, to make the precedence clear
-# "RUF022", # `__all__` is not sorted
-# "RUF023", # `{}.__slots__` is not sorted
- "RUF024", # Do not pass mutable objects as values to `dict.fromkeys`
- "RUF026", # `default_factory` is a positional-only argument to `defaultdict`
-# "RUF027", # Possible f-string without an `f` prefix
-# "RUF028", # This suppression comment is invalid because {}
-# "RUF029", # Function `{name}` is declared `async`, but doesn't `await` or use `async` features.
- "RUF030", # `print()` expression in `assert` statement is likely unintentional
-# "RUF031", # Use parentheses for tuples in subscripts.
- "RUF032", # `Decimal()` called with float literal argument
- "RUF033", # `__post_init__` method with argument defaults
- "RUF034", # Useless if-else condition
-# "RUF100", # Unused `noqa` directive
- "RUF101", # `{original}` is a redirect to `{target}`
- "RUF200", # Failed to parse pyproject.toml: {message}
+ # Ruff-specific rules ('RUF')
+ "RUF",
# flake8-bandit ('S')
"S",
# flake8-simplify ('SIM')
"SIM", # flake8-simplify
# flake8-self ('SLF')
- # NOT YET USED
+ "SLF",
# flake8-slots ('SLOT')
"SLOT",
# flake8-debugger ('T10')
"T10",
# flake8-print ('T20')
"T20",
- # flake8-type-checking ('TCH')
- "TCH",
+ # flake8-type-checking ('TC')
+ "TC",
# flake8-todos ('TD')
-# "TD001", # Invalid TODO tag: `{tag}`
-# "TD002", # Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ...`
-# "TD003", # Missing issue link on the line following this TODO
-# "TD004", # Missing colon in TODO
-# "TD005", # Missing issue description after `TODO`
- "TD006", # Invalid TODO capitalization: `{tag}` should be `TODO`
- "TD007", # Missing space after colon in TODO
+ "TD",
# flake8-tidy-imports ('TID')
"TID",
# flake8-trio ('TRIO')
# Trio is not used in Sphinx
# tryceratops ('TRY')
- # NOT YET USED
+ "TRY",
# pyupgrade ('UP')
"UP",
# pycodestyle ('W')
@@ -287,23 +273,34 @@ select = [
"YTT",
]
+logger-objects = [
+ "sphinx.ext.apidoc._shared.LOGGER",
+ "sphinx.ext.intersphinx._shared.LOGGER",
+]
+
[lint.per-file-ignores]
"doc/*" = [
"ANN", # documentation doesn't need annotations
- "TCH001", # documentation doesn't need type-checking blocks
+ "TC001", # documentation doesn't need type-checking blocks
+]
+"doc/conf.py" = [
+ "INP001", # doc/ is not a namespace package
+]
+"doc/development/tutorials/examples/*" = [
+ "I002", # we don't need the annotations future
+ "INP001", # examples/ is not a namespace package
]
-"doc/conf.py" = ["INP001", "W605"]
-"doc/development/tutorials/examples/*" = ["INP001"]
-# allow print() in the tutorial
"doc/development/tutorials/examples/recipe.py" = [
- "FURB118",
- "T201"
+ "FURB118", # keep the tutorial simple: no itemgetter
+ "T201" # allow print() in the tutorial
+]
+"doc/usage/extensions/example_{google,numpy}.py" = [
+ "D416", # Section name should end with a colon ("{name}")
+ "DOC502", # Raised exception is not explicitly raised: `{id}`
+ "I002", # Missing required import: {name}
+ "INP001", # File {filename} is part of an implicit namespace package. Add an __init__.py.
+ "PLW3201", # Dunder method {name} has no special meaning in Python 3
]
-"sphinx/domains/**" = ["FURB113"]
-"tests/test_domains/test_domain_cpp.py" = ["FURB113"]
-
-# from .flake8
-"sphinx/*" = ["E241"]
# whitelist ``print`` for stdout messages
"sphinx/_cli/__init__.py" = ["T201"]
@@ -317,7 +314,10 @@ select = [
# whitelist ``print`` for stdout messages
"sphinx/cmd/build.py" = ["T201"]
"sphinx/cmd/make_mode.py" = ["T201"]
-"sphinx/cmd/quickstart.py" = ["T201"]
+"sphinx/cmd/quickstart.py" = [
+ "PTH",
+ "T201",
+]
"sphinx/environment/collectors/toctree.py" = ["B026"]
"sphinx/environment/adapters/toctree.py" = ["B026"]
@@ -328,21 +328,20 @@ select = [
# whitelist ``token`` in docstring parsing
"sphinx/ext/napoleon/docstring.py" = ["S105"]
+"sphinx/search/*" = ["RUF001"]
+
# whitelist ``print`` for stdout messages
"sphinx/testing/fixtures.py" = ["T201"]
-# Ruff bug: https://github.com/astral-sh/ruff/issues/6540
-"sphinx/transforms/i18n.py" = ["PGH004"]
-
-# Function wrappers
-"sphinx/ext/autodoc/importer.py" = ["D402"]
-"sphinx/util/requests.py" = ["D402"]
-
-"sphinx/search/*" = ["E501"]
+# whitelist ``os.path`` for deprecated ``sphinx.testing.path``
+"sphinx/testing/path.py" = ["PTH"]
# whitelist ``token`` in date format parsing
"sphinx/util/i18n.py" = ["S105"]
+# whitelist ``os.path`` for ``sphinx.util.osutil``
+"sphinx/util/osutil.py" = ["PTH"]
+
# whitelist ``token`` in literal parsing
"sphinx/writers/html5.py" = ["S105"]
@@ -351,14 +350,25 @@ select = [
"ANN", # tests don't need annotations
"D402",
"PLC1901", # whitelist comparisons to the empty string ('')
+ "RUF001", # ambiguous unicode character
"S301", # allow use of ``pickle``
"S403", # allow use of ``pickle``
"T201", # whitelist ``print`` for tests
]
+# test roots are not packages
+"tests/js/roots/*" = ["I002", "INP001"]
+"tests/roots/*" = [
+ "D403", # permit uncapitalised docstrings
+ "F401", # names may be unused in test roots
+ "I002", # we don't need the annotations future
+ "INP001", # test roots are not packages
+]
+
# these tests need old ``typing`` generic aliases
-"tests/test_util/test_util_typing.py" = ["UP006", "UP007", "UP035"]
-"tests/test_util/typing_test_data.py" = ["FA100", "UP006", "UP007", "UP035"]
+"tests/roots/test-ext-autodoc/target/genericalias.py" = ["UP006", "UP007", "UP035", "UP045"]
+"tests/test_util/test_util_typing.py" = ["RUF036", "UP006", "UP007", "UP035", "UP045"]
+"tests/test_util/typing_test_data.py" = ["FA100", "I002", "PYI030", "UP006", "UP007", "UP035", "UP045"]
"utils/*" = [
"T201", # whitelist ``print`` for stdout messages
@@ -371,62 +381,19 @@ max-line-length = 95
[lint.flake8-quotes]
inline-quotes = "single"
+[lint.flake8-type-checking]
+exempt-modules = []
+strict = true
+
[lint.isort]
forced-separate = [
"tests",
]
-
-[format]
-preview = true
-quote-style = "single"
-exclude = [
- "sphinx/addnodes.py",
- "sphinx/application.py",
- "sphinx/builders/latex/constants.py",
- "sphinx/config.py",
- "sphinx/domains/__init__.py",
- "sphinx/domains/c/_parser.py",
- "sphinx/domains/c/_ids.py",
- "sphinx/domains/c/__init__.py",
- "sphinx/domains/c/_symbol.py",
- "sphinx/domains/c/_ast.py",
- "sphinx/domains/changeset.py",
- "sphinx/domains/citation.py",
- "sphinx/domains/cpp/_parser.py",
- "sphinx/domains/cpp/_ids.py",
- "sphinx/domains/cpp/__init__.py",
- "sphinx/domains/cpp/_symbol.py",
- "sphinx/domains/cpp/_ast.py",
- "sphinx/domains/index.py",
- "sphinx/domains/javascript.py",
- "sphinx/domains/math.py",
- "sphinx/domains/python/_annotations.py",
- "sphinx/domains/python/__init__.py",
- "sphinx/domains/python/_object.py",
- "sphinx/domains/rst.py",
- "sphinx/domains/std/__init__.py",
- "sphinx/ext/autodoc/__init__.py",
- "sphinx/ext/autodoc/directive.py",
- "sphinx/ext/autodoc/importer.py",
- "sphinx/ext/autodoc/mock.py",
- "sphinx/ext/autodoc/preserve_defaults.py",
- "sphinx/ext/autodoc/type_comment.py",
- "sphinx/ext/autodoc/typehints.py",
- "sphinx/ext/autosectionlabel.py",
- "sphinx/ext/autosummary/__init__.py",
- "sphinx/ext/coverage.py",
- "sphinx/ext/doctest.py",
- "sphinx/ext/duration.py",
- "sphinx/ext/extlinks.py",
- "sphinx/ext/githubpages.py",
- "sphinx/ext/graphviz.py",
- "sphinx/ext/ifconfig.py",
- "sphinx/ext/imgconverter.py",
- "sphinx/ext/imgmath.py",
- "sphinx/ext/inheritance_diagram.py",
- "sphinx/ext/linkcode.py",
- "sphinx/ext/mathjax.py",
- "sphinx/ext/todo.py",
- "sphinx/ext/viewcode.py",
- "sphinx/registry.py",
+required-imports = [
+ "from __future__ import annotations",
]
+
+[lint.pydocstyle]
+convention = "pep257"
+ignore-decorators = ["typing.overload"]
+ignore-var-parameters = true
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 8068ae4ae22..f57795d4fa7 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -18,27 +18,39 @@ Contributors
*Listed alphabetically in forename, surname order*
+* Aaron Carlisle -- basic theme and templating improvements
+* Adam Dangoor -- improved static typing
* Adrián Chaves (Gallaecio) -- coverage builder improvements
* Alastair Houghton -- Apple Help builder
+* Alex Gaynor -- linkcheck retry on errors
* Alexander Todorov -- inheritance_diagram tests and improvements
* Andi Albrecht -- agogo theme
* Antonio Valentino -- qthelp builder, docstring inheritance
* Antti Kaihola -- doctest extension (skipif option)
* Barry Warsaw -- setup command improvements
-* Ben Egan -- Napoleon improvements
+* Ben Egan -- Napoleon improvements & viewcode improvements
* Benjamin Peterson -- unittests
* Blaise Laflamme -- pyramid theme
+* Brecht Machiels -- builder entry-points
* Bruce Mitchener -- Minor epub improvement
* Buck Evan -- dummy builder
* Charles Duffy -- original graphviz extension
+* Chris Barrick -- Napoleon type preprocessing logic
+* Chris Holdgraf -- improved documentation structure
* Chris Lamb -- reproducibility fixes
* Christopher Perkins -- autosummary integration
* Dan MacKinlay -- metadata fixes
* Daniel Bültmann -- todo extension
+* Daniel Eades -- improved static typing
+* Daniel Hahler -- testing and CI improvements
* Daniel Pizetta -- inheritance diagram improvements
* Dave Kuhlman -- original LaTeX writer
+* Dimitri Papadopoulos Orfanos -- linting and spelling
+* Dmitry Shachnev -- modernisation and reproducibility
* Doug Hellmann -- graphviz improvements
+* Eric Larson -- better error messages
* Eric N. Vander Weele -- autodoc improvements
+* Eric Wieser -- autodoc improvements
* Etienne Desautels -- apidoc module
* Ezio Melotti -- collapsible sidebar JavaScript
* Filip Vavera -- napoleon todo directive
@@ -53,28 +65,38 @@ Contributors
* Jacob Mason -- websupport library (GSOC project)
* James Addison -- linkcheck and HTML search improvements
* Jeppe Pihl -- literalinclude improvements
+* Jeremy Maitin-Shepard -- C++ domain improvements
* Joel Wurtz -- cellspanning support in LaTeX
* John Waltman -- Texinfo builder
+* Jon Dufresne -- modernisation
* Josip Dzolonga -- coverage builder
+* Juan Luis Cano Rodríguez -- new tutorial (2021)
* Julien Palard -- Colspan and rowspan in text builder
+* Justus Magin -- napoleon improvements
* Kevin Dunn -- MathJax extension
* KINEBUCHI Tomohiko -- typing Sphinx as well as docutils
* Kurt McKee -- documentation updates
* Lars Hupfeldt Nielsen - OpenSSL FIPS mode md5 bug fix
+* Louis Maddox -- better docstrings
* Łukasz Langa -- partial support for autodoc
* Marco Buttu -- doctest extension (pyversion option)
* Martin Hans -- autodoc improvements
* Martin Larralde -- additional napoleon admonitions
+* Martin Liška -- option directive and role improvements
* Martin Mahner -- nature theme
* Matthew Fernandez -- todo extension fix
* Matthew Woodcraft -- text output improvements
+* Matthias Geier -- style improvements
* Michael Droettboom -- inheritance_diagram extension
* Michael Wilson -- Intersphinx HTTP basic auth support
* Nathan Damon -- bugfix in validation of static paths in html builders
+* Nils Kattenbeck -- pygments dark style
* Pauli Virtanen -- autodoc improvements, autosummary extension
+* Rafael Fontenelle -- internationalisation
* \A. Rafey Khan -- improved intersphinx typing
* Roland Meister -- epub builder
* Sebastian Wiesner -- image handling, distutils support
+* Slawek Figiel -- additional warning suppression
* Stefan Seefeld -- toctree improvements
* Stefan van der Walt -- autosummary extension
* \T. Powers -- HTML output improvements
diff --git a/CHANGES.rst b/CHANGES.rst
index b47f417e9a1..4c2fa5de403 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -9,17 +9,113 @@ Dependencies
Incompatible changes
--------------------
+* #13044: Remove the internal and undocumented ``has_equations`` data
+ from the :py:class:`!MathDomain`` domain.
+ The undocumented :py:meth:`!MathDomain.has_equations` method
+ now unconditionally returns ``True``.
+ These are replaced by the ``has_maths_elements`` key of the page context dict.
+ Patch by Adam Turner.
+* #13227: HTML output for sequences of keys in the :rst:role:`kbd` role
+ no longer uses a ```` element to wrap
+ the keys and separators, but places them directly in the relevant parent node.
+ This means that CSS rulesets targeting ``kbd.compound`` or ``.kbd.compound``
+ will no longer have any effect.
+ Patch by Adam Turner.
+
Deprecated
----------
+* #13037: Deprecate the ``SingleHTMLBuilder.fix_refuris`` method.
+ Patch by James Addison.
+
Features added
--------------
+* #13173: Add a new ``duplicate_declaration`` warning type,
+ with ``duplicate_declaration.c`` and ``duplicate_declaration.cpp`` subtypes.
+ Patch by Julien Lecomte and Adam Turner.
+* #11824: linkcode: Allow extensions to add support for a domain by defining
+ the keys that should be present.
+ Patch by Nicolas Peugnet.
+* #13144: Add a ``class`` option to the :rst:dir:`autosummary` directive.
+ Patch by Tim Hoffmann.
+* #13146: Napoleon: Unify the type preprocessing logic to allow
+ Google-style docstrings to use the optional and default keywords.
+ Patch by Chris Barrick.
+* #13227: Implement the :rst:role:`kbd` role as a ``SphinxRole``.
+ Patch by Adam Turner.
+* #13065: Enable colour by default in when running on CI.
+ Patch by Adam Turner.
+* #13230: Allow supressing warnings from the :rst:dir:`toctree` directive
+ when a glob pattern doesn't match any documents,
+ via the new ``toc.empty_glob`` warning sub-type.
+ Patch by Slawek Figiel.
+* #9732: Add the new ``autodoc.mocked_object`` warnings sub-type.
+ Patch by Cyril Roelandt.
+* #7630, #4824: autodoc: Use :file:`.pyi` type stub files
+ to auto-document native modules.
+ Patch by Adam Turner, partially based on work by Allie Fitter.
+* #12975: Enable configuration of trailing commas in multi-line signatures
+ in the Python and Javascript domains, via the new
+ :confval:`python_trailing_comma_in_multi_line_signatures` and
+ :confval:`javascript_trailing_comma_in_multi_line_signatures`
+ configuration options.
+* #13264: Rename the :rst:dir:`math` directive's ``nowrap``option
+ to :rst:dir:`no-wrap``,
+ and rename the :rst:dir:`autosummary` directive's ``nosignatures``option
+ to :rst:dir:`no-signatures``.
+ Patch by Adam Turner.
+* #13269: Added the option to disable the use of type comments in
+ via the new :confval:`autodoc_use_type_comments` option,
+ which defaults to ``True`` for backwards compatibility.
+ The default will change to ``False`` in Sphinx 10.
+ Patch by Adam Turner.
+* #9732: Add the new ``ref.any`` warnings sub-type
+ to allow suppressing the ambiguous 'any' cross-reference warning.
+ Patch by Simão Afonso and Adam Turner.
+* #13272: The Python and JavaScript module directives now support
+ the ``:no-index-entry:`` option.
+ Patch by Adam Turner.
+* #12233: autodoc: Allow directives to use ``:no-index-entry:``
+ and include the ``:no-index:`` and ``:no-index-entry:`` options within
+ :confval:`autodoc_default_options`.
+ Patch by Jonny Saunders and Adam Turner.
+
Bugs fixed
----------
+* #12463: autosummary: Respect an empty module ``__all__``.
+ Patch by Valentin Pratz
* #13060: HTML Search: use ``Map`` to store per-file term scores.
Patch by James Addison
+* #13130: LaTeX docs: ``pdflatex`` index creation may fail for index entries
+ in French. See :confval:`latex_use_xindy`.
+ Patch by Jean-François B.
+* #13152: LaTeX: fix a typo from v7.4.0 in a default for ``\sphinxboxsetup``.
+ Patch by Jean-François B.
+* #13096: HTML Search: check that query terms exist as properties in
+ term indices before accessing them.
+* #11233: linkcheck: match redirect URIs against :confval:`linkcheck_ignore` by
+ overriding session-level ``requests.get_redirect_target``.
+* #13195: viewcode: Fix issue where import paths differ from the directory
+ structure.
+ Patch by Ben Egan and Adam Turner.
+* #13188: autodoc: fix detection of class methods implemented in C.
+ Patch by Bénédikt Tran.
+* #1810: Always copy static files when building, regardless of whether
+ any documents have changed since the previous build.
+ Patch by Adam Turner.
+* #13201: autodoc: fix ordering of members when using ``groupwise``
+ for :confval:`autodoc_member_order`. Class methods are now rendered
+ before static methods, which themselves are rendered before regular
+ methods and attributes.
+ Patch by Bénédikt Tran.
+* #12975: Avoid rendering a trailing comma in C and C++ multi-line signatures.
+* #13178: autodoc: Fix resolution for ``pathlib`` types.
+ Patch by Adam Turner.
Testing
-------
+
+* #13224: Correctness fixup for ``test_html_multi_line_copyright``.
+ Patch by Colin Watson, applied by James Addison.
diff --git a/LICENSE.rst b/LICENSE.rst
index 79c6aee4bc9..de3688cd2c6 100644
--- a/LICENSE.rst
+++ b/LICENSE.rst
@@ -4,7 +4,7 @@ License for Sphinx
Unless otherwise indicated, all code in the Sphinx project is licenced under the
two clause BSD licence below.
-Copyright (c) 2007-2024 by the Sphinx team (see AUTHORS file).
+Copyright (c) 2007-2025 by the Sphinx team (see AUTHORS file).
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/Makefile b/Makefile
index 54dd66dc888..a0fa6314dc2 100644
--- a/Makefile
+++ b/Makefile
@@ -45,7 +45,6 @@ clean: clean
.PHONY: style-check
style-check:
- @echo '[+] running flake8' ; flake8 .
@echo '[+] running ruff' ; ruff check .
.PHONY: format
diff --git a/doc/_static/diagrams/sphinx_build_phases.dot b/doc/_static/diagrams/sphinx_build_phases.dot
new file mode 100644
index 00000000000..2d566df066f
--- /dev/null
+++ b/doc/_static/diagrams/sphinx_build_phases.dot
@@ -0,0 +1,18 @@
+digraph phases {
+
+ graph [
+ rankdir = LR
+ ]
+
+ node [
+ shape = rect;
+ style = filled;
+ fillcolor ="#f7f7f7";
+ fontcolor = "#0a507a"
+ ]
+
+ Initialization -> Reading;
+ Reading -> "Consistency checks";
+ "Consistency checks" -> Resolving;
+ Resolving -> Writing;
+}
diff --git a/doc/_static/translation.svg b/doc/_static/translation.svg
index 4e3ab5ab47d..599a0fc2395 100644
--- a/doc/_static/translation.svg
+++ b/doc/_static/translation.svg
@@ -13,22 +13,22 @@ link .pot to .po-->msgfmtsphinx-build -Dlanguage=')
-def read_svg_depth(filename: str) -> int | None:
- """Read the depth from comment at last line of SVG file
- """
- with open(filename, encoding="utf-8") as f:
+def read_svg_depth(filename: str | os.PathLike[str]) -> int | None:
+ """Read the depth from comment at last line of SVG file"""
+ with open(filename, encoding='utf-8') as f:
for line in f: # NoQA: B007
pass
# Only last line is checked
@@ -80,21 +80,22 @@ def read_svg_depth(filename: str) -> int | None:
return None
-def write_svg_depth(filename: str, depth: int) -> None:
- """Write the depth to SVG file as a comment at end of file
- """
- with open(filename, 'a', encoding="utf-8") as f:
+def write_svg_depth(filename: Path, depth: int) -> None:
+ """Write the depth to SVG file as a comment at end of file"""
+ with open(filename, 'a', encoding='utf-8') as f:
f.write('\n' % depth)
-def generate_latex_macro(image_format: str,
- math: str,
- config: Config,
- confdir: str | os.PathLike[str] = '') -> str:
+def generate_latex_macro(
+ image_format: str,
+ math: str,
+ config: Config,
+ confdir: _StrPath,
+) -> str:
"""Generate LaTeX macro."""
variables = {
'fontsize': config.imgmath_font_size,
- 'baselineskip': int(round(config.imgmath_font_size * 1.2)),
+ 'baselineskip': round(config.imgmath_font_size * 1.2),
'preamble': config.imgmath_latex_preamble,
# the dvips option is important when imgmath_latex in {"xelatex", "tectonic"},
# it has no impact when imgmath_latex="latex"
@@ -109,14 +110,14 @@ def generate_latex_macro(image_format: str,
for template_dir in config.templates_path:
for template_suffix in ('.jinja', '_t'):
- template = os.path.join(confdir, template_dir, template_name + template_suffix)
- if os.path.exists(template):
+ template = confdir / template_dir / (template_name + template_suffix)
+ if template.exists():
return LaTeXRenderer().render(template, variables)
- return LaTeXRenderer(templates_path).render(template_name + '.jinja', variables)
+ return LaTeXRenderer([templates_path]).render(template_name + '.jinja', variables)
-def ensure_tempdir(builder: Builder) -> str:
+def ensure_tempdir(builder: Builder) -> Path:
"""Create temporary directory.
use only one tempdir per build -- the use of a directory is cleaner
@@ -124,15 +125,15 @@ def ensure_tempdir(builder: Builder) -> str:
just removing the whole directory (see cleanup_tempdir)
"""
if not hasattr(builder, '_imgmath_tempdir'):
- builder._imgmath_tempdir = tempfile.mkdtemp() # type: ignore[attr-defined]
+ builder._imgmath_tempdir = Path(tempfile.mkdtemp()) # type: ignore[attr-defined]
return builder._imgmath_tempdir # type: ignore[attr-defined]
-def compile_math(latex: str, builder: Builder) -> str:
+def compile_math(latex: str, builder: Builder) -> Path:
"""Compile LaTeX macros for math to DVI."""
tempdir = ensure_tempdir(builder)
- filename = os.path.join(tempdir, 'math.tex')
+ filename = tempdir / 'math.tex'
with open(filename, 'w', encoding='utf-8') as f:
f.write(latex)
@@ -149,16 +150,21 @@ def compile_math(latex: str, builder: Builder) -> str:
command.append('math.tex')
try:
- subprocess.run(command, capture_output=True, cwd=tempdir, check=True,
- encoding='ascii')
+ subprocess.run(
+ command, capture_output=True, cwd=tempdir, check=True, encoding='ascii'
+ )
if imgmath_latex_name in {'xelatex', 'tectonic'}:
- return os.path.join(tempdir, 'math.xdv')
+ return tempdir / 'math.xdv'
else:
- return os.path.join(tempdir, 'math.dvi')
+ return tempdir / 'math.dvi'
except OSError as exc:
- logger.warning(__('LaTeX command %r cannot be run (needed for math '
- 'display), check the imgmath_latex setting'),
- builder.config.imgmath_latex)
+ logger.warning(
+ __(
+ 'LaTeX command %r cannot be run (needed for math '
+ 'display), check the imgmath_latex setting'
+ ),
+ builder.config.imgmath_latex,
+ )
raise InvokeError from exc
except CalledProcessError as exc:
msg = 'latex exited with error'
@@ -171,15 +177,22 @@ def convert_dvi_to_image(command: list[str], name: str) -> tuple[str, str]:
ret = subprocess.run(command, capture_output=True, check=True, encoding='ascii')
return ret.stdout, ret.stderr
except OSError as exc:
- logger.warning(__('%s command %r cannot be run (needed for math '
- 'display), check the imgmath_%s setting'),
- name, command[0], name)
+ logger.warning(
+ __(
+ '%s command %r cannot be run (needed for math '
+ 'display), check the imgmath_%s setting'
+ ),
+ name,
+ command[0],
+ name,
+ )
raise InvokeError from exc
except CalledProcessError as exc:
- raise MathExtError('%s exited with error' % name, exc.stderr, exc.stdout) from exc
+ msg = f'{name} exited with error'
+ raise MathExtError(msg, exc.stderr, exc.stdout) from exc
-def convert_dvi_to_png(dvipath: str, builder: Builder, out_path: str) -> int | None:
+def convert_dvi_to_png(dvipath: Path, builder: Builder, out_path: Path) -> int | None:
"""Convert DVI file to PNG image."""
name = 'dvipng'
command = [builder.config.imgmath_dvipng, '-o', out_path, '-T', 'tight', '-z9']
@@ -202,7 +215,7 @@ def convert_dvi_to_png(dvipath: str, builder: Builder, out_path: str) -> int | N
return depth
-def convert_dvi_to_svg(dvipath: str, builder: Builder, out_path: str) -> int | None:
+def convert_dvi_to_svg(dvipath: Path, builder: Builder, out_path: Path) -> int | None:
"""Convert DVI file to SVG image."""
name = 'dvisvgm'
command = [builder.config.imgmath_dvisvgm, '-o', out_path]
@@ -226,7 +239,7 @@ def convert_dvi_to_svg(dvipath: str, builder: Builder, out_path: str) -> int | N
def render_math(
self: HTML5Translator,
math: str,
-) -> tuple[str | None, int | None]:
+) -> tuple[_StrPath | None, int | None]:
"""Render the LaTeX math expression *math* using latex and dvipng or
dvisvgm.
@@ -245,15 +258,16 @@ def render_math(
unsupported_format_msg = 'imgmath_image_format must be either "png" or "svg"'
raise MathExtError(unsupported_format_msg)
- latex = generate_latex_macro(image_format,
- math,
- self.builder.config,
- self.builder.confdir)
+ latex = generate_latex_macro(
+ image_format, math, self.builder.config, self.builder.confdir
+ )
- filename = f"{sha1(latex.encode(), usedforsecurity=False).hexdigest()}.{image_format}"
- generated_path = os.path.join(self.builder.outdir, self.builder.imagedir, 'math', filename)
+ filename = (
+ f'{sha1(latex.encode(), usedforsecurity=False).hexdigest()}.{image_format}'
+ )
+ generated_path = self.builder.outdir / self.builder.imagedir / 'math' / filename
ensuredir(os.path.dirname(generated_path))
- if os.path.isfile(generated_path):
+ if generated_path.is_file():
if image_format == 'png':
depth = read_png_depth(generated_path)
elif image_format == 'svg':
@@ -261,8 +275,9 @@ def render_math(
return generated_path, depth
# if latex or dvipng (dvisvgm) has failed once, don't bother to try again
- if hasattr(self.builder, '_imgmath_warned_latex') or \
- hasattr(self.builder, '_imgmath_warned_image_translator'):
+ latex_failed = hasattr(self.builder, '_imgmath_warned_latex')
+ trans_failed = hasattr(self.builder, '_imgmath_warned_image_translator')
+ if latex_failed or trans_failed:
return None, None
# .tex -> .dvi
@@ -285,9 +300,10 @@ def render_math(
return generated_path, depth
-def render_maths_to_base64(image_format: str, generated_path: str) -> str:
- with open(generated_path, "rb") as f:
- encoded = base64.b64encode(f.read()).decode(encoding='utf-8')
+def render_maths_to_base64(image_format: str, generated_path: Path) -> str:
+ with open(generated_path, 'rb') as f:
+ content = f.read()
+ encoded = base64.b64encode(content).decode(encoding='utf-8')
if image_format == 'png':
return f'data:image/png;base64,{encoded}'
if image_format == 'svg':
@@ -308,12 +324,12 @@ def clean_up_files(app: Sphinx, exc: Exception) -> None:
# in embed mode, the images are still generated in the math output dir
# to be shared across workers, but are not useful to the final document
with contextlib.suppress(Exception):
- shutil.rmtree(os.path.join(app.builder.outdir, app.builder.imagedir, 'math'))
+ shutil.rmtree(app.builder.outdir / app.builder.imagedir / 'math')
def get_tooltip(self: HTML5Translator, node: Element) -> str:
if self.builder.config.imgmath_add_tooltips:
- return ' alt="%s"' % self.encode(node.astext()).strip()
+ return f' alt="{self.encode(node.astext()).strip()}"'
return ''
@@ -322,33 +338,35 @@ def html_visit_math(self: HTML5Translator, node: nodes.math) -> None:
rendered_path, depth = render_math(self, '$' + node.astext() + '$')
except MathExtError as exc:
msg = str(exc)
- sm = nodes.system_message(msg, type='WARNING', level=2,
- backrefs=[], source=node.astext())
+ sm = nodes.system_message(
+ msg, type='WARNING', level=2, backrefs=[], source=node.astext()
+ )
sm.walkabout(self)
logger.warning(__('display latex %r: %s'), node.astext(), msg)
raise nodes.SkipNode from exc
if rendered_path is None:
# something failed -- use text-only as a bad substitute
- self.body.append('%s' %
- self.encode(node.astext()).strip())
+ self.body.append(
+ f'{self.encode(node.astext()).strip()}'
+ )
else:
if self.builder.config.imgmath_embed:
image_format = self.builder.config.imgmath_image_format.lower()
img_src = render_maths_to_base64(image_format, rendered_path)
else:
bname = os.path.basename(rendered_path)
- relative_path = os.path.join(self.builder.imgpath, 'math', bname)
- img_src = relative_path.replace(os.path.sep, '/')
- c = f'
')
+ relative_path = Path(self.builder.imgpath, 'math', bname)
+ img_src = relative_path.as_posix()
+ align = f' style="vertical-align: {-depth:d}px"' if depth is not None else ''
+ self.body.append(
+ f'
'
+ )
raise nodes.SkipNode
def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> None:
- if node['nowrap']:
+ if node['no-wrap'] or node['nowrap']:
latex = node.astext()
else:
latex = wrap_displaymath(node.astext(), None, False)
@@ -356,8 +374,9 @@ def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> Non
rendered_path, depth = render_math(self, latex)
except MathExtError as exc:
msg = str(exc)
- sm = nodes.system_message(msg, type='WARNING', level=2,
- backrefs=[], source=node.astext())
+ sm = nodes.system_message(
+ msg, type='WARNING', level=2, backrefs=[], source=node.astext()
+ )
sm.walkabout(self)
logger.warning(__('inline latex %r: %s'), node.astext(), msg)
raise nodes.SkipNode from exc
@@ -371,39 +390,46 @@ def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> Non
if rendered_path is None:
# something failed -- use text-only as a bad substitute
- self.body.append('%s
\n' %
- self.encode(node.astext()).strip())
+ self.body.append(
+ f'{self.encode(node.astext()).strip()}\n'
+ )
else:
if self.builder.config.imgmath_embed:
image_format = self.builder.config.imgmath_image_format.lower()
img_src = render_maths_to_base64(image_format, rendered_path)
else:
bname = os.path.basename(rendered_path)
- relative_path = os.path.join(self.builder.imgpath, 'math', bname)
- img_src = relative_path.replace(os.path.sep, '/')
- self.body.append(f'
\n')
+ relative_path = Path(self.builder.imgpath, 'math', bname)
+ img_src = relative_path.as_posix()
+ self.body.append(f'
\n')
raise nodes.SkipNode
def setup(app: Sphinx) -> ExtensionMetadata:
- app.add_html_math_renderer('imgmath',
- (html_visit_math, None),
- (html_visit_displaymath, None))
+ app.add_html_math_renderer(
+ 'imgmath',
+ inline_renderers=(html_visit_math, None),
+ block_renderers=(html_visit_displaymath, None),
+ )
app.add_config_value('imgmath_image_format', 'png', 'html')
app.add_config_value('imgmath_dvipng', 'dvipng', 'html')
app.add_config_value('imgmath_dvisvgm', 'dvisvgm', 'html')
app.add_config_value('imgmath_latex', 'latex', 'html')
app.add_config_value('imgmath_use_preview', False, 'html')
- app.add_config_value('imgmath_dvipng_args',
- ['-gamma', '1.5', '-D', '110', '-bg', 'Transparent'],
- 'html')
+ app.add_config_value(
+ 'imgmath_dvipng_args',
+ ['-gamma', '1.5', '-D', '110', '-bg', 'Transparent'],
+ 'html',
+ )
app.add_config_value('imgmath_dvisvgm_args', ['--no-fonts'], 'html')
app.add_config_value('imgmath_latex_args', [], 'html')
app.add_config_value('imgmath_latex_preamble', '', 'html')
app.add_config_value('imgmath_add_tooltips', True, 'html')
app.add_config_value('imgmath_font_size', 12, 'html')
- app.add_config_value('imgmath_embed', False, 'html', bool)
+ app.add_config_value('imgmath_embed', False, 'html', types=frozenset({bool}))
app.connect('build-finished', clean_up_files)
- return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
+ return {
+ 'version': sphinx.__display_version__,
+ 'parallel_read_safe': True,
+ }
diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py
index 47dec78aa9f..1834950742b 100644
--- a/sphinx/ext/inheritance_diagram.py
+++ b/sphinx/ext/inheritance_diagram.py
@@ -35,15 +35,13 @@ class E(B): pass
import inspect
import os.path
import re
-from collections.abc import Iterable, Sequence
from importlib import import_module
-from typing import TYPE_CHECKING, Any, ClassVar, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
from docutils.parsers.rst import directives
import sphinx
-from sphinx import addnodes
from sphinx.ext.graphviz import (
figure_wrapper,
graphviz,
@@ -54,22 +52,29 @@ class E(B): pass
from sphinx.util.docutils import SphinxDirective
if TYPE_CHECKING:
+ from collections.abc import Iterable, Sequence
+ from typing import Any, ClassVar, Final
+
from docutils.nodes import Node
+ from sphinx import addnodes
from sphinx.application import Sphinx
- from sphinx.environment import BuildEnvironment
+ from sphinx.config import Config
from sphinx.util.typing import ExtensionMetadata, OptionSpec
from sphinx.writers.html5 import HTML5Translator
from sphinx.writers.latex import LaTeXTranslator
from sphinx.writers.texinfo import TexinfoTranslator
-module_sig_re = re.compile(r'''^(?:([\w.]*)\.)? # module names
- (\w+) \s* $ # class/final module name
- ''', re.VERBOSE)
+module_sig_re = re.compile(
+ r"""^
+ (?:([\w.]*)\.)? # module names
+ (\w+) \s* $ # class/final module name
+ """,
+ re.VERBOSE,
+)
-py_builtins = [obj for obj in vars(builtins).values()
- if inspect.isclass(obj)]
+PY_BUILTINS: Final = frozenset(filter(inspect.isclass, vars(builtins).values()))
def try_import(objname: str) -> Any:
@@ -116,17 +121,21 @@ def import_classes(name: str, currmodule: str) -> Any:
if target is None:
raise InheritanceException(
'Could not import class or module %r specified for '
- 'inheritance diagram' % name)
+ 'inheritance diagram' % name
+ )
if inspect.isclass(target):
# If imported object is a class, just return it
return [target]
elif inspect.ismodule(target):
# If imported object is a module, return classes defined on it
- return [cls for cls in target.__dict__.values()
- if inspect.isclass(cls) and cls.__module__ == target.__name__]
- raise InheritanceException('%r specified for inheritance diagram is '
- 'not a class or module' % name)
+ return [
+ cls
+ for cls in target.__dict__.values()
+ if inspect.isclass(cls) and cls.__module__ == target.__name__
+ ]
+ msg = f'{name!r} specified for inheritance diagram is not a class or module'
+ raise InheritanceException(msg)
class InheritanceException(Exception):
@@ -134,16 +143,21 @@ class InheritanceException(Exception):
class InheritanceGraph:
- """
- Given a list of classes, determines the set of classes that they inherit
+ """Given a list of classes, determines the set of classes that they inherit
from all the way to the root "object", and then is able to generate a
graphviz dot graph from them.
"""
- def __init__(self, class_names: list[str], currmodule: str, show_builtins: bool = False,
- private_bases: bool = False, parts: int = 0,
- aliases: dict[str, str] | None = None, top_classes: Sequence[Any] = (),
- ) -> None:
+ def __init__(
+ self,
+ class_names: list[str],
+ currmodule: str,
+ show_builtins: bool = False,
+ private_bases: bool = False,
+ parts: int = 0,
+ aliases: dict[str, str] | None = None,
+ top_classes: Sequence[Any] = (),
+ ) -> None:
"""*class_names* is a list of child classes to show bases from.
If *show_builtins* is True, then Python builtins will be shown
@@ -151,8 +165,9 @@ def __init__(self, class_names: list[str], currmodule: str, show_builtins: bool
"""
self.class_names = class_names
classes = self._import_classes(class_names, currmodule)
- self.class_info = self._class_info(classes, show_builtins,
- private_bases, parts, aliases, top_classes)
+ self.class_info = self._class_info(
+ classes, show_builtins, private_bases, parts, aliases, top_classes
+ )
if not self.class_info:
msg = 'No classes found for inheritance diagram'
raise InheritanceException(msg)
@@ -164,9 +179,15 @@ def _import_classes(self, class_names: list[str], currmodule: str) -> list[Any]:
classes.extend(import_classes(name, currmodule))
return classes
- def _class_info(self, classes: list[Any], show_builtins: bool, private_bases: bool,
- parts: int, aliases: dict[str, str] | None, top_classes: Sequence[Any],
- ) -> list[tuple[str, str, Sequence[str], str | None]]:
+ def _class_info(
+ self,
+ classes: list[Any],
+ show_builtins: bool,
+ private_bases: bool,
+ parts: int,
+ aliases: dict[str, str] | None,
+ top_classes: Sequence[Any],
+ ) -> list[tuple[str, str, Sequence[str], str | None]]:
"""Return name and bases for all classes that are ancestors of
*classes*.
@@ -185,7 +206,7 @@ def _class_info(self, classes: list[Any], show_builtins: bool, private_bases: bo
all_classes = {}
def recurse(cls: Any) -> None:
- if not show_builtins and cls in py_builtins:
+ if not show_builtins and cls in PY_BUILTINS:
return
if not private_bases and cls.__name__.startswith('_'):
return
@@ -197,7 +218,7 @@ def recurse(cls: Any) -> None:
tooltip = None
try:
if cls.__doc__:
- doc = cls.__doc__.strip().split("\n")[0]
+ doc = cls.__doc__.strip().split('\n')[0]
if doc:
tooltip = '"%s"' % doc.replace('"', '\\"')
except Exception: # might raise AttributeError for strange classes
@@ -210,7 +231,7 @@ def recurse(cls: Any) -> None:
return
for base in cls.__bases__:
- if not show_builtins and base in py_builtins:
+ if not show_builtins and base in PY_BUILTINS:
continue
if not private_bases and base.__name__.startswith('_'):
continue
@@ -223,12 +244,11 @@ def recurse(cls: Any) -> None:
return [
(cls_name, fullname, tuple(bases), tooltip)
- for (cls_name, fullname, bases, tooltip)
- in all_classes.values()
+ for (cls_name, fullname, bases, tooltip) in all_classes.values()
]
def class_name(
- self, cls: Any, parts: int = 0, aliases: dict[str, str] | None = None,
+ self, cls: Any, parts: int = 0, aliases: dict[str, str] | None = None
) -> str:
"""Given a class object, return a fully-qualified name.
@@ -254,37 +274,39 @@ def get_all_class_names(self) -> list[str]:
return [fullname for (_, fullname, _, _) in self.class_info]
# These are the default attrs for graphviz
- default_graph_attrs = {
+ default_graph_attrs: dict[str, float | int | str] = {
'rankdir': 'LR',
'size': '"8.0, 12.0"',
'bgcolor': 'transparent',
}
- default_node_attrs = {
+ default_node_attrs: dict[str, float | int | str] = {
'shape': 'box',
'fontsize': 10,
'height': 0.25,
- 'fontname': '"Vera Sans, DejaVu Sans, Liberation Sans, '
- 'Arial, Helvetica, sans"',
+ 'fontname': '"Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans"',
'style': '"setlinewidth(0.5),filled"',
'fillcolor': 'white',
}
- default_edge_attrs = {
+ default_edge_attrs: dict[str, float | int | str] = {
'arrowsize': 0.5,
'style': '"setlinewidth(0.5)"',
}
- def _format_node_attrs(self, attrs: dict[str, Any]) -> str:
+ def _format_node_attrs(self, attrs: dict[str, float | int | str]) -> str:
return ','.join(f'{k}={v}' for k, v in sorted(attrs.items()))
- def _format_graph_attrs(self, attrs: dict[str, Any]) -> str:
+ def _format_graph_attrs(self, attrs: dict[str, float | int | str]) -> str:
return ''.join(f'{k}={v};\n' for k, v in sorted(attrs.items()))
- def generate_dot(self, name: str, urls: dict[str, str] | None = None,
- env: BuildEnvironment | None = None,
- graph_attrs: dict | None = None,
- node_attrs: dict | None = None,
- edge_attrs: dict | None = None,
- ) -> str:
+ def generate_dot(
+ self,
+ name: str,
+ urls: dict[str, str] | None = None,
+ config: Config | None = None,
+ graph_attrs: dict[str, float | int | str] | None = None,
+ node_attrs: dict[str, float | int | str] | None = None,
+ edge_attrs: dict[str, float | int | str] | None = None,
+ ) -> str:
"""Generate a graphviz dot graph from the classes that were passed in
to __init__.
@@ -306,10 +328,10 @@ def generate_dot(self, name: str, urls: dict[str, str] | None = None,
n_attrs.update(node_attrs)
if edge_attrs is not None:
e_attrs.update(edge_attrs)
- if env:
- g_attrs.update(env.config.inheritance_graph_attrs)
- n_attrs.update(env.config.inheritance_node_attrs)
- e_attrs.update(env.config.inheritance_edge_attrs)
+ if config:
+ g_attrs.update(config.inheritance_graph_attrs)
+ n_attrs.update(config.inheritance_node_attrs)
+ e_attrs.update(config.inheritance_edge_attrs)
res: list[str] = [
f'digraph {name} {{\n',
@@ -320,33 +342,31 @@ def generate_dot(self, name: str, urls: dict[str, str] | None = None,
# Write the node
this_node_attrs = n_attrs.copy()
if fullname in urls:
- this_node_attrs["URL"] = f'"{urls[fullname]}"'
- this_node_attrs["target"] = '"_top"'
+ this_node_attrs['URL'] = f'"{urls[fullname]}"'
+ this_node_attrs['target'] = '"_top"'
if tooltip:
- this_node_attrs["tooltip"] = tooltip
- res.append(f' "{cls_name}" [{self._format_node_attrs(this_node_attrs)}];\n')
+ this_node_attrs['tooltip'] = tooltip
+ res.append(
+ f' "{cls_name}" [{self._format_node_attrs(this_node_attrs)}];\n'
+ )
# Write the edges
res.extend(
f' "{base_name}" -> "{cls_name}" [{self._format_node_attrs(e_attrs)}];\n'
for base_name in bases
)
- res.append("}\n")
- return "".join(res)
+ res.append('}\n')
+ return ''.join(res)
class inheritance_diagram(graphviz):
- """
- A docutils node to use as a placeholder for the inheritance diagram.
- """
+ """A docutils node to use as a placeholder for the inheritance diagram."""
pass
class InheritanceDiagram(SphinxDirective):
- """
- Run when the inheritance_diagram directive is first encountered.
- """
+ """Run when the inheritance_diagram directive is first encountered."""
has_content = False
required_arguments = 1
@@ -376,11 +396,13 @@ def run(self) -> list[Node]:
# Create a graph starting with the list of classes
try:
graph = InheritanceGraph(
- class_names, self.env.ref_context.get('py:module'), # type: ignore[arg-type]
+ class_names,
+ self.env.ref_context.get('py:module'), # type: ignore[arg-type]
parts=node['parts'],
private_bases='private-bases' in self.options,
aliases=self.config.inheritance_alias,
- top_classes=node['top-classes'])
+ top_classes=node['top-classes'],
+ )
except InheritanceException as err:
return [node.document.reporter.warning(err, line=self.lineno)]
@@ -390,7 +412,8 @@ def run(self) -> list[Node]:
# removed from the doctree after we're done with them.
for name in graph.get_all_class_names():
refnodes, x = class_role( # type: ignore[misc]
- 'class', ':class:`%s`' % name, name, 0, self.state.inliner)
+ 'class', f':class:`{name}`', name, 0, self.state.inliner
+ )
node.extend(refnodes)
# Store the graph object so we can use it to generate the
# dot file later
@@ -410,9 +433,10 @@ def get_graph_hash(node: inheritance_diagram) -> str:
return hashlib.md5(encoded, usedforsecurity=False).hexdigest()[-10:]
-def html_visit_inheritance_diagram(self: HTML5Translator, node: inheritance_diagram) -> None:
- """
- Output the graph for HTML. This will insert a PNG with clickable
+def html_visit_inheritance_diagram(
+ self: HTML5Translator, node: inheritance_diagram
+) -> None:
+ """Output the graph for HTML. This will insert a PNG with clickable
image map.
"""
graph = node['graph']
@@ -421,10 +445,12 @@ def html_visit_inheritance_diagram(self: HTML5Translator, node: inheritance_diag
name = 'inheritance%s' % graph_hash
# Create a mapping from fully-qualified class names to URLs.
- graphviz_output_format = self.builder.env.config.graphviz_output_format.upper()
- current_filename = os.path.basename(self.builder.current_docname + self.builder.out_suffix)
+ graphviz_output_format = self.config.graphviz_output_format.upper()
+ current_filename = os.path.basename(
+ self.builder.current_docname + self.builder.out_suffix
+ )
urls = {}
- pending_xrefs = cast(Iterable[addnodes.pending_xref], node)
+ pending_xrefs = cast('Iterable[addnodes.pending_xref]', node)
for child in pending_xrefs:
if child.get('refuri') is not None:
# Construct the name from the URI if the reference is external via intersphinx
@@ -440,39 +466,48 @@ def html_visit_inheritance_diagram(self: HTML5Translator, node: inheritance_diag
else:
urls[child['reftitle']] = '#' + child.get('refid')
- dotcode = graph.generate_dot(name, urls, env=self.builder.env)
- render_dot_html(self, node, dotcode, {}, 'inheritance', 'inheritance',
- alt='Inheritance diagram of ' + node['content'])
+ dotcode = graph.generate_dot(name, urls, config=self.config)
+ render_dot_html(
+ self,
+ node,
+ dotcode,
+ {},
+ 'inheritance',
+ 'inheritance',
+ alt='Inheritance diagram of ' + node['content'],
+ )
raise nodes.SkipNode
-def latex_visit_inheritance_diagram(self: LaTeXTranslator, node: inheritance_diagram) -> None:
- """
- Output the graph for LaTeX. This will insert a PDF.
- """
+def latex_visit_inheritance_diagram(
+ self: LaTeXTranslator, node: inheritance_diagram
+) -> None:
+ """Output the graph for LaTeX. This will insert a PDF."""
graph = node['graph']
graph_hash = get_graph_hash(node)
name = 'inheritance%s' % graph_hash
- dotcode = graph.generate_dot(name, env=self.builder.env,
- graph_attrs={'size': '"6.0,6.0"'})
+ dotcode = graph.generate_dot(
+ name, config=self.config, graph_attrs={'size': '"6.0,6.0"'}
+ )
render_dot_latex(self, node, dotcode, {}, 'inheritance')
raise nodes.SkipNode
-def texinfo_visit_inheritance_diagram(self: TexinfoTranslator, node: inheritance_diagram,
- ) -> None:
- """
- Output the graph for Texinfo. This will insert a PNG.
- """
+def texinfo_visit_inheritance_diagram(
+ self: TexinfoTranslator,
+ node: inheritance_diagram,
+) -> None:
+ """Output the graph for Texinfo. This will insert a PNG."""
graph = node['graph']
graph_hash = get_graph_hash(node)
name = 'inheritance%s' % graph_hash
- dotcode = graph.generate_dot(name, env=self.builder.env,
- graph_attrs={'size': '"6.0,6.0"'})
+ dotcode = graph.generate_dot(
+ name, config=self.config, graph_attrs={'size': '"6.0,6.0"'}
+ )
render_dot_texinfo(self, node, dotcode, {}, 'inheritance')
raise nodes.SkipNode
@@ -489,10 +524,14 @@ def setup(app: Sphinx) -> ExtensionMetadata:
html=(html_visit_inheritance_diagram, None),
text=(skip, None),
man=(skip, None),
- texinfo=(texinfo_visit_inheritance_diagram, None))
+ texinfo=(texinfo_visit_inheritance_diagram, None),
+ )
app.add_directive('inheritance-diagram', InheritanceDiagram)
app.add_config_value('inheritance_graph_attrs', {}, '')
app.add_config_value('inheritance_node_attrs', {}, '')
app.add_config_value('inheritance_edge_attrs', {}, '')
app.add_config_value('inheritance_alias', {}, '')
- return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
+ return {
+ 'version': sphinx.__display_version__,
+ 'parallel_read_safe': True,
+ }
diff --git a/sphinx/ext/intersphinx/__init__.py b/sphinx/ext/intersphinx/__init__.py
index bd8eee3aa0a..ea1b32b1701 100644
--- a/sphinx/ext/intersphinx/__init__.py
+++ b/sphinx/ext/intersphinx/__init__.py
@@ -19,20 +19,20 @@
from __future__ import annotations
__all__ = (
+ 'IntersphinxDispatcher',
+ 'IntersphinxRole',
+ 'IntersphinxRoleResolver',
'InventoryAdapter',
'fetch_inventory',
- 'load_mappings',
- 'validate_intersphinx_mapping',
- 'IntersphinxRoleResolver',
- 'inventory_exists',
+ 'inspect_main',
'install_dispatcher',
- 'resolve_reference_in_inventory',
+ 'inventory_exists',
+ 'load_mappings',
+ 'missing_reference',
'resolve_reference_any_inventory',
'resolve_reference_detect_inventory',
- 'missing_reference',
- 'IntersphinxDispatcher',
- 'IntersphinxRole',
- 'inspect_main',
+ 'resolve_reference_in_inventory',
+ 'validate_intersphinx_mapping',
)
from typing import TYPE_CHECKING
@@ -63,10 +63,17 @@
def setup(app: Sphinx) -> ExtensionMetadata:
- app.add_config_value('intersphinx_mapping', {}, 'env')
- app.add_config_value('intersphinx_cache_limit', 5, '')
- app.add_config_value('intersphinx_timeout', None, '')
- app.add_config_value('intersphinx_disabled_reftypes', ['std:doc'], 'env')
+ app.add_config_value('intersphinx_mapping', {}, 'env', types=frozenset({dict}))
+ app.add_config_value('intersphinx_cache_limit', 5, '', types=frozenset({int}))
+ app.add_config_value(
+ 'intersphinx_timeout', None, '', types=frozenset({int, float, type(None)})
+ )
+ app.add_config_value(
+ 'intersphinx_disabled_reftypes',
+ ['std:doc'],
+ 'env',
+ types=frozenset({frozenset, list, set, tuple}),
+ )
app.connect('config-inited', validate_intersphinx_mapping, priority=800)
app.connect('builder-inited', load_mappings)
app.connect('source-read', install_dispatcher)
diff --git a/sphinx/ext/intersphinx/__main__.py b/sphinx/ext/intersphinx/__main__.py
index 9b788d2362c..03574468baf 100644
--- a/sphinx/ext/intersphinx/__main__.py
+++ b/sphinx/ext/intersphinx/__main__.py
@@ -1,9 +1,11 @@
"""Command line interface for the intersphinx extension."""
+from __future__ import annotations
+
import logging as _logging
import sys
-from sphinx.ext.intersphinx import inspect_main
+from sphinx.ext.intersphinx._cli import inspect_main
_logging.basicConfig()
diff --git a/sphinx/ext/intersphinx/_cli.py b/sphinx/ext/intersphinx/_cli.py
index 04ac2876291..0fa86c2298a 100644
--- a/sphinx/ext/intersphinx/_cli.py
+++ b/sphinx/ext/intersphinx/_cli.py
@@ -3,8 +3,9 @@
from __future__ import annotations
import sys
+from pathlib import Path
-from sphinx.ext.intersphinx._load import _fetch_inventory
+from sphinx.ext.intersphinx._load import _fetch_inventory, _InvConfig
def inspect_main(argv: list[str], /) -> int:
@@ -17,26 +18,29 @@ def inspect_main(argv: list[str], /) -> int:
)
return 1
- class MockConfig:
- intersphinx_timeout: int | None = None
- tls_verify = False
- tls_cacerts: str | dict[str, str] | None = None
- user_agent: str = ''
+ filename = argv[0]
+ config = _InvConfig(
+ intersphinx_cache_limit=5,
+ intersphinx_timeout=None,
+ tls_verify=False,
+ tls_cacerts=None,
+ user_agent='',
+ )
try:
- filename = argv[0]
inv_data = _fetch_inventory(
target_uri='',
inv_location=filename,
- config=MockConfig(), # type: ignore[arg-type]
- srcdir='', # type: ignore[arg-type]
+ config=config,
+ srcdir=Path(),
)
for key in sorted(inv_data or {}):
print(key)
inv_entries = sorted(inv_data[key].items())
- for entry, (_proj, _ver, url_path, display_name) in inv_entries:
+ for entry, inv_item in inv_entries:
+ display_name = inv_item.display_name
display_name = display_name * (display_name != '-')
- print(f' {entry:<40} {display_name:<40}: {url_path}')
+ print(f' {entry:<40} {display_name:<40}: {inv_item.uri}')
except ValueError as exc:
print(exc.args[0] % exc.args[1:], file=sys.stderr)
return 1
diff --git a/sphinx/ext/intersphinx/_load.py b/sphinx/ext/intersphinx/_load.py
index eb8b46be807..1966c568be7 100644
--- a/sphinx/ext/intersphinx/_load.py
+++ b/sphinx/ext/intersphinx/_load.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import concurrent.futures
+import dataclasses
import os.path
import posixpath
import time
@@ -20,8 +21,6 @@
if TYPE_CHECKING:
from pathlib import Path
- from urllib3.response import HTTPResponse
-
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.ext.intersphinx._shared import (
@@ -31,7 +30,7 @@
InventoryName,
InventoryURI,
)
- from sphinx.util.typing import Inventory, _ReadableStream
+ from sphinx.util.typing import Inventory
def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None:
@@ -82,7 +81,7 @@ def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None:
'Invalid value `%r` in intersphinx_mapping[%r]. '
'Values must be a (target URI, inventory locations) pair.'
)
- LOGGER.error(msg, value, name)
+ LOGGER.error(msg, value, name) # NoQA: TRY400
del config.intersphinx_mapping[name]
continue
@@ -140,8 +139,9 @@ def load_mappings(app: Sphinx) -> None:
The intersphinx mappings are expected to be normalized.
"""
+ env = app.env
now = int(time.time())
- inventories = InventoryAdapter(app.builder.env)
+ inventories = InventoryAdapter(env)
intersphinx_cache: dict[InventoryURI, InventoryCacheEntry] = inventories.cache
intersphinx_mapping: IntersphinxMapping = app.config.intersphinx_mapping
@@ -170,6 +170,7 @@ def load_mappings(app: Sphinx) -> None:
# This happens when the URI in `intersphinx_mapping` is changed.
del intersphinx_cache[uri]
+ inv_config = _InvConfig.from_config(app.config)
with concurrent.futures.ThreadPoolExecutor() as pool:
futures = [
pool.submit(
@@ -177,7 +178,7 @@ def load_mappings(app: Sphinx) -> None:
project=project,
cache=intersphinx_cache,
now=now,
- config=app.config,
+ config=inv_config,
srcdir=app.srcdir,
)
for project in projects
@@ -202,12 +203,31 @@ def load_mappings(app: Sphinx) -> None:
inventories.main_inventory.setdefault(objtype, {}).update(objects)
+@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
+class _InvConfig:
+ intersphinx_cache_limit: int
+ intersphinx_timeout: int | float | None
+ tls_verify: bool
+ tls_cacerts: str | dict[str, str] | None
+ user_agent: str
+
+ @classmethod
+ def from_config(cls, config: Config) -> _InvConfig:
+ return cls(
+ intersphinx_cache_limit=config.intersphinx_cache_limit,
+ intersphinx_timeout=config.intersphinx_timeout,
+ tls_verify=config.tls_verify,
+ tls_cacerts=config.tls_cacerts,
+ user_agent=config.user_agent,
+ )
+
+
def _fetch_inventory_group(
*,
project: _IntersphinxProject,
cache: dict[InventoryURI, InventoryCacheEntry],
now: int,
- config: Config,
+ config: _InvConfig,
srcdir: Path,
) -> bool:
if config.intersphinx_cache_limit >= 0:
@@ -272,9 +292,9 @@ def _fetch_inventory_group(
else:
issues = '\n'.join(f[0] % f[1:] for f in failures)
LOGGER.warning(
- __('failed to reach any of the inventories with the following issues:')
- + '\n'
- + issues
+ '%s\n%s',
+ __('failed to reach any of the inventories with the following issues:'),
+ issues,
)
return updated
@@ -284,26 +304,49 @@ def fetch_inventory(app: Sphinx, uri: InventoryURI, inv: str) -> Inventory:
return _fetch_inventory(
target_uri=uri,
inv_location=inv,
- config=app.config,
+ config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
)
def _fetch_inventory(
- *, target_uri: InventoryURI, inv_location: str, config: Config, srcdir: Path
+ *, target_uri: InventoryURI, inv_location: str, config: _InvConfig, srcdir: Path
) -> Inventory:
"""Fetch, parse and return an intersphinx inventory file."""
# both *target_uri* (base URI of the links to generate)
# and *inv_location* (actual location of the inventory file)
# can be local or remote URIs
if '://' in target_uri:
- # case: inv URI points to remote resource; strip any existing auth
+ # inv URI points to remote resource; strip any existing auth
target_uri = _strip_basic_auth(target_uri)
+ if '://' in inv_location:
+ raw_data, target_uri = _fetch_inventory_url(
+ target_uri=target_uri, inv_location=inv_location, config=config
+ )
+ else:
+ raw_data = _fetch_inventory_file(inv_location=inv_location, srcdir=srcdir)
+
try:
- if '://' in inv_location:
- f: _ReadableStream[bytes] = _read_from_url(inv_location, config=config)
- else:
- f = open(os.path.join(srcdir, inv_location), 'rb') # NoQA: SIM115
+ invdata = InventoryFile.loads(raw_data, uri=target_uri)
+ except ValueError as exc:
+ msg = f'unknown or unsupported inventory version: {exc!r}'
+ raise ValueError(msg) from exc
+ return invdata
+
+
+def _fetch_inventory_url(
+ *, target_uri: InventoryURI, inv_location: str, config: _InvConfig
+) -> tuple[bytes, str]:
+ try:
+ with requests.get(
+ inv_location,
+ timeout=config.intersphinx_timeout,
+ _user_agent=config.user_agent,
+ _tls_info=(config.tls_verify, config.tls_cacerts),
+ ) as r:
+ r.raise_for_status()
+ raw_data = r.content
+ new_inv_location = r.url
except Exception as err:
err.args = (
'intersphinx inventory %r not fetchable due to %s: %s',
@@ -312,25 +355,25 @@ def _fetch_inventory(
str(err),
)
raise
+
+ if inv_location != new_inv_location:
+ msg = __('intersphinx inventory has moved: %s -> %s')
+ LOGGER.info(msg, inv_location, new_inv_location)
+
+ if target_uri in {
+ inv_location,
+ os.path.dirname(inv_location),
+ os.path.dirname(inv_location) + '/',
+ }:
+ target_uri = os.path.dirname(new_inv_location)
+
+ return raw_data, target_uri
+
+
+def _fetch_inventory_file(*, inv_location: str, srcdir: Path) -> bytes:
try:
- if hasattr(f, 'url'):
- new_inv_location = f.url
- if inv_location != new_inv_location:
- msg = __('intersphinx inventory has moved: %s -> %s')
- LOGGER.info(msg, inv_location, new_inv_location)
-
- if target_uri in {
- inv_location,
- os.path.dirname(inv_location),
- os.path.dirname(inv_location) + '/',
- }:
- target_uri = os.path.dirname(new_inv_location)
- with f:
- try:
- invdata = InventoryFile.load(f, target_uri, posixpath.join)
- except ValueError as exc:
- msg = f'unknown or unsupported inventory version: {exc!r}'
- raise ValueError(msg) from exc
+ with open(srcdir / inv_location, 'rb') as f:
+ raw_data = f.read()
except Exception as err:
err.args = (
'intersphinx inventory %r not readable due to %s: %s',
@@ -339,8 +382,7 @@ def _fetch_inventory(
str(err),
)
raise
- else:
- return invdata
+ return raw_data
def _get_safe_url(url: str) -> str:
@@ -387,37 +429,3 @@ def _strip_basic_auth(url: str) -> str:
if '@' in frags[1]:
frags[1] = frags[1].split('@')[1]
return urlunsplit(frags)
-
-
-def _read_from_url(url: str, *, config: Config) -> HTTPResponse:
- """Reads data from *url* with an HTTP *GET*.
-
- This function supports fetching from resources which use basic HTTP auth as
- laid out by RFC1738 § 3.1. See § 5 for grammar definitions for URLs.
-
- .. seealso:
-
- https://www.ietf.org/rfc/rfc1738.txt
-
- :param url: URL of an HTTP resource
- :type url: ``str``
-
- :return: data read from resource described by *url*
- :rtype: ``file``-like object
- """
- r = requests.get(
- url,
- stream=True,
- timeout=config.intersphinx_timeout,
- _user_agent=config.user_agent,
- _tls_info=(config.tls_verify, config.tls_cacerts),
- )
- r.raise_for_status()
-
- # For inv_location / new_inv_location
- r.raw.url = r.url # type: ignore[union-attr]
-
- # Decode content-body based on the header.
- # xref: https://github.com/psf/requests/issues/2155
- r.raw.decode_content = True
- return r.raw
diff --git a/sphinx/ext/intersphinx/_resolve.py b/sphinx/ext/intersphinx/_resolve.py
index 0dbab63dc69..8a1bf27bac4 100644
--- a/sphinx/ext/intersphinx/_resolve.py
+++ b/sphinx/ext/intersphinx/_resolve.py
@@ -18,7 +18,7 @@
from sphinx.util.osutil import _relative_path
if TYPE_CHECKING:
- from collections.abc import Iterable
+ from collections.abc import Iterable, Sequence, Set
from types import ModuleType
from typing import Any
@@ -27,31 +27,36 @@
from sphinx.application import Sphinx
from sphinx.domains import Domain
+ from sphinx.domains._domains_container import _DomainsContainer
from sphinx.environment import BuildEnvironment
from sphinx.ext.intersphinx._shared import InventoryName
- from sphinx.util.typing import Inventory, InventoryItem, RoleFunction
+ from sphinx.util.inventory import _InventoryItem
+ from sphinx.util.typing import Inventory, RoleFunction
def _create_element_from_result(
- domain: Domain,
+ domain_name: str,
inv_name: InventoryName | None,
- data: InventoryItem,
+ inv_item: _InventoryItem,
node: pending_xref,
contnode: TextElement,
) -> nodes.reference:
- proj, version, uri, dispname = data
+ uri = inv_item.uri
if '://' not in uri and node.get('refdoc'):
# get correct path in case of subdirectories
uri = (_relative_path(Path(), Path(node['refdoc']).parent) / uri).as_posix()
- if version:
- reftitle = _('(in %s v%s)') % (proj, version)
+ if inv_item.project_version:
+ reftitle = _('(in %s v%s)') % (inv_item.project_name, inv_item.project_version)
else:
- reftitle = _('(in %s)') % (proj,)
+ reftitle = _('(in %s)') % (inv_item.project_name,)
+
newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle)
if node.get('refexplicit'):
# use whatever title was given
newnode.append(contnode)
- elif dispname == '-' or (domain.name == 'std' and node['reftype'] == 'keyword'):
+ elif inv_item.display_name == '-' or (
+ domain_name == 'std' and node['reftype'] == 'keyword'
+ ):
# use whatever title was given, but strip prefix
title = contnode.astext()
if inv_name is not None and title.startswith(inv_name + ':'):
@@ -64,14 +69,14 @@ def _create_element_from_result(
newnode.append(contnode)
else:
# else use the given display name (used for :ref:)
- newnode.append(contnode.__class__(dispname, dispname))
+ newnode.append(contnode.__class__(inv_item.display_name, inv_item.display_name))
return newnode
def _resolve_reference_in_domain_by_target(
inv_name: InventoryName | None,
inventory: Inventory,
- domain: Domain,
+ domain_name: str,
objtypes: Iterable[str],
target: str,
node: pending_xref,
@@ -128,46 +133,47 @@ def _resolve_reference_in_domain_by_target(
# This is a fix for terms specifically, but potentially should apply to
# other types.
continue
- return _create_element_from_result(domain, inv_name, data, node, contnode)
+ return _create_element_from_result(domain_name, inv_name, data, node, contnode)
return None
def _resolve_reference_in_domain(
- env: BuildEnvironment,
inv_name: InventoryName | None,
inventory: Inventory,
honor_disabled_refs: bool,
+ disabled_reftypes: Set[str],
domain: Domain,
objtypes: Iterable[str],
node: pending_xref,
contnode: TextElement,
) -> nodes.reference | None:
- obj_types: dict[str, None] = {}.fromkeys(objtypes)
+ domain_name = domain.name
+ obj_types: dict[str, None] = dict.fromkeys(objtypes)
# we adjust the object types for backwards compatibility
- if domain.name == 'std' and 'cmdoption' in obj_types:
+ if domain_name == 'std' and 'cmdoption' in obj_types:
# cmdoptions were stored as std:option until Sphinx 1.6
obj_types['option'] = None
- if domain.name == 'py' and 'attribute' in obj_types:
+ if domain_name == 'py' and 'attribute' in obj_types:
# properties are stored as py:method since Sphinx 2.1
obj_types['method'] = None
# the inventory contains domain:type as objtype
- domain_name = domain.name
obj_types = {f'{domain_name}:{obj_type}': None for obj_type in obj_types}
# now that the objtypes list is complete we can remove the disabled ones
if honor_disabled_refs:
- disabled = set(env.config.intersphinx_disabled_reftypes)
obj_types = {
- obj_type: None for obj_type in obj_types if obj_type not in disabled
+ obj_type: None
+ for obj_type in obj_types
+ if obj_type not in disabled_reftypes
}
objtypes = [*obj_types.keys()]
# without qualification
res = _resolve_reference_in_domain_by_target(
- inv_name, inventory, domain, objtypes, node['reftarget'], node, contnode
+ inv_name, inventory, domain_name, objtypes, node['reftarget'], node, contnode
)
if res is not None:
return res
@@ -177,39 +183,36 @@ def _resolve_reference_in_domain(
if full_qualified_name is None:
return None
return _resolve_reference_in_domain_by_target(
- inv_name, inventory, domain, objtypes, full_qualified_name, node, contnode
+ inv_name, inventory, domain_name, objtypes, full_qualified_name, node, contnode
)
def _resolve_reference(
- env: BuildEnvironment,
inv_name: InventoryName | None,
+ domains: _DomainsContainer,
inventory: Inventory,
honor_disabled_refs: bool,
+ disabled_reftypes: Set[str],
node: pending_xref,
contnode: TextElement,
) -> nodes.reference | None:
# disabling should only be done if no inventory is given
honor_disabled_refs = honor_disabled_refs and inv_name is None
- intersphinx_disabled_reftypes = env.config.intersphinx_disabled_reftypes
- if honor_disabled_refs and '*' in intersphinx_disabled_reftypes:
+ if honor_disabled_refs and '*' in disabled_reftypes:
return None
typ = node['reftype']
if typ == 'any':
- for domain in env.domains.sorted():
- if (
- honor_disabled_refs
- and f'{domain.name}:*' in intersphinx_disabled_reftypes
- ):
+ for domain in domains.sorted():
+ if honor_disabled_refs and f'{domain.name}:*' in disabled_reftypes:
continue
objtypes: Iterable[str] = domain.object_types.keys()
res = _resolve_reference_in_domain(
- env,
inv_name,
inventory,
honor_disabled_refs,
+ disabled_reftypes,
domain,
objtypes,
node,
@@ -223,17 +226,22 @@ def _resolve_reference(
if not domain_name:
# only objects in domains are in the inventory
return None
- if honor_disabled_refs and f'{domain_name}:*' in intersphinx_disabled_reftypes:
+ if honor_disabled_refs and f'{domain_name}:*' in disabled_reftypes:
return None
- domain = env.get_domain(domain_name)
+ try:
+ domain = domains[domain_name]
+ except KeyError as exc:
+ msg = __('Domain %r is not registered') % domain_name
+ raise ExtensionError(msg) from exc
+
objtypes = domain.objtypes_for_role(typ) or ()
if not objtypes:
return None
return _resolve_reference_in_domain(
- env,
inv_name,
inventory,
honor_disabled_refs,
+ disabled_reftypes,
domain,
objtypes,
node,
@@ -259,10 +267,11 @@ def resolve_reference_in_inventory(
"""
assert inventory_exists(env, inv_name)
return _resolve_reference(
- env,
inv_name,
+ env.domains,
InventoryAdapter(env).named_inventory[inv_name],
False,
+ frozenset(env.config.intersphinx_disabled_reftypes),
node,
contnode,
)
@@ -279,10 +288,11 @@ def resolve_reference_any_inventory(
Resolution is tried with the target as is in any inventory.
"""
return _resolve_reference(
- env,
None,
+ env.domains,
InventoryAdapter(env).main_inventory,
honor_disabled_refs,
+ frozenset(env.config.intersphinx_disabled_reftypes),
node,
contnode,
)
@@ -397,27 +407,30 @@ def run(self) -> tuple[list[Node], list[system_message]]:
else:
# the user did not specify a domain,
# so we check first the default (if available) then standard domains
- domains: list[Domain] = []
- if default_domain := self.env.temp_data.get('default_domain'):
- domains.append(default_domain)
- if (
- std_domain := self.env.domains.standard_domain
- ) is not None and std_domain not in domains:
- domains.append(std_domain)
+ default_domain = self.env.current_document.default_domain
+ std_domain = self.env.domains.standard_domain
+ domains: Sequence[Domain]
+ if default_domain is None or std_domain == default_domain:
+ domains = (std_domain,)
+ else:
+ domains = (default_domain, std_domain)
role_func = None
for domain in domains:
- if (role_func := domain.roles.get(role_name)) is not None:
+ role_func = domain.roles.get(role_name)
+ if role_func is not None:
domain_name = domain.name
break
if role_func is None or domain_name is None:
- domains_str = self._concat_strings(d.name for d in domains)
+ domains_str = self._concat_strings(domain.name for domain in domains)
msg = 'role for external cross-reference not found in domains %s: %r'
- possible_roles: set[str] = set()
- for d in domains:
- if o := d.object_types.get(role_name):
- possible_roles.update(f'{d.name}:{r}' for r in o.roles)
+ possible_roles: set[str] = {
+ f'{domain.name}:{r}'
+ for domain in domains
+ if (object_types := domain.object_types.get(role_name))
+ for r in object_types.roles
+ }
if possible_roles:
msg = f'{msg} (perhaps you meant one of: %s)'
self._emit_warning(
@@ -505,18 +518,19 @@ def get_role_name(self, name: str) -> tuple[str, str] | None:
names = name.split(':')
if len(names) == 1:
# role
- default_domain = self.env.temp_data.get('default_domain')
- domain = default_domain.name if default_domain else None
+ if (domain := self.env.current_document.default_domain) is not None:
+ domain_name = domain.name
+ else:
+ domain_name = None
role = names[0]
elif len(names) == 2:
# domain:role:
- domain = names[0]
- role = names[1]
+ domain_name, role = names
else:
return None
- if domain and self.is_existent_role(domain, role):
- return domain, role
+ if domain_name and self.is_existent_role(domain_name, role):
+ return domain_name, role
elif self.is_existent_role('std', role):
return 'std', role
else:
@@ -527,10 +541,11 @@ def is_existent_role(self, domain_name: str, role_name: str) -> bool:
__name__, f'{self.__class__.__name__}.is_existent_role', '', remove=(9, 0)
)
try:
- domain = self.env.get_domain(domain_name)
- return role_name in domain.roles
- except ExtensionError:
+ domain = self.env.domains[domain_name]
+ except KeyError:
return False
+ else:
+ return role_name in domain.roles
def invoke_role(
self, role: tuple[str, str]
@@ -569,7 +584,7 @@ def run(self, **kwargs: Any) -> None:
for node in self.document.findall(pending_xref):
if 'intersphinx' not in node:
continue
- contnode = cast(nodes.TextElement, node[0].deepcopy())
+ contnode = cast('nodes.TextElement', node[0].deepcopy())
inv_name = node['inventory']
if inv_name is not None:
assert inventory_exists(self.env, inv_name)
diff --git a/sphinx/ext/intersphinx/_shared.py b/sphinx/ext/intersphinx/_shared.py
index 44ad9562383..87612b50151 100644
--- a/sphinx/ext/intersphinx/_shared.py
+++ b/sphinx/ext/intersphinx/_shared.py
@@ -2,13 +2,13 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Final, NoReturn
+from typing import TYPE_CHECKING
from sphinx.util import logging
if TYPE_CHECKING:
from collections.abc import Sequence
- from typing import TypeAlias
+ from typing import Any, Final, NoReturn, TypeAlias
from sphinx.environment import BuildEnvironment
from sphinx.util.typing import Inventory
diff --git a/sphinx/ext/linkcode.py b/sphinx/ext/linkcode.py
index 6b722b483ce..05523794348 100644
--- a/sphinx/ext/linkcode.py
+++ b/sphinx/ext/linkcode.py
@@ -18,12 +18,29 @@
from sphinx.util.typing import ExtensionMetadata
+_DOMAIN_KEYS = {
+ 'py': ['module', 'fullname'],
+ 'c': ['names'],
+ 'cpp': ['names'],
+ 'js': ['object', 'fullname'],
+}
+
+
+def add_linkcode_domain(domain: str, keys: list[str], override: bool = False) -> None:
+ """Register a new list of keys to use for a domain.
+
+ .. versionadded:: 8.2
+ """
+ if override or domain not in _DOMAIN_KEYS:
+ _DOMAIN_KEYS[domain] = list(keys)
+
+
class LinkcodeError(SphinxError):
- category = "linkcode error"
+ category = 'linkcode error'
def doctree_read(app: Sphinx, doctree: Node) -> None:
- env = app.builder.env
+ env = app.env
resolve_target = getattr(env.config, 'linkcode_resolve', None)
if not callable(env.config.linkcode_resolve):
@@ -37,13 +54,6 @@ def doctree_read(app: Sphinx, doctree: Node) -> None:
# ``supported_linkcode`` attribute.
node_only_expr = getattr(app.builder, 'supported_linkcode', 'html')
- domain_keys = {
- 'py': ['module', 'fullname'],
- 'c': ['names'],
- 'cpp': ['names'],
- 'js': ['object', 'fullname'],
- }
-
for objnode in list(doctree.findall(addnodes.desc)):
domain = objnode.get('domain')
uris: set[str] = set()
@@ -53,7 +63,7 @@ def doctree_read(app: Sphinx, doctree: Node) -> None:
# Convert signode to a specified format
info = {}
- for key in domain_keys.get(domain, []):
+ for key in _DOMAIN_KEYS.get(domain, ()):
value = signode.get(key)
if not value:
value = ''
@@ -81,4 +91,7 @@ def doctree_read(app: Sphinx, doctree: Node) -> None:
def setup(app: Sphinx) -> ExtensionMetadata:
app.connect('doctree-read', doctree_read)
app.add_config_value('linkcode_resolve', None, '')
- return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
+ return {
+ 'version': sphinx.__display_version__,
+ 'parallel_read_safe': True,
+ }
diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py
index ba8cd03e945..b3592263690 100644
--- a/sphinx/ext/mathjax.py
+++ b/sphinx/ext/mathjax.py
@@ -8,18 +8,20 @@
from __future__ import annotations
import json
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
import sphinx
-from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.errors import ExtensionError
from sphinx.locale import _
from sphinx.util.math import get_node_equation_number
if TYPE_CHECKING:
+ from typing import Any
+
from sphinx.application import Sphinx
+ from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.util.typing import ExtensionMetadata
from sphinx.writers.html5 import HTML5Translator
@@ -31,16 +33,21 @@
def html_visit_math(self: HTML5Translator, node: nodes.math) -> None:
- self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight'))
- self.body.append(self.builder.config.mathjax_inline[0] +
- self.encode(node.astext()) +
- self.builder.config.mathjax_inline[1] + '')
+ self.body.append(
+ self.starttag(node, 'span', '', CLASS='math notranslate nohighlight')
+ )
+ self.body.append(
+ self.builder.config.mathjax_inline[0]
+ + self.encode(node.astext())
+ + self.builder.config.mathjax_inline[1]
+ + ''
+ )
raise nodes.SkipNode
def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> None:
self.body.append(self.starttag(node, 'div', CLASS='math notranslate nohighlight'))
- if node['nowrap']:
+ if node['no-wrap']:
self.body.append(self.encode(node.astext()))
self.body.append('')
raise nodes.SkipNode
@@ -70,26 +77,31 @@ def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> Non
raise nodes.SkipNode
-def install_mathjax(app: Sphinx, pagename: str, templatename: str, context: dict[str, Any],
- event_arg: Any) -> None:
- if (
- app.builder.format != 'html' or
- app.builder.math_renderer_name != 'mathjax' # type: ignore[attr-defined]
- ):
+def install_mathjax(
+ app: Sphinx,
+ pagename: str,
+ templatename: str,
+ context: dict[str, Any],
+ event_arg: Any,
+) -> None:
+ if app.builder.format != 'html':
+ return
+ if app.builder.math_renderer_name != 'mathjax': # type: ignore[attr-defined]
return
if not app.config.mathjax_path:
msg = 'mathjax_path config value must be set for the mathjax extension to work'
raise ExtensionError(msg)
- domain = app.env.domains.math_domain
- builder = cast(StandaloneHTMLBuilder, app.builder)
- if app.registry.html_assets_policy == 'always' or domain.has_equations(pagename):
+ builder = cast('StandaloneHTMLBuilder', app.builder)
+ page_has_equations = context.get('has_maths_elements', False)
+ if app.registry.html_assets_policy == 'always' or page_has_equations:
# Enable mathjax only if equations exists
if app.config.mathjax2_config:
if app.config.mathjax_path == MATHJAX_URL:
logger.warning(
'mathjax_config/mathjax2_config does not work '
- 'for the current MathJax version, use mathjax3_config instead')
+ 'for the current MathJax version, use mathjax3_config instead'
+ )
body = 'MathJax.Hub.Config(%s)' % json.dumps(app.config.mathjax2_config)
builder.add_js_file('', type='text/x-mathjax-config', body=body)
if app.config.mathjax3_config:
@@ -110,9 +122,11 @@ def install_mathjax(app: Sphinx, pagename: str, templatename: str, context: dict
def setup(app: Sphinx) -> ExtensionMetadata:
- app.add_html_math_renderer('mathjax',
- (html_visit_math, None),
- (html_visit_displaymath, None))
+ app.add_html_math_renderer(
+ 'mathjax',
+ inline_renderers=(html_visit_math, None),
+ block_renderers=(html_visit_displaymath, None),
+ )
app.add_config_value('mathjax_path', MATHJAX_URL, 'html')
app.add_config_value('mathjax_options', {}, 'html')
@@ -123,4 +137,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('mathjax3_config', None, 'html')
app.connect('html-page-context', install_mathjax)
- return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
+ return {
+ 'version': sphinx.__display_version__,
+ 'parallel_read_safe': True,
+ }
diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py
index b2a85815d6f..0c5ec1a00ba 100644
--- a/sphinx/ext/napoleon/__init__.py
+++ b/sphinx/ext/napoleon/__init__.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
import sphinx
from sphinx.application import Sphinx
@@ -10,6 +10,8 @@
from sphinx.util import inspect
if TYPE_CHECKING:
+ from typing import Any
+
from sphinx.config import _ConfigRebuild
from sphinx.util.typing import ExtensionMetadata
@@ -265,7 +267,7 @@ def __unicode__(self):
Use the type annotations of class attributes that are documented in the docstring
but do not have a type in the docstring.
- """ # NoQA: D301
+ """
_config_values: dict[str, tuple[Any, _ConfigRebuild]] = {
'napoleon_google_docstring': (True, 'env'),
@@ -317,7 +319,10 @@ def setup(app: Sphinx) -> ExtensionMetadata:
"""
if not isinstance(app, Sphinx):
# probably called by tests
- return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
+ return {
+ 'version': sphinx.__display_version__,
+ 'parallel_read_safe': True,
+ }
_patch_python_domain()
@@ -327,7 +332,10 @@ def setup(app: Sphinx) -> ExtensionMetadata:
for name, (default, rebuild) in Config._config_values.items():
app.add_config_value(name, default, rebuild)
- return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
+ return {
+ 'version': sphinx.__display_version__,
+ 'parallel_read_safe': True,
+ }
def _patch_python_domain() -> None:
diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py
index d9f1eb357dd..1a3ffd8bdc4 100644
--- a/sphinx/ext/napoleon/docstring.py
+++ b/sphinx/ext/napoleon/docstring.py
@@ -51,12 +51,11 @@
_default_regex = re.compile(
r'^default[^_0-9A-Za-z].*$',
)
-_SINGLETONS = ('None', 'True', 'False', 'Ellipsis')
+_SINGLETONS = frozenset({'None', 'True', 'False', 'Ellipsis', '...'})
class Deque(collections.deque[Any]):
- """
- A subclass of deque that mimics ``pockets.iterators.modify_iter``.
+ """A subclass of deque that mimics ``pockets.iterators.modify_iter``.
The `.Deque.get` and `.Deque.next` methods are added.
"""
@@ -64,8 +63,7 @@ class Deque(collections.deque[Any]):
sentinel = object()
def get(self, n: int) -> Any:
- """
- Return the nth element of the stack, or ``self.sentinel`` if n is
+ """Return the nth element of the stack, or ``self.sentinel`` if n is
greater than the stack size.
"""
return self[n] if n < len(self) else self.sentinel
@@ -77,13 +75,183 @@ def next(self) -> Any:
raise StopIteration
-def _convert_type_spec(_type: str, translations: dict[str, str] | None = None) -> str:
- """Convert type specification to reference in reST."""
- if translations is not None and _type in translations:
- return translations[_type]
- if _type == 'None':
- return ':py:obj:`None`'
- return f':py:class:`{_type}`'
+def _recombine_set_tokens(tokens: list[str]) -> list[str]:
+ token_queue = collections.deque(tokens)
+ keywords = ('optional', 'default')
+
+ def takewhile_set(tokens: collections.deque[str]) -> Iterator[str]:
+ open_braces = 0
+ previous_token = None
+ while True:
+ try:
+ token = tokens.popleft()
+ except IndexError:
+ break
+
+ if token == ', ':
+ previous_token = token
+ continue
+
+ if not token.strip():
+ continue
+
+ if token in keywords:
+ tokens.appendleft(token)
+ if previous_token is not None:
+ tokens.appendleft(previous_token)
+ break
+
+ if previous_token is not None:
+ yield previous_token
+ previous_token = None
+
+ if token == '{':
+ open_braces += 1
+ elif token == '}':
+ open_braces -= 1
+
+ yield token
+
+ if open_braces == 0:
+ break
+
+ def combine_set(tokens: collections.deque[str]) -> Iterator[str]:
+ while True:
+ try:
+ token = tokens.popleft()
+ except IndexError:
+ break
+
+ if token == '{':
+ tokens.appendleft('{')
+ yield ''.join(takewhile_set(tokens))
+ else:
+ yield token
+
+ return list(combine_set(token_queue))
+
+
+def _tokenize_type_spec(spec: str) -> list[str]:
+ def postprocess(item: str) -> list[str]:
+ if _default_regex.match(item):
+ default = item[:7]
+ # can't be separated by anything other than a single space
+ # for now
+ other = item[8:]
+
+ return [default, ' ', other]
+ else:
+ return [item]
+
+ tokens = [
+ item
+ for raw_token in _token_regex.split(spec)
+ for item in postprocess(raw_token)
+ if item
+ ]
+ return tokens
+
+
+def _token_type(token: str, debug_location: str | None = None) -> str:
+ def is_numeric(token: str) -> bool:
+ try:
+ # use complex to make sure every numeric value is detected as literal
+ complex(token)
+ except ValueError:
+ return False
+ else:
+ return True
+
+ if token.startswith(' ') or token.endswith(' '):
+ type_ = 'delimiter'
+ elif (
+ is_numeric(token)
+ or (token.startswith('{') and token.endswith('}'))
+ or (token.startswith('"') and token.endswith('"'))
+ or (token.startswith("'") and token.endswith("'"))
+ ):
+ type_ = 'literal'
+ elif token.startswith('{'):
+ logger.warning(
+ __('invalid value set (missing closing brace): %s'),
+ token,
+ location=debug_location,
+ )
+ type_ = 'literal'
+ elif token.endswith('}'):
+ logger.warning(
+ __('invalid value set (missing opening brace): %s'),
+ token,
+ location=debug_location,
+ )
+ type_ = 'literal'
+ elif token.startswith(("'", '"')):
+ logger.warning(
+ __('malformed string literal (missing closing quote): %s'),
+ token,
+ location=debug_location,
+ )
+ type_ = 'literal'
+ elif token.endswith(("'", '"')):
+ logger.warning(
+ __('malformed string literal (missing opening quote): %s'),
+ token,
+ location=debug_location,
+ )
+ type_ = 'literal'
+ elif token in {'optional', 'default'}:
+ # default is not a official keyword (yet) but supported by the
+ # reference implementation (numpydoc) and widely used
+ type_ = 'control'
+ elif _xref_regex.match(token):
+ type_ = 'reference'
+ else:
+ type_ = 'obj'
+
+ return type_
+
+
+def _convert_type_spec(
+ _type: str,
+ translations: dict[str, str] | None = None,
+ debug_location: str | None = None,
+) -> str:
+ if translations is None:
+ translations = {}
+
+ tokens = _tokenize_type_spec(_type)
+ combined_tokens = _recombine_set_tokens(tokens)
+ types = [(token, _token_type(token, debug_location)) for token in combined_tokens]
+
+ converters = {
+ 'literal': lambda x: f'``{x}``',
+ 'obj': lambda x: _convert_type_spec_obj(x, translations),
+ 'control': lambda x: f'*{x}*',
+ 'delimiter': lambda x: x,
+ 'reference': lambda x: x,
+ }
+
+ converted = ''.join(
+ converters.get(type_)(token) # type: ignore[misc]
+ for token, type_ in types
+ )
+
+ return converted
+
+
+def _convert_type_spec_obj(obj: str, translations: dict[str, str]) -> str:
+ translation = translations.get(obj, obj)
+
+ if _xref_regex.match(translation) is not None:
+ return translation
+
+ # use :py:obj: if obj is a standard singleton
+ if translation in _SINGLETONS:
+ if translation == '...': # allow referencing the builtin ...
+ return ':py:obj:`... `'
+ return f':py:obj:`{translation}`'
+
+ return f':py:class:`{translation}`'
class GoogleDocstring:
@@ -252,6 +420,20 @@ def __str__(self) -> str:
"""
return '\n'.join(self.lines())
+ def _get_location(self) -> str | None:
+ try:
+ filepath = inspect.getfile(self._obj) if self._obj is not None else None
+ except TypeError:
+ filepath = None
+ name = self._name
+
+ if filepath is None and name is None:
+ return None
+ elif filepath is None:
+ filepath = ''
+
+ return f'{filepath}:docstring of {name}'
+
def lines(self) -> list[str]:
"""Return the parsed lines of the docstring in reStructuredText format.
@@ -309,7 +491,11 @@ def _consume_field(
_type, _name = _name, _type
if _type and self._config.napoleon_preprocess_types:
- _type = _convert_type_spec(_type, self._config.napoleon_type_aliases or {})
+ _type = _convert_type_spec(
+ _type,
+ translations=self._config.napoleon_type_aliases or {},
+ debug_location=self._get_location(),
+ )
indent = self._get_indent(line) + 1
_descs = [_desc, *self._dedent(self._consume_indented_block(indent))]
@@ -357,7 +543,9 @@ def _consume_returns_section(
if _type and preprocess_types and self._config.napoleon_preprocess_types:
_type = _convert_type_spec(
- _type, self._config.napoleon_type_aliases or {}
+ _type,
+ translations=self._config.napoleon_type_aliases or {},
+ debug_location=self._get_location(),
)
_desc = self.__class__(_desc, self._config).lines()
@@ -428,9 +616,9 @@ def _format_admonition(self, admonition: str, lines: list[str]) -> list[str]:
return [f'.. {admonition}:: {lines[0].strip()}', '']
elif lines:
lines = self._indent(self._dedent(lines), 3)
- return ['.. %s::' % admonition, '', *lines, '']
+ return [f'.. {admonition}::', '', *lines, '']
else:
- return ['.. %s::' % admonition, '']
+ return [f'.. {admonition}::', '']
def _format_block(
self,
@@ -507,7 +695,7 @@ def _format_fields(
field_type: str,
fields: list[tuple[str, str, list[str]]],
) -> list[str]:
- field_type = ':%s:' % field_type.strip()
+ field_type = f':{field_type.strip()}:'
padding = ' ' * len(field_type)
multi = len(fields) > 1
lines: list[str] = []
@@ -558,7 +746,7 @@ def _indent(self, lines: list[str], n: int = 4) -> list[str]:
return [(' ' * n) + line for line in lines]
def _is_indented(self, line: str, indent: int = 1) -> bool:
- for i, s in enumerate(line): # NoQA: SIM110
+ for i, s in enumerate(line):
if i >= indent:
return True
elif not s.isspace():
@@ -672,7 +860,7 @@ def _parse_attribute_docstring(self) -> list[str]:
_type, _desc = self._consume_inline_attribute()
lines = self._format_field('', '', _desc)
if _type:
- lines.extend(['', ':type: %s' % _type])
+ lines.extend(['', f':type: {_type}'])
return lines
def _parse_attributes_section(self, section: str) -> list[str]:
@@ -681,7 +869,7 @@ def _parse_attributes_section(self, section: str) -> list[str]:
if not _type:
_type = self._lookup_annotation(_name)
if self._config.napoleon_use_ivar:
- field = ':ivar %s: ' % _name
+ field = f':ivar {_name}: '
lines.extend(self._format_block(field, _desc))
if _type:
lines.append(f':vartype {_name}: {_type}')
@@ -696,7 +884,7 @@ def _parse_attributes_section(self, section: str) -> list[str]:
lines.extend(self._indent(fields, 3))
if _type:
lines.append('')
- lines.extend(self._indent([':type: %s' % _type], 3))
+ lines.extend(self._indent([f':type: {_type}'], 3))
lines.append('')
if self._config.napoleon_use_ivar:
lines.append('')
@@ -733,10 +921,10 @@ def _parse_generic_section(self, section: str, use_admonition: bool) -> list[str
lines = self._strip_empty(self._consume_to_next_section())
lines = self._dedent(lines)
if use_admonition:
- header = '.. admonition:: %s' % section
+ header = f'.. admonition:: {section}'
lines = self._indent(lines, 3)
else:
- header = '.. rubric:: %s' % section
+ header = f'.. rubric:: {section}'
if lines:
return [header, '', *lines, '']
else:
@@ -754,7 +942,7 @@ def _parse_keyword_arguments_section(self, section: str) -> list[str]:
def _parse_methods_section(self, section: str) -> list[str]:
lines: list[str] = []
for _name, _type, _desc in self._consume_fields(parse_type=False):
- lines.append('.. method:: %s' % _name)
+ lines.append(f'.. method:: {_name}')
if self._opt:
if 'no-index' in self._opt or 'noindex' in self._opt:
lines.append(' :no-index:')
@@ -837,7 +1025,7 @@ def _parse_returns_section(self, section: str) -> list[str]:
if any(field): # only add :returns: if there's something to say
lines.extend(self._format_block(':returns: ', field))
if _type and use_rtype:
- lines.extend([':rtype: %s' % _type, ''])
+ lines.extend([f':rtype: {_type}', ''])
if lines and lines[-1]:
lines.append('')
return lines
@@ -914,187 +1102,6 @@ def _lookup_annotation(self, _name: str) -> str:
return ''
-def _recombine_set_tokens(tokens: list[str]) -> list[str]:
- token_queue = collections.deque(tokens)
- keywords = ('optional', 'default')
-
- def takewhile_set(tokens: collections.deque[str]) -> Iterator[str]:
- open_braces = 0
- previous_token = None
- while True:
- try:
- token = tokens.popleft()
- except IndexError:
- break
-
- if token == ', ':
- previous_token = token
- continue
-
- if not token.strip():
- continue
-
- if token in keywords:
- tokens.appendleft(token)
- if previous_token is not None:
- tokens.appendleft(previous_token)
- break
-
- if previous_token is not None:
- yield previous_token
- previous_token = None
-
- if token == '{':
- open_braces += 1
- elif token == '}':
- open_braces -= 1
-
- yield token
-
- if open_braces == 0:
- break
-
- def combine_set(tokens: collections.deque[str]) -> Iterator[str]:
- while True:
- try:
- token = tokens.popleft()
- except IndexError:
- break
-
- if token == '{':
- tokens.appendleft('{')
- yield ''.join(takewhile_set(tokens))
- else:
- yield token
-
- return list(combine_set(token_queue))
-
-
-def _tokenize_type_spec(spec: str) -> list[str]:
- def postprocess(item: str) -> list[str]:
- if _default_regex.match(item):
- default = item[:7]
- # can't be separated by anything other than a single space
- # for now
- other = item[8:]
-
- return [default, ' ', other]
- else:
- return [item]
-
- tokens = [
- item
- for raw_token in _token_regex.split(spec)
- for item in postprocess(raw_token)
- if item
- ]
- return tokens
-
-
-def _token_type(token: str, location: str | None = None) -> str:
- def is_numeric(token: str) -> bool:
- try:
- # use complex to make sure every numeric value is detected as literal
- complex(token)
- except ValueError:
- return False
- else:
- return True
-
- if token.startswith(' ') or token.endswith(' '):
- type_ = 'delimiter'
- elif (
- is_numeric(token)
- or (token.startswith('{') and token.endswith('}'))
- or (token.startswith('"') and token.endswith('"'))
- or (token.startswith("'") and token.endswith("'"))
- ):
- type_ = 'literal'
- elif token.startswith('{'):
- logger.warning(
- __('invalid value set (missing closing brace): %s'),
- token,
- location=location,
- )
- type_ = 'literal'
- elif token.endswith('}'):
- logger.warning(
- __('invalid value set (missing opening brace): %s'),
- token,
- location=location,
- )
- type_ = 'literal'
- elif token.startswith(("'", '"')):
- logger.warning(
- __('malformed string literal (missing closing quote): %s'),
- token,
- location=location,
- )
- type_ = 'literal'
- elif token.endswith(("'", '"')):
- logger.warning(
- __('malformed string literal (missing opening quote): %s'),
- token,
- location=location,
- )
- type_ = 'literal'
- elif token in {'optional', 'default'}:
- # default is not a official keyword (yet) but supported by the
- # reference implementation (numpydoc) and widely used
- type_ = 'control'
- elif _xref_regex.match(token):
- type_ = 'reference'
- else:
- type_ = 'obj'
-
- return type_
-
-
-def _convert_numpy_type_spec(
- _type: str,
- location: str | None = None,
- translations: dict[str, str] | None = None,
-) -> str:
- if translations is None:
- translations = {}
-
- def convert_obj(
- obj: str, translations: dict[str, str], default_translation: str
- ) -> str:
- translation = translations.get(obj, obj)
-
- # use :class: (the default) only if obj is not a standard singleton
- if translation in _SINGLETONS and default_translation == ':class:`%s`':
- default_translation = ':obj:`%s`'
- elif translation == '...' and default_translation == ':class:`%s`':
- # allow referencing the builtin ...
- default_translation = ':obj:`%s `'
-
- if _xref_regex.match(translation) is None:
- translation = default_translation % translation
-
- return translation
-
- tokens = _tokenize_type_spec(_type)
- combined_tokens = _recombine_set_tokens(tokens)
- types = [(token, _token_type(token, location)) for token in combined_tokens]
-
- converters = {
- 'literal': lambda x: '``%s``' % x,
- 'obj': lambda x: convert_obj(x, translations, ':class:`%s`'),
- 'control': lambda x: '*%s*' % x,
- 'delimiter': lambda x: x,
- 'reference': lambda x: x,
- }
-
- converted = ''.join(
- converters.get(type_)(token) # type: ignore[misc]
- for token, type_ in types
- )
-
- return converted
-
-
class NumpyDocstring(GoogleDocstring):
"""Convert NumPy style docstrings to reStructuredText.
@@ -1202,20 +1209,6 @@ def __init__(
self._directive_sections = ['.. index::']
super().__init__(docstring, config, app, what, name, obj, options)
- def _get_location(self) -> str | None:
- try:
- filepath = inspect.getfile(self._obj) if self._obj is not None else None
- except TypeError:
- filepath = None
- name = self._name
-
- if filepath is None and name is None:
- return None
- elif filepath is None:
- filepath = ''
-
- return f'{filepath}:docstring of {name}'
-
def _escape_args_and_kwargs(self, name: str) -> str:
func = super()._escape_args_and_kwargs
@@ -1242,10 +1235,10 @@ def _consume_field(
_type, _name = _name, _type
if self._config.napoleon_preprocess_types:
- _type = _convert_numpy_type_spec(
+ _type = _convert_type_spec(
_type,
- location=self._get_location(),
translations=self._config.napoleon_type_aliases or {},
+ debug_location=self._get_location(),
)
indent = self._get_indent(line) + 1
@@ -1298,8 +1291,7 @@ def _parse_see_also_section(self, section: str) -> list[str]:
return self._format_admonition('seealso', lines)
def _parse_numpydoc_see_also_section(self, content: list[str]) -> list[str]:
- """
- See Also
+ """See Also
--------
func_name : Descriptive text
continued text
@@ -1349,7 +1341,8 @@ def parse_item_name(text: str) -> tuple[str, str | None]:
return g[3], None
else:
return g[2], g[1]
- raise ValueError('%s is not a item name' % text)
+ msg = f'{text} is not a item name'
+ raise ValueError(msg)
def push_item(name: str | None, rest: list[str]) -> None:
if not name:
@@ -1417,12 +1410,12 @@ def translate(
if role:
link = f':{role}:`{name}`'
else:
- link = ':obj:`%s`' % name
+ link = f':py:obj:`{name}`'
if desc or last_had_desc:
lines += ['']
lines += [link]
else:
- lines[-1] += ', %s' % link
+ lines[-1] += f', {link}'
if desc:
lines += self._indent([' '.join(desc)])
last_had_desc = True
diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py
index 0625621b6db..0e192dbdadf 100644
--- a/sphinx/ext/todo.py
+++ b/sphinx/ext/todo.py
@@ -9,7 +9,7 @@
import functools
import operator
-from typing import TYPE_CHECKING, Any, ClassVar, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
from docutils.parsers.rst import directives
@@ -25,6 +25,7 @@
if TYPE_CHECKING:
from collections.abc import Set
+ from typing import Any, ClassVar
from docutils.nodes import Element, Node
@@ -46,9 +47,7 @@ class todolist(nodes.General, nodes.Element):
class Todo(BaseAdmonition, SphinxDirective):
- """
- A todo entry, displayed (if configured) in the form of an admonition.
- """
+ """A todo entry, displayed (if configured) in the form of an admonition."""
node_class = todo_node
has_content = True
@@ -75,7 +74,7 @@ def run(self) -> list[Node]:
self.state.document.note_explicit_target(todo)
return [todo]
else:
- raise RuntimeError # never reached here
+ raise TypeError # never reached here
class TodoDomain(Domain):
@@ -93,22 +92,22 @@ def merge_domaindata(self, docnames: Set[str], otherdata: dict[str, Any]) -> Non
for docname in docnames:
self.todos[docname] = otherdata['todos'][docname]
- def process_doc(self, env: BuildEnvironment, docname: str,
- document: nodes.document) -> None:
+ def process_doc(
+ self, env: BuildEnvironment, docname: str, document: nodes.document
+ ) -> None:
todos = self.todos.setdefault(docname, [])
for todo in document.findall(todo_node):
env.events.emit('todo-defined', todo)
todos.append(todo)
if env.config.todo_emit_warnings:
- logger.warning(__("TODO entry found: %s"), todo[1].astext(),
- location=todo)
+ logger.warning(
+ __('TODO entry found: %s'), todo[1].astext(), location=todo
+ )
class TodoList(SphinxDirective):
- """
- A list of all todo entries.
- """
+ """A list of all todo entries."""
has_content = False
required_arguments = 0
@@ -134,7 +133,8 @@ def __init__(self, app: Sphinx, doctree: nodes.document, docname: str) -> None:
def process(self, doctree: nodes.document, docname: str) -> None:
todos: list[todo_node] = functools.reduce(
- operator.iadd, self.domain.todos.values(), [])
+ operator.iadd, self.domain.todos.values(), []
+ )
for node in list(doctree.findall(todolist)):
if not self.config.todo_include_todos:
node.parent.remove(node)
@@ -162,11 +162,13 @@ def create_todo_reference(self, todo: todo_node, docname: str) -> nodes.paragrap
if self.config.todo_link_only:
description = _('<>')
else:
- description = (_('(The <> is located in %s, line %d.)') %
- (todo.source, todo.line))
+ description = _('(The <> is located in %s, line %d.)') % (
+ todo.source,
+ todo.line,
+ )
- prefix = description[:description.find('<<')]
- suffix = description[description.find('>>') + 2:]
+ prefix = description[: description.find('<<')]
+ suffix = description[description.find('>>') + 2 :]
para = nodes.paragraph(classes=['todo-source'])
para += nodes.Text(prefix)
@@ -175,7 +177,9 @@ def create_todo_reference(self, todo: todo_node, docname: str) -> nodes.paragrap
linktext = nodes.emphasis(_('original entry'), _('original entry'))
reference = nodes.reference('', '', linktext, internal=True)
try:
- reference['refuri'] = self.builder.get_relative_uri(docname, todo['docname'])
+ reference['refuri'] = self.builder.get_relative_uri(
+ docname, todo['docname']
+ )
reference['refuri'] += '#' + todo['ids'][0]
except NoUri:
# ignore if no URI can be determined, e.g. for LaTeX output
@@ -214,7 +218,7 @@ def latex_visit_todo_node(self: LaTeXTranslator, node: todo_node) -> None:
self.body.append('\n\\begin{sphinxtodo}{')
self.body.append(self.hypertarget_to(node))
- title_node = cast(nodes.title, node[0])
+ title_node = cast('nodes.title', node[0])
title = texescape.escape(title_node.astext(), self.config.latex_engine)
self.body.append('%s:}' % title)
self.no_latex_floats += 1
@@ -237,12 +241,14 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('todo_emit_warnings', False, 'html')
app.add_node(todolist)
- app.add_node(todo_node,
- html=(visit_todo_node, depart_todo_node),
- latex=(latex_visit_todo_node, latex_depart_todo_node),
- text=(visit_todo_node, depart_todo_node),
- man=(visit_todo_node, depart_todo_node),
- texinfo=(visit_todo_node, depart_todo_node))
+ app.add_node(
+ todo_node,
+ html=(visit_todo_node, depart_todo_node),
+ latex=(latex_visit_todo_node, latex_depart_todo_node),
+ text=(visit_todo_node, depart_todo_node),
+ man=(visit_todo_node, depart_todo_node),
+ texinfo=(visit_todo_node, depart_todo_node),
+ )
app.add_directive('todo', Todo)
app.add_directive('todolist', TodoList)
diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py
index b64d67bdad3..79143ffaa67 100644
--- a/sphinx/ext/viewcode.py
+++ b/sphinx/ext/viewcode.py
@@ -2,19 +2,17 @@
from __future__ import annotations
+import importlib.util
import operator
-import os.path
import posixpath
import traceback
-from importlib import import_module
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
-from docutils.nodes import Element, Node
+from docutils.nodes import Element
import sphinx
from sphinx import addnodes
-from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer
from sphinx.transforms.post_transforms import SphinxPostTransform
@@ -24,10 +22,14 @@
from sphinx.util.osutil import _last_modified_time
if TYPE_CHECKING:
- from collections.abc import Iterable, Iterator
+ from collections.abc import Iterator, Set
+ from typing import Any
+
+ from docutils.nodes import Node
from sphinx.application import Sphinx
from sphinx.builders import Builder
+ from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment import BuildEnvironment
from sphinx.util._pathlib import _StrPath
from sphinx.util.typing import ExtensionMetadata
@@ -48,12 +50,30 @@ class viewcode_anchor(Element):
def _get_full_modname(modname: str, attribute: str) -> str | None:
+ if modname is None:
+ # Prevents a TypeError: if the last getattr() call will return None
+ # then it's better to return it directly
+ return None
+
try:
- if modname is None:
- # Prevents a TypeError: if the last getattr() call will return None
- # then it's better to return it directly
+ # Attempt to find full path of module
+ module_path = modname.split('.')
+ num_parts = len(module_path)
+ for i in range(num_parts, 0, -1):
+ mod_root = '.'.join(module_path[:i])
+ module_spec = importlib.util.find_spec(mod_root)
+ if module_spec is not None:
+ break
+ else:
return None
- module = import_module(modname)
+ # Load and execute the module
+ module = importlib.util.module_from_spec(module_spec)
+ if module_spec.loader is None:
+ return None
+ module_spec.loader.exec_module(module)
+ if i != num_parts:
+ for mod in module_path[i:]:
+ module = getattr(module, mod)
# Allow an attribute to have multiple parts and incidentally allow
# repeated .s in the attribute.
@@ -65,8 +85,8 @@ def _get_full_modname(modname: str, attribute: str) -> str | None:
return getattr(value, '__module__', None)
except AttributeError:
# sphinx.ext.viewcode can't follow class instance attribute
- # then AttributeError logging output only verbose mode.
- logger.verbose("Didn't find %s in %s", attribute, modname)
+ # then AttributeError logging output only debug mode.
+ logger.debug("Didn't find %s in %s", attribute, modname)
return None
except Exception as e:
# sphinx.ext.viewcode follow python domain directives.
@@ -79,15 +99,15 @@ def _get_full_modname(modname: str, attribute: str) -> str | None:
def is_supported_builder(builder: Builder) -> bool:
- if builder.format != 'html':
- return False
- if builder.name == 'singlehtml':
- return False
- return not (builder.name.startswith('epub') and not builder.config.viewcode_enable_epub)
+ return (
+ builder.format == 'html'
+ and builder.name != 'singlehtml'
+ and (not builder.name.startswith('epub') or builder.config.viewcode_enable_epub)
+ )
def doctree_read(app: Sphinx, doctree: Node) -> None:
- env = app.builder.env
+ env = app.env
if not hasattr(env, '_viewcode_modules'):
env._viewcode_modules = {} # type: ignore[attr-defined]
@@ -132,7 +152,7 @@ def has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool:
refname = modname
if env.config.viewcode_follow_imported_members:
new_modname = app.emit_firstresult(
- 'viewcode-follow-imported', modname, fullname,
+ 'viewcode-follow-imported', modname, fullname
)
if not new_modname:
new_modname = _get_full_modname(modname, fullname)
@@ -147,11 +167,14 @@ def has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool:
continue
names.add(fullname)
pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/'))
- signode += viewcode_anchor(reftarget=pagename, refid=fullname, refdoc=env.docname)
+ signode += viewcode_anchor(
+ reftarget=pagename, refid=fullname, refdoc=env.docname
+ )
-def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],
- other: BuildEnvironment) -> None:
+def env_merge_info(
+ app: Sphinx, env: BuildEnvironment, docnames: Set[str], other: BuildEnvironment
+) -> None:
if not hasattr(other, '_viewcode_modules'):
return
# create a _viewcode_modules dict on the main environment
@@ -199,8 +222,13 @@ def run(self, **kwargs: Any) -> None:
def convert_viewcode_anchors(self) -> None:
for node in self.document.findall(viewcode_anchor):
anchor = nodes.inline('', _('[source]'), classes=['viewcode-link'])
- refnode = make_refnode(self.app.builder, node['refdoc'], node['reftarget'],
- node['refid'], anchor)
+ refnode = make_refnode(
+ self.app.builder,
+ node['refdoc'],
+ node['reftarget'],
+ node['refid'],
+ anchor,
+ )
node.replace_self(refnode)
def remove_viewcode_anchors(self) -> None:
@@ -228,9 +256,9 @@ def should_generate_module_page(app: Sphinx, modname: str) -> bool:
# Always (re-)generate module page when module filename is not found.
return True
- builder = cast(StandaloneHTMLBuilder, app.builder)
+ builder = cast('StandaloneHTMLBuilder', app.builder)
basename = modname.replace('.', '/') + builder.out_suffix
- page_filename = os.path.join(app.outdir, '_modules/', basename)
+ page_filename = app.outdir / '_modules' / basename
try:
if _last_modified_time(module_filename) <= _last_modified_time(page_filename):
@@ -243,7 +271,7 @@ def should_generate_module_page(app: Sphinx, modname: str) -> bool:
def collect_pages(app: Sphinx) -> Iterator[tuple[str, dict[str, Any], str]]:
- env = app.builder.env
+ env = app.env
if not hasattr(env, '_viewcode_modules'):
return
if not is_supported_builder(app.builder):
@@ -254,10 +282,13 @@ def collect_pages(app: Sphinx) -> Iterator[tuple[str, dict[str, Any], str]]:
modnames = set(env._viewcode_modules)
for modname, entry in status_iterator(
- sorted(env._viewcode_modules.items()),
- __('highlighting module code... '), "blue",
- len(env._viewcode_modules),
- app.verbosity, operator.itemgetter(0)):
+ sorted(env._viewcode_modules.items()),
+ __('highlighting module code... '),
+ 'blue',
+ len(env._viewcode_modules),
+ app.verbosity,
+ operator.itemgetter(0),
+ ):
if not entry:
continue
if not should_generate_module_page(app, modname):
@@ -287,9 +318,11 @@ def collect_pages(app: Sphinx) -> Iterator[tuple[str, dict[str, Any], str]]:
for name, docname in used.items():
type, start, end = tags[name]
backlink = urito(pagename, docname) + '#' + refname + '.' + name
- lines[start] = (f'\n'
- f'
{link_text}\n'
- + lines[start])
+ lines[start] = (
+ f'
\n'
+ f'
{link_text}\n'
+ + lines[start]
+ )
lines[min(end, max_index)] += '
\n'
# try to find parents (for submodules)
@@ -299,18 +332,22 @@ def collect_pages(app: Sphinx) -> Iterator[tuple[str, dict[str, Any], str]]:
parent = parent.rsplit('.', 1)[0]
if parent in modnames:
parents.append({
- 'link': urito(pagename,
- posixpath.join(OUTPUT_DIRNAME, parent.replace('.', '/'))),
- 'title': parent})
- parents.append({'link': urito(pagename, posixpath.join(OUTPUT_DIRNAME, 'index')),
- 'title': _('Module code')})
+ 'link': urito(
+ pagename,
+ posixpath.join(OUTPUT_DIRNAME, parent.replace('.', '/')),
+ ),
+ 'title': parent,
+ })
+ parents.append({
+ 'link': urito(pagename, posixpath.join(OUTPUT_DIRNAME, 'index')),
+ 'title': _('Module code'),
+ })
parents.reverse()
# putting it all together
context = {
'parents': parents,
'title': modname,
- 'body': (_('
Source code for %s
') % modname +
- '\n'.join(lines)),
+ 'body': (_('
Source code for %s
') % modname + '\n'.join(lines)),
}
yield pagename, context, 'page.html'
@@ -330,14 +367,15 @@ def collect_pages(app: Sphinx) -> Iterator[tuple[str, dict[str, Any], str]]:
stack.pop()
html.append('')
stack.append(modname + '.')
- relative_uri = urito(posixpath.join(OUTPUT_DIRNAME, 'index'),
- posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/')))
+ relative_uri = urito(
+ posixpath.join(OUTPUT_DIRNAME, 'index'),
+ posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/')),
+ )
html.append(f'
{modname}\n')
html.append('' * (len(stack) - 1))
context = {
'title': _('Overview: module code'),
- 'body': (_('
All modules for which code is available
') +
- ''.join(html)),
+ 'body': (_('
All modules for which code is available
') + ''.join(html)),
}
yield posixpath.join(OUTPUT_DIRNAME, 'index'), context, 'page.html'
@@ -347,7 +385,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.add_config_value('viewcode_import', None, '')
app.add_config_value('viewcode_enable_epub', False, '')
app.add_config_value('viewcode_follow_imported_members', True, '')
- app.add_config_value('viewcode_line_numbers', False, 'env', bool)
+ app.add_config_value('viewcode_line_numbers', False, 'env', types=frozenset({bool}))
app.connect('doctree-read', doctree_read)
app.connect('env-merge-info', env_merge_info)
app.connect('env-purge-doc', env_purge_doc)
diff --git a/sphinx/extension.py b/sphinx/extension.py
index 88b9d420acd..0b8d83a8f5a 100644
--- a/sphinx/extension.py
+++ b/sphinx/extension.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from packaging.version import InvalidVersion, Version
@@ -11,6 +11,8 @@
from sphinx.util import logging
if TYPE_CHECKING:
+ from typing import Any
+
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.util.typing import ExtensionMetadata
diff --git a/sphinx/highlighting.py b/sphinx/highlighting.py
index c7ae156689d..5e1277f6cd7 100644
--- a/sphinx/highlighting.py
+++ b/sphinx/highlighting.py
@@ -4,7 +4,7 @@
from functools import partial
from importlib import import_module
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
import pygments
from pygments import highlight
@@ -27,12 +27,14 @@
from sphinx.util import logging, texescape
if TYPE_CHECKING:
+ from typing import Any
+
from pygments.formatter import Formatter
from pygments.lexer import Lexer
from pygments.style import Style
-if tuple(map(int, pygments.__version__.split('.')))[:2] < (2, 18):
- from pygments.formatter import Formatter # NoQA: F811
+if tuple(map(int, pygments.__version__.split('.')[:2])) < (2, 18):
+ from pygments.formatter import Formatter
Formatter.__class_getitem__ = classmethod(lambda cls, name: cls) # type: ignore[attr-defined]
@@ -127,7 +129,7 @@ def get_style(self, stylename: str) -> type[Style]:
else:
return get_style_by_name(stylename)
- def get_formatter(self, **kwargs: Any) -> Formatter:
+ def get_formatter(self, **kwargs: Any) -> Formatter[str]:
kwargs.update(self.formatter_args)
return self.formatter(**kwargs)
@@ -135,7 +137,7 @@ def get_lexer(
self,
source: str,
lang: str,
- opts: dict | None = None,
+ opts: dict[str, Any] | None = None,
force: bool = False,
location: Any = None,
) -> Lexer:
@@ -178,7 +180,7 @@ def highlight_block(
self,
source: str,
lang: str,
- opts: dict | None = None,
+ opts: dict[str, Any] | None = None,
force: bool = False,
location: Any = None,
**kwargs: Any,
diff --git a/sphinx/io.py b/sphinx/io.py
index 7da15e1ca6a..cedd1a20cdb 100644
--- a/sphinx/io.py
+++ b/sphinx/io.py
@@ -2,10 +2,10 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from docutils.core import Publisher
-from docutils.io import FileInput, Input, NullOutput
+from docutils.io import FileInput, NullOutput
from docutils.readers import standalone
from docutils.transforms.references import DanglingReferences
from docutils.writers import UnfilteredWriter
@@ -23,8 +23,11 @@
from sphinx.versioning import UIDTransform
if TYPE_CHECKING:
+ from typing import Any
+
from docutils import nodes
from docutils.frontend import Values
+ from docutils.io import Input
from docutils.parsers import Parser
from docutils.transforms import Transform
@@ -36,8 +39,7 @@
class SphinxBaseReader(standalone.Reader): # type: ignore[misc]
- """
- A base class of readers for Sphinx.
+ """A base class of readers for Sphinx.
This replaces reporter by Sphinx's on generating document.
"""
@@ -70,8 +72,7 @@ def get_transforms(self) -> list[type[Transform]]:
return transforms
def new_document(self) -> nodes.document:
- """
- Creates a new document object which has a special reporter object good
+ """Creates a new document object which has a special reporter object good
for logging.
"""
document = super().new_document()
@@ -89,9 +90,7 @@ def new_document(self) -> nodes.document:
class SphinxStandaloneReader(SphinxBaseReader):
- """
- A basic document reader for Sphinx.
- """
+ """A basic document reader for Sphinx."""
def setup(self, app: Sphinx) -> None:
self.transforms = self.transforms + app.registry.get_transforms()
@@ -117,8 +116,7 @@ def read_source(self, env: BuildEnvironment) -> str:
class SphinxI18nReader(SphinxBaseReader):
- """
- A document reader for i18n.
+ """A document reader for i18n.
This returns the source line number of original text as current source line number
to let users know where the error happened.
diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py
index 909505fe5a2..15a3391b45d 100644
--- a/sphinx/jinja2glue.py
+++ b/sphinx/jinja2glue.py
@@ -4,8 +4,9 @@
import os
import os.path
+from pathlib import Path
from pprint import pformat
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound
from jinja2.sandbox import SandboxedEnvironment
@@ -18,10 +19,15 @@
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
+ from typing import Any
from jinja2.environment import Environment
from sphinx.builders import Builder
+ from sphinx.environment.adapters.indexentries import (
+ _RealIndexEntries,
+ _RealIndexEntry,
+ )
from sphinx.theming import Theme
@@ -39,8 +45,7 @@ def _toint(val: str) -> int:
def _todim(val: int | str) -> str:
- """
- Make val a css dimension. In particular the following transformations
+ """Make val a css dimension. In particular the following transformations
are performed:
- None -> 'initial' (default CSS value)
@@ -56,7 +61,9 @@ def _todim(val: int | str) -> str:
return val # type: ignore[return-value]
-def _slice_index(values: list, slices: int) -> Iterator[list]:
+def _slice_index(
+ values: _RealIndexEntries, slices: int
+) -> Iterator[list[_RealIndexEntry]]:
seq = values.copy()
length = 0
for value in values:
@@ -112,8 +119,7 @@ def warning(context: dict[str, Any], message: str, *args: Any, **kwargs: Any) ->
class SphinxFileSystemLoader(FileSystemLoader):
- """
- FileSystemLoader subclass that is not so strict about '..' entries in
+ """FileSystemLoader subclass that is not so strict about '..' entries in
template names.
"""
@@ -126,14 +132,14 @@ def get_source(
else:
legacy_template = None
- for searchpath in self.searchpath:
- filename = os.path.join(searchpath, template)
- f = open_if_exists(filename)
+ for search_path in map(Path, self.searchpath):
+ filename = search_path / template
+ f = open_if_exists(str(filename))
if f is not None:
break
if legacy_template is not None:
- filename = os.path.join(searchpath, legacy_template)
- f = open_if_exists(filename)
+ filename = search_path / legacy_template
+ f = open_if_exists(str(filename))
if f is not None:
break
else:
@@ -150,13 +156,11 @@ def uptodate() -> bool:
except OSError:
return False
- return contents, filename, uptodate
+ return contents, str(filename), uptodate
class BuiltinTemplateLoader(TemplateBridge, BaseLoader):
- """
- Interfaces the rendering environment of jinja2 for use in Sphinx.
- """
+ """Interfaces the rendering environment of jinja2 for use in Sphinx."""
# TemplateBridge interface
@@ -225,7 +229,7 @@ def newest_template_name(self) -> str:
def _newest_template_mtime_name(self) -> tuple[float, str]:
return max(
- (os.stat(os.path.join(root, sfile)).st_mtime_ns / 10**9, sfile)
+ (Path(root, sfile).stat().st_mtime_ns / 10**9, sfile)
for dirname in self.pathchain
for root, _dirs, files in os.walk(dirname)
for sfile in files
diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py
index 4379ea10367..7465864d942 100644
--- a/sphinx/locale/__init__.py
+++ b/sphinx/locale/__init__.py
@@ -15,8 +15,7 @@
class _TranslationProxy:
- """
- The proxy implementation attempts to be as complete as possible, so that
+ """The proxy implementation attempts to be as complete as possible, so that
the lazy objects should mostly work as expected, for example for sorting.
"""
@@ -207,7 +206,7 @@ def setup(app):
def gettext(message: str) -> str:
if not is_translator_registered(catalog, namespace):
# not initialized yet
- return _TranslationProxy(catalog, namespace, message) # type: ignore[return-value] # NoQA: E501
+ return _TranslationProxy(catalog, namespace, message) # type: ignore[return-value]
else:
translator = get_translator(catalog, namespace)
return translator.gettext(message)
diff --git a/sphinx/parsers.py b/sphinx/parsers.py
index cc10ce184b1..70ff3eaae62 100644
--- a/sphinx/parsers.py
+++ b/sphinx/parsers.py
@@ -6,7 +6,6 @@
import docutils.parsers
import docutils.parsers.rst
-from docutils import nodes
from docutils.parsers.rst import states
from docutils.statemachine import StringList
from docutils.transforms.universal import SmartQuotes
@@ -14,6 +13,7 @@
from sphinx.util.rst import append_epilog, prepend_prolog
if TYPE_CHECKING:
+ from docutils import nodes
from docutils.transforms import Transform
from sphinx.application import Sphinx
@@ -23,10 +23,12 @@
class Parser(docutils.parsers.Parser):
- """
- A base class of source parsers. The additional parsers should inherit this class instead
- of ``docutils.parsers.Parser``. Compared with ``docutils.parsers.Parser``, this class
- improves accessibility to Sphinx APIs.
+ """A base class of source parsers.
+
+ The additional parsers should inherit this class
+ instead of ``docutils.parsers.Parser``.
+ Compared with ``docutils.parsers.Parser``,
+ this class improves accessibility to Sphinx APIs.
The subclasses can access sphinx core runtime objects (app, config and env).
"""
@@ -51,8 +53,7 @@ class RSTParser(docutils.parsers.rst.Parser, Parser):
"""A reST parser for Sphinx."""
def get_transforms(self) -> list[type[Transform]]:
- """
- Sphinx's reST parser replaces a transform class for smart-quotes by its own
+ """Sphinx's reST parser replaces a transform class for smart-quotes by its own
refs: sphinx.io.SphinxStandaloneReader
"""
diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py
index 0242cf9c8be..802ae14a454 100644
--- a/sphinx/pycode/__init__.py
+++ b/sphinx/pycode/__init__.py
@@ -4,7 +4,7 @@
import tokenize
from importlib import import_module
-from typing import TYPE_CHECKING, Any, Literal
+from typing import TYPE_CHECKING
from sphinx.errors import PycodeError
from sphinx.pycode.parser import Parser
@@ -13,6 +13,7 @@
if TYPE_CHECKING:
import os
from inspect import Signature
+ from typing import Any, Literal
class ModuleAnalyzer:
diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py
index f6bcbda9759..b1521595b49 100644
--- a/sphinx/pycode/ast.py
+++ b/sphinx/pycode/ast.py
@@ -3,7 +3,10 @@
from __future__ import annotations
import ast
-from typing import NoReturn, overload
+from typing import TYPE_CHECKING, overload
+
+if TYPE_CHECKING:
+ from typing import NoReturn
OPERATORS: dict[type[ast.AST], str] = {
ast.Add: '+',
@@ -29,11 +32,11 @@
@overload
-def unparse(node: None, code: str = '') -> None: ... # NoQA: E704
+def unparse(node: None, code: str = '') -> None: ...
@overload
-def unparse(node: ast.AST, code: str = '') -> str: ... # NoQA: E704
+def unparse(node: ast.AST, code: str = '') -> str: ...
def unparse(node: ast.AST | None, code: str = '') -> str | None:
diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py
index 6b04c6f0e21..34d30200f75 100644
--- a/sphinx/pycode/parser.py
+++ b/sphinx/pycode/parser.py
@@ -10,13 +10,16 @@
import operator
import re
import tokenize
-from inspect import Signature
from token import DEDENT, INDENT, NAME, NEWLINE, NUMBER, OP, STRING
from tokenize import COMMENT, NL
-from typing import Any
+from typing import TYPE_CHECKING
from sphinx.pycode.ast import unparse as ast_unparse
+if TYPE_CHECKING:
+ from inspect import Signature
+ from typing import Any
+
comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$')
indent_re = re.compile('^\\s*$')
emptyline_re = re.compile('^\\s*(#.*)?$')
@@ -40,7 +43,7 @@ def get_lvar_names(node: ast.AST, self: ast.arg | None = None) -> list[str]:
This raises `TypeError` if the assignment does not create new variable::
ary[0] = 'foo'
- dic["bar"] = 'baz'
+ dic['bar'] = 'baz'
# => TypeError
"""
if self:
@@ -111,7 +114,7 @@ def __init__(
self.end = end
self.source = source
- def __eq__(self, other: Any) -> bool:
+ def __eq__(self, other: object) -> bool:
if isinstance(other, int):
return self.kind == other
elif isinstance(other, str):
@@ -283,13 +286,13 @@ def add_variable_comment(self, name: str, comment: str) -> None:
qualname = self.get_qualname_for(name)
if qualname:
basename = '.'.join(qualname[:-1])
- self.comments[(basename, name)] = comment
+ self.comments[basename, name] = comment
def add_variable_annotation(self, name: str, annotation: ast.AST) -> None:
qualname = self.get_qualname_for(name)
if qualname:
basename = '.'.join(qualname[:-1])
- self.annotations[(basename, name)] = ast_unparse(annotation)
+ self.annotations[basename, name] = ast_unparse(annotation)
def is_final(self, decorators: list[ast.expr]) -> bool:
final = []
diff --git a/sphinx/pygments_styles.py b/sphinx/pygments_styles.py
index 55ca71b19d2..a6eabcb24f7 100644
--- a/sphinx/pygments_styles.py
+++ b/sphinx/pygments_styles.py
@@ -1,5 +1,7 @@
"""Sphinx theme specific highlighting styles."""
+from __future__ import annotations
+
from pygments.style import Style
from pygments.styles.friendly import FriendlyStyle
from pygments.token import (
@@ -20,8 +22,7 @@ class NoneStyle(Style):
class SphinxStyle(Style):
- """
- Like friendly, but a bit darker to enhance contrast on the green
+ """Like friendly, but a bit darker to enhance contrast on the green
background.
"""
@@ -37,9 +38,7 @@ class SphinxStyle(Style):
class PyramidStyle(Style):
- """
- Pylons/pyramid pygments style based on friendly style, by Blaise Laflamme.
- """
+ """Pylons/pyramid pygments style based on friendly style, by Blaise Laflamme."""
# work in progress...
diff --git a/sphinx/registry.py b/sphinx/registry.py
index b4c2478653b..ce52a03b323 100644
--- a/sphinx/registry.py
+++ b/sphinx/registry.py
@@ -6,9 +6,9 @@
from importlib import import_module
from importlib.metadata import entry_points
from types import MethodType
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
-from sphinx.domains import Domain, Index, ObjType
+from sphinx.domains import ObjType
from sphinx.domains.std import GenericObject, Target
from sphinx.errors import ExtensionError, SphinxError, VersionRequirementError
from sphinx.extension import Extension
@@ -22,7 +22,8 @@
if TYPE_CHECKING:
import os
- from collections.abc import Callable, Iterator, Sequence
+ from collections.abc import Callable, Iterator, Mapping, Sequence
+ from typing import Any, TypeAlias
from docutils import nodes
from docutils.core import Publisher
@@ -31,26 +32,43 @@
from docutils.parsers.rst import Directive
from docutils.transforms import Transform
+ from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.config import Config
+ from sphinx.domains import Domain, Index
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc import Documenter
+ from sphinx.util.docfields import Field
from sphinx.util.typing import (
ExtensionMetadata,
RoleFunction,
TitleGetter,
_ExtensionSetupFunc,
)
+ from sphinx.writers.html5 import HTML5Translator
+
+ # visit/depart function
+ # the parameters should be (SphinxTranslator, Element)
+ # or any subtype of either, but mypy rejects this.
+ _NodeHandler: TypeAlias = Callable[[Any, Any], None]
+ _NodeHandlerPair: TypeAlias = tuple[_NodeHandler, _NodeHandler | None]
+
+ _MathsRenderer: TypeAlias = Callable[[HTML5Translator, nodes.math], None]
+ _MathsBlockRenderer: TypeAlias = Callable[[HTML5Translator, nodes.math_block], None]
+ _MathsInlineRenderers: TypeAlias = tuple[_MathsRenderer, _MathsRenderer | None]
+ _MathsBlockRenderers: TypeAlias = tuple[
+ _MathsBlockRenderer, _MathsBlockRenderer | None
+ ]
logger = logging.getLogger(__name__)
# list of deprecated extensions. Keys are extension name.
# Values are Sphinx version that merge the extension.
EXTENSION_BLACKLIST = {
- "sphinxjp.themecore": "1.2",
+ 'sphinxjp.themecore': '1.2',
'sphinxcontrib-napoleon': '1.3',
- "sphinxprettysearchresults": "2.0.0",
+ 'sphinxprettysearchresults': '2.0.0',
}
@@ -93,10 +111,14 @@ def __init__(self) -> None:
#: HTML inline and block math renderers
#: a dict of name -> tuple of visit function and depart function
- self.html_inline_math_renderers: dict[str,
- tuple[Callable, Callable | None]] = {}
- self.html_block_math_renderers: dict[str,
- tuple[Callable, Callable | None]] = {}
+ self.html_inline_math_renderers: dict[
+ str,
+ _MathsInlineRenderers,
+ ] = {}
+ self.html_block_math_renderers: dict[
+ str,
+ _MathsBlockRenderers,
+ ] = {}
#: HTML assets
self.html_assets_policy: str = 'per_page'
@@ -126,7 +148,7 @@ def __init__(self) -> None:
#: custom handlers for translators
#: a dict of builder name -> dict of node name -> visitor and departure functions
- self.translation_handlers: dict[str, dict[str, tuple[Callable, Callable | None]]] = {}
+ self.translation_handlers: dict[str, dict[str, _NodeHandlerPair]] = {}
#: additional transforms; list of transforms
self.transforms: list[type[Transform]] = []
@@ -141,10 +163,14 @@ def autodoc_attrgettrs(self) -> dict[type, Callable[[Any, str, Any], Any]]:
def add_builder(self, builder: type[Builder], override: bool = False) -> None:
logger.debug('[app] adding builder: %r', builder)
if not hasattr(builder, 'name'):
- raise ExtensionError(__('Builder class %s has no "name" attribute') % builder)
+ raise ExtensionError(
+ __('Builder class %s has no "name" attribute') % builder
+ )
if builder.name in self.builders and not override:
- raise ExtensionError(__('Builder %r already exists (in module %s)') %
- (builder.name, self.builders[builder.name].__module__))
+ raise ExtensionError(
+ __('Builder %r already exists (in module %s)')
+ % (builder.name, self.builders[builder.name].__module__)
+ )
self.builders[builder.name] = builder
def preload_builder(self, app: Sphinx, name: str) -> None:
@@ -156,8 +182,13 @@ def preload_builder(self, app: Sphinx, name: str) -> None:
try:
entry_point = builder_entry_points[name]
except KeyError as exc:
- raise SphinxError(__('Builder name %s not registered or available'
- ' through entry point') % name) from exc
+ raise SphinxError(
+ __(
+ 'Builder name %s not registered or available'
+ ' through entry point'
+ )
+ % name
+ ) from exc
self.load_extension(app, entry_point.module)
@@ -189,39 +220,52 @@ def create_domains(self, env: BuildEnvironment) -> Iterator[Domain]:
yield domain
- def add_directive_to_domain(self, domain: str, name: str,
- cls: type[Directive], override: bool = False) -> None:
+ def add_directive_to_domain(
+ self, domain: str, name: str, cls: type[Directive], override: bool = False
+ ) -> None:
logger.debug('[app] adding directive to domain: %r', (domain, name, cls))
if domain not in self.domains:
raise ExtensionError(__('domain %s not yet registered') % domain)
- directives: dict[str, type[Directive]] = self.domain_directives.setdefault(domain, {})
+ directives: dict[str, type[Directive]] = self.domain_directives.setdefault(
+ domain, {}
+ )
if name in directives and not override:
- raise ExtensionError(__('The %r directive is already registered to domain %s') %
- (name, domain))
+ raise ExtensionError(
+ __('The %r directive is already registered to domain %s')
+ % (name, domain)
+ )
directives[name] = cls
- def add_role_to_domain(self, domain: str, name: str,
- role: RoleFunction | XRefRole, override: bool = False,
- ) -> None:
+ def add_role_to_domain(
+ self,
+ domain: str,
+ name: str,
+ role: RoleFunction | XRefRole,
+ override: bool = False,
+ ) -> None:
logger.debug('[app] adding role to domain: %r', (domain, name, role))
if domain not in self.domains:
raise ExtensionError(__('domain %s not yet registered') % domain)
roles = self.domain_roles.setdefault(domain, {})
if name in roles and not override:
- raise ExtensionError(__('The %r role is already registered to domain %s') %
- (name, domain))
+ raise ExtensionError(
+ __('The %r role is already registered to domain %s') % (name, domain)
+ )
roles[name] = role
- def add_index_to_domain(self, domain: str, index: type[Index],
- override: bool = False) -> None:
+ def add_index_to_domain(
+ self, domain: str, index: type[Index], override: bool = False
+ ) -> None:
logger.debug('[app] adding index to domain: %r', (domain, index))
if domain not in self.domains:
raise ExtensionError(__('domain %s not yet registered') % domain)
indices = self.domain_indices.setdefault(domain, [])
if index in indices and not override:
- raise ExtensionError(__('The %r index is already registered to domain %s') %
- (index.name, domain))
+ raise ExtensionError(
+ __('The %r index is already registered to domain %s')
+ % (index.name, domain)
+ )
indices.append(index)
def add_object_type(
@@ -229,30 +273,45 @@ def add_object_type(
directivename: str,
rolename: str,
indextemplate: str = '',
- parse_node: Callable | None = None,
+ parse_node: Callable[[BuildEnvironment, str, addnodes.desc_signature], str]
+ | None = None,
ref_nodeclass: type[TextElement] | None = None,
objname: str = '',
- doc_field_types: Sequence = (),
+ doc_field_types: Sequence[Field] = (),
override: bool = False,
) -> None:
- logger.debug('[app] adding object type: %r',
- (directivename, rolename, indextemplate, parse_node,
- ref_nodeclass, objname, doc_field_types))
+ logger.debug(
+ '[app] adding object type: %r',
+ (
+ directivename,
+ rolename,
+ indextemplate,
+ parse_node,
+ ref_nodeclass,
+ objname,
+ doc_field_types,
+ ),
+ )
# create a subclass of GenericObject as the new directive
- directive = type(directivename,
- (GenericObject, object),
- {'indextemplate': indextemplate,
- 'parse_node': parse_node and staticmethod(parse_node),
- 'doc_field_types': doc_field_types})
+ directive = type(
+ directivename,
+ (GenericObject, object),
+ {
+ 'indextemplate': indextemplate,
+ 'parse_node': parse_node and staticmethod(parse_node),
+ 'doc_field_types': doc_field_types,
+ },
+ )
self.add_directive_to_domain('std', directivename, directive)
self.add_role_to_domain('std', rolename, XRefRole(innernodeclass=ref_nodeclass))
object_types = self.domain_object_types.setdefault('std', {})
if directivename in object_types and not override:
- raise ExtensionError(__('The %r object_type is already registered') %
- directivename)
+ raise ExtensionError(
+ __('The %r object_type is already registered') % directivename
+ )
object_types[directivename] = ObjType(objname or directivename, rolename)
def add_crossref_type(
@@ -264,24 +323,31 @@ def add_crossref_type(
objname: str = '',
override: bool = False,
) -> None:
- logger.debug('[app] adding crossref type: %r',
- (directivename, rolename, indextemplate, ref_nodeclass, objname))
+ logger.debug(
+ '[app] adding crossref type: %r',
+ (directivename, rolename, indextemplate, ref_nodeclass, objname),
+ )
# create a subclass of Target as the new directive
- directive = type(directivename,
- (Target, object),
- {'indextemplate': indextemplate})
+ directive = type(
+ directivename,
+ (Target, object),
+ {'indextemplate': indextemplate},
+ )
self.add_directive_to_domain('std', directivename, directive)
self.add_role_to_domain('std', rolename, XRefRole(innernodeclass=ref_nodeclass))
object_types = self.domain_object_types.setdefault('std', {})
if directivename in object_types and not override:
- raise ExtensionError(__('The %r crossref_type is already registered') %
- directivename)
+ raise ExtensionError(
+ __('The %r crossref_type is already registered') % directivename
+ )
object_types[directivename] = ObjType(objname or directivename, rolename)
- def add_source_suffix(self, suffix: str, filetype: str, override: bool = False) -> None:
+ def add_source_suffix(
+ self, suffix: str, filetype: str, override: bool = False
+ ) -> None:
logger.debug('[app] adding source_suffix: %r, %r', suffix, filetype)
if suffix in self.source_suffix and not override:
raise ExtensionError(__('source_suffix %r is already registered') % suffix)
@@ -293,15 +359,18 @@ def add_source_parser(self, parser: type[Parser], override: bool = False) -> Non
# create a map from filetype to parser
for filetype in parser.supported:
if filetype in self.source_parsers and not override:
- raise ExtensionError(__('source_parser for %r is already registered') %
- filetype)
+ raise ExtensionError(
+ __('source_parser for %r is already registered') % filetype
+ )
self.source_parsers[filetype] = parser
def get_source_parser(self, filetype: str) -> type[Parser]:
try:
return self.source_parsers[filetype]
except KeyError as exc:
- raise SphinxError(__('Source parser for %s not registered') % filetype) from exc
+ raise SphinxError(
+ __('Source parser for %s not registered') % filetype
+ ) from exc
def get_source_parsers(self) -> dict[str, type[Parser]]:
return self.source_parsers
@@ -313,28 +382,32 @@ def create_source_parser(self, app: Sphinx, filename: str) -> Parser:
parser.set_application(app)
return parser
- def add_translator(self, name: str, translator: type[nodes.NodeVisitor],
- override: bool = False) -> None:
+ def add_translator(
+ self, name: str, translator: type[nodes.NodeVisitor], override: bool = False
+ ) -> None:
logger.debug('[app] Change of translator for the %s builder.', name)
if name in self.translators and not override:
raise ExtensionError(__('Translator for %r already exists') % name)
self.translators[name] = translator
def add_translation_handlers(
- self,
- node: type[Element],
- **kwargs: tuple[Callable, Callable | None],
+ self, node: type[Element], **kwargs: _NodeHandlerPair
) -> None:
logger.debug('[app] adding translation_handlers: %r, %r', node, kwargs)
for builder_name, handlers in kwargs.items():
- translation_handlers = self.translation_handlers.setdefault(builder_name, {})
+ translation_handlers = self.translation_handlers.setdefault(
+ builder_name, {}
+ )
try:
visit, depart = handlers # unpack once for assertion
translation_handlers[node.__name__] = (visit, depart)
except ValueError as exc:
raise ExtensionError(
- __('kwargs for add_node() must be a (visit, depart) '
- 'function tuple: %r=%r') % (builder_name, handlers),
+ __(
+ 'kwargs for add_node() must be a (visit, depart) '
+ 'function tuple: %r=%r'
+ )
+ % (builder_name, handlers),
) from exc
def get_translator_class(self, builder: Builder) -> type[nodes.NodeVisitor]:
@@ -381,8 +454,9 @@ def get_post_transforms(self) -> list[type[Transform]]:
def add_documenter(self, objtype: str, documenter: type[Documenter]) -> None:
self.documenters[objtype] = documenter
- def add_autodoc_attrgetter(self, typ: type,
- attrgetter: Callable[[Any, str, Any], Any]) -> None:
+ def add_autodoc_attrgetter(
+ self, typ: type, attrgetter: Callable[[Any, str, Any], Any]
+ ) -> None:
self.autodoc_attrgetters[typ] = attrgetter
def add_css_files(self, filename: str, **attributes: Any) -> None:
@@ -397,7 +471,7 @@ def has_latex_package(self, name: str) -> bool:
return bool([x for x in packages if x[0] == name])
def add_latex_package(
- self, name: str, options: str | None, after_hyperref: bool = False,
+ self, name: str, options: str | None, after_hyperref: bool = False
) -> None:
if self.has_latex_package(name):
logger.warning("latex package '%s' already included", name)
@@ -412,9 +486,12 @@ def add_enumerable_node(
self,
node: type[Node],
figtype: str,
- title_getter: TitleGetter | None = None, override: bool = False,
+ title_getter: TitleGetter | None = None,
+ override: bool = False,
) -> None:
- logger.debug('[app] adding enumerable node: (%r, %r, %r)', node, figtype, title_getter)
+ logger.debug(
+ '[app] adding enumerable node: (%r, %r, %r)', node, figtype, title_getter
+ )
if node in self.enumerable_nodes and not override:
raise ExtensionError(__('enumerable_node %r already registered') % node)
self.enumerable_nodes[node] = (figtype, title_getter)
@@ -422,11 +499,15 @@ def add_enumerable_node(
def add_html_math_renderer(
self,
name: str,
- inline_renderers: tuple[Callable, Callable | None] | None,
- block_renderers: tuple[Callable, Callable | None] | None,
+ inline_renderers: _MathsInlineRenderers | None,
+ block_renderers: _MathsBlockRenderers | None,
) -> None:
- logger.debug('[app] adding html_math_renderer: %s, %r, %r',
- name, inline_renderers, block_renderers)
+ logger.debug(
+ '[app] adding html_math_renderer: %s, %r, %r',
+ name,
+ inline_renderers,
+ block_renderers,
+ )
if name in self.html_inline_math_renderers:
raise ExtensionError(__('math renderer %s is already registered') % name)
@@ -443,9 +524,14 @@ def load_extension(self, app: Sphinx, extname: str) -> None:
if extname in app.extensions: # already loaded
return
if extname in EXTENSION_BLACKLIST:
- logger.warning(__('the extension %r was already merged with Sphinx since '
- 'version %s; this extension is ignored.'),
- extname, EXTENSION_BLACKLIST[extname])
+ logger.warning(
+ __(
+ 'the extension %r was already merged with Sphinx since '
+ 'version %s; this extension is ignored.'
+ ),
+ extname,
+ EXTENSION_BLACKLIST[extname],
+ )
return
# update loading context
@@ -455,13 +541,19 @@ def load_extension(self, app: Sphinx, extname: str) -> None:
mod = import_module(extname)
except ImportError as err:
logger.verbose(__('Original exception:\n') + traceback.format_exc())
- raise ExtensionError(__('Could not import extension %s') % extname,
- err) from err
+ raise ExtensionError(
+ __('Could not import extension %s') % extname, err
+ ) from err
setup: _ExtensionSetupFunc | None = getattr(mod, 'setup', None)
if setup is None:
- logger.warning(__('extension %r has no setup() function; is it really '
- 'a Sphinx extension module?'), extname)
+ logger.warning(
+ __(
+ 'extension %r has no setup() function; is it really '
+ 'a Sphinx extension module?'
+ ),
+ extname,
+ )
metadata: ExtensionMetadata = {}
else:
try:
@@ -469,27 +561,33 @@ def load_extension(self, app: Sphinx, extname: str) -> None:
except VersionRequirementError as err:
# add the extension name to the version required
raise VersionRequirementError(
- __('The %s extension used by this project needs at least '
- 'Sphinx v%s; it therefore cannot be built with this '
- 'version.') % (extname, err),
+ __(
+ 'The %s extension used by this project needs at least '
+ 'Sphinx v%s; it therefore cannot be built with this '
+ 'version.'
+ )
+ % (extname, err),
) from err
if metadata is None:
metadata = {}
elif not isinstance(metadata, dict):
- logger.warning(__('extension %r returned an unsupported object from '
- 'its setup() function; it should return None or a '
- 'metadata dictionary'), extname)
+ logger.warning(
+ __(
+ 'extension %r returned an unsupported object from '
+ 'its setup() function; it should return None or a '
+ 'metadata dictionary'
+ ),
+ extname,
+ )
metadata = {}
app.extensions[extname] = Extension(extname, mod, **metadata)
- def get_envversion(self, app: Sphinx) -> dict[str, int]:
- from sphinx.environment import ENV_VERSION
- envversion = {ext.name: ext.metadata['env_version'] for ext in app.extensions.values()
- if ext.metadata.get('env_version')}
- envversion['sphinx'] = ENV_VERSION
- return envversion
+ def get_envversion(self, app: Sphinx) -> Mapping[str, int]:
+ from sphinx.environment import _get_env_version
+
+ return _get_env_version(app.extensions)
def get_publisher(self, app: Sphinx, filetype: str) -> Publisher:
try:
@@ -504,7 +602,7 @@ def get_publisher(self, app: Sphinx, filetype: str) -> Publisher:
def merge_source_suffix(app: Sphinx, config: Config) -> None:
"""Merge any user-specified source_suffix with any added by extensions."""
for suffix, filetype in app.registry.source_suffix.items():
- if suffix not in app.config.source_suffix: # NoQA: SIM114
+ if suffix not in app.config.source_suffix:
app.config.source_suffix[suffix] = filetype
elif app.config.source_suffix[suffix] == 'restructuredtext':
# The filetype is not specified (default filetype).
diff --git a/sphinx/roles.py b/sphinx/roles.py
index aed317177c5..98843de5a95 100644
--- a/sphinx/roles.py
+++ b/sphinx/roles.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import re
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
import docutils.parsers.rst.directives
import docutils.parsers.rst.roles
@@ -17,7 +17,7 @@
if TYPE_CHECKING:
from collections.abc import Sequence
- from typing import Final
+ from typing import Any, Final
from docutils.nodes import Element, Node, TextElement, system_message
@@ -29,7 +29,6 @@
generic_docroles = {
'command': addnodes.literal_strong,
'dfn': nodes.emphasis,
- 'kbd': nodes.literal,
'mailheader': addnodes.literal_emphasis,
'makevar': addnodes.literal_strong,
'mimetype': addnodes.literal_emphasis,
@@ -43,8 +42,7 @@
class XRefRole(ReferenceRole):
- """
- A generic cross-referencing role. To create a callable that can be used as
+ """A generic cross-referencing role. To create a callable that can be used as
a role function, create an instance of this class.
The general features of this role are:
@@ -371,8 +369,7 @@ def build_uri(self) -> str:
def _format_rfc_target(target: str, /) -> str:
- """
- Takes an RFC number with an optional anchor (like ``123#section-2.5.3``)
+ """Takes an RFC number with an optional anchor (like ``123#section-2.5.3``)
and attempts to produce a human-friendly title for it.
We have a set of known anchors that we format nicely,
@@ -479,6 +476,59 @@ def run(self) -> tuple[list[Node], list[system_message]]:
return [nodes.abbreviation(self.rawtext, text, **options)], []
+class Keyboard(SphinxRole):
+ """Implement the :kbd: role.
+
+ Split words in the text by separator or whitespace,
+ but keep multi-word keys together.
+ """
+
+ # capture ('-', '+', '^', or whitespace) in between any two characters
+ _pattern: Final = re.compile(r'(?<=.)([\-+^]| +)(?=.)')
+
+ def run(self) -> tuple[list[Node], list[system_message]]:
+ classes = ['kbd']
+ if 'classes' in self.options:
+ classes.extend(self.options['classes'])
+
+ parts = self._pattern.split(self.text)
+ if len(parts) == 1 or self._is_multi_word_key(parts):
+ return [nodes.literal(self.rawtext, self.text, classes=classes)], []
+
+ compound: list[Node] = []
+ while parts:
+ if self._is_multi_word_key(parts):
+ key = ''.join(parts[:3])
+ parts[:3] = []
+ else:
+ key = parts.pop(0)
+ compound.append(nodes.literal(key, key, classes=classes))
+
+ try:
+ sep = parts.pop(0) # key separator ('-', '+', '^', etc)
+ except IndexError:
+ break
+ else:
+ compound.append(nodes.Text(sep))
+
+ return compound, []
+
+ @staticmethod
+ def _is_multi_word_key(parts: list[str]) -> bool:
+ if len(parts) <= 2 or not parts[1].isspace():
+ return False
+ name = parts[0].lower(), parts[2].lower()
+ return name in frozenset({
+ ('back', 'space'),
+ ('caps', 'lock'),
+ ('num', 'lock'),
+ ('page', 'down'),
+ ('page', 'up'),
+ ('scroll', 'lock'),
+ ('sys', 'rq'),
+ })
+
+
class Manpage(ReferenceRole):
_manpage_re = re.compile(r'^(?P
(?P.+)[(.](?P[1-9]\w*)?\)?)$')
@@ -576,6 +626,7 @@ def code_role(
'samp': EmphasizedLiteral(),
# other
'abbr': Abbreviation(),
+ 'kbd': Keyboard(),
'manpage': Manpage(),
}
diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py
index 3f19d3663a0..0cc1c6112d5 100644
--- a/sphinx/search/__init__.py
+++ b/sphinx/search/__init__.py
@@ -11,10 +11,10 @@
import re
from importlib import import_module
from pathlib import Path
-from typing import IO, TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from docutils import nodes
-from docutils.nodes import Element, Node
+from docutils.nodes import Element
from sphinx import addnodes, package_dir
from sphinx.util._pathlib import _StrPath
@@ -22,16 +22,29 @@
if TYPE_CHECKING:
from collections.abc import Callable, Iterable
+ from typing import Any, Protocol, TypeVar
+
+ from docutils.nodes import Node
from sphinx.environment import BuildEnvironment
+ _T_co = TypeVar('_T_co', covariant=True)
+ _T_contra = TypeVar('_T_contra', contravariant=True)
+
+ class _ReadableStream(Protocol[_T_co]):
+ def read(self, n: int = ..., /) -> _T_co: ...
+ def readline(self, n: int = ..., /) -> _T_co: ...
+
+ class _WritableStream(Protocol[_T_contra]):
+ def write(self, s: _T_contra, /) -> object: ...
+
+
_NON_MINIFIED_JS_PATH = Path(package_dir, 'search', 'non-minified-js')
_MINIFIED_JS_PATH = Path(package_dir, 'search', 'minified-js')
class SearchLanguage:
- """
- This class is the base class for search natural language preprocessors. If
+ """This class is the base class for search natural language preprocessors. If
you want to add support for a new language, you should override the methods
of this class.
@@ -83,21 +96,17 @@ def __init__(self, options: dict[str, str]) -> None:
self.init(options)
def init(self, options: dict[str, str]) -> None:
- """
- Initialize the class with the options the user has given.
- """
+ """Initialize the class with the options the user has given."""
def split(self, input: str) -> list[str]:
- """
- This method splits a sentence into words. Default splitter splits input
+ """This method splits a sentence into words. Default splitter splits input
at white spaces, which should be enough for most languages except CJK
languages.
"""
return self._word_re.findall(input)
def stem(self, word: str) -> str:
- """
- This method implements stemming algorithm of the Python version.
+ """This method implements stemming algorithm of the Python version.
Default implementation does nothing. You should implement this if the
language has any stemming rules.
@@ -109,8 +118,7 @@ def stem(self, word: str) -> str:
return word
def word_filter(self, word: str) -> bool:
- """
- Return true if the target word should be registered in the search index.
+ """Return true if the target word should be registered in the search index.
This method is called after stemming.
"""
return len(word) == 0 or not (
@@ -124,8 +132,7 @@ def word_filter(self, word: str) -> bool:
def parse_stop_word(source: str) -> set[str]:
- """
- Parse snowball style word list like this:
+ """Parse snowball style word list like this:
* https://snowball.tartarus.org/algorithms/finnish/stop.txt
"""
@@ -159,8 +166,7 @@ def parse_stop_word(source: str) -> set[str]:
class _JavaScriptIndex:
- """
- The search index as JavaScript file that calls a function
+ """The search index as JavaScript file that calls a function
on the documentation search object to register the index.
"""
@@ -178,10 +184,10 @@ def loads(self, s: str) -> Any:
raise ValueError(msg)
return json.loads(data)
- def dump(self, data: Any, f: IO[str]) -> None:
+ def dump(self, data: Any, f: _WritableStream[str]) -> None:
f.write(self.dumps(data))
- def load(self, f: IO[str]) -> Any:
+ def load(self, f: _ReadableStream[str]) -> Any:
return self.loads(f.read())
@@ -209,9 +215,7 @@ class WordStore:
class WordCollector(nodes.NodeVisitor):
- """
- A special visitor that collects words for the `IndexBuilder`.
- """
+ """A special visitor that collects words for the `IndexBuilder`."""
def __init__(self, document: nodes.document, lang: SearchLanguage) -> None:
super().__init__(document)
@@ -259,8 +263,7 @@ def dispatch_visit(self, node: Node) -> None:
class IndexBuilder:
- """
- Helper class that creates a search index based on the doctrees
+ """Helper class that creates a search index based on the doctrees
passed to the `feed` method.
"""
@@ -272,7 +275,8 @@ class IndexBuilder:
def __init__(
self, env: BuildEnvironment, lang: str, options: dict[str, str], scoring: str
) -> None:
- self.env = env
+ self._domains = env.domains
+ self._env_version = env.version
# docname -> title
self._titles: dict[str, str | None] = env._search_index_titles
# docname -> filename
@@ -317,13 +321,16 @@ def __init__(
self.js_scorer_code = ''
self.js_splitter_code = ''
- def load(self, stream: IO, format: Any) -> None:
+ def load(self, stream: _ReadableStream[str | bytes], format: Any) -> None:
"""Reconstruct from frozen data."""
if isinstance(format, str):
format = self.formats[format]
frozen = format.load(stream)
# if an old index is present, we treat it as not existing.
- if not isinstance(frozen, dict) or frozen.get('envversion') != self.env.version:
+ if (
+ not isinstance(frozen, dict)
+ or frozen.get('envversion') != self._env_version
+ ):
msg = 'old format'
raise ValueError(msg)
index2fn = frozen['docnames']
@@ -350,7 +357,9 @@ def load_terms(mapping: dict[str, Any]) -> dict[str, set[str]]:
self._title_mapping = load_terms(frozen['titleterms'])
# no need to load keywords/objtypes
- def dump(self, stream: IO, format: Any) -> None:
+ def dump(
+ self, stream: _WritableStream[str] | _WritableStream[bytes], format: Any
+ ) -> None:
"""Dump the frozen index to a stream."""
if isinstance(format, str):
format = self.formats[format]
@@ -362,7 +371,7 @@ def get_objects(
rv: dict[str, list[tuple[int, int, int, str, str]]] = {}
otypes = self._objtypes
onames = self._objnames
- for domain in self.env.domains.sorted():
+ for domain in self._domains.sorted():
sorted_objects = sorted(domain.get_objects())
for fullname, dispname, type, docname, anchor, prio in sorted_objects:
if docname not in fn2index:
@@ -400,12 +409,12 @@ def get_objects(
def get_terms(
self, fn2index: dict[str, int]
) -> tuple[dict[str, list[int] | int], dict[str, list[int] | int]]:
- """
- Return a mapping of document and title terms to their corresponding sorted document IDs.
+ """Return a mapping of document and title terms to sorted document IDs.
- When a term is only found within a single document, then the value for that term will be
- an integer value. When a term is found within multiple documents, the value will be a list
- of integers.
+ When a term is only found within a single document,
+ then the value for that term will be an integer value.
+ When a term is found within multiple documents,
+ the value will be a list of integers.
"""
rvs: tuple[dict[str, list[int] | int], dict[str, list[int] | int]] = ({}, {})
for rv, mapping in zip(rvs, (self._mapping, self._title_mapping), strict=True):
@@ -452,7 +461,7 @@ def freeze(self) -> dict[str, Any]:
'objtypes': objtypes,
'objnames': objnames,
'titleterms': title_terms,
- 'envversion': self.env.version,
+ 'envversion': self._env_version,
'alltitles': alltitles,
'indexentries': index_entries,
}
diff --git a/sphinx/search/en.py b/sphinx/search/en.py
index 11ebb683836..5173dc03fc0 100644
--- a/sphinx/search/en.py
+++ b/sphinx/search/en.py
@@ -6,19 +6,17 @@
from sphinx.search import SearchLanguage
-english_stopwords = set(
- """
-a and are as at
-be but by
-for
-if in into is it
-near no not
-of on or
-such
-that the their then there these they this to
-was will with
-""".split()
-)
+english_stopwords = {
+ 'a', 'and', 'are', 'as', 'at',
+ 'be', 'but', 'by',
+ 'for',
+ 'if', 'in', 'into', 'is', 'it',
+ 'near', 'no', 'not',
+ 'of', 'on', 'or',
+ 'such',
+ 'that', 'the', 'their', 'then', 'there', 'these', 'they', 'this', 'to',
+ 'was', 'will', 'with',
+} # fmt: skip
js_porter_stemmer = """
/**
diff --git a/sphinx/search/fi.py b/sphinx/search/fi.py
index f6c22264352..24ef7502300 100644
--- a/sphinx/search/fi.py
+++ b/sphinx/search/fi.py
@@ -95,7 +95,7 @@
niin | so
nyt | now
itse | self
-""")
+""") # NoQA: E501
class SearchFinnish(SearchLanguage):
diff --git a/sphinx/search/ja.py b/sphinx/search/ja.py
index 65e626fdd57..9d6df1fe1ba 100644
--- a/sphinx/search/ja.py
+++ b/sphinx/search/ja.py
@@ -13,7 +13,10 @@
import os
import re
import sys
-from typing import Any
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any
try:
import MeCab # type: ignore[import-not-found]
@@ -39,8 +42,7 @@ def __init__(self, options: dict[str, str]) -> None:
self.options = options
def split(self, input: str) -> list[str]:
- """
- :param str input:
+ """:param str input:
:return:
:rtype: list[str]
"""
@@ -513,8 +515,7 @@ def split(self, input: str) -> list[str]:
class SearchJapanese(SearchLanguage):
- """
- Japanese search implementation: uses no stemmer, but word splitting is quite
+ """Japanese search implementation: uses no stemmer, but word splitting is quite
complicated.
"""
diff --git a/sphinx/search/nl.py b/sphinx/search/nl.py
index 98f924dde86..2d2f2b8a8b6 100644
--- a/sphinx/search/nl.py
+++ b/sphinx/search/nl.py
@@ -109,7 +109,7 @@
iemand | somebody
geweest | been; past participle of 'be'
andere | other
-""")
+""") # NoQA: E501
class SearchDutch(SearchLanguage):
diff --git a/sphinx/search/zh.py b/sphinx/search/zh.py
index 28251e8c1db..a63b6d63aa0 100644
--- a/sphinx/search/zh.py
+++ b/sphinx/search/zh.py
@@ -18,19 +18,17 @@
JIEBA = False
JIEBA_DEFAULT_DICT = Path()
-english_stopwords = set(
- """
-a and are as at
-be but by
-for
-if in into is it
-near no not
-of on or
-such
-that the their then there these they this to
-was will with
-""".split()
-)
+english_stopwords = {
+ 'a', 'and', 'are', 'as', 'at',
+ 'be', 'but', 'by',
+ 'for',
+ 'if', 'in', 'into', 'is', 'it',
+ 'near', 'no', 'not',
+ 'of', 'on', 'or',
+ 'such',
+ 'that', 'the', 'their', 'then', 'there', 'these', 'they', 'this', 'to',
+ 'was', 'will', 'with',
+} # fmt: skip
js_porter_stemmer = """
/**
@@ -220,9 +218,7 @@
class SearchChinese(SearchLanguage):
- """
- Chinese search implementation
- """
+ """Chinese search implementation"""
lang = 'zh'
language_name = 'Chinese'
@@ -262,7 +258,7 @@ def stem(self, word: str) -> str:
stemmed = self.stemmer.stemWord(word.lower())
should_not_be_stemmed = (
len(word) >= 3 > len(stemmed) and word in self.latin_terms
- ) # fmt: skip
+ )
if should_not_be_stemmed:
return word.lower()
return stemmed
diff --git a/sphinx/templates/htmlhelp/project.stp b/sphinx/templates/htmlhelp/project.stp
index bae1f90c4e4..16c49b01daa 100644
--- a/sphinx/templates/htmlhelp/project.stp
+++ b/sphinx/templates/htmlhelp/project.stp
@@ -1,33 +1,33 @@
-a
-and
-are
-as
-at
-be
-but
-by
-for
-if
-in
-into
-is
-it
-near
-no
-not
-of
-on
-or
-such
-that
-the
-their
-then
-there
-these
-they
-this
-to
-was
-will
-with
+a
+and
+are
+as
+at
+be
+but
+by
+for
+if
+in
+into
+is
+it
+near
+no
+not
+of
+on
+or
+such
+that
+the
+their
+then
+there
+these
+they
+this
+to
+was
+will
+with
diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py
index 6f1c29cdba2..4b4766be08f 100644
--- a/sphinx/testing/fixtures.py
+++ b/sphinx/testing/fixtures.py
@@ -76,8 +76,7 @@ def app_params(
sphinx_test_tempdir: str,
rootdir: Path,
) -> _app_params:
- """
- Parameters that are specified by 'pytest.mark.sphinx' for
+ """Parameters that are specified by 'pytest.mark.sphinx' for
sphinx.application.Sphinx initialization
"""
# ##### process pytest.mark.sphinx
@@ -114,13 +113,12 @@ def app_params(
return _app_params(args, kwargs)
-_app_params = namedtuple('_app_params', 'args,kwargs')
+_app_params = namedtuple('_app_params', 'args,kwargs') # NoQA: PYI024
@pytest.fixture
def test_params(request: Any) -> dict[str, Any]:
- """
- Test parameters that are specified by 'pytest.mark.test_params'
+ """Test parameters that are specified by 'pytest.mark.test_params'
:param Union[str] shared_result:
If the value is provided, app._status and app._warning objects will be
@@ -148,9 +146,7 @@ def app(
make_app: Callable[[], SphinxTestApp],
shared_result: SharedResult,
) -> Iterator[SphinxTestApp]:
- """
- Provides the 'sphinx.application.Sphinx' object
- """
+ """Provides the 'sphinx.application.Sphinx' object"""
args, kwargs = app_params
app_ = make_app(*args, **kwargs)
yield app_
@@ -168,24 +164,19 @@ def app(
@pytest.fixture
def status(app: SphinxTestApp) -> StringIO:
- """
- Back-compatibility for testing with previous @with_app decorator
- """
+ """Back-compatibility for testing with previous @with_app decorator"""
return app.status
@pytest.fixture
def warning(app: SphinxTestApp) -> StringIO:
- """
- Back-compatibility for testing with previous @with_app decorator
- """
+ """Back-compatibility for testing with previous @with_app decorator"""
return app.warning
@pytest.fixture
def make_app(test_params: dict[str, Any]) -> Iterator[Callable[[], SphinxTestApp]]:
- """
- Provides make_app function to initialize SphinxTestApp instance.
+ """Provides make_app function to initialize SphinxTestApp instance.
if you want to initialize 'app' in your test function. please use this
instead of using SphinxTestApp class directory.
"""
@@ -222,9 +213,8 @@ def _shared_result_cache() -> None:
@pytest.fixture
-def if_graphviz_found(app: SphinxTestApp) -> None: # NoQA: PT004
- """
- The test will be skipped when using 'if_graphviz_found' fixture and graphviz
+def if_graphviz_found(app: SphinxTestApp) -> None:
+ """The test will be skipped when using 'if_graphviz_found' fixture and graphviz
dot command is not found.
"""
graphviz_dot = getattr(app.config, 'graphviz_dot', '')
@@ -246,9 +236,8 @@ def sphinx_test_tempdir(tmp_path_factory: pytest.TempPathFactory) -> Path:
@pytest.fixture
-def rollback_sysmodules() -> Iterator[None]: # NoQA: PT004
- """
- Rollback sys.modules to its value before testing to unload modules
+def rollback_sysmodules() -> Iterator[None]:
+ """Rollback sys.modules to its value before testing to unload modules
during tests.
For example, used in test_ext_autosummary.py to permit unloading the
diff --git a/sphinx/testing/path.py b/sphinx/testing/path.py
index b469588ea6d..2d01f935062 100644
--- a/sphinx/testing/path.py
+++ b/sphinx/testing/path.py
@@ -4,13 +4,14 @@
import shutil
import sys
import warnings
-from typing import IO, TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from sphinx.deprecation import RemovedInSphinx90Warning
if TYPE_CHECKING:
import builtins
from collections.abc import Callable
+ from typing import IO, Any
warnings.warn(
"'sphinx.testing.path' is deprecated. Use 'os.path' or 'pathlib' instead.",
@@ -32,57 +33,41 @@ def getumask() -> int:
UMASK = getumask()
-class path(str):
- """
- Represents a path which behaves like a string.
- """
+class path(str): # NoQA: FURB189
+ """Represents a path which behaves like a string."""
__slots__ = ()
@property
def parent(self) -> path:
- """
- The name of the directory the file or directory is in.
- """
+ """The name of the directory the file or directory is in."""
return self.__class__(os.path.dirname(self))
def basename(self) -> str:
return os.path.basename(self)
def abspath(self) -> path:
- """
- Returns the absolute path.
- """
+ """Returns the absolute path."""
return self.__class__(os.path.abspath(self))
def isabs(self) -> bool:
- """
- Returns ``True`` if the path is absolute.
- """
+ """Returns ``True`` if the path is absolute."""
return os.path.isabs(self)
def isdir(self) -> bool:
- """
- Returns ``True`` if the path is a directory.
- """
+ """Returns ``True`` if the path is a directory."""
return os.path.isdir(self)
def isfile(self) -> bool:
- """
- Returns ``True`` if the path is a file.
- """
+ """Returns ``True`` if the path is a file."""
return os.path.isfile(self)
def islink(self) -> bool:
- """
- Returns ``True`` if the path is a symbolic link.
- """
+ """Returns ``True`` if the path is a symbolic link."""
return os.path.islink(self)
def ismount(self) -> bool:
- """
- Returns ``True`` if the path is a mount point.
- """
+ """Returns ``True`` if the path is a mount point."""
return os.path.ismount(self)
def rmtree(
@@ -90,8 +75,7 @@ def rmtree(
ignore_errors: bool = False,
onerror: Callable[[Callable[..., Any], str, Any], object] | None = None,
) -> None:
- """
- Removes the file or directory and any files or directories it may
+ """Removes the file or directory and any files or directories it may
contain.
:param ignore_errors:
@@ -108,8 +92,7 @@ def rmtree(
shutil.rmtree(self, ignore_errors=ignore_errors, onerror=onerror)
def copytree(self, destination: str, symlinks: bool = False) -> None:
- """
- Recursively copy a directory to the given `destination`. If the given
+ """Recursively copy a directory to the given `destination`. If the given
`destination` does not exist it will be created.
:param symlinks:
@@ -130,8 +113,7 @@ def copytree(self, destination: str, symlinks: bool = False) -> None:
os.chmod(os.path.join(root, name), 0o644 & ~UMASK)
def movetree(self, destination: str) -> None:
- """
- Recursively move the file or directory to the given `destination`
+ """Recursively move the file or directory to the given `destination`
similar to the Unix "mv" command.
If the `destination` is a file it may be overwritten depending on the
@@ -142,47 +124,36 @@ def movetree(self, destination: str) -> None:
move = movetree
def unlink(self) -> None:
- """
- Removes a file.
- """
+ """Removes a file."""
os.unlink(self)
def stat(self) -> Any:
- """
- Returns a stat of the file.
- """
+ """Returns a stat of the file."""
return os.stat(self)
def utime(self, arg: Any) -> None:
os.utime(self, arg)
def open(self, mode: str = 'r', **kwargs: Any) -> IO[str]:
- return open(self, mode, **kwargs) # NoQA: SIM115
+ return open(self, mode, **kwargs)
def write_text(self, text: str, encoding: str = 'utf-8', **kwargs: Any) -> None:
- """
- Writes the given `text` to the file.
- """
+ """Writes the given `text` to the file."""
with open(self, 'w', encoding=encoding, **kwargs) as f:
f.write(text)
def read_text(self, encoding: str = 'utf-8', **kwargs: Any) -> str:
- """
- Returns the text in the file.
- """
+ """Returns the text in the file."""
with open(self, encoding=encoding, **kwargs) as f:
return f.read()
def read_bytes(self) -> builtins.bytes:
- """
- Returns the bytes in the file.
- """
+ """Returns the bytes in the file."""
with open(self, mode='rb') as f:
return f.read()
def write_bytes(self, bytes: bytes, append: bool = False) -> None:
- """
- Writes the given `bytes` to the file.
+ """Writes the given `bytes` to the file.
:param append:
If ``True`` given `bytes` are added at the end of the file.
@@ -195,28 +166,21 @@ def write_bytes(self, bytes: bytes, append: bool = False) -> None:
f.write(bytes)
def exists(self) -> bool:
- """
- Returns ``True`` if the path exist.
- """
+ """Returns ``True`` if the path exist."""
return os.path.exists(self)
def lexists(self) -> bool:
- """
- Returns ``True`` if the path exists unless it is a broken symbolic
+ """Returns ``True`` if the path exists unless it is a broken symbolic
link.
"""
return os.path.lexists(self)
def makedirs(self, mode: int = 0o777, exist_ok: bool = False) -> None:
- """
- Recursively create directories.
- """
+ """Recursively create directories."""
os.makedirs(self, mode, exist_ok=exist_ok)
def joinpath(self, *args: Any) -> path:
- """
- Joins the path with the argument given and returns the result.
- """
+ """Joins the path with the argument given and returns the result."""
return self.__class__(os.path.join(self, *map(self.__class__, args)))
def listdir(self) -> list[str]:
diff --git a/sphinx/testing/restructuredtext.py b/sphinx/testing/restructuredtext.py
index 620e8483492..b04b61a4021 100644
--- a/sphinx/testing/restructuredtext.py
+++ b/sphinx/testing/restructuredtext.py
@@ -1,28 +1,36 @@
-from docutils import nodes
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
from docutils.core import publish_doctree
-from sphinx.application import Sphinx
from sphinx.io import SphinxStandaloneReader
from sphinx.parsers import RSTParser
from sphinx.util.docutils import sphinx_domains
+if TYPE_CHECKING:
+ from docutils import nodes
+
+ from sphinx.application import Sphinx
+
def parse(app: Sphinx, text: str, docname: str = 'index') -> nodes.document:
"""Parse a string as reStructuredText with Sphinx application."""
+ env = app.env
try:
- app.env.temp_data['docname'] = docname
+ app.env.current_document.docname = docname
reader = SphinxStandaloneReader()
reader.setup(app)
parser = RSTParser()
parser.set_application(app)
- with sphinx_domains(app.env):
+ with sphinx_domains(env):
return publish_doctree(
text,
str(app.srcdir / f'{docname}.rst'),
reader=reader,
parser=parser,
settings_overrides={
- 'env': app.env,
+ 'env': env,
'gettext_compact': True,
'input_encoding': 'utf-8',
'output_encoding': 'unicode',
@@ -30,4 +38,4 @@ def parse(app: Sphinx, text: str, docname: str = 'index') -> nodes.document:
},
)
finally:
- app.env.temp_data.pop('docname', None)
+ env.current_document.docname = ''
diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py
index d95ffb46bc3..66a2a22fa03 100644
--- a/sphinx/testing/util.py
+++ b/sphinx/testing/util.py
@@ -4,8 +4,6 @@
__all__ = ('SphinxTestApp', 'SphinxTestAppWrapperForSkipBuilding')
-import contextlib
-import os
import sys
from io import StringIO
from types import MappingProxyType
@@ -17,10 +15,11 @@
import sphinx.application
import sphinx.locale
import sphinx.pycode
-from sphinx.util.console import strip_colors
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.util.docutils import additional_nodes
if TYPE_CHECKING:
+ import os
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any
@@ -224,8 +223,7 @@ def warning(self) -> StringIO:
def cleanup(self, doctrees: bool = False) -> None:
sys.path[:] = self._saved_path
_clean_up_global_state()
- with contextlib.suppress(FileNotFoundError):
- os.remove(self.docutils_conf_path)
+ self.docutils_conf_path.unlink(missing_ok=True)
def __repr__(self) -> str:
return f'<{self.__class__.__name__} buildername={self._builder_name!r}>'
@@ -247,7 +245,7 @@ class SphinxTestAppWrapperForSkipBuilding(SphinxTestApp):
def build(
self, force_all: bool = False, filenames: list[str] | None = None
) -> None:
- if not os.listdir(self.outdir):
+ if not list(self.outdir.iterdir()):
# if listdir is empty, do build.
super().build(force_all, filenames)
# otherwise, we can use built cache
@@ -273,7 +271,11 @@ def _clean_up_global_state() -> None:
# deprecated name -> (object to return, canonical path or '', removal version)
_DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = {
- 'strip_escseq': (strip_colors, 'sphinx.util.console.strip_colors', (9, 0)),
+ 'strip_escseq': (
+ strip_escape_sequences,
+ 'sphinx.util.console.strip_escape_sequences',
+ (9, 0),
+ ),
}
diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty
index 0d52676c4fe..8837485c5f7 100644
--- a/sphinx/texinputs/sphinx.sty
+++ b/sphinx/texinputs/sphinx.sty
@@ -9,7 +9,7 @@
% by the Sphinx LaTeX writer.
\NeedsTeXFormat{LaTeX2e}[1995/12/01]
-\ProvidesPackage{sphinx}[2024/10/11 v8.1.1 Sphinx LaTeX package (sphinx-doc)]
+\ProvidesPackage{sphinx}[2024/11/23 v8.2.0 Sphinx LaTeX package (sphinx-doc)]
% provides \ltx@ifundefined
% (many packages load ltxcmds: graphicx does for pdftex and lualatex but
@@ -1098,12 +1098,13 @@
% Some of these defaults got already set. But we now list them all explicitly
% for a complete initial configuration of reset storage.
% At 7.4.0, \fboxrule and \fboxsep replaced by 0.4pt and 3pt which are anyhow
-% the defaults for these LaTeX dimensions.
+% the defaults for these LaTeX dimensions. 8.2.0 corrected border-radius
+% default back to 3pt (\fboxsep) not 0.4pt (\fboxrule).
\let\spx@boxes@sphinxbox@defaults\@gobble
\sphinxboxsetup{%
border-width=0.4pt,
padding=3pt,
- border-radius=0.4pt,
+ border-radius=3pt,
box-shadow=none,
% MEMO: as xcolor is loaded, \spx@defineorletcolor has a "\colorlet" branch
% which makes this syntax acceptable and avoids duplicating here the values.
diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js
index aaf078d2b91..91f4be57fc8 100644
--- a/sphinx/themes/basic/static/searchtools.js
+++ b/sphinx/themes/basic/static/searchtools.js
@@ -513,9 +513,11 @@ const Search = {
// perform the search on the required terms
searchTerms.forEach((word) => {
const files = [];
+ // find documents, if any, containing the query word in their text/title term indices
+ // use Object.hasOwnProperty to avoid mismatching against prototype properties
const arr = [
- { files: terms[word], score: Scorer.term },
- { files: titleTerms[word], score: Scorer.title },
+ { files: terms.hasOwnProperty(word) ? terms[word] : undefined, score: Scorer.term },
+ { files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, score: Scorer.title },
];
// add support for partial matches
if (word.length > 2) {
diff --git a/sphinx/theming.py b/sphinx/theming.py
index 7f1c53d1ebc..6a549d838b0 100644
--- a/sphinx/theming.py
+++ b/sphinx/theming.py
@@ -12,7 +12,7 @@
import tomllib
from importlib.metadata import entry_points
from pathlib import Path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from zipfile import ZipFile
from sphinx import package_dir
@@ -25,7 +25,7 @@
if TYPE_CHECKING:
from collections.abc import Callable
- from typing import Required, TypedDict
+ from typing import Any, Required, TypedDict
from sphinx.application import Sphinx
diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py
index ca3a533fab0..8724ad12ac6 100644
--- a/sphinx/transforms/__init__.py
+++ b/sphinx/transforms/__init__.py
@@ -4,7 +4,7 @@
import re
import unicodedata
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
from docutils.transforms import Transform, Transformer
@@ -23,7 +23,7 @@
if TYPE_CHECKING:
from collections.abc import Iterator
- from typing import Literal, TypeAlias
+ from typing import Any, Literal, TypeAlias
from docutils.nodes import Node, Text
from typing_extensions import TypeIs
@@ -76,9 +76,7 @@ def config(self) -> Config:
class SphinxTransformer(Transformer):
- """
- A transformer for Sphinx.
- """
+ """A transformer for Sphinx."""
document: nodes.document
env: BuildEnvironment | None = None
@@ -106,9 +104,7 @@ def apply_transforms(self) -> None:
class DefaultSubstitutions(SphinxTransform):
- """
- Replace some substitutions if they aren't defined in the document.
- """
+ """Replace some substitutions if they aren't defined in the document."""
# run before the default Substitutions
default_priority = 210
@@ -150,8 +146,7 @@ def _calculate_translation_progress(document: nodes.document) -> str:
class MoveModuleTargets(SphinxTransform):
- """
- Move module targets that are the first thing in a section to the section
+ """Move module targets that are the first thing in a section to the section
title.
XXX Python specific
@@ -176,9 +171,7 @@ def apply(self, **kwargs: Any) -> None:
class HandleCodeBlocks(SphinxTransform):
- """
- Several code block related transformations.
- """
+ """Several code block related transformations."""
default_priority = 210
@@ -200,9 +193,7 @@ def apply(self, **kwargs: Any) -> None:
class AutoNumbering(SphinxTransform):
- """
- Register IDs of tables, figures and literal_blocks to assign numbers.
- """
+ """Register IDs of tables, figures and literal_blocks to assign numbers."""
default_priority = 210
@@ -219,9 +210,7 @@ def apply(self, **kwargs: Any) -> None:
class SortIds(SphinxTransform):
- """
- Sort section IDs so that the "id[0-9]+" one comes last.
- """
+ """Sort section IDs so that the "id[0-9]+" one comes last."""
default_priority = 261
@@ -241,9 +230,7 @@ def apply(self, **kwargs: Any) -> None:
class ApplySourceWorkaround(SphinxTransform):
- """
- Update source and rawsource attributes
- """
+ """Update source and rawsource attributes"""
default_priority = 10
@@ -254,9 +241,7 @@ def apply(self, **kwargs: Any) -> None:
class AutoIndexUpgrader(SphinxTransform):
- """
- Detect old style (4 column based indices) and automatically upgrade to new style.
- """
+ """Detect old style (4 column based indices) and automatically upgrade to new style."""
default_priority = 210
@@ -277,9 +262,7 @@ def apply(self, **kwargs: Any) -> None:
class ExtraTranslatableNodes(SphinxTransform):
- """
- Make nodes translatable
- """
+ """Make nodes translatable"""
default_priority = 10
@@ -297,9 +280,7 @@ def is_translatable_node(node: Node) -> TypeIs[nodes.Element]:
class UnreferencedFootnotesDetector(SphinxTransform):
- """
- Detect unreferenced footnotes and emit warnings
- """
+ """Detect unreferenced footnotes and emit warnings"""
default_priority = Footnotes.default_priority + 2
@@ -361,8 +342,7 @@ def apply(self, **kwargs: Any) -> None:
class SphinxContentsFilter(ContentsFilter):
- """
- Used with BuildEnvironment.add_toc_from() to discard cross-file links
+ """Used with BuildEnvironment.add_toc_from() to discard cross-file links
within table-of-contents link nodes.
"""
@@ -373,8 +353,7 @@ def visit_image(self, node: nodes.image) -> None:
class SphinxSmartQuotes(SmartQuotes, SphinxTransform):
- """
- Customized SmartQuotes to avoid transform for some extra node types.
+ """Customized SmartQuotes to avoid transform for some extra node types.
refs: sphinx.parsers.RSTParser
"""
@@ -443,11 +422,11 @@ class GlossarySorter(SphinxTransform):
def apply(self, **kwargs: Any) -> None:
for glossary in self.document.findall(addnodes.glossary):
if glossary['sorted']:
- definition_list = cast(nodes.definition_list, glossary[0])
+ definition_list = cast('nodes.definition_list', glossary[0])
definition_list[:] = sorted(
definition_list,
key=lambda item: unicodedata.normalize(
- 'NFD', cast(nodes.term, item)[0].astext().lower()
+ 'NFD', cast('nodes.term', item)[0].astext().lower()
),
)
diff --git a/sphinx/transforms/compact_bullet_list.py b/sphinx/transforms/compact_bullet_list.py
index f144df87fb2..293d1218890 100644
--- a/sphinx/transforms/compact_bullet_list.py
+++ b/sphinx/transforms/compact_bullet_list.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
@@ -10,6 +10,8 @@
from sphinx.transforms import SphinxTransform
if TYPE_CHECKING:
+ from typing import Any
+
from docutils.nodes import Node
from sphinx.application import Sphinx
@@ -75,8 +77,8 @@ def check_refonly_list(node: Node) -> bool:
for node in self.document.findall(nodes.bullet_list):
if check_refonly_list(node):
for item in node.findall(nodes.list_item):
- para = cast(nodes.paragraph, item[0])
- ref = cast(nodes.reference, para[0])
+ para = cast('nodes.paragraph', item[0])
+ ref = cast('nodes.reference', para[0])
compact_para = addnodes.compact_paragraph()
compact_para += ref
item.replace(para, compact_para)
diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py
index 31542e81e5f..815ca606bce 100644
--- a/sphinx/transforms/i18n.py
+++ b/sphinx/transforms/i18n.py
@@ -100,9 +100,7 @@ def parse_noqa(source: str) -> tuple[str, bool]:
class PreserveTranslatableMessages(SphinxTransform):
- """
- Preserve original translatable messages before translation
- """
+ """Preserve original translatable messages before translation"""
default_priority = 10 # this MUST be invoked before Locale transform
@@ -380,9 +378,7 @@ def update_leaves(self) -> None:
class Locale(SphinxTransform):
- """
- Replace translatable nodes with their translated doctree.
- """
+ """Replace translatable nodes with their translated doctree."""
default_priority = 20
@@ -609,9 +605,7 @@ def apply(self, **kwargs: Any) -> None:
class TranslationProgressTotaliser(SphinxTransform):
- """
- Calculate the number of translated and untranslated nodes.
- """
+ """Calculate the number of translated and untranslated nodes."""
default_priority = 25 # MUST happen after Locale
@@ -634,9 +628,7 @@ def apply(self, **kwargs: Any) -> None:
class AddTranslationClasses(SphinxTransform):
- """
- Add ``translated`` or ``untranslated`` classes to indicate translation status.
- """
+ """Add ``translated`` or ``untranslated`` classes to indicate translation status."""
default_priority = 950
@@ -674,9 +666,7 @@ def apply(self, **kwargs: Any) -> None:
class RemoveTranslatableInline(SphinxTransform):
- """
- Remove inline nodes used for translation as placeholders.
- """
+ """Remove inline nodes used for translation as placeholders."""
default_priority = 999
diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py
index cb590b77a10..8c92fdb83c4 100644
--- a/sphinx/transforms/post_transforms/__init__.py
+++ b/sphinx/transforms/post_transforms/__init__.py
@@ -4,10 +4,9 @@
import re
from itertools import starmap
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
-from docutils.nodes import Element, Node
from sphinx import addnodes
from sphinx.errors import NoUri
@@ -19,6 +18,9 @@
if TYPE_CHECKING:
from collections.abc import Sequence
+ from typing import Any
+
+ from docutils.nodes import Element, Node
from sphinx.addnodes import pending_xref
from sphinx.application import Sphinx
@@ -58,9 +60,7 @@ def run(self, **kwargs: Any) -> None:
class ReferencesResolver(SphinxPostTransform):
- """
- Resolves cross-references on doctrees.
- """
+ """Resolves cross-references on doctrees."""
default_priority = 10
@@ -68,9 +68,9 @@ def run(self, **kwargs: Any) -> None:
for node in self.document.findall(addnodes.pending_xref):
content = self.find_pending_xref_condition(node, ('resolved', '*'))
if content:
- contnode = cast(Element, content[0].deepcopy())
+ contnode = cast('Element', content[0].deepcopy())
else:
- contnode = cast(Element, node[0].deepcopy())
+ contnode = cast('Element', node[0].deepcopy())
newnode = None
@@ -188,6 +188,8 @@ def stringify(name: str, node: Element) -> str:
target,
candidates,
location=node,
+ type='ref',
+ subtype='any',
)
res_role, newnode = results[0]
# Override "any" class with the actual role type to get the styling
@@ -276,7 +278,7 @@ def run(self, **kwargs: Any) -> None:
# result in a "Losing ids" exception if there is a target node before
# the only node, so we make sure docutils can transfer the id to
# something, even if it's just a comment and will lose the id anyway...
- process_only_nodes(self.document, self.app.builder.tags)
+ process_only_nodes(self.document, self.app.tags)
class SigElementFallbackTransform(SphinxPostTransform):
diff --git a/sphinx/transforms/post_transforms/code.py b/sphinx/transforms/post_transforms/code.py
index 4abf5aed10f..2d89bc65ac8 100644
--- a/sphinx/transforms/post_transforms/code.py
+++ b/sphinx/transforms/post_transforms/code.py
@@ -3,7 +3,7 @@
from __future__ import annotations
import sys
-from typing import TYPE_CHECKING, Any, NamedTuple
+from typing import TYPE_CHECKING, NamedTuple
from docutils import nodes
from pygments.lexers import PythonConsoleLexer, guess_lexer
@@ -13,6 +13,8 @@
from sphinx.transforms import SphinxTransform
if TYPE_CHECKING:
+ from typing import Any
+
from docutils.nodes import Node, TextElement
from sphinx.application import Sphinx
@@ -26,8 +28,7 @@ class HighlightSetting(NamedTuple):
class HighlightLanguageTransform(SphinxTransform):
- """
- Apply highlight_language to all literal_block nodes.
+ """Apply highlight_language to all literal_block nodes.
This refers both :confval:`highlight_language` setting and
:rst:dir:`highlight` directive. After processing, this transform
@@ -86,8 +87,7 @@ def visit_literal_block(self, node: nodes.literal_block) -> None:
class TrimDoctestFlagsTransform(SphinxTransform):
- """
- Trim doctest flags like ``# doctest: +FLAG`` from python code-blocks.
+ """Trim doctest flags like ``# doctest: +FLAG`` from python code-blocks.
see :confval:`trim_doctest_flags` for more information.
"""
diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py
index 7040952ac14..d4c6262e529 100644
--- a/sphinx/transforms/post_transforms/images.py
+++ b/sphinx/transforms/post_transforms/images.py
@@ -7,7 +7,7 @@
from hashlib import sha1
from math import ceil
from pathlib import Path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from docutils import nodes
@@ -20,6 +20,8 @@
from sphinx.util.osutil import ensuredir
if TYPE_CHECKING:
+ from typing import Any
+
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
@@ -42,8 +44,8 @@ def handle(self, node: nodes.image) -> None:
pass
@property
- def imagedir(self) -> str:
- return os.path.join(self.app.doctreedir, 'images')
+ def imagedir(self) -> _StrPath:
+ return self.app.doctreedir / 'images'
class ImageDownloader(BaseImageConverter):
@@ -83,7 +85,7 @@ def _download_image(self, node: nodes.image, path: Path) -> None:
timestamp: float = ceil(path.stat().st_mtime)
headers['If-Modified-Since'] = epoch_to_rfc1123(timestamp)
- config = self.app.config
+ config = self.config
r = requests.get(
node['uri'],
headers=headers,
@@ -94,7 +96,7 @@ def _download_image(self, node: nodes.image, path: Path) -> None:
msg = __('Could not fetch remote image: %s [%d]')
logger.warning(msg, node['uri'], r.status_code)
else:
- self.app.env.original_image_uri[_StrPath(path)] = node['uri']
+ self.env.original_image_uri[_StrPath(path)] = node['uri']
if r.status_code == 200:
path.write_bytes(r.content)
@@ -106,22 +108,22 @@ def _download_image(self, node: nodes.image, path: Path) -> None:
def _process_image(self, node: nodes.image, path: Path) -> None:
str_path = _StrPath(path)
- self.app.env.original_image_uri[str_path] = node['uri']
+ self.env.original_image_uri[str_path] = node['uri']
mimetype = guess_mimetype(path, default='*')
if mimetype != '*' and not path.suffix:
# append a suffix if URI does not contain suffix
ext = get_image_extension(mimetype) or ''
with_ext = path.with_name(path.name + ext)
- os.replace(path, with_ext)
- self.app.env.original_image_uri.pop(str_path)
- self.app.env.original_image_uri[_StrPath(with_ext)] = node['uri']
+ path.replace(with_ext)
+ self.env.original_image_uri.pop(str_path)
+ self.env.original_image_uri[_StrPath(with_ext)] = node['uri']
path = with_ext
path_str = str(path)
node['candidates'].pop('?')
node['candidates'][mimetype] = path_str
node['uri'] = path_str
- self.app.env.images.add_file(self.env.docname, path_str)
+ self.env.images.add_file(self.env.docname, path_str)
class DataURIExtractor(BaseImageConverter):
@@ -142,10 +144,10 @@ def handle(self, node: nodes.image) -> None:
)
return
- ensuredir(os.path.join(self.imagedir, 'embeded'))
+ ensuredir(self.imagedir / 'embeded')
digest = sha1(image.data, usedforsecurity=False).hexdigest()
- path = _StrPath(self.imagedir, 'embeded', digest + ext)
- self.app.env.original_image_uri[path] = node['uri']
+ path = self.imagedir / 'embeded' / (digest + ext)
+ self.env.original_image_uri[path] = node['uri']
with open(path, 'wb') as f:
f.write(image.data)
@@ -154,7 +156,7 @@ def handle(self, node: nodes.image) -> None:
node['candidates'].pop('?')
node['candidates'][image.mimetype] = path_str
node['uri'] = path_str
- self.app.env.images.add_file(self.env.docname, path_str)
+ self.env.images.add_file(self.env.docname, path_str)
def get_filename_for(filename: str, mimetype: str) -> str:
@@ -248,7 +250,7 @@ def guess_mimetypes(self, node: nodes.image) -> list[str]:
if '?' in node['candidates']:
return []
elif '*' in node['candidates']:
- path = os.path.join(self.app.srcdir, node['uri'])
+ path = self.app.srcdir / node['uri']
guessed = guess_mimetype(path)
return [guessed] if guessed is not None else []
else:
@@ -265,20 +267,22 @@ def handle(self, node: nodes.image) -> None:
filename = self.env.images[srcpath][1]
filename = get_filename_for(filename, _to)
ensuredir(self.imagedir)
- destpath = os.path.join(self.imagedir, filename)
+ destpath = self.imagedir / filename
- abs_srcpath = os.path.join(self.app.srcdir, srcpath)
+ abs_srcpath = self.app.srcdir / srcpath
if self.convert(abs_srcpath, destpath):
if '*' in node['candidates']:
- node['candidates']['*'] = destpath
+ node['candidates']['*'] = str(destpath)
else:
- node['candidates'][_to] = destpath
- node['uri'] = destpath
+ node['candidates'][_to] = str(destpath)
+ node['uri'] = str(destpath)
- self.env.original_image_uri[_StrPath(destpath)] = srcpath
+ self.env.original_image_uri[destpath] = srcpath
self.env.images.add_file(self.env.docname, destpath)
- def convert(self, _from: str, _to: str) -> bool:
+ def convert(
+ self, _from: str | os.PathLike[str], _to: str | os.PathLike[str]
+ ) -> bool:
"""Convert an image file to the expected format.
*_from* is a path of the source image file, and *_to* is a path
diff --git a/sphinx/transforms/references.py b/sphinx/transforms/references.py
index 15afd92d0de..447e9ded568 100644
--- a/sphinx/transforms/references.py
+++ b/sphinx/transforms/references.py
@@ -2,13 +2,15 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from docutils.transforms.references import DanglingReferences
from sphinx.transforms import SphinxTransform
if TYPE_CHECKING:
+ from typing import Any
+
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py
index 73a66a9a60d..65f47d31f9e 100644
--- a/sphinx/util/__init__.py
+++ b/sphinx/util/__init__.py
@@ -2,46 +2,23 @@
from __future__ import annotations
-import hashlib
import os
import posixpath
import re
-from typing import Any
+from typing import TYPE_CHECKING
-from sphinx.errors import ExtensionError as _ExtensionError
from sphinx.errors import FiletypeNotFoundError
-from sphinx.util import _files, _importer, logging
-from sphinx.util import index_entries as _index_entries
-from sphinx.util._lines import parse_line_num_spec as parselinenos # NoQA: F401
-from sphinx.util._uri import encode_uri # NoQA: F401
-from sphinx.util._uri import is_url as isurl # NoQA: F401
-from sphinx.util.console import strip_colors # NoQA: F401
-from sphinx.util.matching import patfilter # NoQA: F401
-from sphinx.util.nodes import ( # NoQA: F401
- caption_ref_re,
- explicit_title_re,
- nested_parse_with_titles,
- split_explicit_title,
-)
-
-# import other utilities; partly for backwards compatibility, so don't
-# prune unused ones indiscriminately
-from sphinx.util.osutil import ( # NoQA: F401
- SEP,
- copyfile,
- ensuredir,
- make_filename,
- os_path,
- relative_uri,
-)
-
-logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+ import hashlib
+ from collections.abc import Callable
+ from types import ModuleType
+ from typing import Any
# Generally useful regular expressions.
ws_re: re.Pattern[str] = re.compile(r'\s+')
url_re: re.Pattern[str] = re.compile(r'(?P.+)://.*')
-
# High-level utility functions.
@@ -64,6 +41,8 @@ def _md5(data: bytes = b'', **_kw: Any) -> hashlib._Hash:
To be removed in Sphinx 9.0
"""
+ import hashlib
+
return hashlib.md5(data, usedforsecurity=False)
@@ -72,37 +51,108 @@ def _sha1(data: bytes = b'', **_kw: Any) -> hashlib._Hash:
To be removed in Sphinx 9.0
"""
+ import hashlib
+
return hashlib.sha1(data, usedforsecurity=False)
-# deprecated name -> (object to return, canonical path or empty string)
-_DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = {
- 'split_index_msg': (
- _index_entries.split_index_msg,
- 'sphinx.util.index_entries.split_index_msg',
- (9, 0),
- ),
- 'split_into': (
- _index_entries.split_index_msg,
- 'sphinx.util.index_entries.split_into',
- (9, 0),
- ),
- 'ExtensionError': (_ExtensionError, 'sphinx.errors.ExtensionError', (9, 0)),
- 'md5': (_md5, '', (9, 0)),
- 'sha1': (_sha1, '', (9, 0)),
- 'import_object': (_importer.import_object, '', (10, 0)),
- 'FilenameUniqDict': (_files.FilenameUniqDict, '', (10, 0)),
- 'DownloadFiles': (_files.DownloadFiles, '', (10, 0)),
-}
+def __getattr__(name: str) -> Any:
+ from sphinx.deprecation import _deprecation_warning
+ obj: Callable[..., Any]
+ mod: ModuleType
-def __getattr__(name: str) -> Any:
- if name not in _DEPRECATED_OBJECTS:
- msg = f'module {__name__!r} has no attribute {name!r}'
- raise AttributeError(msg)
+ # RemovedInSphinx90Warning
+ if name == 'split_index_msg':
+ from sphinx.util.index_entries import split_index_msg as obj
- from sphinx.deprecation import _deprecation_warning
+ canonical_name = f'{obj.__module__}.{obj.__qualname__}'
+ _deprecation_warning(__name__, name, canonical_name, remove=(9, 0))
+ return obj
+
+ if name == 'split_into':
+ from sphinx.util.index_entries import _split_into as obj
+
+ _deprecation_warning(__name__, name, '', remove=(9, 0))
+ return obj
+
+ if name == 'ExtensionError':
+ from sphinx.errors import ExtensionError as obj # NoQA: N813
+
+ canonical_name = f'{obj.__module__}.{obj.__qualname__}'
+ _deprecation_warning(__name__, name, canonical_name, remove=(9, 0))
+ return obj
+
+ if name in {'md5', 'sha1'}:
+ obj = globals()[f'_{name}']
+ canonical_name = f'hashlib.{name}'
+ _deprecation_warning(__name__, name, canonical_name, remove=(9, 0))
+ return obj
+
+ # RemovedInSphinx10Warning
+
+ if name in {'DownloadFiles', 'FilenameUniqDict'}:
+ from sphinx.util import _files as mod
+
+ obj = getattr(mod, name)
+ _deprecation_warning(__name__, name, '', remove=(10, 0))
+ return obj
+
+ if name == 'import_object':
+ from sphinx.util._importer import import_object
+
+ _deprecation_warning(__name__, name, '', remove=(10, 0))
+ return import_object
+
+ # Re-exported for backwards compatibility,
+ # but not currently deprecated
+
+ if name == 'encode_uri':
+ from sphinx.util._uri import encode_uri
+
+ return encode_uri
+
+ if name == 'isurl':
+ from sphinx.util._uri import is_url
+
+ return is_url
+
+ if name == 'parselinenos':
+ from sphinx.util._lines import parse_line_num_spec
+
+ return parse_line_num_spec
+
+ if name == 'patfilter':
+ from sphinx.util.matching import patfilter
+
+ return patfilter
+
+ if name == 'strip_escape_sequences':
+ from sphinx._cli.util.errors import strip_escape_sequences
+
+ return strip_escape_sequences
+
+ if name in {
+ 'caption_ref_re',
+ 'explicit_title_re',
+ 'nested_parse_with_titles',
+ 'split_explicit_title',
+ }:
+ from sphinx.util import nodes as mod
+
+ return getattr(mod, name)
+
+ if name in {
+ 'SEP',
+ 'copyfile',
+ 'ensuredir',
+ 'make_filename',
+ 'os_path',
+ 'relative_uri',
+ }:
+ from sphinx.util import osutil as mod
+
+ return getattr(mod, name)
- deprecated_object, canonical_name, remove = _DEPRECATED_OBJECTS[name]
- _deprecation_warning(__name__, name, canonical_name, remove=remove)
- return deprecated_object
+ msg = f'module {__name__!r} has no attribute {name!r}'
+ raise AttributeError(msg)
diff --git a/sphinx/util/_files.py b/sphinx/util/_files.py
index 85f63359e98..65313801be0 100644
--- a/sphinx/util/_files.py
+++ b/sphinx/util/_files.py
@@ -2,12 +2,15 @@
import hashlib
import os.path
-from typing import Any
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from collections.abc import Set
+ from typing import Any
class FilenameUniqDict(dict[str, tuple[set[str], str]]):
- """
- A dictionary that automatically generates unique names for its keys,
+ """A dictionary that automatically generates unique names for its keys,
interpreted as filenames, and keeps track of a set of docnames they
appear in. Used for images and downloadable files in the environment.
"""
@@ -15,7 +18,8 @@ class FilenameUniqDict(dict[str, tuple[set[str], str]]):
def __init__(self) -> None:
self._existing: set[str] = set()
- def add_file(self, docname: str, newfile: str) -> str:
+ def add_file(self, docname: str, newfile: str | os.PathLike[str]) -> str:
+ newfile = str(newfile)
if newfile in self:
self[newfile][0].add(docname)
return self[newfile][1]
@@ -37,7 +41,7 @@ def purge_doc(self, docname: str) -> None:
self._existing.discard(unique)
def merge_other(
- self, docnames: set[str], other: dict[str, tuple[set[str], Any]]
+ self, docnames: Set[str], other: dict[str, tuple[set[str], Any]]
) -> None:
for filename, (docs, _unique) in other.items():
for doc in docs & set(docnames):
@@ -73,7 +77,7 @@ def purge_doc(self, docname: str) -> None:
del self[filename]
def merge_other(
- self, docnames: set[str], other: dict[str, tuple[set[str], Any]]
+ self, docnames: Set[str], other: dict[str, tuple[set[str], Any]]
) -> None:
for filename, (docs, _dest) in other.items():
for docname in docs & set(docnames):
diff --git a/sphinx/util/_importer.py b/sphinx/util/_importer.py
index 915750d2d88..3cbd69c78a2 100644
--- a/sphinx/util/_importer.py
+++ b/sphinx/util/_importer.py
@@ -1,10 +1,13 @@
from __future__ import annotations
from importlib import import_module
-from typing import Any
+from typing import TYPE_CHECKING
from sphinx.errors import ExtensionError
+if TYPE_CHECKING:
+ from typing import Any
+
def import_object(object_name: str, /, source: str = '') -> Any:
"""Import python object by qualname."""
diff --git a/sphinx/util/_inventory_file_reader.py b/sphinx/util/_inventory_file_reader.py
new file mode 100644
index 00000000000..d19faa87ea9
--- /dev/null
+++ b/sphinx/util/_inventory_file_reader.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+
+import zlib
+from typing import TYPE_CHECKING
+
+from sphinx.util import logging
+
+BUFSIZE = 16 * 1024
+logger = logging.getLogger(__name__)
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+ from typing import Protocol
+
+ # Readable file stream for inventory loading
+ class _SupportsRead(Protocol):
+ def read(self, size: int = ...) -> bytes: ...
+
+
+__all__ = ('InventoryFileReader',)
+
+
+class InventoryFileReader:
+ """A file reader for an inventory file.
+
+ This reader supports mixture of texts and compressed texts.
+ """
+
+ def __init__(self, stream: _SupportsRead) -> None:
+ self.stream = stream
+ self.buffer = b''
+ self.eof = False
+
+ def read_buffer(self) -> None:
+ chunk = self.stream.read(BUFSIZE)
+ if chunk == b'':
+ self.eof = True
+ self.buffer += chunk
+
+ def readline(self) -> str:
+ pos = self.buffer.find(b'\n')
+ if pos != -1:
+ line = self.buffer[:pos].decode()
+ self.buffer = self.buffer[pos + 1 :]
+ elif self.eof:
+ line = self.buffer.decode()
+ self.buffer = b''
+ else:
+ self.read_buffer()
+ line = self.readline()
+
+ return line
+
+ def readlines(self) -> Iterator[str]:
+ while not self.eof:
+ line = self.readline()
+ if line:
+ yield line
+
+ def read_compressed_chunks(self) -> Iterator[bytes]:
+ decompressor = zlib.decompressobj()
+ while not self.eof:
+ self.read_buffer()
+ yield decompressor.decompress(self.buffer)
+ self.buffer = b''
+ yield decompressor.flush()
+
+ def read_compressed_lines(self) -> Iterator[str]:
+ buf = b''
+ for chunk in self.read_compressed_chunks():
+ buf += chunk
+ pos = buf.find(b'\n')
+ while pos != -1:
+ yield buf[:pos].decode()
+ buf = buf[pos + 1 :]
+ pos = buf.find(b'\n')
diff --git a/sphinx/util/_io.py b/sphinx/util/_io.py
index 9a36097bcf4..94ad9f8f0d0 100644
--- a/sphinx/util/_io.py
+++ b/sphinx/util/_io.py
@@ -2,13 +2,13 @@
from typing import TYPE_CHECKING
-from sphinx.util.console import strip_escape_sequences
+from sphinx._cli.util.errors import strip_escape_sequences
if TYPE_CHECKING:
from typing import Protocol
class SupportsWrite(Protocol):
- def write(self, text: str, /) -> int | None: ... # NoQA: E704
+ def write(self, text: str, /) -> int | None: ...
class TeeStripANSI:
diff --git a/sphinx/util/_lines.py b/sphinx/util/_lines.py
index e9f8899cd29..019cfd2fa16 100644
--- a/sphinx/util/_lines.py
+++ b/sphinx/util/_lines.py
@@ -1,3 +1,6 @@
+from __future__ import annotations
+
+
def parse_line_num_spec(spec: str, total: int) -> list[int]:
"""Parse a line number spec (such as "1,2,4-6") and return a list of
wanted line numbers.
@@ -8,17 +11,17 @@ def parse_line_num_spec(spec: str, total: int) -> list[int]:
try:
begend = part.strip().split('-')
if begend == ['', '']:
- raise ValueError
+ raise ValueError # NoQA: TRY301
if len(begend) == 1:
items.append(int(begend[0]) - 1)
elif len(begend) == 2:
start = int(begend[0] or 1) # left half open (cf. -10)
end = int(begend[1] or max(start, total)) # right half open (cf. 10-)
if start > end: # invalid range (cf. 10-1)
- raise ValueError
+ raise ValueError # NoQA: TRY301
items.extend(range(start - 1, end))
else:
- raise ValueError
+ raise ValueError # NoQA: TRY301
except ValueError as exc:
msg = f'invalid line number spec: {spec!r}'
raise ValueError(msg) from exc
diff --git a/sphinx/util/_pathlib.py b/sphinx/util/_pathlib.py
index 31b47ce5a67..a304ff75ca4 100644
--- a/sphinx/util/_pathlib.py
+++ b/sphinx/util/_pathlib.py
@@ -17,10 +17,13 @@
import sys
import warnings
from pathlib import Path, PosixPath, PurePath, WindowsPath
-from typing import Any
+from typing import TYPE_CHECKING, overload
from sphinx.deprecation import RemovedInSphinx90Warning
+if TYPE_CHECKING:
+ from typing import Any
+
_STR_METHODS = frozenset(str.__dict__)
_PATH_NAME = Path().__class__.__name__
@@ -133,3 +136,38 @@ def __getitem__(self, item: int | slice) -> str:
def __len__(self) -> int:
warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
return len(self.__str__())
+
+
+class _StrPathProperty:
+ def __init__(self) -> None:
+ self.instance_attr: str = ''
+
+ def __set_name__(self, owner: object, name: str) -> None:
+ self.instance_attr = f'_{name}' # i.e. '_srcdir'
+
+ @overload
+ def __get__(self, obj: None, objtype: None) -> _StrPathProperty: ...
+
+ @overload
+ def __get__(self, obj: object, objtype: type[object]) -> _StrPath: ...
+
+ def __get__(
+ self, obj: object | None, objtype: type[object] | None = None
+ ) -> _StrPathProperty | _StrPath:
+ if obj is None:
+ return self
+ if not self.instance_attr:
+ raise AttributeError
+ return getattr(obj, self.instance_attr)
+
+ def __set__(self, obj: Any, value: _StrPath | Path) -> None:
+ try:
+ setattr(obj, self.instance_attr, _StrPath(value))
+ except TypeError as err:
+ cls_name = type(obj).__qualname__
+ name = self.instance_attr.removeprefix('_')
+ msg = f'{cls_name}.{name} may only be set to path-like objects'
+ raise TypeError(msg) from err
+
+ def __delete__(self, obj: Any) -> None:
+ delattr(obj, self.instance_attr)
diff --git a/sphinx/util/build_phase.py b/sphinx/util/build_phase.py
index 76e94a9b0d2..382fc722ee8 100644
--- a/sphinx/util/build_phase.py
+++ b/sphinx/util/build_phase.py
@@ -1,5 +1,7 @@
"""Build phase of Sphinx application."""
+from __future__ import annotations
+
from enum import IntEnum
diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py
index 127b1b247b5..6071d90cf74 100644
--- a/sphinx/util/cfamily.py
+++ b/sphinx/util/cfamily.py
@@ -13,7 +13,7 @@
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
- from typing import Any, TypeAlias
+ from typing import Any, NoReturn, TypeAlias
from docutils.nodes import TextElement
@@ -33,7 +33,7 @@
| (@[a-zA-Z0-9_]) # our extension for names of anonymous entities
)
[a-zA-Z0-9_]*\b
-""",
+ """,
flags=re.VERBOSE,
)
integer_literal_re = re.compile(r'[1-9][0-9]*(\'[0-9]+)*')
@@ -50,7 +50,7 @@
)\b
# the ending word boundary is important for distinguishing
# between suffixes and UDLs in C++
-""",
+ """,
flags=re.VERBOSE,
)
float_literal_re = re.compile(
@@ -66,7 +66,7 @@
[0-9a-fA-F]+(\'[0-9a-fA-F]+)*([pP][+-]?[0-9a-fA-F]+(\'[0-9a-fA-F]+)*)?)
| (0[xX][0-9a-fA-F]+(\'[0-9a-fA-F]+)*\.([pP][+-]?[0-9a-fA-F]+(\'[0-9a-fA-F]+)*)?)
)
-""",
+ """,
flags=re.VERBOSE,
)
float_literal_suffix_re = re.compile(r'[fFlL]\b')
@@ -84,7 +84,7 @@
| (?:U[0-9a-fA-F]{8})
))
)'
-""",
+ """,
flags=re.VERBOSE,
)
@@ -115,7 +115,7 @@ def clone(self) -> Any:
return deepcopy(self)
def _stringify(self, transform: StringifyTransform) -> str:
- raise NotImplementedError(repr(self))
+ raise NotImplementedError
def __str__(self) -> str:
return self._stringify(str)
@@ -124,7 +124,9 @@ def get_display_string(self) -> str:
return self._stringify(lambda ast: ast.get_display_string())
def __repr__(self) -> str:
- return f'<{self.__class__.__name__}: {self._stringify(repr)}>'
+ if repr_string := self._stringify(repr):
+ return f'<{self.__class__.__name__}: {repr_string}>'
+ return f'<{self.__class__.__name__}>'
################################################################################
@@ -339,7 +341,7 @@ def status(self, msg: str) -> None:
indicator = '-' * self.pos + '^'
logger.debug(f'{msg}\n{self.definition}\n{indicator}') # NoQA: G004
- def fail(self, msg: str) -> None:
+ def fail(self, msg: str) -> NoReturn:
errors = []
indicator = '-' * self.pos + '^'
msg = (
@@ -434,7 +436,7 @@ def paren_attributes(self) -> Sequence[str]:
def _parse_balanced_token_seq(self, end: list[str]) -> str:
# TODO: add handling of string literals and similar
brackets = {'(': ')', '[': ']', '{': '}'}
- startPos = self.pos
+ start_pos = self.pos
symbols: list[str] = []
while not self.eof:
if len(symbols) == 0 and self.current_char in end:
@@ -448,17 +450,17 @@ def _parse_balanced_token_seq(self, end: list[str]) -> str:
self.pos += 1
if self.eof:
self.fail(
- f'Could not find end of balanced-token-seq starting at {startPos}.'
+ f'Could not find end of balanced-token-seq starting at {start_pos}.'
)
- return self.definition[startPos : self.pos]
+ return self.definition[start_pos : self.pos]
def _parse_attribute(self) -> ASTAttribute | None:
self.skip_ws()
# try C++11 style
- startPos = self.pos
+ start_pos = self.pos
if self.skip_string_and_ws('['):
if not self.skip_string('['):
- self.pos = startPos
+ self.pos = start_pos
else:
# TODO: actually implement the correct grammar
arg = self._parse_balanced_token_seq(end=[']'])
diff --git a/sphinx/util/console.py b/sphinx/util/console.py
index 86e4223782a..90d69411c86 100644
--- a/sphinx/util/console.py
+++ b/sphinx/util/console.py
@@ -2,71 +2,44 @@
from __future__ import annotations
-import os
-import re
import shutil
-import sys
-from typing import TYPE_CHECKING
-if TYPE_CHECKING:
- from typing import Final
-
- # fmt: off
- def reset(text: str) -> str: ... # NoQA: E704
- def bold(text: str) -> str: ... # NoQA: E704
- def faint(text: str) -> str: ... # NoQA: E704
- def standout(text: str) -> str: ... # NoQA: E704
- def underline(text: str) -> str: ... # NoQA: E704
- def blink(text: str) -> str: ... # NoQA: E704
-
- def black(text: str) -> str: ... # NoQA: E704
- def white(text: str) -> str: ... # NoQA: E704
- def red(text: str) -> str: ... # NoQA: E704
- def green(text: str) -> str: ... # NoQA: E704
- def yellow(text: str) -> str: ... # NoQA: E704
- def blue(text: str) -> str: ... # NoQA: E704
- def fuchsia(text: str) -> str: ... # NoQA: E704
- def teal(text: str) -> str: ... # NoQA: E704
-
- def darkgray(text: str) -> str: ... # NoQA: E704
- def lightgray(text: str) -> str: ... # NoQA: E704
- def darkred(text: str) -> str: ... # NoQA: E704
- def darkgreen(text: str) -> str: ... # NoQA: E704
- def brown(text: str) -> str: ... # NoQA: E704
- def darkblue(text: str) -> str: ... # NoQA: E704
- def purple(text: str) -> str: ... # NoQA: E704
- def turquoise(text: str) -> str: ... # NoQA: E704
- # fmt: on
-
-try:
- # check if colorama is installed to support color on Windows
- import colorama
-
- COLORAMA_AVAILABLE = True
-except ImportError:
- COLORAMA_AVAILABLE = False
-
-_CSI: Final[str] = re.escape('\x1b[') # 'ESC [': Control Sequence Introducer
-
-# Pattern matching ANSI control sequences containing colors.
-_ansi_color_re: Final[re.Pattern[str]] = re.compile(r'\x1b\[(?:\d+;){0,2}\d*m')
-
-_ansi_re: Final[re.Pattern[str]] = re.compile(
- _CSI
- + r"""
- (?:
- (?:\d+;){0,2}\d*m # ANSI color code ('m' is equivalent to '0m')
- |
- [012]?K # ANSI Erase in Line ('K' is equivalent to '0K')
- )""",
- re.VERBOSE | re.ASCII,
+import sphinx._cli.util.colour
+from sphinx._cli.util.colour import ( # NoQA: F401
+ _create_input_mode_colour_func,
+ black,
+ blink,
+ blue,
+ bold,
+ brown,
+ colourise,
+ darkblue,
+ darkgray,
+ darkgreen,
+ darkred,
+ disable_colour,
+ enable_colour,
+ faint,
+ fuchsia,
+ green,
+ lightgray,
+ purple,
+ red,
+ reset,
+ standout,
+ teal,
+ terminal_supports_colour,
+ turquoise,
+ underline,
+ white,
+ yellow,
)
-"""Pattern matching ANSI CSI colors (SGR) and erase line (EL) sequences.
+from sphinx._cli.util.errors import strip_escape_sequences
-See :func:`strip_escape_sequences` for details.
-"""
-
-codes: dict[str, str] = {}
+color_terminal = terminal_supports_colour
+nocolor = disable_colour
+coloron = enable_colour
+strip_colors = strip_escape_sequences
def terminal_safe(s: str) -> str:
@@ -83,7 +56,7 @@ def get_terminal_width() -> int:
def term_width_line(text: str) -> str:
- if not codes:
+ if sphinx._cli.util.colour._COLOURING_DISABLED:
# if no coloring, don't output fancy backspaces
return text + '\n'
else:
@@ -91,121 +64,13 @@ def term_width_line(text: str) -> str:
return text.ljust(_tw + len(text) - len(strip_escape_sequences(text))) + '\r'
-def color_terminal() -> bool:
- if 'NO_COLOR' in os.environ:
- return False
- if sys.platform == 'win32' and COLORAMA_AVAILABLE:
- colorama.just_fix_windows_console()
- return True
- if 'FORCE_COLOR' in os.environ:
- return True
- if not hasattr(sys.stdout, 'isatty'):
- return False
- if not sys.stdout.isatty():
- return False
- if 'COLORTERM' in os.environ:
- return True
- term = os.environ.get('TERM', 'dumb').lower()
- return term in {'xterm', 'linux'} or 'color' in term
-
-
-def nocolor() -> None:
- if sys.platform == 'win32' and COLORAMA_AVAILABLE:
- colorama.deinit()
- codes.clear()
-
-
-def coloron() -> None:
- codes.update(_orig_codes)
-
-
def colorize(name: str, text: str, input_mode: bool = False) -> str:
- def escseq(name: str) -> str:
- # Wrap escape sequence with ``\1`` and ``\2`` to let readline know
- # it is non-printable characters
- # ref: https://tiswww.case.edu/php/chet/readline/readline.html
- #
- # Note: This hack does not work well in Windows (see #5059)
- escape = codes.get(name, '')
- if input_mode and escape and sys.platform != 'win32':
- return '\1' + escape + '\2'
- else:
- return escape
-
- return escseq(name) + text + escseq('reset')
-
-
-def strip_colors(s: str) -> str:
- """Remove the ANSI color codes in a string *s*.
-
- .. caution::
-
- This function is not meant to be used in production and should only
- be used for testing Sphinx's output messages.
-
- .. seealso:: :func:`strip_escape_sequences`
- """
- return _ansi_color_re.sub('', s)
-
-
-def strip_escape_sequences(text: str, /) -> str:
- r"""Remove the ANSI CSI colors and "erase in line" sequences.
-
- Other `escape sequences `__ (e.g., VT100-specific functions) are not
- supported and only control sequences *natively* known to Sphinx (i.e.,
- colors declared in this module and "erase entire line" (``'\x1b[2K'``))
- are eliminated by this function.
-
- .. caution::
-
- This function is not meant to be used in production and should only
- be used for testing Sphinx's output messages that were not tempered
- with by third-party extensions.
-
- .. versionadded:: 7.3
-
- This function is added as an *experimental* feature.
-
- __ https://en.wikipedia.org/wiki/ANSI_escape_code
- """
- return _ansi_re.sub('', text)
-
-
-def create_color_func(name: str) -> None:
- def inner(text: str) -> str:
- return colorize(name, text)
-
- globals()[name] = inner
-
-
-_attrs = {
- 'reset': '39;49;00m',
- 'bold': '01m',
- 'faint': '02m',
- 'standout': '03m',
- 'underline': '04m',
- 'blink': '05m',
-}
-
-for __name, __value in _attrs.items():
- codes[__name] = '\x1b[' + __value
-
-_colors = [
- ('black', 'darkgray'),
- ('darkred', 'red'),
- ('darkgreen', 'green'),
- ('brown', 'yellow'),
- ('darkblue', 'blue'),
- ('purple', 'fuchsia'),
- ('turquoise', 'teal'),
- ('lightgray', 'white'),
-]
-
-for __i, (__dark, __light) in enumerate(_colors, 30):
- codes[__dark] = '\x1b[%im' % __i
- codes[__light] = '\x1b[%im' % (__i + 60)
-
-_orig_codes = codes.copy()
-
-for _name in codes:
- create_color_func(_name)
+ if input_mode:
+ colour_func = globals()[name]
+ escape_code = getattr(colour_func, '__escape_code', '')
+ if not escape_code:
+ return colour_func(text)
+ inner = _create_input_mode_colour_func(escape_code)
+ return inner(text)
+
+ return colourise(name, text)
diff --git a/sphinx/util/display.py b/sphinx/util/display.py
index 95cb42bbfe8..196b0f128d1 100644
--- a/sphinx/util/display.py
+++ b/sphinx/util/display.py
@@ -2,9 +2,9 @@
import functools
+from sphinx._cli.util.colour import bold, terminal_supports_colour
from sphinx.locale import __
from sphinx.util import logging
-from sphinx.util.console import bold, color_terminal
if False:
from collections.abc import Callable, Iterable, Iterator
@@ -35,12 +35,12 @@ def status_iterator(
stringify_func: Callable[[Any], str] = display_chunk,
) -> Iterator[T]:
# printing on a single line requires ANSI control sequences
- single_line = verbosity < 1 and color_terminal()
+ single_line = verbosity < 1 and terminal_supports_colour()
bold_summary = bold(summary)
if length == 0:
logger.info(bold_summary, nonl=True)
for item in iterable:
- logger.info(stringify_func(item) + ' ', nonl=True, color=color)
+ logger.info('%s ', stringify_func(item), nonl=True, color=color)
yield item
else:
for i, item in enumerate(iterable, start=1):
@@ -78,14 +78,14 @@ def __exit__(
) -> bool:
prefix = '' if self.nonl else bold(self.message + ': ')
if isinstance(val, SkipProgressMessage):
- logger.info(prefix + __('skipped'))
+ logger.info(prefix + __('skipped')) # NoQA: G003
if val.args:
logger.info(*val.args)
return True
elif val:
- logger.info(prefix + __('failed'))
+ logger.info(prefix + __('failed')) # NoQA: G003
else:
- logger.info(prefix + __('done'))
+ logger.info(prefix + __('done')) # NoQA: G003
return False
diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py
index 33fda6f5dcd..1c24a73bf2e 100644
--- a/sphinx/util/docfields.py
+++ b/sphinx/util/docfields.py
@@ -7,10 +7,9 @@
from __future__ import annotations
import contextlib
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
-from docutils.nodes import Element, Node
from sphinx import addnodes
from sphinx.locale import __
@@ -18,12 +17,20 @@
from sphinx.util.nodes import get_node_line
if TYPE_CHECKING:
+ from typing import TypeAlias, TypeVar
+
+ from docutils.nodes import Element, Node
from docutils.parsers.rst.states import Inliner
from sphinx.directives import ObjectDescription
from sphinx.environment import BuildEnvironment
from sphinx.util.typing import TextlikeNode
+ ObjDescT = TypeVar('ObjDescT')
+ _FieldEntry: TypeAlias = tuple[str, list[Node]]
+ _FieldTypes: TypeAlias = dict[str, list[Node]]
+ _EntriesTriple: TypeAlias = tuple['Field', _FieldEntry | list[_FieldEntry], Element]
+
logger = logging.getLogger(__name__)
@@ -131,14 +138,14 @@ def make_xrefs(
)
]
- def make_entry(self, fieldarg: str, content: list[Node]) -> tuple[str, list[Node]]:
+ def make_entry(self, fieldarg: str, content: list[Node]) -> _FieldEntry:
return fieldarg, content
def make_field(
self,
- types: dict[str, list[Node]],
+ types: _FieldTypes,
domain: str,
- item: tuple,
+ item: _FieldEntry,
env: BuildEnvironment | None = None,
inliner: Inliner | None = None,
location: Element | None = None,
@@ -181,8 +188,7 @@ def make_field(
class GroupedField(Field):
- """
- A doc field that is grouped; i.e., all fields of that type will be
+ """A doc field that is grouped; i.e., all fields of that type will be
transformed into one field with its body being a bulleted list. It always
has an argument. The argument can be linked using the given *rolename*.
GroupedField should be used for doc fields that can occur more than once.
@@ -210,9 +216,9 @@ def __init__(
def make_field(
self,
- types: dict[str, list[Node]],
+ types: _FieldTypes,
domain: str,
- items: tuple,
+ items: list[_FieldEntry], # type: ignore[override]
env: BuildEnvironment | None = None,
inliner: Inliner | None = None,
location: Element | None = None,
@@ -237,7 +243,7 @@ def make_field(
listnode += nodes.list_item('', par)
if len(items) == 1 and self.can_collapse:
- list_item = cast(nodes.list_item, listnode[0])
+ list_item = cast('nodes.list_item', listnode[0])
fieldbody = nodes.field_body('', list_item[0])
return nodes.field('', fieldname, fieldbody)
@@ -246,8 +252,7 @@ def make_field(
class TypedField(GroupedField):
- """
- A doc field that is grouped and has type information for the arguments. It
+ """A doc field that is grouped and has type information for the arguments. It
always has an argument. The argument can be linked using the given
*rolename*, the type using the given *typerolename*.
@@ -283,9 +288,9 @@ def __init__(
def make_field(
self,
- types: dict[str, list[Node]],
+ types: _FieldTypes,
domain: str,
- items: tuple,
+ items: list[_FieldEntry], # type: ignore[override]
env: BuildEnvironment | None = None,
inliner: Inliner | None = None,
location: Element | None = None,
@@ -338,14 +343,13 @@ def handle_item(fieldarg: str, content: list[Node]) -> nodes.paragraph:
class DocFieldTransformer:
- """
- Transforms field lists in "doc field" syntax into better-looking
+ """Transforms field lists in "doc field" syntax into better-looking
equivalents, using the field type definitions given on a domain.
"""
typemap: dict[str, tuple[Field, bool]]
- def __init__(self, directive: ObjectDescription) -> None:
+ def __init__(self, directive: ObjectDescription[ObjDescT]) -> None:
self.directive = directive
self.typemap = directive.get_field_type_map()
@@ -359,115 +363,129 @@ def transform_all(self, node: addnodes.desc_content) -> None:
def transform(self, node: nodes.field_list) -> None:
"""Transform a single field list *node*."""
- typemap = self.typemap
-
- entries: list[nodes.field | tuple[Field, Any, Element]] = []
+ entries: list[nodes.field | _EntriesTriple] = []
groupindices: dict[str, int] = {}
- types: dict[str, dict] = {}
+ types: dict[str, _FieldTypes] = {}
# step 1: traverse all fields and collect field types and content
- for field in cast(list[nodes.field], node):
- assert len(field) == 2
- field_name = cast(nodes.field_name, field[0])
- field_body = cast(nodes.field_body, field[1])
+ for field in cast('list[nodes.field]', node):
+ self._transform_step_1(field, entries, types, groupindices)
+
+ new_list = self._transform_step_2(entries, types)
+ node.replace_self(new_list)
+
+ def _transform_step_1(
+ self,
+ field: nodes.field,
+ entries: list[nodes.field | _EntriesTriple],
+ types: dict[str, _FieldTypes],
+ group_indices: dict[str, int],
+ ) -> None:
+ assert len(field) == 2
+ field_name = cast('nodes.field_name', field[0])
+ field_body = cast('nodes.field_body', field[1])
+ try:
+ # split into field type and argument
+ fieldtype_name, fieldarg = field_name.astext().split(None, 1)
+ except ValueError:
+ # maybe an argument-less field type?
+ fieldtype_name, fieldarg = field_name.astext(), ''
+ typedesc, is_typefield = self.typemap.get(fieldtype_name, (None, None))
+
+ # collect the content, trying not to keep unnecessary paragraphs
+ if _is_single_paragraph(field_body):
+ paragraph = cast('nodes.paragraph', field_body[0])
+ content = paragraph.children
+ else:
+ content = field_body.children
+
+ # sort out unknown fields
+ if typedesc is None or typedesc.has_arg != bool(fieldarg):
+ # either the field name is unknown, or the argument doesn't
+ # match the spec; capitalize field name and be done with it
+ new_fieldname = fieldtype_name[0:1].upper() + fieldtype_name[1:]
+ if fieldarg:
+ new_fieldname += ' ' + fieldarg
+ field_name[0] = nodes.Text(new_fieldname)
+ entries.append(field)
+
+ # but if this has a type then we can at least link it
+ if (
+ typedesc
+ and is_typefield
+ and content
+ and len(content) == 1
+ and isinstance(content[0], nodes.Text)
+ ):
+ typed_field = cast('TypedField', typedesc)
+ target = content[0].astext()
+ xrefs = typed_field.make_xrefs(
+ typed_field.typerolename,
+ self.directive.domain or '',
+ target,
+ contnode=content[0],
+ env=self.directive.env,
+ )
+ if _is_single_paragraph(field_body):
+ paragraph = cast('nodes.paragraph', field_body[0])
+ paragraph.clear()
+ paragraph.extend(xrefs)
+ else:
+ field_body.clear()
+ field_body += nodes.paragraph('', '', *xrefs)
+
+ return
+
+ typename = typedesc.name
+
+ # if the field specifies a type, put it in the types collection
+ if is_typefield:
+ # filter out only inline nodes; others will result in invalid
+ # markup being written out
+ content = [n for n in content if isinstance(n, nodes.Inline | nodes.Text)]
+ if content:
+ types.setdefault(typename, {})[fieldarg] = content
+ return
+
+ # also support syntax like ``:param type name:``
+ if typedesc.is_typed:
try:
- # split into field type and argument
- fieldtype_name, fieldarg = field_name.astext().split(None, 1)
+ argtype, argname = fieldarg.rsplit(None, 1)
except ValueError:
- # maybe an argument-less field type?
- fieldtype_name, fieldarg = field_name.astext(), ''
- typedesc, is_typefield = typemap.get(fieldtype_name, (None, None))
-
- # collect the content, trying not to keep unnecessary paragraphs
- if _is_single_paragraph(field_body):
- paragraph = cast(nodes.paragraph, field_body[0])
- content = paragraph.children
+ pass
else:
- content = field_body.children
-
- # sort out unknown fields
- if typedesc is None or typedesc.has_arg != bool(fieldarg):
- # either the field name is unknown, or the argument doesn't
- # match the spec; capitalize field name and be done with it
- new_fieldname = fieldtype_name[0:1].upper() + fieldtype_name[1:]
- if fieldarg:
- new_fieldname += ' ' + fieldarg
- field_name[0] = nodes.Text(new_fieldname)
- entries.append(field)
-
- # but if this has a type then we can at least link it
- if (
- typedesc
- and is_typefield
- and content
- and len(content) == 1
- and isinstance(content[0], nodes.Text)
- ):
- typed_field = cast(TypedField, typedesc)
- target = content[0].astext()
- xrefs = typed_field.make_xrefs(
- typed_field.typerolename,
- self.directive.domain or '',
- target,
- contnode=content[0],
- env=self.directive.state.document.settings.env,
- )
- if _is_single_paragraph(field_body):
- paragraph = cast(nodes.paragraph, field_body[0])
- paragraph.clear()
- paragraph.extend(xrefs)
- else:
- field_body.clear()
- field_body += nodes.paragraph('', '', *xrefs)
-
- continue
-
- typename = typedesc.name
-
- # if the field specifies a type, put it in the types collection
- if is_typefield:
- # filter out only inline nodes; others will result in invalid
- # markup being written out
- content = [
- n for n in content if isinstance(n, nodes.Inline | nodes.Text)
- ]
- if content:
- types.setdefault(typename, {})[fieldarg] = content
- continue
-
- # also support syntax like ``:param type name:``
- if typedesc.is_typed:
- try:
- argtype, argname = fieldarg.rsplit(None, 1)
- except ValueError:
- pass
- else:
- types.setdefault(typename, {})[argname] = [nodes.Text(argtype)]
- fieldarg = argname
-
- translatable_content = nodes.inline(field_body.rawsource, translatable=True)
- translatable_content.document = field_body.parent.document
- translatable_content.source = field_body.parent.source
- translatable_content.line = field_body.parent.line
- translatable_content += content
-
- # grouped entries need to be collected in one entry, while others
- # get one entry per field
- if typedesc.is_grouped:
- if typename in groupindices:
- group = cast(
- tuple[Field, list, Node], entries[groupindices[typename]]
- )
- else:
- groupindices[typename] = len(entries)
- group = (typedesc, [], field)
- entries.append(group)
- new_entry = typedesc.make_entry(fieldarg, [translatable_content])
- group[1].append(new_entry)
+ types.setdefault(typename, {})[argname] = [nodes.Text(argtype)]
+ fieldarg = argname
+
+ translatable_content = nodes.inline(field_body.rawsource, translatable=True)
+ translatable_content.document = field_body.parent.document
+ translatable_content.source = field_body.parent.source
+ translatable_content.line = field_body.parent.line
+ translatable_content += content
+
+ # grouped entries need to be collected in one entry, while others
+ # get one entry per field
+ if typedesc.is_grouped:
+ if typename in group_indices:
+ group = cast(
+ 'tuple[Field, list[_FieldEntry], Node]',
+ entries[group_indices[typename]],
+ )
else:
- new_entry = typedesc.make_entry(fieldarg, [translatable_content])
- entries.append((typedesc, new_entry, field))
+ group_indices[typename] = len(entries)
+ group = (typedesc, [], field)
+ entries.append(group)
+ new_entry = typedesc.make_entry(fieldarg, [translatable_content])
+ group[1].append(new_entry)
+ else:
+ new_entry = typedesc.make_entry(fieldarg, [translatable_content])
+ entries.append((typedesc, new_entry, field))
+ def _transform_step_2(
+ self,
+ entries: list[nodes.field | _EntriesTriple],
+ types: dict[str, _FieldTypes],
+ ) -> nodes.field_list:
# step 2: all entries are collected, construct the new field list
new_list = nodes.field_list()
for entry in entries:
@@ -477,16 +495,16 @@ def transform(self, node: nodes.field_list) -> None:
else:
fieldtype, items, location = entry
fieldtypes = types.get(fieldtype.name, {})
- env = self.directive.state.document.settings.env
+ env = self.directive.env
inliner = self.directive.state.inliner
domain = self.directive.domain or ''
new_list += fieldtype.make_field(
fieldtypes,
domain,
- items,
+ items, # type: ignore[arg-type]
env=env,
inliner=inliner,
location=location,
)
- node.replace_self(new_list)
+ return new_list
diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py
index 30c87595f31..ba6116af61d 100644
--- a/sphinx/util/docutils.py
+++ b/sphinx/util/docutils.py
@@ -4,22 +4,20 @@
import os
import re
-from collections.abc import Sequence # NoQA: TCH003
from contextlib import contextmanager
from copy import copy
from pathlib import Path
-from typing import IO, TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING
import docutils
from docutils import nodes
from docutils.io import FileOutput
from docutils.parsers.rst import Directive, directives, roles
-from docutils.parsers.rst.states import Inliner # NoQA: TCH002
-from docutils.statemachine import State, StateMachine, StringList
+from docutils.statemachine import StateMachine
from docutils.utils import Reporter, unescape
from sphinx.errors import SphinxError
-from sphinx.locale import _, __
+from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.parsing import nested_parse_to_nodes
@@ -29,17 +27,44 @@
)
if TYPE_CHECKING:
- from collections.abc import Callable, Iterator # NoQA: TCH003
- from types import ModuleType
+ from collections.abc import Iterator, Sequence
+ from types import ModuleType, TracebackType
+ from typing import Any, Protocol
from docutils.frontend import Values
from docutils.nodes import Element, Node, system_message
+ from docutils.parsers.rst.states import Inliner
+ from docutils.statemachine import State, StringList
from sphinx.builders import Builder
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
from sphinx.util.typing import RoleFunction
+ class _LanguageModule(Protocol):
+ labels: dict[str, str]
+ author_separators: list[str]
+ bibliographic_fields: list[str]
+
+ class _DirectivesDispatcher(Protocol):
+ def __call__(
+ self,
+ directive_name: str,
+ language_module: _LanguageModule,
+ document: nodes.document,
+ /,
+ ) -> tuple[type[Directive] | None, list[system_message]]: ...
+
+ class _RolesDispatcher(Protocol):
+ def __call__(
+ self,
+ role_name: str,
+ language_module: _LanguageModule,
+ lineno: int,
+ reporter: Reporter,
+ /,
+ ) -> tuple[RoleFunction | None, list[system_message]]: ...
+
additional_nodes: set[type[Element]] = set()
@@ -205,14 +230,17 @@ class CustomReSTDispatcher:
"""
def __init__(self) -> None:
- self.directive_func: Callable = lambda *args: (None, [])
- self.roles_func: Callable = lambda *args: (None, [])
+ self.directive_func: _DirectivesDispatcher = lambda *args: (None, [])
+ self.roles_func: _RolesDispatcher = lambda *args: (None, [])
def __enter__(self) -> None:
self.enable()
def __exit__(
- self, exc_type: type[Exception], exc_value: Exception, traceback: Any
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
) -> None:
self.disable()
@@ -224,7 +252,7 @@ def enable(self) -> None:
roles.role = self.role # type: ignore[assignment]
def disable(self) -> None:
- directives.directive = self.directive_func
+ directives.directive = self.directive_func # type: ignore[assignment]
roles.role = self.role_func
def directive(
@@ -260,51 +288,44 @@ class sphinx_domains(CustomReSTDispatcher):
"""
def __init__(self, env: BuildEnvironment) -> None:
- self.env = env
+ self.domains = env.domains
+ self.current_document = env.current_document
super().__init__()
- def lookup_domain_element(self, type: str, name: str) -> Any:
- """Lookup a markup element (directive or role), given its name which can
- be a full name (with domain).
- """
- name = name.lower()
+ def directive(
+ self,
+ directive_name: str,
+ language_module: ModuleType,
+ document: nodes.document,
+ ) -> tuple[type[Directive] | None, list[system_message]]:
+ """Lookup a directive, given its name which can include a domain."""
+ directive_name = directive_name.lower()
# explicit domain given?
- if ':' in name:
- domain_name, name = name.split(':', 1)
- if domain_name in self.env.domains:
- domain = self.env.get_domain(domain_name)
- element = getattr(domain, type)(name)
+ if ':' in directive_name:
+ domain_name, _, name = directive_name.partition(':')
+ try:
+ domain = self.domains[domain_name]
+ except KeyError:
+ logger.warning(__('unknown directive name: %s'), directive_name)
+ else:
+ element = domain.directive(name)
if element is not None:
return element, []
- else:
- logger.warning(
- _('unknown directive or role name: %s:%s'), domain_name, name
- )
# else look in the default domain
else:
- def_domain = self.env.temp_data.get('default_domain')
- if def_domain is not None:
- element = getattr(def_domain, type)(name)
+ name = directive_name
+ default_domain = self.current_document.default_domain
+ if default_domain is not None:
+ element = default_domain.directive(name)
if element is not None:
return element, []
# always look in the std domain
- element = getattr(self.env.domains.standard_domain, type)(name)
+ element = self.domains.standard_domain.directive(name)
if element is not None:
return element, []
- raise ElementLookupError
-
- def directive(
- self,
- directive_name: str,
- language_module: ModuleType,
- document: nodes.document,
- ) -> tuple[type[Directive] | None, list[system_message]]:
- try:
- return self.lookup_domain_element('directive', directive_name)
- except ElementLookupError:
- return super().directive(directive_name, language_module, document)
+ return super().directive(directive_name, language_module, document)
def role(
self,
@@ -313,10 +334,34 @@ def role(
lineno: int,
reporter: Reporter,
) -> tuple[RoleFunction, list[system_message]]:
- try:
- return self.lookup_domain_element('role', role_name)
- except ElementLookupError:
- return super().role(role_name, language_module, lineno, reporter)
+ """Lookup a role, given its name which can include a domain."""
+ role_name = role_name.lower()
+ # explicit domain given?
+ if ':' in role_name:
+ domain_name, _, name = role_name.partition(':')
+ try:
+ domain = self.domains[domain_name]
+ except KeyError:
+ logger.warning(__('unknown role name: %s'), role_name)
+ else:
+ element = domain.role(name)
+ if element is not None:
+ return element, []
+ # else look in the default domain
+ else:
+ name = role_name
+ default_domain = self.current_document.default_domain
+ if default_domain is not None:
+ element = default_domain.role(name)
+ if element is not None:
+ return element, []
+
+ # always look in the std domain
+ element = self.domains.standard_domain.role(name)
+ if element is not None:
+ return element, []
+
+ return super().role(role_name, language_module, lineno, reporter)
class WarningStream:
@@ -352,7 +397,7 @@ def __init__(
debug: bool = False,
error_handler: str = 'backslashreplace',
) -> None:
- stream = cast(IO, WarningStream())
+ stream = WarningStream()
super().__init__(
source, report_level, halt_level, stream, debug, error_handler=error_handler
)
@@ -366,7 +411,7 @@ def __init__(self) -> None:
@contextmanager
-def switch_source_input(state: State, content: StringList) -> Iterator[None]:
+def switch_source_input(state: State[list[str]], content: StringList) -> Iterator[None]:
"""Switch current source input of state temporarily."""
try:
# remember the original ``get_source_and_line()`` method
@@ -375,7 +420,7 @@ def switch_source_input(state: State, content: StringList) -> Iterator[None]:
# replace it by new one
state_machine: StateMachine[None] = StateMachine([], None) # type: ignore[arg-type]
state_machine.input_lines = content
- state.memo.reporter.get_source_and_line = state_machine.get_source_and_line # type: ignore[attr-defined] # NoQA: E501
+ state.memo.reporter.get_source_and_line = state_machine.get_source_and_line # type: ignore[attr-defined]
yield
finally:
@@ -400,9 +445,10 @@ def write(self, data: str) -> str:
and os.path.exists(self.destination_path)
):
with open(self.destination_path, encoding=self.encoding) as f:
- # skip writing: content not changed
- if f.read() == data:
- return data
+ on_disk = f.read()
+ # skip writing: content not changed
+ if on_disk == data:
+ return data
return super().write(data)
@@ -570,7 +616,7 @@ def __call__(
text: str,
lineno: int,
inliner: Inliner,
- options: dict | None = None,
+ options: dict[str, Any] | None = None,
content: Sequence[str] = (),
) -> tuple[list[Node], list[system_message]]:
self.rawtext = rawtext
@@ -584,7 +630,7 @@ def __call__(
if name:
self.name = name.lower()
else:
- self.name = self.env.temp_data.get('default_role', '')
+ self.name = self.env.current_document.default_role
if not self.name:
self.name = self.env.config.default_role
if not self.name:
@@ -664,7 +710,7 @@ def __call__(
text: str,
lineno: int,
inliner: Inliner,
- options: dict | None = None,
+ options: dict[str, Any] | None = None,
content: Sequence[str] = (),
) -> tuple[list[Node], list[system_message]]:
if options is None:
@@ -705,10 +751,10 @@ def __init__(self, document: nodes.document, builder: Builder) -> None:
self.builder = builder
self.config = builder.config
self.settings = document.settings
+ self._domains = builder.env.domains
def dispatch_visit(self, node: Node) -> None:
- """
- Dispatch node to appropriate visitor method.
+ """Dispatch node to appropriate visitor method.
The priority of visitor method is:
1. ``self.visit_{node_class}()``
@@ -724,8 +770,7 @@ def dispatch_visit(self, node: Node) -> None:
super().dispatch_visit(node)
def dispatch_departure(self, node: Node) -> None:
- """
- Dispatch node to appropriate departure method.
+ """Dispatch node to appropriate departure method.
The priority of departure method is:
1. ``self.depart_{node_class}()``
diff --git a/sphinx/util/exceptions.py b/sphinx/util/exceptions.py
deleted file mode 100644
index f12732558b5..00000000000
--- a/sphinx/util/exceptions.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from __future__ import annotations
-
-import sys
-import traceback
-from tempfile import NamedTemporaryFile
-from typing import TYPE_CHECKING
-
-from sphinx.errors import SphinxParallelError
-from sphinx.util.console import strip_escape_sequences
-
-if TYPE_CHECKING:
- from sphinx.application import Sphinx
-
-
-def save_traceback(app: Sphinx | None, exc: BaseException) -> str:
- """Save the given exception's traceback in a temporary file."""
- import platform
-
- import docutils
- import jinja2
- import pygments
-
- import sphinx
-
- if isinstance(exc, SphinxParallelError):
- exc_format = '(Error in parallel process)\n' + exc.traceback
- else:
- exc_format = traceback.format_exc()
-
- if app is None:
- last_msgs = exts_list = ''
- else:
- extensions = app.extensions.values()
- last_msgs = '\n'.join(
- f'# {strip_escape_sequences(s).strip()}' for s in app.messagelog
- )
- exts_list = '\n'.join(
- f'# {ext.name} ({ext.version})'
- for ext in extensions
- if ext.version != 'builtin'
- )
-
- with NamedTemporaryFile(
- 'w', encoding='utf-8', suffix='.log', prefix='sphinx-err-', delete=False
- ) as f:
- f.write(f"""\
-# Platform: {sys.platform}; ({platform.platform()})
-# Sphinx version: {sphinx.__display_version__}
-# Python version: {platform.python_version()} ({platform.python_implementation()})
-# Docutils version: {docutils.__version__}
-# Jinja2 version: {jinja2.__version__}
-# Pygments version: {pygments.__version__}
-
-# Last messages:
-{last_msgs}
-
-# Loaded extensions:
-{exts_list}
-
-# Traceback:
-{exc_format}
-""")
- return f.name
-
-
-def format_exception_cut_frames(x: int = 1) -> str:
- """Format an exception with traceback, but only the last x frames."""
- typ, val, tb = sys.exc_info()
- # res = ['Traceback (most recent call last):\n']
- res: list[str] = []
- tbres = traceback.format_tb(tb)
- res += tbres[-x:]
- res += traceback.format_exception_only(typ, val)
- return ''.join(res)
diff --git a/sphinx/util/fileutil.py b/sphinx/util/fileutil.py
index d5e2e2692d5..4e665bfb0e3 100644
--- a/sphinx/util/fileutil.py
+++ b/sphinx/util/fileutil.py
@@ -5,7 +5,7 @@
import os
import posixpath
from pathlib import Path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from sphinx.locale import __
from sphinx.util import logging
@@ -13,6 +13,7 @@
if TYPE_CHECKING:
from collections.abc import Callable
+ from typing import Any
from sphinx.util.template import BaseRenderer
from sphinx.util.typing import PathMatcher
diff --git a/sphinx/util/http_date.py b/sphinx/util/http_date.py
index d8c51ae9b58..db0ae306b54 100644
--- a/sphinx/util/http_date.py
+++ b/sphinx/util/http_date.py
@@ -3,6 +3,8 @@
Reference: https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1
"""
+from __future__ import annotations
+
import time
import warnings
from email.utils import parsedate_tz
diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py
index 826696c83e3..ae7cf2fd70d 100644
--- a/sphinx/util/i18n.py
+++ b/sphinx/util/i18n.py
@@ -32,7 +32,7 @@
from sphinx.environment import BuildEnvironment
class DateFormatter(Protocol):
- def __call__( # NoQA: E704
+ def __call__(
self,
date: dt.date | None = ...,
format: str = ...,
@@ -40,7 +40,7 @@ def __call__( # NoQA: E704
) -> str: ...
class TimeFormatter(Protocol):
- def __call__( # NoQA: E704
+ def __call__(
self,
time: dt.time | dt.datetime | float | None = ...,
format: str = ...,
@@ -49,7 +49,7 @@ def __call__( # NoQA: E704
) -> str: ...
class DatetimeFormatter(Protocol):
- def __call__( # NoQA: E704
+ def __call__(
self,
datetime: dt.date | dt.time | float | None = ...,
format: str = ...,
@@ -65,7 +65,7 @@ def __call__( # NoQA: E704
class CatalogInfo:
- __slots__ = ('base_dir', 'domain', 'charset')
+ __slots__ = 'base_dir', 'domain', 'charset'
def __init__(
self, base_dir: str | os.PathLike[str], domain: str, charset: str
@@ -167,11 +167,11 @@ def docname_to_domain(docname: str, compaction: bool | str) -> str:
# date_format mappings: ustrftime() to babel.dates.format_datetime()
date_format_mappings = {
- '%a': 'EEE', # Weekday as locale’s abbreviated name.
- '%A': 'EEEE', # Weekday as locale’s full name.
- '%b': 'MMM', # Month as locale’s abbreviated name.
- '%B': 'MMMM', # Month as locale’s full name.
- '%c': 'medium', # Locale’s appropriate date and time representation.
+ '%a': 'EEE', # Weekday as locale's abbreviated name.
+ '%A': 'EEEE', # Weekday as locale's full name.
+ '%b': 'MMM', # Month as locale's abbreviated name.
+ '%B': 'MMMM', # Month as locale's full name.
+ '%c': 'medium', # Locale's appropriate date and time representation.
'%-d': 'd', # Day of the month as a decimal number.
'%d': 'dd', # Day of the month as a zero-padded decimal number.
'%-H': 'H', # Hour (24-hour clock) as a decimal number [0,23].
@@ -184,7 +184,7 @@ def docname_to_domain(docname: str, compaction: bool | str) -> str:
'%m': 'MM', # Month as a zero-padded decimal number.
'%-M': 'm', # Minute as a decimal number [0,59].
'%M': 'mm', # Minute as a zero-padded decimal number [00,59].
- '%p': 'a', # Locale’s equivalent of either AM or PM.
+ '%p': 'a', # Locale's equivalent of either AM or PM.
'%-S': 's', # Second as a decimal number.
'%S': 'ss', # Second as a zero-padded decimal number.
'%U': 'WW', # Week number of the year (Sunday as the first day of the week)
@@ -196,8 +196,8 @@ def docname_to_domain(docname: str, compaction: bool | str) -> str:
# Monday are considered to be in week 0.
'%W': 'WW', # Week number of the year (Monday as the first day of the week)
# as a zero-padded decimal number.
- '%x': 'medium', # Locale’s appropriate date representation.
- '%X': 'medium', # Locale’s appropriate time representation.
+ '%x': 'medium', # Locale's appropriate date representation.
+ '%X': 'medium', # Locale's appropriate time representation.
'%y': 'YY', # Year without century as a zero-padded decimal number.
'%Y': 'yyyy', # Year with century as a decimal number.
'%Z': 'zzz', # Time zone name (no characters if no time zone exists).
@@ -249,6 +249,9 @@ def format_date(
source_date_epoch = os.getenv('SOURCE_DATE_EPOCH')
if source_date_epoch is not None:
date = datetime.fromtimestamp(float(source_date_epoch), tz=UTC)
+ # If SOURCE_DATE_EPOCH is set, users likely want a reproducible result,
+ # so enforce GMT/UTC for consistency.
+ local_time = False
else:
date = datetime.now(tz=UTC)
diff --git a/sphinx/util/images.py b/sphinx/util/images.py
index f1e7344eb7a..b43a0705d36 100644
--- a/sphinx/util/images.py
+++ b/sphinx/util/images.py
@@ -56,11 +56,11 @@ def get_image_size(filename: str | PathLike[str]) -> tuple[int, int] | None:
@overload
-def guess_mimetype(filename: PathLike[str] | str, default: str) -> str: ... # NoQA: E704
+def guess_mimetype(filename: PathLike[str] | str, default: str) -> str: ...
@overload
-def guess_mimetype( # NoQA: E704
+def guess_mimetype(
filename: PathLike[str] | str, default: None = None
) -> str | None: ...
diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py
index c925485e11a..73d75c40447 100644
--- a/sphinx/util/inspect.py
+++ b/sphinx/util/inspect.py
@@ -32,15 +32,18 @@
from typing_extensions import TypeIs
class _SupportsGet(Protocol):
- def __get__(self, __instance: Any, __owner: type | None = ...) -> Any: ... # NoQA: E704
+ def __get__(self, instance: Any, owner: type | None = ..., /) -> Any: ...
class _SupportsSet(Protocol):
# instance and value are contravariants but we do not need that precision
- def __set__(self, __instance: Any, __value: Any) -> None: ... # NoQA: E704
+ def __set__(self, instance: Any, value: Any, /) -> None: ...
class _SupportsDelete(Protocol):
# instance is contravariant but we do not need that precision
- def __delete__(self, __instance: Any) -> None: ... # NoQA: E704
+ def __delete__(self, instance: Any, /) -> None: ...
+
+ class _AttrGetter(Protocol):
+ def __call__(self, obj: Any, name: str, default: Any = ..., /) -> Any: ...
_RoutineType: TypeAlias = (
types.FunctionType
@@ -52,7 +55,9 @@ def __delete__(self, __instance: Any) -> None: ... # NoQA: E704
| types.MethodDescriptorType
| types.ClassMethodDescriptorType
)
- _SignatureType: TypeAlias = Callable[..., Any] | staticmethod | classmethod
+ _SignatureType: TypeAlias = (
+ Callable[..., Any] | staticmethod[Any, Any] | classmethod[Any, Any, Any]
+ )
logger = logging.getLogger(__name__)
@@ -216,16 +221,14 @@ def unpartial(obj: Any) -> Any:
return obj
-def ispartial(obj: Any) -> TypeIs[partial | partialmethod]:
+def ispartial(obj: Any) -> TypeIs[partial[Any] | partialmethod[Any]]:
"""Check if the object is a partial function or method."""
return isinstance(obj, partial | partialmethod)
def isclassmethod(
- obj: Any,
- cls: Any = None,
- name: str | None = None,
-) -> TypeIs[classmethod]:
+ obj: Any, cls: Any = None, name: str | None = None
+) -> TypeIs[classmethod[Any, Any, Any]]:
"""Check if the object is a :class:`classmethod`."""
if isinstance(obj, classmethod):
return True
@@ -241,14 +244,71 @@ def isclassmethod(
return False
+def is_classmethod_descriptor(
+ obj: Any, cls: Any = None, name: str | None = None
+) -> TypeIs[types.ClassMethodDescriptorType]:
+ """Check if the object is a :class:`~types.ClassMethodDescriptorType`.
+
+ This check is stricter than :func:`is_builtin_classmethod_like` as
+ a classmethod descriptor does not have a ``__func__`` attribute.
+ """
+ if isinstance(obj, types.ClassMethodDescriptorType):
+ return True
+ if cls and name:
+ # trace __mro__ if the method is defined in parent class
+ sentinel = object()
+ for basecls in getmro(cls):
+ meth = basecls.__dict__.get(name, sentinel)
+ if meth is not sentinel:
+ return isinstance(meth, types.ClassMethodDescriptorType)
+ return False
+
+
+def is_builtin_classmethod_like(
+ obj: Any, cls: Any = None, name: str | None = None
+) -> bool:
+ """Check if the object looks like a class method implemented in C.
+
+ This is equivalent to test that *obj* is a class method descriptor
+ or is a built-in object with a ``__self__`` attribute that is a type,
+ or that ``parent_class.__dict__[name]`` satisfies those properties
+ for some parent class in *cls* MRO.
+ """
+ if is_classmethod_descriptor(obj, cls, name):
+ return True
+ if (
+ isbuiltin(obj)
+ and getattr(obj, '__self__', None) is not None
+ and isclass(obj.__self__)
+ ):
+ return True
+ if cls and name:
+ # trace __mro__ if the method is defined in parent class
+ sentinel = object()
+ for basecls in getmro(cls):
+ meth = basecls.__dict__.get(name, sentinel)
+ if meth is not sentinel:
+ return is_classmethod_descriptor(meth, None, None) or (
+ isbuiltin(meth)
+ and getattr(meth, '__self__', None) is not None
+ and isclass(meth.__self__)
+ )
+ return False
+
+
+def is_classmethod_like(obj: Any, cls: Any = None, name: str | None = None) -> bool:
+ """Check if the object looks like a class method."""
+ return isclassmethod(obj, cls, name) or is_builtin_classmethod_like(obj, cls, name)
+
+
def isstaticmethod(
- obj: Any,
- cls: Any = None,
- name: str | None = None,
-) -> TypeIs[staticmethod]:
+ obj: Any, cls: Any = None, name: str | None = None
+) -> TypeIs[staticmethod[Any, Any]]:
"""Check if the object is a :class:`staticmethod`."""
if isinstance(obj, staticmethod):
return True
+ # Unlike built-in class methods, built-in static methods
+ # satisfy "isinstance(cls.__dict__[name], staticmethod)".
if cls and name:
# trace __mro__ if the method is defined in parent class
sentinel = object()
@@ -327,7 +387,7 @@ def is_singledispatch_function(obj: Any) -> bool:
)
-def is_singledispatch_method(obj: Any) -> TypeIs[singledispatchmethod]:
+def is_singledispatch_method(obj: Any) -> TypeIs[singledispatchmethod[Any]]:
"""Check if the object is a :class:`~functools.singledispatchmethod`."""
return isinstance(obj, singledispatchmethod)
@@ -362,7 +422,9 @@ def isroutine(obj: Any) -> TypeIs[_RoutineType]:
return inspect.isroutine(unpartial(obj))
-def iscoroutinefunction(obj: Any) -> TypeIs[Callable[..., types.CoroutineType]]:
+def iscoroutinefunction(
+ obj: Any,
+) -> TypeIs[Callable[..., types.CoroutineType[Any, Any, Any]]]:
"""Check if the object is a :external+python:term:`coroutine` function."""
obj = unwrap_all(obj, stop=_is_wrapped_coroutine)
return inspect.iscoroutinefunction(obj)
@@ -377,7 +439,7 @@ def _is_wrapped_coroutine(obj: Any) -> bool:
return hasattr(obj, '__wrapped__')
-def isproperty(obj: Any) -> TypeIs[property | cached_property]:
+def isproperty(obj: Any) -> TypeIs[property | cached_property[Any]]:
"""Check if the object is property (possibly cached)."""
return isinstance(obj, property | cached_property)
@@ -543,7 +605,7 @@ def __call__(self) -> None:
# Dummy method to imitate special typing classes
pass
- def __eq__(self, other: Any) -> bool:
+ def __eq__(self, other: object) -> bool:
return self.name == other
def __hash__(self) -> int:
@@ -645,7 +707,9 @@ def signature(
) -> Signature:
"""Return a Signature object for the given *subject*.
- :param bound_method: Specify *subject* is a bound method or not
+ :param bound_method: Specify *subject* is a bound method or not.
+
+ When *subject* is a built-in callable, *bound_method* is ignored.
"""
if type_aliases is None:
type_aliases = {}
@@ -681,7 +745,10 @@ def signature(
# ForwardRef and so on.
pass
- if bound_method:
+ # For built-in objects, we use the signature that was specified
+ # by the extension module even if we detected the subject to be
+ # a possible bound method.
+ if bound_method and not inspect.isbuiltin(subject):
if inspect.ismethod(subject):
# ``inspect.signature()`` considers the subject is a bound method and removes
# first argument from signature. Therefore no skips are needed here.
@@ -848,7 +915,7 @@ def signature_from_str(signature: str) -> Signature:
"""Create a :class:`~inspect.Signature` object from a string."""
code = 'def func' + signature + ': pass'
module = ast.parse(code)
- function = typing.cast(ast.FunctionDef, module.body[0])
+ function = typing.cast('ast.FunctionDef', module.body[0])
return signature_from_ast(function, code)
@@ -920,7 +987,7 @@ def _define(
def getdoc(
obj: Any,
- attrgetter: Callable = safe_getattr,
+ attrgetter: _AttrGetter = safe_getattr,
allow_inherited: bool = False,
cls: Any = None,
name: str | None = None,
@@ -933,11 +1000,15 @@ def getdoc(
* inherited docstring
* inherited decorated methods
"""
- if cls and name and isclassmethod(obj, cls, name):
+ if cls and name and is_classmethod_like(obj, cls, name):
for basecls in getmro(cls):
meth = basecls.__dict__.get(name)
- if meth and hasattr(meth, '__func__'):
- doc: str | None = getdoc(meth.__func__)
+ if not meth:
+ continue
+ # Built-in class methods do not have '__func__'
+ # but they may have a docstring.
+ if hasattr(meth, '__func__') or is_classmethod_descriptor(meth):
+ doc: str | None = getdoc(getattr(meth, '__func__', meth))
if doc is not None or not allow_inherited:
return doc
diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py
index 507d7a1d8ed..d2f3594100f 100644
--- a/sphinx/util/inventory.py
+++ b/sphinx/util/inventory.py
@@ -2,10 +2,13 @@
from __future__ import annotations
+import posixpath
import re
+import warnings
import zlib
from typing import TYPE_CHECKING
+from sphinx.deprecation import RemovedInSphinx10Warning
from sphinx.locale import __
from sphinx.util import logging
@@ -14,127 +17,100 @@
if TYPE_CHECKING:
import os
- from collections.abc import Callable, Iterator
+ from collections.abc import Callable, Iterator, Sequence
+ from typing import NoReturn, Protocol
from sphinx.builders import Builder
from sphinx.environment import BuildEnvironment
- from sphinx.util.typing import Inventory, InventoryItem, _ReadableStream
-
-
-class InventoryFileReader:
- """A file reader for an inventory file.
-
- This reader supports mixture of texts and compressed texts.
- """
-
- def __init__(self, stream: _ReadableStream[bytes]) -> None:
- self.stream = stream
- self.buffer = b''
- self.eof = False
-
- def read_buffer(self) -> None:
- chunk = self.stream.read(BUFSIZE)
- if chunk == b'':
- self.eof = True
- self.buffer += chunk
-
- def readline(self) -> str:
- pos = self.buffer.find(b'\n')
- if pos != -1:
- line = self.buffer[:pos].decode()
- self.buffer = self.buffer[pos + 1 :]
- elif self.eof:
- line = self.buffer.decode()
- self.buffer = b''
- else:
- self.read_buffer()
- line = self.readline()
-
- return line
-
- def readlines(self) -> Iterator[str]:
- while not self.eof:
- line = self.readline()
- if line:
- yield line
-
- def read_compressed_chunks(self) -> Iterator[bytes]:
- decompressor = zlib.decompressobj()
- while not self.eof:
- self.read_buffer()
- yield decompressor.decompress(self.buffer)
- self.buffer = b''
- yield decompressor.flush()
-
- def read_compressed_lines(self) -> Iterator[str]:
- buf = b''
- for chunk in self.read_compressed_chunks():
- buf += chunk
- pos = buf.find(b'\n')
- while pos != -1:
- yield buf[:pos].decode()
- buf = buf[pos + 1 :]
- pos = buf.find(b'\n')
+ from sphinx.util.typing import Inventory
+
+ # Readable file stream for inventory loading
+ class _SupportsRead(Protocol):
+ def read(self, size: int = ...) -> bytes: ...
+
+ _JoinFunc = Callable[[str, str], str]
+
+
+def __getattr__(name: str) -> object:
+ if name == 'InventoryFileReader':
+ from sphinx.util._inventory_file_reader import InventoryFileReader
+
+ return InventoryFileReader
+ msg = f'module {__name__!r} has no attribute {name!r}'
+ raise AttributeError(msg)
class InventoryFile:
@classmethod
- def load(
- cls: type[InventoryFile],
- stream: _ReadableStream[bytes],
+ def loads(
+ cls,
+ content: bytes,
+ *,
uri: str,
- joinfunc: Callable[[str, str], str],
) -> Inventory:
- reader = InventoryFileReader(stream)
- line = reader.readline().rstrip()
- if line == '# Sphinx inventory version 1':
- return cls.load_v1(reader, uri, joinfunc)
- elif line == '# Sphinx inventory version 2':
- return cls.load_v2(reader, uri, joinfunc)
- else:
- raise ValueError('invalid inventory header: %s' % line)
+ format_line, _, content = content.partition(b'\n')
+ format_line = format_line.rstrip() # remove trailing \r or spaces
+ if format_line == b'# Sphinx inventory version 2':
+ return cls._loads_v2(content, uri=uri)
+ if format_line == b'# Sphinx inventory version 1':
+ lines = content.decode().splitlines()
+ return cls._loads_v1(lines, uri=uri)
+ if format_line.startswith(b'# Sphinx inventory version '):
+ unknown_version = format_line[27:].decode()
+ msg = f'unknown or unsupported inventory version: {unknown_version!r}'
+ raise ValueError(msg)
+ msg = f'invalid inventory header: {format_line.decode()}'
+ raise ValueError(msg)
@classmethod
- def load_v1(
- cls: type[InventoryFile],
- stream: InventoryFileReader,
- uri: str,
- join: Callable[[str, str], str],
- ) -> Inventory:
+ def load(cls, stream: _SupportsRead, uri: str, joinfunc: _JoinFunc) -> Inventory:
+ return cls.loads(stream.read(), uri=uri)
+
+ @classmethod
+ def _loads_v1(cls, lines: Sequence[str], *, uri: str) -> Inventory:
+ if len(lines) < 2:
+ msg = 'invalid inventory header: missing project name or version'
+ raise ValueError(msg)
invdata: Inventory = {}
- projname = stream.readline().rstrip()[11:]
- version = stream.readline().rstrip()[11:]
- for line in stream.readlines():
- name, type, location = line.rstrip().split(None, 2)
- location = join(uri, location)
+ projname = lines[0].rstrip()[11:] # Project name
+ version = lines[1].rstrip()[11:] # Project version
+ for line in lines[2:]:
+ name, item_type, location = line.rstrip().split(None, 2)
+ location = posixpath.join(uri, location)
# version 1 did not add anchors to the location
- if type == 'mod':
- type = 'py:module'
- location += '#module-' + name
+ if item_type == 'mod':
+ item_type = 'py:module'
+ location += f'#module-{name}'
else:
- type = 'py:' + type
- location += '#' + name
- invdata.setdefault(type, {})[name] = (projname, version, location, '-')
+ item_type = f'py:{item_type}'
+ location += f'#{name}'
+ invdata.setdefault(item_type, {})[name] = _InventoryItem(
+ project_name=projname,
+ project_version=version,
+ uri=location,
+ display_name='-',
+ )
return invdata
@classmethod
- def load_v2(
- cls: type[InventoryFile],
- stream: InventoryFileReader,
- uri: str,
- join: Callable[[str, str], str],
- ) -> Inventory:
+ def _loads_v2(cls, inv_data: bytes, *, uri: str) -> Inventory:
+ try:
+ line_1, line_2, check_line, compressed = inv_data.split(b'\n', maxsplit=3)
+ except ValueError:
+ msg = 'invalid inventory header: missing project name or version'
+ raise ValueError(msg) from None
invdata: Inventory = {}
- projname = stream.readline().rstrip()[11:]
- version = stream.readline().rstrip()[11:]
+ projname = line_1.rstrip()[11:].decode() # Project name
+ version = line_2.rstrip()[11:].decode() # Project version
# definition -> priority, location, display name
potential_ambiguities: dict[str, tuple[str, str, str]] = {}
actual_ambiguities = set()
- line = stream.readline()
- if 'zlib' not in line:
- raise ValueError('invalid inventory header (not compressed): %s' % line)
+ if b'zlib' not in check_line: # '... compressed using zlib'
+ msg = f'invalid inventory header (not compressed): {check_line.decode()}'
+ raise ValueError(msg)
- for line in stream.read_compressed_lines():
+ decompressed_content = zlib.decompress(compressed)
+ for line in decompressed_content.decode().splitlines():
# be careful to handle names with embedded spaces correctly
m = re.match(
r'(.+?)\s+(\S+)\s+(-?\d+)\s+?(\S*)\s+(.*)',
@@ -177,9 +153,13 @@ def load_v2(
potential_ambiguities[lowercase_definition] = content
if location.endswith('$'):
location = location[:-1] + name
- location = join(uri, location)
- inv_item: InventoryItem = projname, version, location, dispname
- invdata.setdefault(type, {})[name] = inv_item
+ location = posixpath.join(uri, location)
+ invdata.setdefault(type, {})[name] = _InventoryItem(
+ project_name=projname,
+ project_version=version,
+ uri=location,
+ display_name=dispname,
+ )
for ambiguity in actual_ambiguities:
logger.info(
__('inventory <%s> contains multiple definitions for %s'),
@@ -192,10 +172,7 @@ def load_v2(
@classmethod
def dump(
- cls: type[InventoryFile],
- filename: str | os.PathLike[str],
- env: BuildEnvironment,
- builder: Builder,
+ cls, filename: str | os.PathLike[str], env: BuildEnvironment, builder: Builder
) -> None:
def escape(string: str) -> str:
return re.sub('\\s+', ' ', string)
@@ -227,3 +204,89 @@ def escape(string: str) -> str:
entry = f'{fullname} {domain.name}:{type} {prio} {uri} {dispname}\n'
f.write(compressor.compress(entry.encode()))
f.write(compressor.flush())
+
+
+class _InventoryItem:
+ __slots__ = 'project_name', 'project_version', 'uri', 'display_name'
+
+ project_name: str
+ project_version: str
+ uri: str
+ display_name: str
+
+ def __init__(
+ self,
+ *,
+ project_name: str,
+ project_version: str,
+ uri: str,
+ display_name: str,
+ ) -> None:
+ object.__setattr__(self, 'project_name', project_name)
+ object.__setattr__(self, 'project_version', project_version)
+ object.__setattr__(self, 'uri', uri)
+ object.__setattr__(self, 'display_name', display_name)
+
+ def __repr__(self) -> str:
+ return (
+ '_InventoryItem('
+ f'project_name={self.project_name!r}, '
+ f'project_version={self.project_version!r}, '
+ f'uri={self.uri!r}, '
+ f'display_name={self.display_name!r}'
+ ')'
+ )
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, _InventoryItem):
+ return NotImplemented
+ return (
+ self.project_name == other.project_name
+ and self.project_version == other.project_version
+ and self.uri == other.uri
+ and self.display_name == other.display_name
+ )
+
+ def __hash__(self) -> int:
+ return hash((
+ self.project_name,
+ self.project_version,
+ self.uri,
+ self.display_name,
+ ))
+
+ def __setattr__(self, key: str, value: object) -> NoReturn:
+ msg = '_InventoryItem is immutable'
+ raise AttributeError(msg)
+
+ def __delattr__(self, key: str) -> NoReturn:
+ msg = '_InventoryItem is immutable'
+ raise AttributeError(msg)
+
+ def __getstate__(self) -> tuple[str, str, str, str]:
+ return self.project_name, self.project_version, self.uri, self.display_name
+
+ def __setstate__(self, state: tuple[str, str, str, str]) -> None:
+ project_name, project_version, uri, display_name = state
+ object.__setattr__(self, 'project_name', project_name)
+ object.__setattr__(self, 'project_version', project_version)
+ object.__setattr__(self, 'uri', uri)
+ object.__setattr__(self, 'display_name', display_name)
+
+ def __getitem__(self, key: int | slice) -> str | tuple[str, ...]:
+ warnings.warn(
+ 'The tuple interface for _InventoryItem objects is deprecated.',
+ RemovedInSphinx10Warning,
+ stacklevel=2,
+ )
+ tpl = self.project_name, self.project_version, self.uri, self.display_name
+ return tpl[key]
+
+ def __iter__(self) -> Iterator[str]:
+ warnings.warn(
+ 'The iter() interface for _InventoryItem objects is deprecated.',
+ RemovedInSphinx10Warning,
+ stacklevel=2,
+ )
+ tpl = self.project_name, self.project_version, self.uri, self.display_name
+ return iter(tpl)
diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py
index 20293c8fabe..efc5dbb2de0 100644
--- a/sphinx/util/logging.py
+++ b/sphinx/util/logging.py
@@ -7,17 +7,17 @@
from collections import defaultdict
from contextlib import contextmanager, nullcontext
from os.path import abspath
-from typing import IO, TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from docutils import nodes
from docutils.utils import get_source_line
+from sphinx._cli.util.colour import colourise
from sphinx.errors import SphinxWarning
-from sphinx.util.console import colorize
if TYPE_CHECKING:
- from collections.abc import Iterator, Sequence, Set
- from typing import NoReturn
+ from collections.abc import Iterator, Mapping, Sequence, Set
+ from typing import IO, Any, NoReturn
from docutils.nodes import Node
@@ -49,14 +49,11 @@
},
)
-COLOR_MAP: defaultdict[int, str] = defaultdict(
- lambda: 'blue',
- {
- logging.ERROR: 'darkred',
- logging.WARNING: 'red',
- logging.DEBUG: 'darkgray',
- },
-)
+COLOR_MAP: dict[int, str] = {
+ logging.ERROR: 'darkred',
+ logging.WARNING: 'red',
+ logging.DEBUG: 'darkgray',
+}
def getLogger(name: str) -> SphinxLoggerAdapter:
@@ -129,7 +126,7 @@ def prefix(self) -> str: # type: ignore[override]
return 'WARNING: '
-class SphinxLoggerAdapter(logging.LoggerAdapter):
+class SphinxLoggerAdapter(logging.LoggerAdapter[logging.Logger]):
"""LoggerAdapter allowing ``type`` and ``subtype`` keywords."""
KEYWORDS = ['type', 'subtype', 'location', 'nonl', 'color', 'once']
@@ -146,7 +143,7 @@ def log( # type: ignore[override]
def verbose(self, msg: str, *args: Any, **kwargs: Any) -> None:
self.log(VERBOSE, msg, *args, **kwargs)
- def process(self, msg: str, kwargs: dict) -> tuple[str, dict]: # type: ignore[override]
+ def process(self, msg: str, kwargs: dict[str, Any]) -> tuple[str, dict[str, Any]]: # type: ignore[override]
extra = kwargs.setdefault('extra', {})
for keyword in self.KEYWORDS:
if keyword in kwargs:
@@ -161,9 +158,9 @@ def warning( # type: ignore[override]
self,
msg: object,
*args: object,
- type: None | str = None,
- subtype: None | str = None,
- location: None | str | tuple[str | None, int | None] | Node = None,
+ type: str | None = None,
+ subtype: str | None = None,
+ location: str | tuple[str | None, int | None] | Node | None = None,
nonl: bool = True,
color: str | None = None,
once: bool = False,
@@ -204,13 +201,13 @@ def warning( # type: ignore[override]
)
-class WarningStreamHandler(logging.StreamHandler):
+class WarningStreamHandler(logging.StreamHandler['SafeEncodingWriter']):
"""StreamHandler for warnings."""
pass
-class NewLineStreamHandler(logging.StreamHandler):
+class NewLineStreamHandler(logging.StreamHandler['SafeEncodingWriter']):
"""StreamHandler which switches line terminator by record.nonl flag."""
def emit(self, record: logging.LogRecord) -> None:
@@ -471,7 +468,9 @@ class OnceFilter(logging.Filter):
def __init__(self, name: str = '') -> None:
super().__init__(name)
- self.messages: dict[str, list] = {}
+ self.messages: dict[
+ str, list[tuple[object, ...] | Mapping[str, object] | None]
+ ] = {}
def filter(self, record: logging.LogRecord) -> bool:
once = getattr(record, 'once', '')
@@ -566,20 +565,21 @@ def get_node_location(node: Node) -> str | None:
class ColorizeFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
message = super().format(record)
- color = getattr(record, 'color', None)
- if color is None:
- color = COLOR_MAP.get(record.levelno)
-
- if color:
- return colorize(color, message)
- else:
+ colour_name = getattr(record, 'color', '')
+ if not colour_name:
+ colour_name = COLOR_MAP.get(record.levelno, '')
+ if not colour_name:
+ return message
+ try:
+ return colourise(colour_name, message)
+ except ValueError:
return message
class SafeEncodingWriter:
"""Stream writer which ignores UnicodeEncodeError silently"""
- def __init__(self, stream: IO) -> None:
+ def __init__(self, stream: IO[str]) -> None:
self.stream = stream
self.encoding = getattr(stream, 'encoding', 'ascii') or 'ascii'
@@ -601,14 +601,14 @@ def flush(self) -> None:
class LastMessagesWriter:
"""Stream writer storing last 10 messages in memory to save trackback"""
- def __init__(self, app: Sphinx, stream: IO) -> None:
+ def __init__(self, app: Sphinx, stream: IO[str]) -> None:
self.app = app
def write(self, data: str) -> None:
self.app.messagelog.append(data)
-def setup(app: Sphinx, status: IO, warning: IO) -> None:
+def setup(app: Sphinx, status: IO[str], warning: IO[str]) -> None:
"""Setup root logger for Sphinx"""
logger = logging.getLogger(NAMESPACE)
logger.setLevel(logging.DEBUG)
diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py
index 7f06ae194fc..aaa10e09d7e 100644
--- a/sphinx/util/nodes.py
+++ b/sphinx/util/nodes.py
@@ -95,12 +95,11 @@ def findall(self, node: Node) -> Iterator[N]:
confounds type checkers' ability to determine the return type of the iterator.
"""
for found in node.findall(self):
- yield cast(N, found)
+ yield cast('N', found)
def get_full_module_name(node: Node) -> str:
- """
- Return full module dotted path like: 'docutils.nodes.paragraph'
+ """Return full module dotted path like: 'docutils.nodes.paragraph'
:param nodes.Node node: target node
:return: full module dotted path
@@ -109,8 +108,7 @@ def get_full_module_name(node: Node) -> str:
def repr_domxml(node: Node, length: int = 80) -> str:
- """
- return DOM XML representation of the specified node like:
+ """Return DOM XML representation of the specified node like:
'Added in version...'
:param nodes.Node node: target node
@@ -471,7 +469,7 @@ def inline_all_toctrees(
if includefile not in traversed:
try:
traversed.append(includefile)
- logger.info(indent + colorfunc(includefile))
+ logger.info(indent + colorfunc(includefile)) # NoQA: G003
subtree = inline_all_toctrees(
builder,
docnameset,
diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py
index 779727b394b..d24bbf55b2d 100644
--- a/sphinx/util/osutil.py
+++ b/sphinx/util/osutil.py
@@ -17,7 +17,8 @@
from sphinx.locale import __
if TYPE_CHECKING:
- from typing import Any
+ from types import TracebackType
+ from typing import Any, Self
# SEP separates path elements in the canonical file names
#
@@ -226,19 +227,22 @@ def close(self) -> None:
try:
with open(self._path, encoding='utf-8') as old_f:
old_content = old_f.read()
- if old_content == buf:
- return
+ if old_content == buf:
+ return
except OSError:
pass
with open(self._path, 'w', encoding='utf-8') as f:
f.write(buf)
- def __enter__(self) -> FileAvoidWrite:
+ def __enter__(self) -> Self:
return self
def __exit__(
- self, exc_type: type[Exception], exc_value: Exception, traceback: Any
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
) -> bool:
self.close()
return True
diff --git a/sphinx/util/parallel.py b/sphinx/util/parallel.py
index 27ea0d591cc..3dd5e574c58 100644
--- a/sphinx/util/parallel.py
+++ b/sphinx/util/parallel.py
@@ -6,7 +6,7 @@
import time
import traceback
from math import sqrt
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
try:
import multiprocessing
@@ -20,6 +20,7 @@
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
+ from typing import Any
logger = logging.getLogger(__name__)
@@ -34,12 +35,15 @@ def __init__(self, nproc: int = 1) -> None:
pass
def add_task(
- self, task_func: Callable, arg: Any = None, result_func: Callable | None = None
+ self,
+ task_func: Callable[[Any], Any] | Callable[[], Any],
+ arg: Any = None,
+ result_func: Callable[[Any], Any] | None = None,
) -> None:
if arg is not None:
- res = task_func(arg)
+ res = task_func(arg) # type: ignore[call-arg]
else:
- res = task_func()
+ res = task_func() # type: ignore[call-arg]
if result_func:
result_func(res)
@@ -53,7 +57,7 @@ class ParallelTasks:
def __init__(self, nproc: int) -> None:
self.nproc = nproc
# (optional) function performed by each task on the result of main task
- self._result_funcs: dict[int, Callable] = {}
+ self._result_funcs: dict[int, Callable[[Any, Any], Any]] = {}
# task arguments
self._args: dict[int, list[Any] | None] = {}
# list of subprocesses (both started and waiting)
@@ -61,20 +65,22 @@ def __init__(self, nproc: int) -> None:
# list of receiving pipe connections of running subprocesses
self._precvs: dict[int, Any] = {}
# list of receiving pipe connections of waiting subprocesses
- self._precvsWaiting: dict[int, Any] = {}
+ self._precvs_waiting: dict[int, Any] = {}
# number of working subprocesses
self._pworking = 0
# task number of each subprocess
self._taskid = 0
- def _process(self, pipe: Any, func: Callable, arg: Any) -> None:
+ def _process(
+ self, pipe: Any, func: Callable[[Any], Any] | Callable[[], Any], arg: Any
+ ) -> None:
try:
collector = logging.LogCollector()
with collector.collect():
if arg is None:
- ret = func()
+ ret = func() # type: ignore[call-arg]
else:
- ret = func(arg)
+ ret = func(arg) # type: ignore[call-arg]
failed = False
except BaseException as err:
failed = True
@@ -84,7 +90,10 @@ def _process(self, pipe: Any, func: Callable, arg: Any) -> None:
pipe.send((failed, collector.logs, ret))
def add_task(
- self, task_func: Callable, arg: Any = None, result_func: Callable | None = None
+ self,
+ task_func: Callable[[Any], Any] | Callable[[], Any],
+ arg: Any = None,
+ result_func: Callable[[Any, Any], Any] | None = None,
) -> None:
tid = self._taskid
self._taskid += 1
@@ -94,7 +103,7 @@ def add_task(
context: Any = multiprocessing.get_context('fork')
proc = context.Process(target=self._process, args=(psend, task_func, arg))
self._procs[tid] = proc
- self._precvsWaiting[tid] = precv
+ self._precvs_waiting[tid] = precv
try:
self._join_one()
except Exception:
@@ -135,8 +144,8 @@ def _join_one(self) -> bool:
joined_any = True
break
- while self._precvsWaiting and self._pworking < self.nproc:
- newtid, newprecv = self._precvsWaiting.popitem()
+ while self._precvs_waiting and self._pworking < self.nproc:
+ newtid, newprecv = self._precvs_waiting.popitem()
self._precvs[newtid] = newprecv
self._procs[newtid].start()
self._pworking += 1
diff --git a/sphinx/util/parsing.py b/sphinx/util/parsing.py
index 33893f8807c..4c4a6477683 100644
--- a/sphinx/util/parsing.py
+++ b/sphinx/util/parsing.py
@@ -5,12 +5,13 @@
import contextlib
from typing import TYPE_CHECKING
-from docutils.nodes import Element, Node
+from docutils.nodes import Element
from docutils.statemachine import StringList, string2lines
if TYPE_CHECKING:
from collections.abc import Iterator
+ from docutils.nodes import Node
from docutils.parsers.rst.states import RSTState
diff --git a/sphinx/util/png.py b/sphinx/util/png.py
index c90e27ec14e..c4d78162422 100644
--- a/sphinx/util/png.py
+++ b/sphinx/util/png.py
@@ -4,6 +4,10 @@
import binascii
import struct
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ import os
LEN_IEND = 12
LEN_DEPTH = 22
@@ -13,7 +17,7 @@
IEND_CHUNK = b'\x00\x00\x00\x00IEND\xae\x42\x60\x82'
-def read_png_depth(filename: str) -> int | None:
+def read_png_depth(filename: str | os.PathLike[str]) -> int | None:
"""Read the special tEXt chunk indicating the depth from a PNG file."""
with open(filename, 'rb') as f:
f.seek(-(LEN_IEND + LEN_DEPTH), 2)
@@ -25,7 +29,7 @@ def read_png_depth(filename: str) -> int | None:
return struct.unpack('!i', depthchunk[14:18])[0]
-def write_png_depth(filename: str, depth: int) -> None:
+def write_png_depth(filename: str | os.PathLike[str], depth: int) -> None:
"""Write the special tEXt chunk indicating the depth to a PNG file.
The chunk is placed immediately before the special IEND chunk.
diff --git a/sphinx/util/requests.py b/sphinx/util/requests.py
index a647bf5ba50..b439ce437e8 100644
--- a/sphinx/util/requests.py
+++ b/sphinx/util/requests.py
@@ -3,20 +3,34 @@
from __future__ import annotations
import warnings
-from typing import Any
-from urllib.parse import urlsplit
+from typing import TYPE_CHECKING
+from urllib.parse import urljoin, urlsplit
import requests
from urllib3.exceptions import InsecureRequestWarning
import sphinx
+if TYPE_CHECKING:
+ import re
+ from collections.abc import Sequence
+ from typing import Any
+
+
_USER_AGENT = (
f'Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0 '
f'Sphinx/{sphinx.__version__}'
)
+class _IgnoredRedirection(Exception):
+ """Sphinx-internal exception raised when an HTTP redirect is ignored"""
+
+ def __init__(self, destination: str, status_code: int) -> None:
+ self.destination = destination
+ self.status_code = status_code
+
+
def _get_tls_cacert(url: str, certs: str | dict[str, str] | None) -> str | bool:
"""Get additional CA cert for a specific URL."""
if not certs:
@@ -50,6 +64,23 @@ def head(url: str, **kwargs: Any) -> requests.Response:
class _Session(requests.Session):
+ _ignored_redirects: Sequence[re.Pattern[str]]
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ self._ignored_redirects = kwargs.pop('_ignored_redirects', ())
+ super().__init__(*args, **kwargs)
+
+ def get_redirect_target(self, resp: requests.Response) -> str | None:
+ """Overrides the default requests.Session.get_redirect_target"""
+ # do not follow redirections that match ignored URI patterns
+ if resp.is_redirect:
+ destination = urljoin(resp.url, resp.headers['location'])
+ if any(pat.match(destination) for pat in self._ignored_redirects):
+ raise _IgnoredRedirection(
+ destination=destination, status_code=resp.status_code
+ )
+ return super().get_redirect_target(resp)
+
def request( # type: ignore[override]
self,
method: str,
diff --git a/sphinx/util/rst.py b/sphinx/util/rst.py
index 85915ef894b..c848a9b3657 100644
--- a/sphinx/util/rst.py
+++ b/sphinx/util/rst.py
@@ -12,7 +12,7 @@
from docutils.parsers.rst.languages import en as english # type: ignore[attr-defined]
from docutils.parsers.rst.states import Body
from docutils.utils import Reporter
-from jinja2 import Environment, pass_environment
+from jinja2 import pass_environment
from sphinx.locale import __
from sphinx.util import docutils, logging
@@ -21,6 +21,7 @@
from collections.abc import Iterator
from docutils.statemachine import StringList
+ from jinja2 import Environment
logger = logging.getLogger(__name__)
@@ -105,7 +106,7 @@ def append_epilog(content: StringList, epilog: str) -> None:
if len(content) > 0:
source, lineno = content.info(-1)
# lineno will never be None, since len(content) > 0
- lineno = cast(int, lineno)
+ lineno = cast('int', lineno)
else:
source = ''
lineno = 0
diff --git a/sphinx/util/tags.py b/sphinx/util/tags.py
index 7d9c72b2e85..4467534a945 100644
--- a/sphinx/util/tags.py
+++ b/sphinx/util/tags.py
@@ -112,4 +112,4 @@ def _eval_node(self, node: jinja2.nodes.Node | None) -> bool:
return node.name in self._tags
else:
msg = 'invalid node, check parsing'
- raise ValueError(msg)
+ raise TypeError(msg)
diff --git a/sphinx/util/template.py b/sphinx/util/template.py
index b428d3cf777..d5228b339d4 100644
--- a/sphinx/util/template.py
+++ b/sphinx/util/template.py
@@ -5,7 +5,7 @@
import os
from functools import partial
from pathlib import Path
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from jinja2 import TemplateNotFound
from jinja2.loaders import BaseLoader
@@ -18,6 +18,7 @@
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
+ from typing import Any
from jinja2.environment import Environment
diff --git a/sphinx/util/texescape.py b/sphinx/util/texescape.py
index ba5acf4f9aa..ce6555a308b 100644
--- a/sphinx/util/texescape.py
+++ b/sphinx/util/texescape.py
@@ -29,8 +29,8 @@
# map some special Unicode characters to similar ASCII ones
# (even for Unicode LaTeX as may not be supported by OpenType font)
('⎽', r'\_'),
- ('ℯ', r'e'),
- ('ⅈ', r'i'),
+ ('ℯ', r'e'), # U+212F # NoQA: RUF001
+ ('ⅈ', r'i'), # U+2148 # NoQA: RUF001
# Greek alphabet not escaped: pdflatex handles it via textalpha and inputenc
# OHM SIGN U+2126 is handled by LaTeX textcomp package
]
@@ -63,7 +63,7 @@
('±', r'\(\pm\)'),
('→', r'\(\rightarrow\)'),
('‣', r'\(\rightarrow\)'),
- ('–', r'\textendash{}'),
+ ('\N{EN DASH}', r'\textendash{}'),
# superscript
('⁰', r'\(\sp{\text{0}}\)'),
('¹', r'\(\sp{\text{1}}\)'),
diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py
index 2e8d9e689f7..ef7b9462364 100644
--- a/sphinx/util/typing.py
+++ b/sphinx/util/typing.py
@@ -7,19 +7,7 @@
import types
import typing
from collections.abc import Callable, Sequence
-from contextvars import Context, ContextVar, Token
-from struct import Struct
-from typing import (
- TYPE_CHECKING,
- Annotated,
- Any,
- ForwardRef,
- NewType,
- TypedDict,
- TypeVar,
- Union,
- Unpack,
-)
+from typing import TYPE_CHECKING
from docutils import nodes
from docutils.parsers.rst.states import Inliner
@@ -28,11 +16,12 @@
if TYPE_CHECKING:
from collections.abc import Mapping
- from typing import Final, Literal, Protocol, TypeAlias
+ from typing import Annotated, Any, Final, Literal, Protocol, TypeAlias
from typing_extensions import TypeIs
from sphinx.application import Sphinx
+ from sphinx.util.inventory import _InventoryItem
_RestifyMode: TypeAlias = Literal[
'fully-qualified-except-typing',
@@ -48,41 +37,82 @@
# classes that have an incorrect .__module__ attribute
-_INVALID_BUILTIN_CLASSES: Final[Mapping[object, str]] = {
- Context: 'contextvars.Context', # Context.__module__ == '_contextvars'
- ContextVar: 'contextvars.ContextVar', # ContextVar.__module__ == '_contextvars'
- Token: 'contextvars.Token', # Token.__module__ == '_contextvars'
- Struct: 'struct.Struct', # Struct.__module__ == '_struct'
- # types in 'types' with .__module__ == 'builtins':
- types.AsyncGeneratorType: 'types.AsyncGeneratorType',
- types.BuiltinFunctionType: 'types.BuiltinFunctionType',
- types.BuiltinMethodType: 'types.BuiltinMethodType',
- types.CellType: 'types.CellType',
- types.ClassMethodDescriptorType: 'types.ClassMethodDescriptorType',
- types.CodeType: 'types.CodeType',
- types.CoroutineType: 'types.CoroutineType',
- types.FrameType: 'types.FrameType',
- types.FunctionType: 'types.FunctionType',
- types.GeneratorType: 'types.GeneratorType',
- types.GetSetDescriptorType: 'types.GetSetDescriptorType',
- types.LambdaType: 'types.LambdaType',
- types.MappingProxyType: 'types.MappingProxyType',
- types.MemberDescriptorType: 'types.MemberDescriptorType',
- types.MethodDescriptorType: 'types.MethodDescriptorType',
- types.MethodType: 'types.MethodType',
- types.MethodWrapperType: 'types.MethodWrapperType',
- types.ModuleType: 'types.ModuleType',
- types.TracebackType: 'types.TracebackType',
- types.WrapperDescriptorType: 'types.WrapperDescriptorType',
+# Map of (__module__, __qualname__) to the correct fully-qualified name
+_INVALID_BUILTIN_CLASSES: Final[Mapping[tuple[str, str], str]] = {
+ # types from 'contextvars'
+ ('_contextvars', 'Context'): 'contextvars.Context',
+ ('_contextvars', 'ContextVar'): 'contextvars.ContextVar',
+ ('_contextvars', 'Token'): 'contextvars.Token',
+ # types from 'ctypes':
+ ('_ctypes', 'Array'): 'ctypes.Array',
+ ('_ctypes', 'Structure'): 'ctypes.Structure',
+ ('_ctypes', 'Union'): 'ctypes.Union',
+ # types from 'io':
+ ('_io', 'BufferedRandom'): 'io.BufferedRandom',
+ ('_io', 'BufferedReader'): 'io.BufferedReader',
+ ('_io', 'BufferedRWPair'): 'io.BufferedRWPair',
+ ('_io', 'BufferedWriter'): 'io.BufferedWriter',
+ ('_io', 'BytesIO'): 'io.BytesIO',
+ ('_io', 'FileIO'): 'io.FileIO',
+ ('_io', 'StringIO'): 'io.StringIO',
+ ('_io', 'TextIOWrapper'): 'io.TextIOWrapper',
+ # types from 'json':
+ ('json.decoder', 'JSONDecoder'): 'json.JSONDecoder',
+ ('json.encoder', 'JSONEncoder'): 'json.JSONEncoder',
+ # types from 'lzma':
+ ('_lzma', 'LZMACompressor'): 'lzma.LZMACompressor',
+ ('_lzma', 'LZMADecompressor'): 'lzma.LZMADecompressor',
+ # types from 'multiprocessing':
+ ('multiprocessing.context', 'Process'): 'multiprocessing.Process',
+ # types from 'pathlib':
+ ('pathlib._local', 'Path'): 'pathlib.Path',
+ ('pathlib._local', 'PosixPath'): 'pathlib.PosixPath',
+ ('pathlib._local', 'PurePath'): 'pathlib.PurePath',
+ ('pathlib._local', 'PurePosixPath'): 'pathlib.PurePosixPath',
+ ('pathlib._local', 'PureWindowsPath'): 'pathlib.PureWindowsPath',
+ ('pathlib._local', 'WindowsPath'): 'pathlib.WindowsPath',
+ # types from 'pickle':
+ ('_pickle', 'Pickler'): 'pickle.Pickler',
+ ('_pickle', 'Unpickler'): 'pickle.Unpickler',
+ # types from 'struct':
+ ('_struct', 'Struct'): 'struct.Struct',
+ # types from 'types':
+ ('builtins', 'async_generator'): 'types.AsyncGeneratorType',
+ ('builtins', 'builtin_function_or_method'): 'types.BuiltinMethodType',
+ ('builtins', 'cell'): 'types.CellType',
+ ('builtins', 'classmethod_descriptor'): 'types.ClassMethodDescriptorType',
+ ('builtins', 'code'): 'types.CodeType',
+ ('builtins', 'coroutine'): 'types.CoroutineType',
+ ('builtins', 'ellipsis'): 'types.EllipsisType',
+ ('builtins', 'frame'): 'types.FrameType',
+ ('builtins', 'function'): 'types.LambdaType',
+ ('builtins', 'generator'): 'types.GeneratorType',
+ ('builtins', 'getset_descriptor'): 'types.GetSetDescriptorType',
+ ('builtins', 'mappingproxy'): 'types.MappingProxyType',
+ ('builtins', 'member_descriptor'): 'types.MemberDescriptorType',
+ ('builtins', 'method'): 'types.MethodType',
+ ('builtins', 'method-wrapper'): 'types.MethodWrapperType',
+ ('builtins', 'method_descriptor'): 'types.MethodDescriptorType',
+ ('builtins', 'module'): 'types.ModuleType',
+ ('builtins', 'NoneType'): 'types.NoneType',
+ ('builtins', 'NotImplementedType'): 'types.NotImplementedType',
+ ('builtins', 'traceback'): 'types.TracebackType',
+ ('builtins', 'wrapper_descriptor'): 'types.WrapperDescriptorType',
+ # types from 'weakref':
+ ('_weakrefset', 'WeakSet'): 'weakref.WeakSet',
+ # types from 'zipfile':
+ ('zipfile._path', 'CompleteDirs'): 'zipfile.CompleteDirs',
+ ('zipfile._path', 'Path'): 'zipfile.Path',
}
-def is_invalid_builtin_class(obj: Any) -> bool:
+def is_invalid_builtin_class(obj: Any) -> str:
"""Check *obj* is an invalid built-in class."""
try:
- return obj in _INVALID_BUILTIN_CLASSES
- except TypeError: # unhashable type
- return False
+ key = obj.__module__, obj.__qualname__
+ except AttributeError: # non-standard type
+ return ''
+ return _INVALID_BUILTIN_CLASSES.get(key, '')
# Text like nodes which are initialized with text and rawsource
@@ -95,7 +125,7 @@ def is_invalid_builtin_class(obj: Any) -> bool:
if TYPE_CHECKING:
class RoleFunction(Protocol):
- def __call__( # NoQA: E704
+ def __call__(
self,
name: str,
rawtext: str,
@@ -109,47 +139,21 @@ def __call__( # NoQA: E704
else:
RoleFunction: TypeAlias = Callable[
- [str, str, str, int, Inliner, dict[str, Any], Sequence[str]],
+ [str, str, str, int, Inliner, dict[str, typing.Any], Sequence[str]],
tuple[list[nodes.Node], list[nodes.system_message]],
]
# A option spec for directive
-OptionSpec: TypeAlias = dict[str, Callable[[str], Any]]
+OptionSpec: TypeAlias = dict[str, Callable[[str], typing.Any]]
# title getter functions for enumerable nodes (see sphinx.domains.std)
TitleGetter: TypeAlias = Callable[[nodes.Node], str]
-# Readable file stream for inventory loading
-if TYPE_CHECKING:
- from types import TracebackType
- from typing import Self
-
- _T_co = TypeVar('_T_co', str, bytes, covariant=True)
-
- class _ReadableStream(Protocol[_T_co]):
- def read(self, size: int = ...) -> _T_co: ... # NoQA: E704
-
- def __enter__(self) -> Self: ... # NoQA: E704
-
- def __exit__( # NoQA: E704
- self,
- exc_type: type[BaseException] | None,
- exc_val: BaseException | None,
- exc_tb: TracebackType | None,
- ) -> None: ...
-
-
# inventory data on memory
-InventoryItem: TypeAlias = tuple[
- str, # project name
- str, # project version
- str, # URL
- str, # display name
-]
-Inventory: TypeAlias = dict[str, dict[str, InventoryItem]]
+Inventory: TypeAlias = dict[str, dict[str, '_InventoryItem']]
-class ExtensionMetadata(TypedDict, total=False):
+class ExtensionMetadata(typing.TypedDict, total=False):
"""The metadata returned by an extension's ``setup()`` function.
See :ref:`ext-metadata`.
@@ -170,7 +174,7 @@ class ExtensionMetadata(TypedDict, total=False):
if TYPE_CHECKING:
- _ExtensionSetupFunc: TypeAlias = Callable[[Sphinx], ExtensionMetadata]
+ _ExtensionSetupFunc: TypeAlias = Callable[[Sphinx], ExtensionMetadata] # NoQA: PYI047 (false positive)
def get_type_hints(
@@ -208,20 +212,20 @@ def get_type_hints(
def is_system_TypeVar(typ: Any) -> bool:
"""Check *typ* is system defined TypeVar."""
modname = getattr(typ, '__module__', '')
- return modname == 'typing' and isinstance(typ, TypeVar)
+ return modname == 'typing' and isinstance(typ, typing.TypeVar)
def _is_annotated_form(obj: Any) -> TypeIs[Annotated[Any, ...]]:
"""Check if *obj* is an annotated type."""
return (
- typing.get_origin(obj) is Annotated
+ typing.get_origin(obj) is typing.Annotated
or str(obj).startswith('typing.Annotated')
) # fmt: skip
def _is_unpack_form(obj: Any) -> bool:
"""Check if the object is :class:`typing.Unpack` or equivalent."""
- return typing.get_origin(obj) is Unpack
+ return typing.get_origin(obj) is typing.Unpack
def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str:
@@ -264,11 +268,11 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
return f':py:class:`{module_prefix}{cls.__name__}`'
elif ismock(cls):
return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`'
- elif is_invalid_builtin_class(cls):
+ elif fixed_cls := is_invalid_builtin_class(cls):
# The above predicate never raises TypeError but should not be
# evaluated before determining whether *cls* is a mocked object
# or not; instead of two try-except blocks, we keep it here.
- return f':py:class:`{module_prefix}{_INVALID_BUILTIN_CLASSES[cls]}`'
+ return f':py:class:`{module_prefix}{fixed_cls}`'
elif _is_annotated_form(cls):
args = restify(cls.__args__[0], mode)
meta_args = []
@@ -293,10 +297,12 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`'
rf'\ [{args}, {meta}]'
)
- elif isinstance(cls, NewType):
+ elif isinstance(cls, typing.NewType):
return f':py:class:`{module_prefix}{cls.__module__}.{cls.__name__}`' # type: ignore[attr-defined]
elif isinstance(cls, types.UnionType) or (
- isgenericalias(cls) and cls_module_is_typing and cls.__origin__ is Union
+ isgenericalias(cls)
+ and cls_module_is_typing
+ and cls.__origin__ is typing.Union
):
# Union types (PEP 585) retain their definition order when they
# are printed natively and ``None``-like types are kept as is.
@@ -353,7 +359,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
return f':py:obj:`~{cls.__module__}.{cls.__name__}`'
elif hasattr(cls, '__qualname__'):
return f':py:class:`{module_prefix}{cls.__module__}.{cls.__qualname__}`'
- elif isinstance(cls, ForwardRef):
+ elif isinstance(cls, typing.ForwardRef):
return f':py:class:`{cls.__forward_arg__}`'
else:
# not a class (ex. TypeVar) but should have a __name__
@@ -424,12 +430,12 @@ def stringify_annotation(
annotation_module: str = getattr(annotation, '__module__', '')
annotation_name: str = getattr(annotation, '__name__', '')
annotation_module_is_typing = annotation_module == 'typing'
- if sys.version_info[:2] >= (3, 14) and isinstance(annotation, ForwardRef):
+ if sys.version_info[:2] >= (3, 14) and isinstance(annotation, typing.ForwardRef):
# ForwardRef moved from `typing` to `annotationlib` in Python 3.14.
annotation_module_is_typing = True
# Extract the annotation's base type by considering formattable cases
- if isinstance(annotation, TypeVar) and not _is_unpack_form(annotation):
+ if isinstance(annotation, typing.TypeVar) and not _is_unpack_form(annotation):
# typing_extensions.Unpack is incorrectly determined as a TypeVar
if annotation_module_is_typing and mode in {
'fully-qualified-except-typing',
@@ -437,14 +443,14 @@ def stringify_annotation(
}:
return annotation_name
return module_prefix + f'{annotation_module}.{annotation_name}'
- elif isinstance(annotation, NewType):
+ elif isinstance(annotation, typing.NewType):
return module_prefix + f'{annotation_module}.{annotation_name}'
elif ismockmodule(annotation):
return module_prefix + annotation_name
elif ismock(annotation):
return module_prefix + f'{annotation_module}.{annotation_name}'
- elif is_invalid_builtin_class(annotation):
- return module_prefix + _INVALID_BUILTIN_CLASSES[annotation]
+ elif fixed_annotation := is_invalid_builtin_class(annotation):
+ return module_prefix + fixed_annotation
elif _is_annotated_form(annotation): # for py310+
pass
elif annotation_module == 'builtins' and annotation_qualname:
diff --git a/sphinx/versioning.py b/sphinx/versioning.py
index 778c7ab5da0..3de5a17ec9c 100644
--- a/sphinx/versioning.py
+++ b/sphinx/versioning.py
@@ -5,13 +5,14 @@
import pickle
from itertools import product, zip_longest
from operator import itemgetter
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from uuid import uuid4
from sphinx.transforms import SphinxTransform
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
+ from typing import Any
from docutils.nodes import Node
diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py
index 4b193345da0..e2c04ca32db 100644
--- a/sphinx/writers/html.py
+++ b/sphinx/writers/html.py
@@ -27,11 +27,12 @@ class HTMLWriter(Writer): # type: ignore[misc]
def __init__(self, builder: StandaloneHTMLBuilder) -> None:
super().__init__()
self.builder = builder
+ self._has_maths_elements: bool = False
def translate(self) -> None:
# sadly, this is mostly copied from parent class
visitor = self.builder.create_translator(self.document, self.builder)
- self.visitor = cast(HTML5Translator, visitor)
+ self.visitor = cast('HTML5Translator', visitor)
self.document.walkabout(visitor)
self.output = self.visitor.astext()
for attr in (
@@ -57,3 +58,4 @@ def translate(self) -> None:
):
setattr(self, attr, getattr(visitor, attr, None))
self.clean_meta = ''.join(self.visitor.meta[2:])
+ self._has_maths_elements = getattr(visitor, '_has_maths_elements', False)
diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py
index 608a84f34db..6c9c8cfee63 100644
--- a/sphinx/writers/html5.py
+++ b/sphinx/writers/html5.py
@@ -5,7 +5,6 @@
import posixpath
import re
import urllib.parse
-from collections.abc import Iterable
from typing import TYPE_CHECKING, cast
from docutils import nodes
@@ -18,6 +17,8 @@
from sphinx.util.images import get_image_size
if TYPE_CHECKING:
+ from collections.abc import Iterable
+
from docutils.nodes import Element, Node, Text
from sphinx.builders import Builder
@@ -43,9 +44,7 @@ def multiply_length(length: str, scale: int) -> str:
class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
- """
- Our custom HTML translator.
- """
+ """Our custom HTML translator."""
builder: StandaloneHTMLBuilder
# Override docutils.writers.html5_polyglot:HTMLTranslator
@@ -65,6 +64,7 @@ def __init__(self, document: nodes.document, builder: Builder) -> None:
self._table_row_indices = [0]
self._fieldlist_row_indices = [0]
self.required_params_left = 0
+ self._has_maths_elements: bool = False
def visit_start_of_file(self, node: Element) -> None:
# only occurs in the single-file builder
@@ -174,6 +174,7 @@ def _visit_sig_parameter_list(
self.required_params_left = sum(self.list_is_required_param)
self.param_separator = node.child_text_separator
self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
+ self.trailing_comma = node.get('multi_line_trailing_comma', False)
if self.multi_line_parameter_list:
self.body.append('\n\n')
self.body.append(self.starttag(node, 'dl'))
@@ -231,14 +232,15 @@ def depart_desc_parameter(self, node: Element) -> None:
next_is_required = (
not is_last_group
and self.list_is_required_param[self.param_group_index + 1]
- ) # fmt: skip
+ )
opt_param_left_at_level = self.params_left_at_level > 0
if (
opt_param_left_at_level
or is_required
and (is_last_group or next_is_required)
):
- self.body.append(self.param_separator)
+ if not is_last_group or opt_param_left_at_level or self.trailing_comma:
+ self.body.append(self.param_separator)
self.body.append('\n')
elif self.required_params_left:
@@ -281,19 +283,26 @@ def visit_desc_optional(self, node: Element) -> None:
def depart_desc_optional(self, node: Element) -> None:
self.optional_param_level -= 1
+ level = self.optional_param_level
if self.multi_line_parameter_list:
- # If it's the first time we go down one level, add the separator
- # before the bracket.
- if self.optional_param_level == self.max_optional_param_level - 1:
+ max_level = self.max_optional_param_level
+ len_lirp = len(self.list_is_required_param)
+ is_last_group = self.param_group_index + 1 == len_lirp
+ # If it's the first time we go down one level, add the separator before the
+ # bracket, except if this is the last parameter and the parameter list
+ # should not feature a trailing comma.
+ if level == max_level - 1 and (
+ not is_last_group or level > 0 or self.trailing_comma
+ ):
self.body.append(self.param_separator)
self.body.append(']')
# End the line if we have just closed the last bracket of this
# optional parameter group.
- if self.optional_param_level == 0:
+ if level == 0:
self.body.append('\n')
else:
self.body.append(']')
- if self.optional_param_level == 0:
+ if level == 0:
self.param_group_index += 1
def visit_desc_annotation(self, node: Element) -> None:
@@ -418,9 +427,7 @@ def append_fignumber(figtype: str, figure_id: str) -> None:
self.body.append(prefix % '.'.join(map(str, numbers)) + ' ')
self.body.append('')
- figtype = self.builder.env.domains.standard_domain.get_enumerable_node_type(
- node
- )
+ figtype = self._domains.standard_domain.get_enumerable_node_type(node)
if figtype:
if len(node['ids']) == 0:
msg = __('Any IDs not assigned for %s node') % node.tagname
@@ -670,7 +677,7 @@ def depart_literal(self, node: Element) -> None:
def visit_productionlist(self, node: Element) -> None:
self.body.append(self.starttag(node, 'pre'))
- productionlist = cast(Iterable[addnodes.production], node)
+ productionlist = cast('Iterable[addnodes.production]', node)
names = (production['tokenname'] for production in productionlist)
maxlen = max(len(name) for name in names)
lastname = None
@@ -955,26 +962,30 @@ def visit_field(self, node: Element) -> None:
else:
node['classes'].append('field-odd')
- def visit_math(self, node: Element, math_env: str = '') -> None:
+ def visit_math(self, node: nodes.math, math_env: str = '') -> None:
+ self._has_maths_elements = True
+
# see validate_math_renderer
name: str = self.builder.math_renderer_name # type: ignore[assignment]
visit, _ = self.builder.app.registry.html_inline_math_renderers[name]
visit(self, node)
- def depart_math(self, node: Element, math_env: str = '') -> None:
+ def depart_math(self, node: nodes.math, math_env: str = '') -> None:
# see validate_math_renderer
name: str = self.builder.math_renderer_name # type: ignore[assignment]
_, depart = self.builder.app.registry.html_inline_math_renderers[name]
if depart:
depart(self, node)
- def visit_math_block(self, node: Element, math_env: str = '') -> None:
+ def visit_math_block(self, node: nodes.math_block, math_env: str = '') -> None:
+ self._has_maths_elements = True
+
# see validate_math_renderer
name: str = self.builder.math_renderer_name # type: ignore[assignment]
visit, _ = self.builder.app.registry.html_block_math_renderers[name]
visit(self, node)
- def depart_math_block(self, node: Element, math_env: str = '') -> None:
+ def depart_math_block(self, node: nodes.math_block, math_env: str = '') -> None:
# see validate_math_renderer
name: str = self.builder.math_renderer_name # type: ignore[assignment]
_, depart = self.builder.app.registry.html_block_math_renderers[name]
diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py
index 2dffd7e3f15..c6a78344059 100644
--- a/sphinx/writers/latex.py
+++ b/sphinx/writers/latex.py
@@ -8,11 +8,11 @@
import re
from collections import defaultdict
-from collections.abc import Iterable
from pathlib import Path
-from typing import TYPE_CHECKING, Any, ClassVar, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes, writers
+from roman_numerals import RomanNumeral
from sphinx import addnodes, highlighting
from sphinx.errors import SphinxError
@@ -24,13 +24,10 @@
from sphinx.util.template import LaTeXRenderer
from sphinx.util.texescape import tex_replace_map
-try:
- from docutils.utils.roman import toRoman
-except ImportError:
- # In Debian/Ubuntu, roman package is provided as roman, not as docutils.utils.roman
- from roman import toRoman # type: ignore[no-redef, import-not-found]
-
if TYPE_CHECKING:
+ from collections.abc import Iterable
+ from typing import Any, ClassVar
+
from docutils.nodes import Element, Node, Text
from sphinx.builders.latex import LaTeXBuilder
@@ -100,7 +97,7 @@ def translate(self) -> None:
self.document, self.builder, self.theme
)
self.document.walkabout(visitor)
- self.output = cast(LaTeXTranslator, visitor).astext()
+ self.output = cast('LaTeXTranslator', visitor).astext()
# Helper classes
@@ -219,8 +216,8 @@ def add_cell(self, height: int, width: int) -> None:
self.cell_id += 1
for col in range(width):
for row in range(height):
- assert self.cells[(self.row + row, self.col + col)] == 0
- self.cells[(self.row + row, self.col + col)] = self.cell_id
+ assert self.cells[self.row + row, self.col + col] == 0
+ self.cells[self.row + row, self.col + col] = self.cell_id
def cell(
self,
@@ -246,25 +243,25 @@ class TableCell:
"""Data of a cell in a table."""
def __init__(self, table: Table, row: int, col: int) -> None:
- if table.cells[(row, col)] == 0:
+ if table.cells[row, col] == 0:
raise IndexError
self.table = table
- self.cell_id = table.cells[(row, col)]
+ self.cell_id = table.cells[row, col]
self.row = row
self.col = col
# adjust position for multirow/multicol cell
- while table.cells[(self.row - 1, self.col)] == self.cell_id:
+ while table.cells[self.row - 1, self.col] == self.cell_id:
self.row -= 1
- while table.cells[(self.row, self.col - 1)] == self.cell_id:
+ while table.cells[self.row, self.col - 1] == self.cell_id:
self.col -= 1
@property
def width(self) -> int:
"""Returns the cell width."""
width = 0
- while self.table.cells[(self.row, self.col + width)] == self.cell_id:
+ while self.table.cells[self.row, self.col + width] == self.cell_id:
width += 1
return width
@@ -272,7 +269,7 @@ def width(self) -> int:
def height(self) -> int:
"""Returns the cell height."""
height = 0
- while self.table.cells[(self.row + height, self.col)] == self.cell_id:
+ while self.table.cells[self.row + height, self.col] == self.cell_id:
height += 1
return height
@@ -564,7 +561,7 @@ def generate(
indices_config = frozenset(indices_config)
else:
check_names = False
- for domain in self.builder.env.domains.sorted():
+ for domain in self._domains.sorted():
for index_cls in domain.indices:
index_name = f'{domain.name}-{index_cls.name}'
if check_names and index_name not in indices_config:
@@ -957,6 +954,7 @@ def _visit_sig_parameter_list(
self.required_params_left = sum(self.list_is_required_param)
self.param_separator = r'\sphinxparamcomma '
self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
+ self.trailing_comma = node.get('multi_line_trailing_comma', False)
def visit_desc_parameterlist(self, node: Element) -> None:
if self.has_tp_list:
@@ -1016,7 +1014,7 @@ def _depart_sig_parameter(self, node: Element) -> None:
if (
opt_param_left_at_level
or is_required
- and (is_last_group or next_is_required)
+ and (next_is_required or self.trailing_comma)
):
self.body.append(self.param_separator)
@@ -1058,13 +1056,20 @@ def visit_desc_optional(self, node: Element) -> None:
def depart_desc_optional(self, node: Element) -> None:
self.optional_param_level -= 1
+ level = self.optional_param_level
if self.multi_line_parameter_list:
+ max_level = self.max_optional_param_level
+ len_lirp = len(self.list_is_required_param)
+ is_last_group = self.param_group_index + 1 == len_lirp
# If it's the first time we go down one level, add the separator before the
- # bracket.
- if self.optional_param_level == self.max_optional_param_level - 1:
+ # bracket, except if this is the last parameter and the parameter list
+ # should not feature a trailing comma.
+ if level == max_level - 1 and (
+ not is_last_group or level > 0 or self.trailing_comma
+ ):
self.body.append(self.param_separator)
self.body.append('}')
- if self.optional_param_level == 0:
+ if level == 0:
self.param_group_index += 1
def visit_desc_annotation(self, node: Element) -> None:
@@ -1116,7 +1121,7 @@ def depart_rubric(self, node: nodes.rubric) -> None:
def visit_footnote(self, node: Element) -> None:
self.in_footnote += 1
- label = cast(nodes.label, node[0])
+ label = cast('nodes.label', node[0])
if self.in_parsed_literal:
self.body.append(r'\begin{footnote}[%s]' % label.astext())
else:
@@ -1387,8 +1392,8 @@ def depart_entry(self, node: Element) -> None:
def visit_acks(self, node: Element) -> None:
# this is a list in the source, but should be rendered as a
# comma-separated list here
- bullet_list = cast(nodes.bullet_list, node[0])
- list_items = cast(Iterable[nodes.list_item], bullet_list)
+ bullet_list = cast('nodes.bullet_list', node[0])
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
self.body.append(BLANKLINE)
self.body.append(', '.join(n.astext() for n in list_items) + '.')
self.body.append(BLANKLINE)
@@ -1421,8 +1426,9 @@ def get_nested_level(node: Element) -> int:
else:
return get_nested_level(node.parent)
- enum = 'enum%s' % toRoman(get_nested_level(node)).lower()
- enumnext = 'enum%s' % toRoman(get_nested_level(node) + 1).lower()
+ nested_level = get_nested_level(node)
+ enum = f'enum{RomanNumeral(nested_level).to_lowercase()}'
+ enumnext = f'enum{RomanNumeral(nested_level + 1).to_lowercase()}'
style = ENUMERATE_LIST_STYLE.get(get_enumtype(node))
prefix = node.get('prefix', '')
suffix = node.get('suffix', '.')
@@ -1819,7 +1825,7 @@ def add_target(id: str) -> None:
while isinstance(next_node, nodes.target):
next_node = next_node.next_node(ascend=True)
- domain = self.builder.env.domains.standard_domain
+ domain = self._domains.standard_domain
if isinstance(next_node, HYPERLINK_SUPPORT_NODES):
return
if (
@@ -2095,8 +2101,8 @@ def depart_title_reference(self, node: Element) -> None:
self.body.append('}')
def visit_thebibliography(self, node: Element) -> None:
- citations = cast(Iterable[nodes.citation], node)
- labels = (cast(nodes.label, citation[0]) for citation in citations)
+ citations = cast('Iterable[nodes.citation]', node)
+ labels = (cast('nodes.label', citation[0]) for citation in citations)
longest_label = max((label.astext() for label in labels), key=len)
if len(longest_label) > MAX_CITATION_LABEL_LENGTH:
# adjust max width of citation labels not to break the layout
@@ -2110,7 +2116,7 @@ def depart_thebibliography(self, node: Element) -> None:
self.body.append(r'\end{sphinxthebibliography}' + CR)
def visit_citation(self, node: Element) -> None:
- label = cast(nodes.label, node[0])
+ label = cast('nodes.label', node[0])
self.body.append(
rf'\bibitem[{self.encode(label.astext())}]'
rf'{{{node["docname"]}:{node["ids"][0]}}}'
@@ -2163,7 +2169,7 @@ def depart_footnotemark(self, node: Element) -> None:
self.body.append(']')
def visit_footnotetext(self, node: Element) -> None:
- label = cast(nodes.label, node[0])
+ label = cast('nodes.label', node[0])
self.body.append('%' + CR)
self.body.append(r'\begin{footnotetext}[%s]' % label.astext())
self.body.append(r'\sphinxAtStartFootnote' + CR)
@@ -2446,20 +2452,20 @@ def visit_system_message(self, node: Element) -> None:
def depart_system_message(self, node: Element) -> None:
self.body.append(CR)
- def visit_math(self, node: Element) -> None:
+ def visit_math(self, node: nodes.math) -> None:
if self.in_title:
self.body.append(r'\protect\(%s\protect\)' % node.astext())
else:
self.body.append(r'\(%s\)' % node.astext())
raise nodes.SkipNode
- def visit_math_block(self, node: Element) -> None:
+ def visit_math_block(self, node: nodes.math_block) -> None:
if node.get('label'):
label = f'equation:{node["docname"]}:{node["label"]}'
else:
label = None
- if node.get('nowrap'):
+ if node.get('no-wrap'):
if label:
self.body.append(r'\label{%s}' % label)
self.body.append(node.astext())
diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py
index 7b7db13961f..63900584d04 100644
--- a/sphinx/writers/manpage.py
+++ b/sphinx/writers/manpage.py
@@ -2,8 +2,7 @@
from __future__ import annotations
-from collections.abc import Iterable
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes
from docutils.writers.manpage import Translator as BaseTranslator
@@ -17,6 +16,9 @@
from sphinx.util.nodes import NodeMatcher
if TYPE_CHECKING:
+ from collections.abc import Iterable
+ from typing import Any
+
from docutils.nodes import Element
from sphinx.builders import Builder
@@ -33,14 +35,13 @@ def translate(self) -> None:
transform = NestedInlineTransform(self.document)
transform.apply()
visitor = self.builder.create_translator(self.document, self.builder)
- self.visitor = cast(ManualPageTranslator, visitor)
+ self.visitor = cast('ManualPageTranslator', visitor)
self.document.walkabout(visitor)
self.output = self.visitor.astext()
class NestedInlineTransform:
- """
- Flatten nested inline nodes:
+ """Flatten nested inline nodes:
Before:
foo=1
@@ -71,9 +72,7 @@ def apply(self, **kwargs: Any) -> None:
class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
- """
- Custom man page translator.
- """
+ """Custom man page translator."""
_docinfo: dict[str, Any] = {}
@@ -277,7 +276,7 @@ def visit_productionlist(self, node: Element) -> None:
self.ensure_eol()
self.in_productionlist += 1
self.body.append('.sp\n.nf\n')
- productionlist = cast(Iterable[addnodes.production], node)
+ productionlist = cast('Iterable[addnodes.production]', node)
names = (production['tokenname'] for production in productionlist)
maxlen = max(len(name) for name in names)
lastname = None
@@ -379,11 +378,11 @@ def depart_glossary(self, node: Element) -> None:
pass
def visit_acks(self, node: Element) -> None:
- bullet_list = cast(nodes.bullet_list, node[0])
- list_items = cast(Iterable[nodes.list_item], bullet_list)
+ bullet_list = cast('nodes.bullet_list', node[0])
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
self.ensure_eol()
- bullet_list = cast(nodes.bullet_list, node[0])
- list_items = cast(Iterable[nodes.list_item], bullet_list)
+ bullet_list = cast('nodes.bullet_list', node[0])
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
self.body.append(', '.join(n.astext() for n in list_items) + '.')
self.body.append('\n')
raise nodes.SkipNode
@@ -477,14 +476,14 @@ def visit_inline(self, node: Element) -> None:
def depart_inline(self, node: Element) -> None:
pass
- def visit_math(self, node: Element) -> None:
+ def visit_math(self, node: nodes.math) -> None:
pass
- def depart_math(self, node: Element) -> None:
+ def depart_math(self, node: nodes.math) -> None:
pass
- def visit_math_block(self, node: Element) -> None:
+ def visit_math_block(self, node: nodes.math_block) -> None:
self.visit_centered(node)
- def depart_math_block(self, node: Element) -> None:
+ def depart_math_block(self, node: nodes.math_block) -> None:
self.depart_centered(node)
diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py
index 396e48bf91a..3bf8b913f0a 100644
--- a/sphinx/writers/texinfo.py
+++ b/sphinx/writers/texinfo.py
@@ -5,13 +5,11 @@
import os.path
import re
import textwrap
-from collections.abc import Iterable, Iterator
-from typing import TYPE_CHECKING, Any, ClassVar, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes, writers
from sphinx import __display_version__, addnodes
-from sphinx.errors import ExtensionError
from sphinx.locale import _, __, admonitionlabels
from sphinx.util import logging
from sphinx.util.docutils import SphinxTranslator
@@ -19,6 +17,9 @@
from sphinx.writers.latex import collected_footnote
if TYPE_CHECKING:
+ from collections.abc import Iterable, Iterator
+ from typing import Any, ClassVar
+
from docutils.nodes import Element, Node, Text
from sphinx.builders.texinfo import TexinfoBuilder
@@ -133,7 +134,7 @@ def __init__(self, builder: TexinfoBuilder) -> None:
def translate(self) -> None:
assert isinstance(self.document, nodes.document)
visitor = self.builder.create_translator(self.document, self.builder)
- self.visitor = cast(TexinfoTranslator, visitor)
+ self.visitor = cast('TexinfoTranslator', visitor)
self.document.walkabout(visitor)
self.visitor.finish()
for attr in self.visitor_attributes:
@@ -288,7 +289,7 @@ def add_node_name(name: str) -> str:
]
# each section is also a node
for section in self.document.findall(nodes.section):
- title = cast(nodes.TextElement, section.next_node(nodes.Titular)) # type: ignore[type-var]
+ title = cast('nodes.TextElement', section.next_node(nodes.Titular)) # type: ignore[type-var]
name = title.astext() if title else ''
section['node_name'] = add_node_name(name)
@@ -492,7 +493,7 @@ def generate(
indices_config = frozenset(indices_config)
else:
check_names = False
- for domain in self.builder.env.domains.sorted():
+ for domain in self._domains.sorted():
for index_cls in domain.indices:
index_name = f'{domain.name}-{index_cls.name}'
if check_names and index_name not in indices_config:
@@ -506,7 +507,7 @@ def generate(
generate(content, collapsed),
))
# only add the main Index if it's not empty
- domain = self.builder.env.domains.index_domain
+ domain = self._domains.index_domain
for docname in self.builder.docnames:
if domain.entries[docname]:
self.indices.append((_('Index'), '\n@printindex ge\n'))
@@ -530,7 +531,7 @@ def footnotes_under(n: Element) -> Iterator[nodes.footnote]:
fnotes: dict[str, list[collected_footnote | bool]] = {}
for fn in footnotes_under(node):
- label = cast(nodes.label, fn[0])
+ label = cast('nodes.label', fn[0])
num = label.astext().strip()
fnotes[num] = [collected_footnote('', *fn.children), False]
return fnotes
@@ -609,7 +610,7 @@ def visit_section(self, node: Element) -> None:
self.add_anchor(id, node)
self.next_section_ids.clear()
- self.previous_section = cast(nodes.section, node)
+ self.previous_section = cast('nodes.section', node)
self.section_level += 1
def depart_section(self, node: Element) -> None:
@@ -1110,7 +1111,7 @@ def depart_field_body(self, node: Element) -> None:
def visit_admonition(self, node: Element, name: str = '') -> None:
if not name:
- title = cast(nodes.title, node[0])
+ title = cast('nodes.title', node[0])
name = self.escape(title.astext())
self.body.append('\n@cartouche\n@quotation %s ' % name)
@@ -1171,9 +1172,9 @@ def depart_decoration(self, node: Element) -> None:
def visit_topic(self, node: Element) -> None:
# ignore TOC's since we have to have a "menu" anyway
- if 'contents' in node.get('classes', []):
+ if 'contents' in node.get('classes', ()):
raise nodes.SkipNode
- title = cast(nodes.title, node[0])
+ title = cast('nodes.title', node[0])
self.visit_rubric(title)
self.body.append('%s\n' % self.escape(title.astext()))
self.depart_rubric(title)
@@ -1307,7 +1308,7 @@ def unknown_departure(self, node: Node) -> None:
def visit_productionlist(self, node: Element) -> None:
self.visit_literal_block(None)
- productionlist = cast(Iterable[addnodes.production], node)
+ productionlist = cast('Iterable[addnodes.production]', node)
names = (production['tokenname'] for production in productionlist)
maxlen = max(len(name) for name in names)
@@ -1388,8 +1389,8 @@ def depart_glossary(self, node: Element) -> None:
pass
def visit_acks(self, node: Element) -> None:
- bullet_list = cast(nodes.bullet_list, node[0])
- list_items = cast(Iterable[nodes.list_item], bullet_list)
+ bullet_list = cast('nodes.bullet_list', node[0])
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
self.body.append('\n\n')
self.body.append(', '.join(n.astext() for n in list_items) + '.')
self.body.append('\n\n')
@@ -1419,11 +1420,11 @@ def visit_desc_signature(self, node: Element) -> None:
self.add_anchor(id, node)
# use the full name of the objtype for the category
try:
- domain = self.builder.env.get_domain(node.parent['domain'])
+ domain = self._domains[node.parent['domain']]
name = domain.get_type_name(
domain.object_types[objtype], self.config.primary_domain == domain.name
)
- except (KeyError, ExtensionError):
+ except KeyError:
name = objtype
# by convention, the deffn category should be capitalized like a title
category = self.escape_arg(smart_capwords(name))
@@ -1502,7 +1503,7 @@ def visit_desc_parameter(self, node: Element) -> None:
self.first_param = 0
text = self.escape(node.astext())
# replace no-break spaces with normal ones
- text = text.replace(' ', '@w{ }')
+ text = text.replace('\N{NO-BREAK SPACE}', '@w{ }')
self.body.append(text)
raise nodes.SkipNode
@@ -1580,11 +1581,11 @@ def visit_pending_xref(self, node: Element) -> None:
def depart_pending_xref(self, node: Element) -> None:
pass
- def visit_math(self, node: Element) -> None:
+ def visit_math(self, node: nodes.math) -> None:
self.body.append('@math{' + self.escape_arg(node.astext()) + '}')
raise nodes.SkipNode
- def visit_math_block(self, node: Element) -> None:
+ def visit_math_block(self, node: nodes.math_block) -> None:
if node.get('label'):
self.add_anchor(node['label'], node)
self.body.append(
diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py
index c16181ec247..7ef6e808ef9 100644
--- a/sphinx/writers/text.py
+++ b/sphinx/writers/text.py
@@ -6,9 +6,8 @@
import os
import re
import textwrap
-from collections.abc import Iterable, Iterator, Sequence
from itertools import chain, groupby, pairwise
-from typing import TYPE_CHECKING, Any, ClassVar, cast
+from typing import TYPE_CHECKING, cast
from docutils import nodes, writers
from docutils.utils import column_width
@@ -18,6 +17,9 @@
from sphinx.util.docutils import SphinxTranslator
if TYPE_CHECKING:
+ from collections.abc import Iterable, Iterator, Sequence
+ from typing import Any, ClassVar
+
from docutils.nodes import Element, Text
from sphinx.builders.text import TextBuilder
@@ -381,7 +383,7 @@ def translate(self) -> None:
assert isinstance(self.document, nodes.document)
visitor = self.builder.create_translator(self.document, self.builder)
self.document.walkabout(visitor)
- self.output = cast(TextTranslator, visitor).body
+ self.output = cast('TextTranslator', visitor).body
class TextTranslator(SphinxTranslator):
@@ -646,6 +648,7 @@ def _visit_sig_parameter_list(
self.required_params_left = sum(self.list_is_required_param)
self.param_separator = ', '
self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
+ self.trailing_comma = node.get('multi_line_trailing_comma', False)
if self.multi_line_parameter_list:
self.param_separator = self.param_separator.rstrip()
self.context.append(sig_close_paren)
@@ -697,7 +700,8 @@ def visit_desc_parameter(self, node: Element) -> None:
or is_required
and (is_last_group or next_is_required)
):
- self.add_text(self.param_separator)
+ if not is_last_group or opt_param_left_at_level or self.trailing_comma:
+ self.add_text(self.param_separator)
self.end_state(wrap=False, end=None)
elif self.required_params_left:
@@ -738,20 +742,27 @@ def visit_desc_optional(self, node: Element) -> None:
def depart_desc_optional(self, node: Element) -> None:
self.optional_param_level -= 1
+ level = self.optional_param_level
if self.multi_line_parameter_list:
+ max_level = self.max_optional_param_level
+ len_lirp = len(self.list_is_required_param)
+ is_last_group = self.param_group_index + 1 == len_lirp
# If it's the first time we go down one level, add the separator before the
- # bracket.
- if self.optional_param_level == self.max_optional_param_level - 1:
+ # bracket, except if this is the last parameter and the parameter list
+ # should not feature a trailing comma.
+ if level == max_level - 1 and (
+ not is_last_group or level > 0 or self.trailing_comma
+ ):
self.add_text(self.param_separator)
self.add_text(']')
# End the line if we have just closed the last bracket of this group of
# optional parameters.
- if self.optional_param_level == 0:
+ if level == 0:
self.end_state(wrap=False, end=None)
else:
self.add_text(']')
- if self.optional_param_level == 0:
+ if level == 0:
self.param_group_index += 1
def visit_desc_annotation(self, node: Element) -> None:
@@ -776,7 +787,7 @@ def depart_caption(self, node: Element) -> None:
def visit_productionlist(self, node: Element) -> None:
self.new_state()
- productionlist = cast(Iterable[addnodes.production], node)
+ productionlist = cast('Iterable[addnodes.production]', node)
names = (production['tokenname'] for production in productionlist)
maxlen = max(len(name) for name in names)
lastname = None
@@ -791,7 +802,7 @@ def visit_productionlist(self, node: Element) -> None:
raise nodes.SkipNode
def visit_footnote(self, node: Element) -> None:
- label = cast(nodes.label, node[0])
+ label = cast('nodes.label', node[0])
self._footnote = label.astext().strip()
self.new_state(len(self._footnote) + 3)
@@ -923,8 +934,8 @@ def depart_table(self, node: Element) -> None:
self.end_state(wrap=False)
def visit_acks(self, node: Element) -> None:
- bullet_list = cast(nodes.bullet_list, node[0])
- list_items = cast(Iterable[nodes.list_item], bullet_list)
+ bullet_list = cast('nodes.bullet_list', node[0])
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
self.new_state(0)
self.add_text(', '.join(n.astext() for n in list_items) + '.')
self.end_state()
@@ -1316,14 +1327,14 @@ def visit_raw(self, node: Element) -> None:
self.end_state(wrap=False)
raise nodes.SkipNode
- def visit_math(self, node: Element) -> None:
+ def visit_math(self, node: nodes.math) -> None:
pass
- def depart_math(self, node: Element) -> None:
+ def depart_math(self, node: nodes.math) -> None:
pass
- def visit_math_block(self, node: Element) -> None:
+ def visit_math_block(self, node: nodes.math_block) -> None:
self.new_state()
- def depart_math_block(self, node: Element) -> None:
+ def depart_math_block(self, node: nodes.math_block) -> None:
self.end_state()
diff --git a/sphinx/writers/xml.py b/sphinx/writers/xml.py
index 825f6da5ca7..51f77ee2f01 100644
--- a/sphinx/writers/xml.py
+++ b/sphinx/writers/xml.py
@@ -2,11 +2,13 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from docutils.writers.docutils_xml import Writer as BaseXMLWriter
if TYPE_CHECKING:
+ from typing import Any
+
from sphinx.builders import Builder
@@ -16,10 +18,11 @@ class XMLWriter(BaseXMLWriter): # type: ignore[misc]
def __init__(self, builder: Builder) -> None:
super().__init__()
self.builder = builder
+ self._config = builder.config
def translate(self, *args: Any, **kwargs: Any) -> None:
self.document.settings.newlines = self.document.settings.indents = (
- self.builder.env.config.xml_pretty
+ self._config.xml_pretty
)
self.document.settings.xml_declaration = True
self.document.settings.doctype_declaration = True
diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js
index e5837e65d56..42adb88db92 100644
--- a/tests/js/fixtures/cpp/searchindex.js
+++ b/tests/js/fixtures/cpp/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{"sphinx (c++ class)":[[0,"_CPPv46Sphinx",false]]},"objects":{"":[[0,0,1,"_CPPv46Sphinx","Sphinx"]]},"objnames":{"0":["cpp","class","C++ class"]},"objtypes":{"0":"cpp:class"},"terms":{"The":0,"becaus":0,"c":0,"can":0,"cardin":0,"challeng":0,"charact":0,"class":0,"descript":0,"drop":0,"engin":0,"fixtur":0,"frequent":0,"gener":0,"i":0,"index":0,"inflat":0,"mathemat":0,"occur":0,"often":0,"project":0,"punctuat":0,"queri":0,"relat":0,"sampl":0,"search":0,"size":0,"sphinx":0,"term":0,"thei":0,"thi":0,"token":0,"us":0,"web":0,"would":0},"titles":["<no title>"],"titleterms":{}})
\ No newline at end of file
+Search.setIndex({"alltitles":{},"docnames":["index"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{"sphinx (c++ class)":[[0,"_CPPv46Sphinx",false]]},"objects":{"":[[0,0,1,"_CPPv46Sphinx","Sphinx"]]},"objnames":{"0":["cpp","class","C++ class"]},"objtypes":{"0":"cpp:class"},"terms":{"The":0,"becaus":0,"c":0,"can":0,"cardin":0,"challeng":0,"charact":0,"class":0,"descript":0,"drop":0,"engin":0,"fixtur":0,"frequent":0,"gener":0,"i":0,"index":0,"inflat":0,"mathemat":0,"occur":0,"often":0,"project":0,"punctuat":0,"queri":0,"relat":0,"sampl":0,"search":0,"size":0,"sphinx":0,"term":0,"thei":0,"thi":0,"token":0,"us":0,"web":0,"would":0},"titles":["<no title>"],"titleterms":{}})
\ No newline at end of file
diff --git a/tests/js/fixtures/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js
index b3e2977792c..6f27d39329b 100644
--- a/tests/js/fixtures/multiterm/searchindex.js
+++ b/tests/js/fixtures/multiterm/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"Main Page":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"At":0,"adjac":0,"all":0,"an":0,"appear":0,"applic":0,"ar":0,"built":0,"can":0,"check":0,"contain":0,"do":0,"document":0,"doesn":0,"each":0,"fixtur":0,"format":0,"function":0,"futur":0,"html":0,"i":0,"includ":0,"match":0,"messag":0,"multipl":0,"multiterm":0,"order":0,"other":0,"output":0,"perform":0,"perhap":0,"phrase":0,"project":0,"queri":0,"requir":0,"same":0,"search":0,"successfulli":0,"support":0,"t":0,"term":0,"test":0,"thi":0,"time":0,"us":0,"when":0,"write":0},"titles":["Main Page"],"titleterms":{"main":0,"page":0}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"Main Page":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"At":0,"adjac":0,"all":0,"an":0,"appear":0,"applic":0,"ar":0,"built":0,"can":0,"check":0,"contain":0,"do":0,"document":0,"doesn":0,"each":0,"fixtur":0,"format":0,"function":0,"futur":0,"html":0,"i":0,"includ":0,"match":0,"messag":0,"multipl":0,"multiterm":0,"order":0,"other":0,"output":0,"perform":0,"perhap":0,"phrase":0,"project":0,"queri":0,"requir":0,"same":0,"search":0,"successfulli":0,"support":0,"t":0,"term":0,"test":0,"thi":0,"time":0,"us":0,"when":0,"write":0},"titles":["Main Page"],"titleterms":{"main":0,"page":0}})
\ No newline at end of file
diff --git a/tests/js/fixtures/partial/searchindex.js b/tests/js/fixtures/partial/searchindex.js
index ac024bf0c6e..cd9dbabb149 100644
--- a/tests/js/fixtures/partial/searchindex.js
+++ b/tests/js/fixtures/partial/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"sphinx_utils module":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"ar":0,"both":0,"built":0,"confirm":0,"document":0,"function":0,"html":0,"i":0,"includ":0,"input":0,"javascript":0,"match":0,"partial":0,"possibl":0,"project":0,"provid":0,"restructuredtext":0,"sampl":0,"search":0,"should":0,"term":0,"thi":0,"titl":0,"us":0,"when":0},"titles":["sphinx_utils module"],"titleterms":{"modul":0,"sphinx_util":0}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"sphinx_utils module":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"ar":0,"both":0,"built":0,"confirm":0,"document":0,"function":0,"html":0,"i":0,"includ":0,"input":0,"javascript":0,"match":0,"partial":0,"possibl":0,"project":0,"provid":0,"restructuredtext":0,"sampl":0,"search":0,"should":0,"term":0,"thi":0,"titl":0,"us":0,"when":0},"titles":["sphinx_utils module"],"titleterms":{"modul":0,"sphinx_util":0}})
\ No newline at end of file
diff --git a/tests/js/fixtures/titles/searchindex.js b/tests/js/fixtures/titles/searchindex.js
index 987be77992a..cb9abd1da07 100644
--- a/tests/js/fixtures/titles/searchindex.js
+++ b/tests/js/fixtures/titles/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"Main Page":[[0,null]],"Relevance":[[0,"relevance"],[1,null]],"Result Scoring":[[0,"result-scoring"]]},"docnames":["index","relevance"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst","relevance.rst"],"indexentries":{"example (class in relevance)":[[0,"relevance.Example",false]],"module":[[0,"module-relevance",false]],"relevance":[[0,"index-1",false],[0,"module-relevance",false]],"relevance (relevance.example attribute)":[[0,"relevance.Example.relevance",false]],"scoring":[[0,"index-0",true]]},"objects":{"":[[0,0,0,"-","relevance"]],"relevance":[[0,1,1,"","Example"]],"relevance.Example":[[0,2,1,"","relevance"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:attribute"},"terms":{"":[0,1],"A":1,"By":0,"For":[0,1],"In":[0,1],"against":0,"align":0,"also":1,"an":0,"answer":0,"appear":1,"ar":1,"area":0,"ask":0,"assign":0,"attempt":0,"attribut":0,"both":0,"built":1,"can":[0,1],"class":0,"code":[0,1],"collect":0,"consid":1,"contain":0,"context":0,"corpu":1,"could":1,"demonstr":0,"describ":1,"detail":1,"determin":[0,1],"docstr":0,"document":[0,1],"domain":1,"dure":0,"engin":0,"evalu":0,"exampl":[0,1],"extract":0,"feedback":0,"find":0,"found":0,"from":0,"function":1,"ha":1,"handl":0,"happen":1,"head":0,"help":0,"highli":[0,1],"how":0,"i":[0,1],"improv":0,"inform":0,"intend":0,"issu":[0,1],"itself":1,"knowledg":0,"languag":1,"less":1,"like":[0,1],"mani":0,"match":0,"mention":1,"more":0,"name":[0,1],"numer":0,"object":0,"often":0,"one":[0,1],"onli":[0,1],"order":0,"other":0,"over":0,"page":1,"part":1,"particular":0,"present":0,"printf":1,"program":1,"project":0,"queri":[0,1],"question":0,"re":0,"rel":0,"research":0,"result":1,"retriev":0,"sai":0,"same":1,"search":[0,1],"seem":0,"softwar":1,"some":1,"sphinx":0,"straightforward":1,"subject":0,"subsect":0,"term":[0,1],"test":0,"text":0,"than":[0,1],"thei":0,"them":0,"thi":0,"time":0,"titl":0,"two":0,"typic":0,"us":0,"user":[0,1],"we":[0,1],"when":0,"whether":1,"which":0,"within":0,"word":0,"would":[0,1]},"titles":["Main Page","Relevance"],"titleterms":{"main":0,"page":0,"relev":[0,1],"result":0,"score":0}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"Main Page":[[0,null]],"Relevance":[[0,"relevance"],[1,null]],"Result Scoring":[[0,"result-scoring"]]},"docnames":["index","relevance"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst","relevance.rst"],"indexentries":{"example (class in relevance)":[[0,"relevance.Example",false]],"module":[[0,"module-relevance",false]],"relevance":[[0,"index-1",false],[0,"module-relevance",false]],"relevance (relevance.example attribute)":[[0,"relevance.Example.relevance",false]],"scoring":[[0,"index-0",true]]},"objects":{"":[[0,0,0,"-","relevance"]],"relevance":[[0,1,1,"","Example"]],"relevance.Example":[[0,2,1,"","relevance"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:attribute"},"terms":{"":[0,1],"A":1,"By":0,"For":[0,1],"In":[0,1],"against":0,"align":0,"also":1,"an":0,"answer":0,"appear":1,"ar":1,"area":0,"ask":0,"assign":0,"attempt":0,"attribut":0,"both":0,"built":1,"can":[0,1],"class":0,"code":[0,1],"collect":0,"consid":1,"contain":0,"context":0,"corpu":1,"could":1,"demonstr":0,"describ":1,"detail":1,"determin":[0,1],"docstr":0,"document":[0,1],"domain":1,"dure":0,"engin":0,"evalu":0,"exampl":[0,1],"extract":0,"feedback":0,"find":0,"found":0,"from":0,"function":1,"ha":1,"handl":0,"happen":1,"head":0,"help":0,"highli":[0,1],"how":0,"i":[0,1],"improv":0,"inform":0,"intend":0,"issu":[0,1],"itself":1,"knowledg":0,"languag":1,"less":1,"like":[0,1],"mani":0,"match":0,"mention":1,"more":0,"name":[0,1],"numer":0,"object":0,"often":0,"one":[0,1],"onli":[0,1],"order":0,"other":0,"over":0,"page":1,"part":1,"particular":0,"present":0,"printf":1,"program":1,"project":0,"queri":[0,1],"question":0,"re":0,"rel":0,"research":0,"result":1,"retriev":0,"sai":0,"same":1,"search":[0,1],"seem":0,"softwar":1,"some":1,"sphinx":0,"straightforward":1,"subject":0,"subsect":0,"term":[0,1],"test":0,"text":0,"than":[0,1],"thei":0,"them":0,"thi":0,"time":0,"titl":0,"two":0,"typic":0,"us":0,"user":[0,1],"we":[0,1],"when":0,"whether":1,"which":0,"within":0,"word":0,"would":[0,1]},"titles":["Main Page","Relevance"],"titleterms":{"main":0,"page":0,"relev":[0,1],"result":0,"score":0}})
\ No newline at end of file
diff --git a/tests/js/roots/titles/relevance.py b/tests/js/roots/titles/relevance.py
index c4d0eec557f..b9ebc2b00e8 100644
--- a/tests/js/roots/titles/relevance.py
+++ b/tests/js/roots/titles/relevance.py
@@ -1,7 +1,8 @@
class Example:
"""Example class"""
+
num_attribute = 5
- text_attribute = "string"
+ text_attribute = 'string'
- relevance = "testing"
+ relevance = 'testing'
"""attribute docstring"""
diff --git a/tests/js/searchtools.spec.js b/tests/js/searchtools.spec.js
index cfe5fdcf7ed..2ee55ad00ac 100644
--- a/tests/js/searchtools.spec.js
+++ b/tests/js/searchtools.spec.js
@@ -209,6 +209,19 @@ describe('Basic html theme search', function() {
});
+ describe('can handle edge-case search queries', function() {
+
+ it('does not find the javascript prototype property in unrelated documents', function() {
+ eval(loadFixture("partial/searchindex.js"));
+
+ searchParameters = Search._parseQuery('__proto__');
+
+ hits = [];
+ expect(Search._performSearch(...searchParameters)).toEqual(hits);
+ });
+
+ });
+
});
describe("htmlToText", function() {
diff --git a/tests/roots/test-add_enumerable_node/enumerable_node.py b/tests/roots/test-add_enumerable_node/enumerable_node.py
index 782365e655b..2cf93e9104f 100644
--- a/tests/roots/test-add_enumerable_node/enumerable_node.py
+++ b/tests/roots/test-add_enumerable_node/enumerable_node.py
@@ -37,7 +37,7 @@ def visit_numbered_text(self, node):
raise nodes.SkipNode
-def get_title(node):
+def get_title(node): # NoQA: FURB118
return node['title']
@@ -51,12 +51,14 @@ def run(self):
def setup(app):
# my-figure
- app.add_enumerable_node(my_figure, 'figure',
- html=(visit_my_figure, depart_my_figure))
+ app.add_enumerable_node(
+ my_figure, 'figure', html=(visit_my_figure, depart_my_figure)
+ )
app.add_directive('my-figure', MyFigure)
# numbered_label
- app.add_enumerable_node(numbered_text, 'original', get_title,
- html=(visit_numbered_text, None))
+ app.add_enumerable_node(
+ numbered_text, 'original', get_title, html=(visit_numbered_text, None)
+ )
app.add_directive('numbered-text', NumberedText)
app.config.numfig_format.setdefault('original', 'No.%s')
diff --git a/tests/roots/test-add_source_parser-conflicts-with-users-setting/conf.py b/tests/roots/test-add_source_parser-conflicts-with-users-setting/conf.py
index 5f979c70918..25d1e76be4d 100644
--- a/tests/roots/test-add_source_parser-conflicts-with-users-setting/conf.py
+++ b/tests/roots/test-add_source_parser-conflicts-with-users-setting/conf.py
@@ -16,5 +16,5 @@ class DummyTestParser(Parser):
'.test': 'restructuredtext',
}
source_parsers = {
- '.test': DummyTestParser
+ '.test': DummyTestParser,
}
diff --git a/tests/roots/test-apidoc-pep420/a/b/c/__init__.py b/tests/roots/test-apidoc-pep420/a/b/c/__init__.py
deleted file mode 100644
index 5b727c1139b..00000000000
--- a/tests/roots/test-apidoc-pep420/a/b/c/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"Package C"
diff --git a/tests/roots/test-apidoc-pep420/a/b/c/d.py b/tests/roots/test-apidoc-pep420/a/b/c/d.py
deleted file mode 100644
index 63b0e3436b8..00000000000
--- a/tests/roots/test-apidoc-pep420/a/b/c/d.py
+++ /dev/null
@@ -1 +0,0 @@
-"Module d"
diff --git a/tests/roots/test-apidoc-pep420/a/b/e/f.py b/tests/roots/test-apidoc-pep420/a/b/e/f.py
deleted file mode 100644
index a09affe861f..00000000000
--- a/tests/roots/test-apidoc-pep420/a/b/e/f.py
+++ /dev/null
@@ -1 +0,0 @@
-"Module f"
diff --git a/tests/roots/test-apidoc-pep420/a/b/x/y.py b/tests/roots/test-apidoc-pep420/a/b/x/y.py
deleted file mode 100644
index 46bc245051b..00000000000
--- a/tests/roots/test-apidoc-pep420/a/b/x/y.py
+++ /dev/null
@@ -1 +0,0 @@
-"Module y"
diff --git a/tests/roots/test-apidoc-subpackage-in-toc/parent/child/foo.py b/tests/roots/test-apidoc-subpackage-in-toc/parent/child/foo.py
deleted file mode 100644
index 810c96eeeb7..00000000000
--- a/tests/roots/test-apidoc-subpackage-in-toc/parent/child/foo.py
+++ /dev/null
@@ -1 +0,0 @@
-"foo"
diff --git a/tests/roots/test-apidoc-toc/mypackage/something/__init__.py b/tests/roots/test-apidoc-toc/mypackage/something/__init__.py
deleted file mode 100644
index 6401e43ec46..00000000000
--- a/tests/roots/test-apidoc-toc/mypackage/something/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"Subpackage Something"
diff --git a/tests/roots/test-apidoc-trailing-underscore/package_/__init__.py b/tests/roots/test-apidoc-trailing-underscore/package_/__init__.py
deleted file mode 100644
index b09612b8326..00000000000
--- a/tests/roots/test-apidoc-trailing-underscore/package_/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-""" A package with trailing underscores """
diff --git a/tests/roots/test-apidoc-trailing-underscore/package_/module_.py b/tests/roots/test-apidoc-trailing-underscore/package_/module_.py
deleted file mode 100644
index e16461c21ca..00000000000
--- a/tests/roots/test-apidoc-trailing-underscore/package_/module_.py
+++ /dev/null
@@ -1,9 +0,0 @@
-""" A module with a trailing underscore """
-
-
-class SomeClass_:
- """ A class with a trailing underscore """
-
-
-def some_function_(some_arg_):
- """ A function with a trailing underscore in name and argument """
diff --git a/tests/roots/test-autosummary/underscore_module_.py b/tests/roots/test-autosummary/underscore_module_.py
deleted file mode 100644
index 8584e60787b..00000000000
--- a/tests/roots/test-autosummary/underscore_module_.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""
-module with trailing underscores everywhere
-"""
-
-
-class class_:
- """ Class """
- def method_(_arg):
- """ Method """
- pass
-
-
-def function_(_arg):
- """ Function """
- pass
diff --git a/tests/roots/test-basic/conf.py b/tests/roots/test-basic/conf.py
index 69a316101c9..c4fb1abdda2 100644
--- a/tests/roots/test-basic/conf.py
+++ b/tests/roots/test-basic/conf.py
@@ -1,4 +1,10 @@
html_theme = 'basic'
latex_documents = [
- ('index', 'test.tex', 'The basic Sphinx documentation for testing', 'Sphinx', 'report')
+ (
+ 'index',
+ 'test.tex',
+ 'The basic Sphinx documentation for testing',
+ 'Sphinx',
+ 'report',
+ )
]
diff --git a/tests/roots/test-build-text/conf.py b/tests/roots/test-build-text/conf.py
index b0fdaf8d231..b20b895493f 100644
--- a/tests/roots/test-build-text/conf.py
+++ b/tests/roots/test-build-text/conf.py
@@ -1,4 +1,4 @@
source_suffix = {
- '.txt': 'restructuredtext'
+ '.txt': 'restructuredtext',
}
exclude_patterns = ['_build']
diff --git a/tests/roots/test-directive-code/emphasize.rst b/tests/roots/test-directive-code/emphasize.rst
index 95db574cebe..ae444e9a89a 100644
--- a/tests/roots/test-directive-code/emphasize.rst
+++ b/tests/roots/test-directive-code/emphasize.rst
@@ -3,5 +3,4 @@ Literal Includes with Highlighted Lines
.. literalinclude:: target.py
:language: python
- :emphasize-lines: 5-6, 13-15, 24-
-
+ :emphasize-lines: 6-7, 16-19, 29-
diff --git a/tests/roots/test-directive-code/python.rst b/tests/roots/test-directive-code/python.rst
index 794c190f107..17e3d7d0e49 100644
--- a/tests/roots/test-directive-code/python.rst
+++ b/tests/roots/test-directive-code/python.rst
@@ -1,13 +1,13 @@
-===========================
-Literal Includes for python
-===========================
-
-block start with blank or comment
-=================================
-
-.. literalinclude:: target.py
- :pyobject: block_start_with_comment
-
-.. literalinclude:: target.py
- :pyobject: block_start_with_blank
-
+===========================
+Literal Includes for python
+===========================
+
+block start with blank or comment
+=================================
+
+.. literalinclude:: target.py
+ :pyobject: block_start_with_comment
+
+.. literalinclude:: target.py
+ :pyobject: block_start_with_blank
+
diff --git a/tests/roots/test-directive-code/target.py b/tests/roots/test-directive-code/target.py
index b95dffbf9ef..31f3822ac0b 100644
--- a/tests/roots/test-directive-code/target.py
+++ b/tests/roots/test-directive-code/target.py
@@ -1,21 +1,26 @@
# Literally included file using Python highlighting
-foo = "Including Unicode characters: üöä"
+foo = 'Including Unicode characters: üöä'
+
class Foo:
pass
+
class Bar:
def baz():
pass
+
# comment after Bar class definition
def bar(): pass
+
def block_start_with_comment():
# Comment
return 1
+
def block_start_with_blank():
return 1
diff --git a/tests/roots/test-domain-c-c_maximum_signature_line_length/conf.py b/tests/roots/test-domain-c-c_maximum_signature_line_length/conf.py
index ba480ed2884..817983754b6 100644
--- a/tests/roots/test-domain-c-c_maximum_signature_line_length/conf.py
+++ b/tests/roots/test-domain-c-c_maximum_signature_line_length/conf.py
@@ -1 +1 @@
-c_maximum_signature_line_length = len("str hello(str name)") - 1
+c_maximum_signature_line_length = len('str hello(str name)') - 1
diff --git a/tests/roots/test-domain-c-intersphinx/conf.py b/tests/roots/test-domain-c-intersphinx/conf.py
index c176af77528..896cad799b9 100644
--- a/tests/roots/test-domain-c-intersphinx/conf.py
+++ b/tests/roots/test-domain-c-intersphinx/conf.py
@@ -1,4 +1,4 @@
exclude_patterns = ['_build']
extensions = [
- 'sphinx.ext.intersphinx',
+ 'sphinx.ext.intersphinx',
]
diff --git a/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/conf.py b/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/conf.py
index 1eb3a64bfc4..b75c1418f1a 100644
--- a/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/conf.py
+++ b/tests/roots/test-domain-cpp-cpp_maximum_signature_line_length/conf.py
@@ -1 +1 @@
-cpp_maximum_signature_line_length = len("str hello(str name)") - 1
+cpp_maximum_signature_line_length = len('str hello(str name)') - 1
diff --git a/tests/roots/test-domain-cpp-intersphinx/conf.py b/tests/roots/test-domain-cpp-intersphinx/conf.py
index c176af77528..896cad799b9 100644
--- a/tests/roots/test-domain-cpp-intersphinx/conf.py
+++ b/tests/roots/test-domain-cpp-intersphinx/conf.py
@@ -1,4 +1,4 @@
exclude_patterns = ['_build']
extensions = [
- 'sphinx.ext.intersphinx',
+ 'sphinx.ext.intersphinx',
]
diff --git a/tests/roots/test-epub-anchor-id/conf.py b/tests/roots/test-epub-anchor-id/conf.py
index 2a56f1f6689..eb614a04051 100644
--- a/tests/roots/test-epub-anchor-id/conf.py
+++ b/tests/roots/test-epub-anchor-id/conf.py
@@ -1,2 +1,2 @@
def setup(app):
- app.add_crossref_type(directivename="setting", rolename="setting")
+ app.add_crossref_type(directivename='setting', rolename='setting')
diff --git a/tests/roots/test-apidoc-custom-templates/_templates/module.rst.jinja b/tests/roots/test-ext-apidoc-custom-templates/_templates/module.rst.jinja
similarity index 100%
rename from tests/roots/test-apidoc-custom-templates/_templates/module.rst.jinja
rename to tests/roots/test-ext-apidoc-custom-templates/_templates/module.rst.jinja
diff --git a/tests/roots/test-apidoc-custom-templates/_templates/module.rst_t b/tests/roots/test-ext-apidoc-custom-templates/_templates/module.rst_t
similarity index 100%
rename from tests/roots/test-apidoc-custom-templates/_templates/module.rst_t
rename to tests/roots/test-ext-apidoc-custom-templates/_templates/module.rst_t
diff --git a/tests/roots/test-apidoc-custom-templates/_templates/package.rst_t b/tests/roots/test-ext-apidoc-custom-templates/_templates/package.rst_t
similarity index 100%
rename from tests/roots/test-apidoc-custom-templates/_templates/package.rst_t
rename to tests/roots/test-ext-apidoc-custom-templates/_templates/package.rst_t
diff --git a/tests/roots/test-apidoc-custom-templates/mypackage/__init__.py b/tests/roots/test-ext-apidoc-custom-templates/mypackage/__init__.py
similarity index 100%
rename from tests/roots/test-apidoc-custom-templates/mypackage/__init__.py
rename to tests/roots/test-ext-apidoc-custom-templates/mypackage/__init__.py
diff --git a/tests/roots/test-apidoc-custom-templates/mypackage/mymodule.py b/tests/roots/test-ext-apidoc-custom-templates/mypackage/mymodule.py
old mode 100755
new mode 100644
similarity index 100%
rename from tests/roots/test-apidoc-custom-templates/mypackage/mymodule.py
rename to tests/roots/test-ext-apidoc-custom-templates/mypackage/mymodule.py
diff --git a/tests/roots/test-apidoc-duplicates/fish_licence/halibut.cpython-38-x86_64-linux-gnu.so b/tests/roots/test-ext-apidoc-duplicates/fish_licence/halibut.cpython-38-x86_64-linux-gnu.so
similarity index 100%
rename from tests/roots/test-apidoc-duplicates/fish_licence/halibut.cpython-38-x86_64-linux-gnu.so
rename to tests/roots/test-ext-apidoc-duplicates/fish_licence/halibut.cpython-38-x86_64-linux-gnu.so
diff --git a/tests/roots/test-apidoc-duplicates/fish_licence/halibut.pyx b/tests/roots/test-ext-apidoc-duplicates/fish_licence/halibut.pyd
similarity index 100%
rename from tests/roots/test-apidoc-duplicates/fish_licence/halibut.pyx
rename to tests/roots/test-ext-apidoc-duplicates/fish_licence/halibut.pyd
diff --git a/tests/roots/test-apidoc-pep420/a/b/e/__init__.py b/tests/roots/test-ext-apidoc-duplicates/fish_licence/halibut.pyi
similarity index 100%
rename from tests/roots/test-apidoc-pep420/a/b/e/__init__.py
rename to tests/roots/test-ext-apidoc-duplicates/fish_licence/halibut.pyi
diff --git a/tests/roots/test-apidoc-subpackage-in-toc/parent/__init__.py b/tests/roots/test-ext-apidoc-duplicates/fish_licence/halibut.pyx
similarity index 100%
rename from tests/roots/test-apidoc-subpackage-in-toc/parent/__init__.py
rename to tests/roots/test-ext-apidoc-duplicates/fish_licence/halibut.pyx
diff --git a/tests/roots/test-ext-apidoc-pep420/a/b/c/__init__.py b/tests/roots/test-ext-apidoc-pep420/a/b/c/__init__.py
new file mode 100644
index 00000000000..0dda7cf32df
--- /dev/null
+++ b/tests/roots/test-ext-apidoc-pep420/a/b/c/__init__.py
@@ -0,0 +1 @@
+"""Package C"""
diff --git a/tests/roots/test-ext-apidoc-pep420/a/b/c/d.py b/tests/roots/test-ext-apidoc-pep420/a/b/c/d.py
new file mode 100644
index 00000000000..7566ec33bc2
--- /dev/null
+++ b/tests/roots/test-ext-apidoc-pep420/a/b/c/d.py
@@ -0,0 +1 @@
+"""Module d"""
diff --git a/tests/roots/test-apidoc-subpackage-in-toc/parent/child/__init__.py b/tests/roots/test-ext-apidoc-pep420/a/b/e/__init__.py
similarity index 100%
rename from tests/roots/test-apidoc-subpackage-in-toc/parent/child/__init__.py
rename to tests/roots/test-ext-apidoc-pep420/a/b/e/__init__.py
diff --git a/tests/roots/test-ext-apidoc-pep420/a/b/e/f.py b/tests/roots/test-ext-apidoc-pep420/a/b/e/f.py
new file mode 100644
index 00000000000..1a33f3970b4
--- /dev/null
+++ b/tests/roots/test-ext-apidoc-pep420/a/b/e/f.py
@@ -0,0 +1 @@
+"""Module f"""
diff --git a/tests/roots/test-ext-apidoc-pep420/a/b/x/y.py b/tests/roots/test-ext-apidoc-pep420/a/b/x/y.py
new file mode 100644
index 00000000000..14beabd289b
--- /dev/null
+++ b/tests/roots/test-ext-apidoc-pep420/a/b/x/y.py
@@ -0,0 +1 @@
+"""Module y"""
diff --git a/tests/roots/test-apidoc-toc/mypackage/__init__.py b/tests/roots/test-ext-apidoc-subpackage-in-toc/parent/__init__.py
similarity index 100%
rename from tests/roots/test-apidoc-toc/mypackage/__init__.py
rename to tests/roots/test-ext-apidoc-subpackage-in-toc/parent/__init__.py
diff --git a/tests/roots/test-apidoc-toc/mypackage/resource/__init__.py b/tests/roots/test-ext-apidoc-subpackage-in-toc/parent/child/__init__.py
similarity index 100%
rename from tests/roots/test-apidoc-toc/mypackage/resource/__init__.py
rename to tests/roots/test-ext-apidoc-subpackage-in-toc/parent/child/__init__.py
diff --git a/tests/roots/test-ext-apidoc-subpackage-in-toc/parent/child/foo.py b/tests/roots/test-ext-apidoc-subpackage-in-toc/parent/child/foo.py
new file mode 100644
index 00000000000..6d3c1bc67e3
--- /dev/null
+++ b/tests/roots/test-ext-apidoc-subpackage-in-toc/parent/child/foo.py
@@ -0,0 +1 @@
+"""foo"""
diff --git a/tests/roots/test-ext-apidoc-toc/mypackage/__init__.py b/tests/roots/test-ext-apidoc-toc/mypackage/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/roots/test-apidoc-toc/mypackage/main.py b/tests/roots/test-ext-apidoc-toc/mypackage/main.py
old mode 100755
new mode 100644
similarity index 50%
rename from tests/roots/test-apidoc-toc/mypackage/main.py
rename to tests/roots/test-ext-apidoc-toc/mypackage/main.py
index 1f6d1376cbb..aacbfdde76e
--- a/tests/roots/test-apidoc-toc/mypackage/main.py
+++ b/tests/roots/test-ext-apidoc-toc/mypackage/main.py
@@ -1,13 +1,11 @@
-#!/usr/bin/env python3
-
from pathlib import Path
import mod_resource
import mod_something
-if __name__ == "__main__":
- print(f"Hello, world! -> something returns: {mod_something.something()}")
+if __name__ == '__main__':
+ print(f'Hello, world! -> something returns: {mod_something.something()}')
res_path = Path(mod_resource.__file__).parent / 'resource.txt'
text = res_path.read_text(encoding='utf-8')
- print(f"From mod_resource:resource.txt -> {text}")
+ print(f'From mod_resource:resource.txt -> {text}')
diff --git a/tests/roots/test-apidoc-toc/mypackage/no_init/foo.py b/tests/roots/test-ext-apidoc-toc/mypackage/no_init/foo.py
similarity index 100%
rename from tests/roots/test-apidoc-toc/mypackage/no_init/foo.py
rename to tests/roots/test-ext-apidoc-toc/mypackage/no_init/foo.py
diff --git a/tests/roots/test-ext-apidoc-toc/mypackage/resource/__init__.py b/tests/roots/test-ext-apidoc-toc/mypackage/resource/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/roots/test-apidoc-toc/mypackage/resource/resource.txt b/tests/roots/test-ext-apidoc-toc/mypackage/resource/resource.txt
similarity index 100%
rename from tests/roots/test-apidoc-toc/mypackage/resource/resource.txt
rename to tests/roots/test-ext-apidoc-toc/mypackage/resource/resource.txt
diff --git a/tests/roots/test-ext-apidoc-toc/mypackage/something/__init__.py b/tests/roots/test-ext-apidoc-toc/mypackage/something/__init__.py
new file mode 100644
index 00000000000..a8cbeecd923
--- /dev/null
+++ b/tests/roots/test-ext-apidoc-toc/mypackage/something/__init__.py
@@ -0,0 +1 @@
+"""Subpackage Something"""
diff --git a/tests/roots/test-ext-apidoc-trailing-underscore/package_/__init__.py b/tests/roots/test-ext-apidoc-trailing-underscore/package_/__init__.py
new file mode 100644
index 00000000000..ce09465758b
--- /dev/null
+++ b/tests/roots/test-ext-apidoc-trailing-underscore/package_/__init__.py
@@ -0,0 +1 @@
+"""A package with trailing underscores"""
diff --git a/tests/roots/test-ext-apidoc-trailing-underscore/package_/module_.py b/tests/roots/test-ext-apidoc-trailing-underscore/package_/module_.py
new file mode 100644
index 00000000000..25d311ca4cf
--- /dev/null
+++ b/tests/roots/test-ext-apidoc-trailing-underscore/package_/module_.py
@@ -0,0 +1,9 @@
+"""A module with a trailing underscore"""
+
+
+class SomeClass_:
+ """A class with a trailing underscore"""
+
+
+def some_function_(some_arg_):
+ """A function with a trailing underscore in name and argument"""
diff --git a/tests/roots/test-ext-autodoc/autodoc_dummy_bar.py b/tests/roots/test-ext-autodoc/autodoc_dummy_bar.py
index 3b5bbfdd1cb..c66a33741d9 100644
--- a/tests/roots/test-ext-autodoc/autodoc_dummy_bar.py
+++ b/tests/roots/test-ext-autodoc/autodoc_dummy_bar.py
@@ -3,4 +3,5 @@
class Bar:
"""Dummy class Bar with alias."""
+
my_name = Foo
diff --git a/tests/roots/test-ext-autodoc/autodoc_dummy_module.py b/tests/roots/test-ext-autodoc/autodoc_dummy_module.py
index c05d96e0d6d..21380310662 100644
--- a/tests/roots/test-ext-autodoc/autodoc_dummy_module.py
+++ b/tests/roots/test-ext-autodoc/autodoc_dummy_module.py
@@ -1,6 +1,6 @@
-from dummy import *
+from dummy import * # NoQA: F403
def test():
"""Dummy function using dummy.*"""
- dummy_function()
+ dummy_function() # NoQA: F405
diff --git a/tests/roots/test-ext-autodoc/bug2437/autodoc_dummy_foo.py b/tests/roots/test-ext-autodoc/bug2437/autodoc_dummy_foo.py
index 9c954d80a52..227c28f3d25 100644
--- a/tests/roots/test-ext-autodoc/bug2437/autodoc_dummy_foo.py
+++ b/tests/roots/test-ext-autodoc/bug2437/autodoc_dummy_foo.py
@@ -1,3 +1,4 @@
class Foo:
"""Dummy class Foo."""
+
pass
diff --git a/tests/roots/test-ext-autodoc/conf.py b/tests/roots/test-ext-autodoc/conf.py
index abaea1c996b..f134359a32a 100644
--- a/tests/roots/test-ext-autodoc/conf.py
+++ b/tests/roots/test-ext-autodoc/conf.py
@@ -6,7 +6,7 @@
extensions = ['sphinx.ext.autodoc']
autodoc_mock_imports = [
- 'dummy'
+ 'dummy',
]
nitpicky = True
diff --git a/tests/roots/test-ext-autodoc/target/TYPE_CHECKING.py b/tests/roots/test-ext-autodoc/target/TYPE_CHECKING.py
index 85aea3a090e..6aa83e0a5a6 100644
--- a/tests/roots/test-ext-autodoc/target/TYPE_CHECKING.py
+++ b/tests/roots/test-ext-autodoc/target/TYPE_CHECKING.py
@@ -1,6 +1,7 @@
+# NoQA: N999
from __future__ import annotations
-from gettext import NullTranslations
+from gettext import NullTranslations # NoQA: TC003
from typing import TYPE_CHECKING
if TYPE_CHECKING:
diff --git a/tests/roots/test-ext-autodoc/target/__init__.py b/tests/roots/test-ext-autodoc/target/__init__.py
index d7ee4ac0f37..2b0b822e898 100644
--- a/tests/roots/test-ext-autodoc/target/__init__.py
+++ b/tests/roots/test-ext-autodoc/target/__init__.py
@@ -22,11 +22,13 @@ def f(self):
def _funky_classmethod(name, b, c, d, docstring=None):
- """Generates a classmethod for a class from a template by filling out
- some arguments."""
+ """Generates a classmethod for a class from a template by filling out some arguments."""
+
def template(cls, a, b, c, d=4, e=5, f=6):
return a, b, c, d, e, f
+
from functools import partial
+
function = partial(template, b=b, c=c, d=d)
function.__name__ = name
function.__doc__ = docstring
@@ -64,10 +66,19 @@ def excludemeth(self):
mdocattr = StringIO()
"""should be documented as well - süß"""
- roger = _funky_classmethod("roger", 2, 3, 4)
+ roger = _funky_classmethod('roger', 2, 3, 4)
+
+ moore = _funky_classmethod(
+ 'moore', 9, 8, 7, docstring='moore(a, e, f) -> happiness'
+ )
+
+ @staticmethod
+ def b_staticmeth():
+ pass
- moore = _funky_classmethod("moore", 9, 8, 7,
- docstring="moore(a, e, f) -> happiness")
+ @staticmethod
+ def a_staticmeth():
+ pass
def __init__(self, arg):
self.inst_attr_inline = None #: an inline documented instance attr
@@ -77,22 +88,20 @@ def __init__(self, arg):
"""a documented instance attribute"""
self._private_inst_attr = None #: a private instance attribute
- def __special1__(self):
+ def __special1__(self): # NoQA: PLW3201
"""documented special method"""
- def __special2__(self):
+ def __special2__(self): # NoQA: PLW3201
# undocumented special method
pass
-class CustomDict(dict):
+class CustomDict(dict): # NoQA: FURB189
"""Docstring."""
def function(foo, *args, **kwds):
- """
- Return spam.
- """
+ """Return spam."""
pass
@@ -116,21 +125,21 @@ class InnerChild(Outer.Inner):
class DocstringSig:
def __new__(cls, *new_args, **new_kwargs):
"""__new__(cls, d, e=1) -> DocstringSig
-First line of docstring
+ First line of docstring
rest of docstring
"""
def __init__(self, *init_args, **init_kwargs):
"""__init__(self, a, b=1) -> None
-First line of docstring
+ First line of docstring
rest of docstring
"""
def meth(self):
"""meth(FOO, BAR=1) -> BAZ
-First line of docstring
+ First line of docstring
rest of docstring
"""
@@ -157,7 +166,7 @@ def prop2(self):
return 456
-class StrRepr(str):
+class StrRepr(str): # NoQA: FURB189,SLOT000
"""docstring"""
def __repr__(self):
@@ -176,7 +185,7 @@ class InstAttCls:
#: It can have multiple lines.
ca1 = 'a'
- ca2 = 'b' #: Doc comment for InstAttCls.ca2. One line only.
+ ca2 = 'b' #: Doc comment for InstAttCls.ca2. One line only.
ca3 = 'c'
"""Docstring for class attribute InstAttCls.ca3."""
@@ -197,8 +206,8 @@ def __init__(self):
def __iter__(self):
"""Iterate squares of each value."""
for i in self.values:
- yield i ** 2
+ yield i**2
def snafucate(self):
"""Makes this snafucated."""
- print("snafucated")
+ print('snafucated')
diff --git a/tests/roots/test-ext-autodoc/target/_functions_to_import.py b/tests/roots/test-ext-autodoc/target/_functions_to_import.py
index 7663e979842..5e96c9f9ff6 100644
--- a/tests/roots/test-ext-autodoc/target/_functions_to_import.py
+++ b/tests/roots/test-ext-autodoc/target/_functions_to_import.py
@@ -4,5 +4,5 @@
from sphinx.application import Sphinx
-def function_to_be_imported(app: Optional["Sphinx"]) -> str:
+def function_to_be_imported(app: Optional['Sphinx']) -> str:
"""docstring"""
diff --git a/tests/roots/test-ext-autodoc/target/annotated.py b/tests/roots/test-ext-autodoc/target/annotated.py
index 7adc3e0f152..d9cdb83b9e9 100644
--- a/tests/roots/test-ext-autodoc/target/annotated.py
+++ b/tests/roots/test-ext-autodoc/target/annotated.py
@@ -24,7 +24,7 @@ def validate(value: str) -> str:
ValidatedString = Annotated[str, FuncValidator(validate)]
-def hello(name: Annotated[str, "attribute"]) -> None:
+def hello(name: Annotated[str, 'attribute']) -> None:
"""docstring"""
pass
@@ -33,7 +33,7 @@ class AnnotatedAttributes:
"""docstring"""
#: Docstring about the ``name`` attribute.
- name: Annotated[str, "attribute"]
+ name: Annotated[str, 'attribute']
#: Docstring about the ``max_len`` attribute.
max_len: list[Annotated[str, MaxLen(10, ['word_one', 'word_two'])]]
diff --git a/tests/roots/test-ext-autodoc/target/autoclass_content.py b/tests/roots/test-ext-autodoc/target/autoclass_content.py
index 52b98064a14..d5900ccf237 100644
--- a/tests/roots/test-ext-autodoc/target/autoclass_content.py
+++ b/tests/roots/test-ext-autodoc/target/autoclass_content.py
@@ -4,30 +4,35 @@ class A:
class B:
"""A class having __init__(no docstring), no __new__"""
+
def __init__(self):
pass
class C:
"""A class having __init__, no __new__"""
+
def __init__(self):
"""__init__ docstring"""
class D:
"""A class having no __init__, __new__(no docstring)"""
+
def __new__(cls):
pass
class E:
"""A class having no __init__, __new__"""
+
def __new__(cls):
"""__new__ docstring"""
class F:
"""A class having both __init__ and __new__"""
+
def __init__(self):
"""__init__ docstring"""
@@ -37,11 +42,13 @@ def __new__(cls):
class G(C):
"""A class inherits __init__ without docstring."""
+
def __init__(self):
pass
class H(E):
"""A class inherits __new__ without docstring."""
+
def __init__(self):
pass
diff --git a/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py b/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py
index f2c07a0c7cc..846c48a432c 100644
--- a/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py
+++ b/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py
@@ -1,7 +1,10 @@
from __future__ import annotations
-import io
-from typing import Optional, overload
+import io # NoQA: TC003
+from typing import TYPE_CHECKING, overload
+
+if TYPE_CHECKING:
+ from typing import Optional
myint = int
@@ -12,7 +15,7 @@
variable2 = None # type: myint
#: docstring
-variable3: Optional[myint]
+variable3: Optional[myint] # NoQA: UP045
def read(r: io.BytesIO) -> io.StringIO:
@@ -25,13 +28,11 @@ def sum(x: myint, y: myint) -> myint:
@overload
-def mult(x: myint, y: myint) -> myint:
- ...
+def mult(x: myint, y: myint) -> myint: ...
@overload
-def mult(x: float, y: float) -> float:
- ...
+def mult(x: float, y: float) -> float: ...
def mult(x, y):
diff --git a/tests/roots/test-ext-autodoc/target/classes.py b/tests/roots/test-ext-autodoc/target/classes.py
index e5cce7a69de..fd36ded9525 100644
--- a/tests/roots/test-ext-autodoc/target/classes.py
+++ b/tests/roots/test-ext-autodoc/target/classes.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from inspect import Parameter, Signature
-from typing import List, Union
+from typing import List, Union # NoQA: UP035
class Foo:
@@ -19,15 +19,20 @@ def __new__(cls, x, y):
class Qux:
- __signature__ = Signature(parameters=[Parameter('foo', Parameter.POSITIONAL_OR_KEYWORD),
- Parameter('bar', Parameter.POSITIONAL_OR_KEYWORD)])
+ __signature__ = Signature(
+ parameters=[
+ Parameter('foo', Parameter.POSITIONAL_OR_KEYWORD),
+ Parameter('bar', Parameter.POSITIONAL_OR_KEYWORD),
+ ]
+ )
def __init__(self, x, y):
pass
-class Quux(List[Union[int, float]]):
+class Quux(List[Union[int, float]]): # NoQA: UP006,UP007
"""A subclass of List[Union[int, float]]"""
+
pass
diff --git a/tests/roots/test-ext-autodoc/target/coroutine.py b/tests/roots/test-ext-autodoc/target/coroutine.py
index f977b6e77e3..d1355ed4a8e 100644
--- a/tests/roots/test-ext-autodoc/target/coroutine.py
+++ b/tests/roots/test-ext-autodoc/target/coroutine.py
@@ -22,8 +22,8 @@ async def do_asyncgen(self):
yield
-async def _other_coro_func():
- return "run"
+async def _other_coro_func(): # NoQA: RUF029
+ return 'run'
def myawait(f):
@@ -31,6 +31,7 @@ def myawait(f):
def wrapper(*args, **kwargs):
awaitable = f(*args, **kwargs)
return asyncio.run(awaitable)
+
return wrapper
diff --git a/tests/roots/test-ext-autodoc/target/decorator.py b/tests/roots/test-ext-autodoc/target/decorator.py
index faad3fff954..b614d9f002f 100644
--- a/tests/roots/test-ext-autodoc/target/decorator.py
+++ b/tests/roots/test-ext-autodoc/target/decorator.py
@@ -3,6 +3,7 @@
def deco1(func):
"""docstring for deco1"""
+
@wraps(func)
def wrapper():
return func()
@@ -12,11 +13,13 @@ def wrapper():
def deco2(condition, message):
"""docstring for deco2"""
+
def decorator(func):
def wrapper():
return func()
return wrapper
+
return decorator
@@ -39,13 +42,13 @@ def __init__(self, name=None, age=None):
class Qux:
@deco1
- def __new__(self, name=None, age=None):
+ def __new__(cls, name=None, age=None):
pass
class _Metaclass(type):
@deco1
- def __call__(self, name=None, age=None):
+ def __call__(cls, name=None, age=None):
pass
diff --git a/tests/roots/test-ext-autodoc/target/descriptor.py b/tests/roots/test-ext-autodoc/target/descriptor.py
index 2857c99f9d4..2c7d7389b4e 100644
--- a/tests/roots/test-ext-autodoc/target/descriptor.py
+++ b/tests/roots/test-ext-autodoc/target/descriptor.py
@@ -11,7 +11,7 @@ def __get__(self, obj, type=None):
def meth(self):
"""Function."""
- return "The Answer"
+ return 'The Answer'
class CustomDataDescriptorMeta(type):
@@ -20,11 +20,12 @@ class CustomDataDescriptorMeta(type):
class CustomDataDescriptor2(CustomDataDescriptor):
"""Descriptor class with custom metaclass docstring."""
+
__metaclass__ = CustomDataDescriptorMeta
class Class:
- descr = CustomDataDescriptor("Descriptor instance docstring.")
+ descr = CustomDataDescriptor('Descriptor instance docstring.')
@property
def prop(self):
diff --git a/tests/roots/test-ext-autodoc/target/docstring_signature.py b/tests/roots/test-ext-autodoc/target/docstring_signature.py
index 981d936cd13..a6c5aa504c5 100644
--- a/tests/roots/test-ext-autodoc/target/docstring_signature.py
+++ b/tests/roots/test-ext-autodoc/target/docstring_signature.py
@@ -4,12 +4,14 @@ class A:
class B:
"""B(foo, bar)"""
+
def __init__(self):
"""B(foo, bar, baz)"""
class C:
"""C(foo, bar)"""
+
def __new__(cls):
"""C(foo, bar, baz)"""
@@ -21,13 +23,13 @@ def __init__(self):
class E:
def __init__(self):
- """E(foo: int, bar: int, baz: int) -> None \\
- E(foo: str, bar: str, baz: str) -> None \\
- E(foo: float, bar: float, baz: float)"""
+ r"""E(foo: int, bar: int, baz: int) -> None \
+ E(foo: str, bar: str, baz: str) -> None \
+ E(foo: float, bar: float, baz: float)""" # NoQA: D209
class F:
def __init__(self):
"""F(foo: int, bar: int, baz: int) -> None
F(foo: str, bar: str, baz: str) -> None
- F(foo: float, bar: float, baz: float)"""
+ F(foo: float, bar: float, baz: float)""" # NoQA: D209
diff --git a/tests/roots/test-ext-autodoc/target/empty_all.py b/tests/roots/test-ext-autodoc/target/empty_all.py
index c094cff70fe..94d0dd7e354 100644
--- a/tests/roots/test-ext-autodoc/target/empty_all.py
+++ b/tests/roots/test-ext-autodoc/target/empty_all.py
@@ -1,6 +1,5 @@
-"""
-docsting of empty_all module.
-"""
+"""docsting of empty_all module."""
+
__all__ = []
diff --git a/tests/roots/test-ext-autodoc/target/functions.py b/tests/roots/test-ext-autodoc/target/functions.py
index 0265fb34612..54c8803a745 100644
--- a/tests/roots/test-ext-autodoc/target/functions.py
+++ b/tests/roots/test-ext-autodoc/target/functions.py
@@ -9,14 +9,16 @@ async def coroutinefunc():
pass
-async def asyncgenerator():
+async def asyncgenerator(): # NoQA: RUF029
yield
+
partial_func = partial(func)
partial_coroutinefunc = partial(coroutinefunc)
builtin_func = print
partial_builtin_func = partial(print)
-def slice_arg_func(arg: 'float64[:, :]'):
+
+def slice_arg_func(arg: 'float64[:, :]'): # NoQA: F821
pass
diff --git a/tests/roots/test-ext-autodoc/target/generic_class.py b/tests/roots/test-ext-autodoc/target/generic_class.py
index 1ec80584db3..957681ae485 100644
--- a/tests/roots/test-ext-autodoc/target/generic_class.py
+++ b/tests/roots/test-ext-autodoc/target/generic_class.py
@@ -9,5 +9,6 @@
# __init__ signature.
class A(Generic[T]):
"""docstring for A"""
+
def __init__(self, a, b=None):
pass
diff --git a/tests/roots/test-ext-autodoc/target/genericalias.py b/tests/roots/test-ext-autodoc/target/genericalias.py
index 06026fbbc12..fee22881b26 100644
--- a/tests/roots/test-ext-autodoc/target/genericalias.py
+++ b/tests/roots/test-ext-autodoc/target/genericalias.py
@@ -12,5 +12,6 @@ class Class:
#: A list of int
T = List[int]
+
#: A list of Class
L = List[Class]
diff --git a/tests/roots/test-ext-autodoc/target/inheritance.py b/tests/roots/test-ext-autodoc/target/inheritance.py
index e06f7a842b2..5c65aa65afd 100644
--- a/tests/roots/test-ext-autodoc/target/inheritance.py
+++ b/tests/roots/test-ext-autodoc/target/inheritance.py
@@ -10,7 +10,7 @@ def inheritedclassmeth(cls):
"""Inherited class method."""
@staticmethod
- def inheritedstaticmeth(cls):
+ def inheritedstaticmeth(cls): # NoQA: PLW0211
"""Inherited static method."""
@@ -20,6 +20,6 @@ def inheritedmeth(self):
pass
-class MyList(list):
+class MyList(list): # NoQA: FURB189
def meth(self):
"""docstring"""
diff --git a/tests/roots/test-ext-autodoc/target/inherited_annotations.py b/tests/roots/test-ext-autodoc/target/inherited_annotations.py
index 3ae58a852e4..abd16f5c89d 100644
--- a/tests/roots/test-ext-autodoc/target/inherited_annotations.py
+++ b/tests/roots/test-ext-autodoc/target/inherited_annotations.py
@@ -1,17 +1,18 @@
+"""Test case for #11387 corner case involving inherited
+members with type annotations on python 3.9 and earlier
"""
- Test case for #11387 corner case involving inherited
- members with type annotations on python 3.9 and earlier
-"""
+
class HasTypeAnnotatedMember:
inherit_me: int
"""Inherited"""
+
class NoTypeAnnotation(HasTypeAnnotatedMember):
a = 1
"""Local"""
+
class NoTypeAnnotation2(HasTypeAnnotatedMember):
a = 1
"""Local"""
-
diff --git a/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py b/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py
index 0a6f4965305..6ed930c9335 100644
--- a/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py
+++ b/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py
@@ -3,4 +3,5 @@
class foo:
"""docstring of target.name_conflict::foo."""
+
pass
diff --git a/tests/roots/test-ext-autodoc/target/need_mocks.py b/tests/roots/test-ext-autodoc/target/need_mocks.py
index 881220bd09e..1b8af7055d6 100644
--- a/tests/roots/test-ext-autodoc/target/need_mocks.py
+++ b/tests/roots/test-ext-autodoc/target/need_mocks.py
@@ -2,16 +2,16 @@
import missing_package1.missing_module1
from missing_module import missing_name
from missing_package2 import missing_module2
-from missing_package3.missing_module3 import missing_name
+from missing_package3.missing_module3 import missing_name # NoQA: F811
import sphinx.missing_module4
from sphinx.missing_module4 import missing_name2
@missing_name(int)
-def decoratedFunction():
- """decoratedFunction docstring"""
- return None
+def decorated_function():
+ """decorated_function docstring"""
+ return None # NoQA: RET501
def func(arg: missing_module.Class):
@@ -26,13 +26,14 @@ class TestAutodoc:
Alias = missing_module2.Class
@missing_name
- def decoratedMethod(self):
- """TestAutodoc::decoratedMethod docstring"""
- return None
+ def decorated_method(self):
+ """TestAutodoc::decorated_method docstring"""
+ return None # NoQA: RET501
class Inherited(missing_module.Class):
"""docstring"""
+
pass
diff --git a/tests/roots/test-ext-autodoc/target/overload.py b/tests/roots/test-ext-autodoc/target/overload.py
index 4bcb6ea3cad..d9d63170f8d 100644
--- a/tests/roots/test-ext-autodoc/target/overload.py
+++ b/tests/roots/test-ext-autodoc/target/overload.py
@@ -1,21 +1,21 @@
from __future__ import annotations
-from typing import Any, overload
+from typing import TYPE_CHECKING, overload
+
+if TYPE_CHECKING:
+ from typing import Any
@overload
-def sum(x: int, y: int = 0) -> int:
- ...
+def sum(x: int, y: int = 0) -> int: ...
@overload
-def sum(x: float, y: float = 0.0) -> float:
- ...
+def sum(x: float, y: float = 0.0) -> float: ...
@overload
-def sum(x: str, y: str = ...) -> str:
- ...
+def sum(x: str, y: str = ...) -> str: ...
def sum(x, y=None):
@@ -27,16 +27,13 @@ class Math:
"""docstring"""
@overload
- def sum(self, x: int, y: int = 0) -> int:
- ...
+ def sum(self, x: int, y: int = 0) -> int: ...
@overload
- def sum(self, x: float, y: float = 0.0) -> float:
- ...
+ def sum(self, x: float, y: float = 0.0) -> float: ...
@overload
- def sum(self, x: str, y: str = ...) -> str:
- ...
+ def sum(self, x: str, y: str = ...) -> str: ...
def sum(self, x, y=None):
"""docstring"""
@@ -47,12 +44,10 @@ class Foo:
"""docstring"""
@overload
- def __new__(cls, x: int, y: int) -> Foo:
- ...
+ def __new__(cls, x: int, y: int) -> Foo: ...
@overload
- def __new__(cls, x: str, y: str) -> Foo:
- ...
+ def __new__(cls, x: str, y: str) -> Foo: ...
def __new__(cls, x, y):
pass
@@ -62,25 +57,21 @@ class Bar:
"""docstring"""
@overload
- def __init__(cls, x: int, y: int) -> None:
- ...
+ def __init__(self, x: int, y: int) -> None: ...
@overload
- def __init__(cls, x: str, y: str) -> None:
- ...
+ def __init__(self, x: str, y: str) -> None: ...
- def __init__(cls, x, y):
+ def __init__(self, x, y):
pass
class Meta(type):
@overload
- def __call__(cls, x: int, y: int) -> Any:
- ...
+ def __call__(cls, x: int, y: int) -> Any: ...
@overload
- def __call__(cls, x: str, y: str) -> Any:
- ...
+ def __call__(cls, x: str, y: str) -> Any: ...
def __call__(cls, x, y):
pass
diff --git a/tests/roots/test-ext-autodoc/target/partialfunction.py b/tests/roots/test-ext-autodoc/target/partialfunction.py
index 3be63eeee6e..30ba045ad26 100644
--- a/tests/roots/test-ext-autodoc/target/partialfunction.py
+++ b/tests/roots/test-ext-autodoc/target/partialfunction.py
@@ -8,5 +8,5 @@ def func1(a, b, c):
func2 = partial(func1, 1)
func3 = partial(func2, 2)
-func3.__doc__ = "docstring of func3"
+func3.__doc__ = 'docstring of func3'
func4 = partial(func3, 3)
diff --git a/tests/roots/test-ext-autodoc/target/preserve_defaults.py b/tests/roots/test-ext-autodoc/target/preserve_defaults.py
index 86e103840d2..42a148a87b0 100644
--- a/tests/roots/test-ext-autodoc/target/preserve_defaults.py
+++ b/tests/roots/test-ext-autodoc/target/preserve_defaults.py
@@ -1,38 +1,54 @@
from __future__ import annotations
from datetime import datetime
-from typing import Any
+from typing import Any # NoQA: TC003
CONSTANT = 'foo'
SENTINEL = object()
-def foo(name: str = CONSTANT,
- sentinel: Any = SENTINEL,
- now: datetime = datetime.now(),
- color: int = 0xFFFFFF,
- *,
- kwarg1,
- kwarg2 = 0xFFFFFF) -> None:
+def foo(
+ name: str = CONSTANT,
+ sentinel: Any = SENTINEL,
+ now: datetime = datetime.now(), # NoQA: B008,DTZ005
+ color: int = 0xFFFFFF,
+ *,
+ kwarg1,
+ kwarg2=0xFFFFFF,
+) -> None:
"""docstring"""
class Class:
"""docstring"""
- def meth(self, name: str = CONSTANT, sentinel: Any = SENTINEL,
- now: datetime = datetime.now(), color: int = 0xFFFFFF,
- *, kwarg1, kwarg2 = 0xFFFFFF) -> None:
+ def meth(
+ self,
+ name: str = CONSTANT,
+ sentinel: Any = SENTINEL,
+ now: datetime = datetime.now(), # NoQA: B008,DTZ005
+ color: int = 0xFFFFFF,
+ *,
+ kwarg1,
+ kwarg2=0xFFFFFF,
+ ) -> None:
"""docstring"""
@classmethod
- def clsmeth(cls, name: str = CONSTANT, sentinel: Any = SENTINEL,
- now: datetime = datetime.now(), color: int = 0xFFFFFF,
- *, kwarg1, kwarg2 = 0xFFFFFF) -> None:
+ def clsmeth(
+ cls,
+ name: str = CONSTANT,
+ sentinel: Any = SENTINEL,
+ now: datetime = datetime.now(), # NoQA: B008,DTZ005
+ color: int = 0xFFFFFF,
+ *,
+ kwarg1,
+ kwarg2=0xFFFFFF,
+ ) -> None:
"""docstring"""
-get_sentinel = lambda custom=SENTINEL: custom
+get_sentinel = lambda custom=SENTINEL: custom # NoQA: E731
"""docstring"""
@@ -44,17 +60,19 @@ class MultiLine:
# only prop3 will not fail because it's on a single line whereas the others
# will fail to parse.
+ # fmt: off
prop1 = property(
- lambda self: 1, doc="docstring")
+ lambda self: 1, doc='docstring')
prop2 = property(
- lambda self: 2, doc="docstring"
+ lambda self: 2, doc='docstring'
)
- prop3 = property(lambda self: 3, doc="docstring")
+ prop3 = property(lambda self: 3, doc='docstring')
prop4 = (property
- (lambda self: 4, doc="docstring"))
+ (lambda self: 4, doc='docstring'))
prop5 = property\
- (lambda self: 5, doc="docstring")
+ (lambda self: 5, doc='docstring') # NoQA: E211
+ # fmt: on
diff --git a/tests/roots/test-ext-autodoc/target/preserve_defaults_special_constructs.py b/tests/roots/test-ext-autodoc/target/preserve_defaults_special_constructs.py
index 0fdb11ac874..ba397f86711 100644
--- a/tests/roots/test-ext-autodoc/target/preserve_defaults_special_constructs.py
+++ b/tests/roots/test-ext-autodoc/target/preserve_defaults_special_constructs.py
@@ -9,7 +9,7 @@
#: docstring
-ze_lambda = lambda z=SENTINEL: None
+ze_lambda = lambda z=SENTINEL: None # NoQA: E731
def foo(x, y, z=SENTINEL):
@@ -19,6 +19,7 @@ def foo(x, y, z=SENTINEL):
@dataclass
class DataClass:
"""docstring"""
+
a: int
b: object = SENTINEL
c: list[int] = field(default_factory=lambda: [1, 2, 3])
@@ -27,6 +28,7 @@ class DataClass:
@dataclass(init=False)
class DataClassNoInit:
"""docstring"""
+
a: int
b: object = SENTINEL
c: list[int] = field(default_factory=lambda: [1, 2, 3])
@@ -34,6 +36,7 @@ class DataClassNoInit:
class MyTypedDict(TypedDict):
"""docstring"""
+
a: int
b: object
c: list[int]
@@ -41,10 +44,11 @@ class MyTypedDict(TypedDict):
class MyNamedTuple1(NamedTuple):
"""docstring"""
+
a: int
b: object = object()
c: list[int] = [1, 2, 3]
-class MyNamedTuple2(namedtuple('Base', ('a', 'b'), defaults=(0, SENTINEL))):
+class MyNamedTuple2(namedtuple('Base', ('a', 'b'), defaults=(0, SENTINEL))): # NoQA: PYI024,SLOT002
"""docstring"""
diff --git a/tests/roots/test-ext-autodoc/target/private.py b/tests/roots/test-ext-autodoc/target/private.py
index e46344818a7..de8a43c0b47 100644
--- a/tests/roots/test-ext-autodoc/target/private.py
+++ b/tests/roots/test-ext-autodoc/target/private.py
@@ -4,6 +4,7 @@ def private_function(name):
:meta private:
"""
+
def _public_function(name):
"""public_function is a docstring().
diff --git a/tests/roots/test-ext-autodoc/target/process_docstring.py b/tests/roots/test-ext-autodoc/target/process_docstring.py
index 6005943b669..492e5169290 100644
--- a/tests/roots/test-ext-autodoc/target/process_docstring.py
+++ b/tests/roots/test-ext-autodoc/target/process_docstring.py
@@ -1,6 +1,5 @@
def func():
- """
- first line
+ """first line
---
second line
---
diff --git a/tests/roots/test-ext-autodoc/target/properties.py b/tests/roots/test-ext-autodoc/target/properties.py
index 018f51ee45e..84d1c2a1a19 100644
--- a/tests/roots/test-ext-autodoc/target/properties.py
+++ b/tests/roots/test-ext-autodoc/target/properties.py
@@ -7,7 +7,7 @@ def prop1(self) -> int:
@classmethod
@property
- def prop2(self) -> int:
+ def prop2(cls) -> int:
"""docstring"""
@property
@@ -17,6 +17,6 @@ def prop1_with_type_comment(self):
@classmethod
@property
- def prop2_with_type_comment(self):
+ def prop2_with_type_comment(cls):
# type: () -> int
"""docstring"""
diff --git a/tests/roots/test-ext-autodoc/target/singledispatch.py b/tests/roots/test-ext-autodoc/target/singledispatch.py
index 3dd5aaf388a..718504e5273 100644
--- a/tests/roots/test-ext-autodoc/target/singledispatch.py
+++ b/tests/roots/test-ext-autodoc/target/singledispatch.py
@@ -33,4 +33,3 @@ def _func_dict(arg: dict, kwarg=None):
"""A function for dict."""
# This function tests for specifying type through annotations
pass
-
diff --git a/tests/roots/test-ext-autodoc/target/slots.py b/tests/roots/test-ext-autodoc/target/slots.py
index 75c7a4a5227..3fa3f0798c4 100644
--- a/tests/roots/test-ext-autodoc/target/slots.py
+++ b/tests/roots/test-ext-autodoc/target/slots.py
@@ -7,9 +7,11 @@ class Foo:
class Bar:
"""docstring"""
- __slots__ = {'attr1': 'docstring of attr1',
- 'attr2': 'docstring of attr2',
- 'attr3': None}
+ __slots__ = {
+ 'attr1': 'docstring of attr1',
+ 'attr2': 'docstring of attr2',
+ 'attr3': None,
+ }
__annotations__ = {'attr1': int}
def __init__(self):
@@ -19,4 +21,4 @@ def __init__(self):
class Baz:
"""docstring"""
- __slots__ = 'attr'
+ __slots__ = 'attr' # NoQA: PLC0205
diff --git a/tests/roots/test-ext-autodoc/target/typed_vars.py b/tests/roots/test-ext-autodoc/target/typed_vars.py
index 0fe7468c84f..a87bd6accd3 100644
--- a/tests/roots/test-ext-autodoc/target/typed_vars.py
+++ b/tests/roots/test-ext-autodoc/target/typed_vars.py
@@ -8,8 +8,9 @@
class _Descriptor:
def __init__(self, name):
- self.__doc__ = f"This is {name}"
- def __get__(self):
+ self.__doc__ = f'This is {name}'
+
+ def __get__(self): # NoQA: PLE0302
pass
@@ -18,12 +19,14 @@ class Class:
attr2: int
attr3 = 0 # type: int
- descr4: int = _Descriptor("descr4")
+ descr4: int = _Descriptor('descr4')
def __init__(self):
+ # fmt: off
self.attr4: int = 0 #: attr4
self.attr5: int #: attr5
self.attr6 = 0 # type: int
+ # fmt: on
"""attr6"""
diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py
index 90715945f14..b2448bf4b28 100644
--- a/tests/roots/test-ext-autodoc/target/typehints.py
+++ b/tests/roots/test-ext-autodoc/target/typehints.py
@@ -1,22 +1,25 @@
from __future__ import annotations
import pathlib
-from typing import Any, Tuple, TypeVar, Union
+from typing import TYPE_CHECKING, Tuple, TypeVar, Union # NoQA: UP035
+
+if TYPE_CHECKING:
+ from typing import Any
CONST1: int
#: docstring
CONST2: int = 1
#: docstring
-CONST3: pathlib.PurePosixPath = pathlib.PurePosixPath("/a/b/c")
+CONST3: pathlib.PurePosixPath = pathlib.PurePosixPath('/a/b/c')
#: docstring
-T = TypeVar("T", bound=pathlib.PurePosixPath)
+T = TypeVar('T', bound=pathlib.PurePosixPath)
def incr(a: int, b: int = 1) -> int:
return a + b
-def decr(a, b = 1):
+def decr(a, b=1):
# type: (int, int) -> int
return a - b
@@ -24,7 +27,7 @@ def decr(a, b = 1):
class Math:
CONST1: int
CONST2: int = 1
- CONST3: pathlib.PurePosixPath = pathlib.PurePosixPath("/a/b/c")
+ CONST3: pathlib.PurePosixPath = pathlib.PurePosixPath('/a/b/c')
def __init__(self, s: str, o: Any = None) -> None:
pass
@@ -32,7 +35,7 @@ def __init__(self, s: str, o: Any = None) -> None:
def incr(self, a: int, b: int = 1) -> int:
return a + b
- def decr(self, a, b = 1):
+ def decr(self, a, b=1):
# type: (int, int) -> int
return a - b
@@ -40,10 +43,11 @@ def nothing(self):
# type: () -> None
pass
- def horse(self,
- a, # type: str
- b, # type: int
- ):
+ def horse(
+ self,
+ a, # type: str
+ b, # type: int
+ ):
# type: (...) -> None
return
@@ -53,7 +57,7 @@ def prop(self) -> int:
@property
def path(self) -> pathlib.PurePosixPath:
- return pathlib.PurePosixPath("/a/b/c")
+ return pathlib.PurePosixPath('/a/b/c')
def tuple_args(x: tuple[int, int | str]) -> tuple[int, int]:
@@ -61,7 +65,7 @@ def tuple_args(x: tuple[int, int | str]) -> tuple[int, int]:
class NewAnnotation:
- def __new__(cls, i: int) -> NewAnnotation:
+ def __new__(cls, i: int) -> NewAnnotation: # NoQA: PYI034
pass
@@ -85,12 +89,13 @@ def complex_func(arg1, arg2, arg3=None, *args, **kwargs):
pass
-def missing_attr(c,
- a, # type: str
- b=None # type: Optional[str]
- ):
+def missing_attr(
+ c,
+ a, # type: str
+ b=None, # type: Optional[str]
+):
# type: (...) -> str
- return a + (b or "")
+ return a + (b or '')
class _ClassWithDocumentedInit:
diff --git a/tests/roots/test-ext-autodoc/target/typevar.py b/tests/roots/test-ext-autodoc/target/typevar.py
index 1a02f3e2e76..6cc088a8219 100644
--- a/tests/roots/test-ext-autodoc/target/typevar.py
+++ b/tests/roots/test-ext-autodoc/target/typevar.py
@@ -4,29 +4,29 @@
from typing import NewType, TypeVar
#: T1
-T1 = TypeVar("T1")
+T1 = TypeVar('T1')
-T2 = TypeVar("T2") # A TypeVar not having doc comment
+T2 = TypeVar('T2') # A TypeVar not having doc comment
#: T3
-T3 = TypeVar("T3", int, str)
+T3 = TypeVar('T3', int, str)
#: T4
-T4 = TypeVar("T4", covariant=True)
+T4 = TypeVar('T4', covariant=True) # NoQA: PLC0105
#: T5
-T5 = TypeVar("T5", contravariant=True)
+T5 = TypeVar('T5', contravariant=True) # NoQA: PLC0105
#: T6
-T6 = NewType("T6", date)
+T6 = NewType('T6', date)
#: T7
-T7 = TypeVar("T7", bound=int)
+T7 = TypeVar('T7', bound=int)
class Class:
#: T1
- T1 = TypeVar("T1")
+ T1 = TypeVar('T1')
#: T6
- T6 = NewType("T6", date)
+ T6 = NewType('T6', date)
diff --git a/tests/roots/test-ext-autodoc/target/wrappedfunction.py b/tests/roots/test-ext-autodoc/target/wrappedfunction.py
index 064d7774247..d868c5756c0 100644
--- a/tests/roots/test-ext-autodoc/target/wrappedfunction.py
+++ b/tests/roots/test-ext-autodoc/target/wrappedfunction.py
@@ -2,10 +2,10 @@
from contextlib import contextmanager
from functools import lru_cache
-from typing import Generator
+from typing import Generator # NoQA: TC003,UP035
-@lru_cache(maxsize=None)
+@lru_cache(maxsize=None) # NoQA: UP033
def slow_function(message, timeout):
"""This function is slow."""
print(message)
diff --git a/tests/roots/test-autosummary/conf.py b/tests/roots/test-ext-autosummary-ext/conf.py
similarity index 100%
rename from tests/roots/test-autosummary/conf.py
rename to tests/roots/test-ext-autosummary-ext/conf.py
diff --git a/tests/roots/test-autosummary/dummy_module.py b/tests/roots/test-ext-autosummary-ext/dummy_module.py
similarity index 72%
rename from tests/roots/test-autosummary/dummy_module.py
rename to tests/roots/test-ext-autosummary-ext/dummy_module.py
index 4adc0313ecd..a54b8362ac7 100644
--- a/tests/roots/test-autosummary/dummy_module.py
+++ b/tests/roots/test-ext-autosummary-ext/dummy_module.py
@@ -7,30 +7,30 @@
C.prop_attr1
C.prop_attr2
C.C2
-"""
+""" # NoQA: D212
-def withSentence():
- '''I have a sentence which
+def with_sentence():
+ """I have a sentence which
spans multiple lines. Then I have
more stuff
- '''
+ """
pass
-def noSentence():
- '''this doesn't start with a
+def no_sentence():
+ """this doesn't start with a
capital. so it's not considered
a sentence
- '''
+ """
pass
-def emptyLine():
- '''This is the real summary
+def empty_line():
+ """This is the real summary
However, it did't end with a period.
- '''
+ """
pass
@@ -41,11 +41,10 @@ def emptyLine():
class C:
- '''
- My C class
+ """My C class
with class_attr attribute
- '''
+ """
#: This is a class attribute
#:
@@ -56,11 +55,10 @@ def __init__(self):
#: This is an instance attribute
#:
#: value is a string
- self.instance_attr = "42"
+ self.instance_attr = '42'
def _prop_attr_get(self):
- """
- This is a function docstring
+ """This is a function docstring
return value is string.
"""
@@ -76,9 +74,7 @@ def _prop_attr_get(self):
"""
class C2:
- '''
- This is a nested inner class docstring
- '''
+ """This is a nested inner class docstring"""
def func(arg_, *args, **kwargs):
diff --git a/tests/roots/test-autosummary/index.rst b/tests/roots/test-ext-autosummary-ext/index.rst
similarity index 80%
rename from tests/roots/test-autosummary/index.rst
rename to tests/roots/test-ext-autosummary-ext/index.rst
index 5ddc4bd40fe..1a7bb2177c6 100644
--- a/tests/roots/test-autosummary/index.rst
+++ b/tests/roots/test-ext-autosummary-ext/index.rst
@@ -1,6 +1,6 @@
.. autosummary::
- :nosignatures:
+ :no-signatures:
:toctree:
dummy_module
diff --git a/tests/roots/test-autosummary/sphinx.rst b/tests/roots/test-ext-autosummary-ext/sphinx.rst
similarity index 100%
rename from tests/roots/test-autosummary/sphinx.rst
rename to tests/roots/test-ext-autosummary-ext/sphinx.rst
diff --git a/tests/roots/test-ext-autosummary-ext/underscore_module_.py b/tests/roots/test-ext-autosummary-ext/underscore_module_.py
new file mode 100644
index 00000000000..908015ac8d0
--- /dev/null
+++ b/tests/roots/test-ext-autosummary-ext/underscore_module_.py
@@ -0,0 +1,14 @@
+"""module with trailing underscores everywhere"""
+
+
+class class_:
+ """Class"""
+
+ def method_(_arg): # NoQA: N805
+ """Method"""
+ pass
+
+
+def function_(_arg):
+ """Function"""
+ pass
diff --git a/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py
index b88e33520b3..9ed0ce66877 100644
--- a/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py
+++ b/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py
@@ -5,7 +5,7 @@
class Foo:
- class Bar:
+ class Bar: # NoQA: D106
pass
def __init__(self):
diff --git a/tests/roots/test-ext-autosummary-filename-map/conf.py b/tests/roots/test-ext-autosummary-filename-map/conf.py
index ea64caec23d..7ef1c6fd7fb 100644
--- a/tests/roots/test-ext-autosummary-filename-map/conf.py
+++ b/tests/roots/test-ext-autosummary-filename-map/conf.py
@@ -6,6 +6,6 @@
extensions = ['sphinx.ext.autosummary']
autosummary_generate = True
autosummary_filename_map = {
- "autosummary_dummy_module": "module_mangled",
- "autosummary_dummy_module.bar": "bar"
+ 'autosummary_dummy_module': 'module_mangled',
+ 'autosummary_dummy_module.bar': 'bar',
}
diff --git a/tests/roots/test-ext-autosummary-import_cycle/spam/eggs.py b/tests/roots/test-ext-autosummary-import_cycle/spam/eggs.py
index 12122e88de8..9bef80f06a7 100644
--- a/tests/roots/test-ext-autosummary-import_cycle/spam/eggs.py
+++ b/tests/roots/test-ext-autosummary-import_cycle/spam/eggs.py
@@ -5,6 +5,7 @@
class Ham:
"""``spam.eggs.Ham`` class docstring."""
+
a = 1
b = 2
c = 3
diff --git a/tests/roots/test-ext-autosummary-imported_members/autosummary_dummy_package/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary-imported_members/autosummary_dummy_package/autosummary_dummy_module.py
index 9c93f064e03..837a617093a 100644
--- a/tests/roots/test-ext-autosummary-imported_members/autosummary_dummy_package/autosummary_dummy_module.py
+++ b/tests/roots/test-ext-autosummary-imported_members/autosummary_dummy_package/autosummary_dummy_module.py
@@ -1,5 +1,6 @@
class Bar:
"""Bar class"""
+
pass
diff --git a/tests/roots/test-ext-autosummary-mock_imports/foo.py b/tests/roots/test-ext-autosummary-mock_imports/foo.py
index ab4460ef0af..36b8b4de5c2 100644
--- a/tests/roots/test-ext-autosummary-mock_imports/foo.py
+++ b/tests/roots/test-ext-autosummary-mock_imports/foo.py
@@ -3,4 +3,5 @@
class Foo(unknown.Class):
"""Foo class"""
+
pass
diff --git a/tests/roots/test-ext-autosummary-module_all/autosummary_dummy_package_all/__init__.py b/tests/roots/test-ext-autosummary-module_all/autosummary_dummy_package_all/__init__.py
index 82f2060fb58..e1ba34e63cf 100644
--- a/tests/roots/test-ext-autosummary-module_all/autosummary_dummy_package_all/__init__.py
+++ b/tests/roots/test-ext-autosummary-module_all/autosummary_dummy_package_all/__init__.py
@@ -10,4 +10,4 @@ def public_baz():
"""Public Baz function"""
-__all__ = ["PublicBar", "public_foo", "public_baz", "extra_dummy_module"]
+__all__ = ['PublicBar', 'public_foo', 'public_baz', 'extra_dummy_module'] # NoQA: F822
diff --git a/tests/roots/test-ext-autosummary-module_empty_all/autosummary_dummy_package_empty_all/__init__.py b/tests/roots/test-ext-autosummary-module_empty_all/autosummary_dummy_package_empty_all/__init__.py
new file mode 100644
index 00000000000..ea9b7835485
--- /dev/null
+++ b/tests/roots/test-ext-autosummary-module_empty_all/autosummary_dummy_package_empty_all/__init__.py
@@ -0,0 +1 @@
+__all__ = ()
diff --git a/tests/roots/test-ext-autosummary-module_empty_all/conf.py b/tests/roots/test-ext-autosummary-module_empty_all/conf.py
new file mode 100644
index 00000000000..9062c5cebcb
--- /dev/null
+++ b/tests/roots/test-ext-autosummary-module_empty_all/conf.py
@@ -0,0 +1,11 @@
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path.cwd().resolve()))
+
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary']
+autodoc_default_options = {'members': True}
+autosummary_ignore_module_all = False
+autosummary_imorted_members = False
+
+templates_path = ['templates']
diff --git a/tests/roots/test-ext-autosummary-module_empty_all/index.rst b/tests/roots/test-ext-autosummary-module_empty_all/index.rst
new file mode 100644
index 00000000000..41afaf3783e
--- /dev/null
+++ b/tests/roots/test-ext-autosummary-module_empty_all/index.rst
@@ -0,0 +1,8 @@
+test-ext-autosummary-module_all
+===============================
+
+.. autosummary::
+ :toctree: generated
+ :recursive:
+
+ autosummary_dummy_package_empty_all
diff --git a/tests/roots/test-ext-autosummary-module_empty_all/templates/autosummary/module.rst b/tests/roots/test-ext-autosummary-module_empty_all/templates/autosummary/module.rst
new file mode 100644
index 00000000000..c7803af698d
--- /dev/null
+++ b/tests/roots/test-ext-autosummary-module_empty_all/templates/autosummary/module.rst
@@ -0,0 +1,13 @@
+{{ fullname | escape | underline}}
+
+.. automodule:: {{ fullname }}
+
+ {% block members %}
+ Summary
+ -------
+ .. autosummary::
+
+ {% for item in members %}
+ {{ item }}
+ {%- endfor %}
+ {% endblock %}
diff --git a/tests/roots/test-ext-autosummary-recursive/package/module.py b/tests/roots/test-ext-autosummary-recursive/package/module.py
index c76e7330246..30e6cb819e6 100644
--- a/tests/roots/test-ext-autosummary-recursive/package/module.py
+++ b/tests/roots/test-ext-autosummary-recursive/package/module.py
@@ -1,4 +1,4 @@
-from os import *
+from os import * # NoQA: F403
class Foo:
diff --git a/tests/roots/test-ext-autosummary-recursive/package/package/module.py b/tests/roots/test-ext-autosummary-recursive/package/package/module.py
index c76e7330246..30e6cb819e6 100644
--- a/tests/roots/test-ext-autosummary-recursive/package/package/module.py
+++ b/tests/roots/test-ext-autosummary-recursive/package/package/module.py
@@ -1,4 +1,4 @@
-from os import *
+from os import * # NoQA: F403
class Foo:
diff --git a/tests/roots/test-ext-autosummary-recursive/package2/module.py b/tests/roots/test-ext-autosummary-recursive/package2/module.py
index c76e7330246..30e6cb819e6 100644
--- a/tests/roots/test-ext-autosummary-recursive/package2/module.py
+++ b/tests/roots/test-ext-autosummary-recursive/package2/module.py
@@ -1,4 +1,4 @@
-from os import *
+from os import * # NoQA: F403
class Foo:
diff --git a/tests/roots/test-ext-autosummary-skip-member/conf.py b/tests/roots/test-ext-autosummary-skip-member/conf.py
index f409bdc5c17..ff6cd3ccd00 100644
--- a/tests/roots/test-ext-autosummary-skip-member/conf.py
+++ b/tests/roots/test-ext-autosummary-skip-member/conf.py
@@ -13,6 +13,7 @@ def skip_member(app, what, name, obj, skip, options):
return True
elif name == '_privatemeth':
return False
+ return None
def setup(app):
diff --git a/tests/roots/test-ext-autosummary/autosummary_class_module.py b/tests/roots/test-ext-autosummary/autosummary_class_module.py
index 2b1f40419d6..050e8a09605 100644
--- a/tests/roots/test-ext-autosummary/autosummary_class_module.py
+++ b/tests/roots/test-ext-autosummary/autosummary_class_module.py
@@ -1,2 +1,2 @@
class Class:
- pass
+ pass
diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py
index 2b3d2da84ca..3672c13b400 100644
--- a/tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py
+++ b/tests/roots/test-ext-autosummary/autosummary_dummy_inherited_module.py
@@ -2,12 +2,11 @@
class InheritedAttrClass(Foo):
-
def __init__(self):
#: other docstring
- self.subclassattr = "subclassattr"
+ self.subclassattr = 'subclassattr'
super().__init__()
-__all__ = ["InheritedAttrClass"]
+__all__ = ['InheritedAttrClass']
diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py
index 2d8829a2375..c8575d3a122 100644
--- a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py
+++ b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py
@@ -4,13 +4,13 @@
from autosummary_class_module import Class
__all__ = [
- "CONSTANT1",
- "Exc",
- "Foo",
- "_Baz",
- "bar",
- "qux",
- "path",
+ 'CONSTANT1',
+ 'Exc',
+ 'Foo',
+ '_Baz',
+ 'bar',
+ 'qux',
+ 'path',
]
#: module variable
@@ -23,7 +23,7 @@ class Foo:
CONSTANT3 = None
CONSTANT4 = None
- class Bar:
+ class Bar: # NoQA: D106
pass
def __init__(self):
@@ -42,7 +42,7 @@ class _Baz:
pass
-def bar(x: Union[int, str], y: int = 1) -> None:
+def bar(x: Union[int, str], y: int = 1) -> None: # NoQA: UP007
pass
diff --git a/tests/roots/test-ext-coverage/grog/coverage_missing.py b/tests/roots/test-ext-coverage/grog/coverage_missing.py
index 2fe44338caa..b737f0d3776 100644
--- a/tests/roots/test-ext-coverage/grog/coverage_missing.py
+++ b/tests/roots/test-ext-coverage/grog/coverage_missing.py
@@ -1,5 +1,6 @@
"""This module is intentionally not documented."""
+
class Missing:
"""An undocumented class."""
diff --git a/tests/roots/test-ext-doctest-skipif/conf.py b/tests/roots/test-ext-doctest-skipif/conf.py
index ae00e35407f..fc999b43494 100644
--- a/tests/roots/test-ext-doctest-skipif/conf.py
+++ b/tests/roots/test-ext-doctest-skipif/conf.py
@@ -3,16 +3,16 @@
project = 'test project for the doctest :skipif: directive'
root_doc = 'skipif'
source_suffix = {
- '.txt': 'restructuredtext'
+ '.txt': 'restructuredtext',
}
exclude_patterns = ['_build']
-doctest_global_setup = '''
+doctest_global_setup = """
from tests.test_extensions.test_ext_doctest import record
record('doctest_global_setup', 'body', True)
-'''
+"""
-doctest_global_cleanup = '''
+doctest_global_cleanup = """
record('doctest_global_cleanup', 'body', True)
-'''
+"""
diff --git a/tests/roots/test-ext-doctest-with-autodoc/dir/bar.py b/tests/roots/test-ext-doctest-with-autodoc/dir/bar.py
index 122fdf736a0..d13536e6953 100644
--- a/tests/roots/test-ext-doctest-with-autodoc/dir/bar.py
+++ b/tests/roots/test-ext-doctest-with-autodoc/dir/bar.py
@@ -1,4 +1 @@
-"""
->>> 'dir/bar.py:2'
-
-"""
+""">>> 'dir/bar.py:2'"""
diff --git a/tests/roots/test-ext-doctest-with-autodoc/foo.py b/tests/roots/test-ext-doctest-with-autodoc/foo.py
index 9f62a19b123..ac8ce857c0f 100644
--- a/tests/roots/test-ext-doctest-with-autodoc/foo.py
+++ b/tests/roots/test-ext-doctest-with-autodoc/foo.py
@@ -1,5 +1 @@
-"""
-
->>> 'foo.py:3'
-
-"""
+""">>> 'foo.py:3'"""
diff --git a/tests/roots/test-ext-doctest/conf.py b/tests/roots/test-ext-doctest/conf.py
index 57fc40607b6..ce73258e5b6 100644
--- a/tests/roots/test-ext-doctest/conf.py
+++ b/tests/roots/test-ext-doctest/conf.py
@@ -3,6 +3,6 @@
project = 'test project for doctest'
root_doc = 'doctest'
source_suffix = {
- '.txt': 'restructuredtext'
+ '.txt': 'restructuredtext',
}
exclude_patterns = ['_build']
diff --git a/tests/roots/test-ext-graphviz/conf.py b/tests/roots/test-ext-graphviz/conf.py
index 317457ff95b..1a12f2c2b54 100644
--- a/tests/roots/test-ext-graphviz/conf.py
+++ b/tests/roots/test-ext-graphviz/conf.py
@@ -1,3 +1,3 @@
extensions = ['sphinx.ext.graphviz']
exclude_patterns = ['_build']
-html_static_path = ["_static"]
+html_static_path = ['_static']
diff --git a/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py b/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py
index 234732ddc0c..97b08a9a3a7 100644
--- a/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py
+++ b/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py
@@ -1,6 +1,4 @@
-"""
- Does foo.svg --> foo.pdf with no change to the file.
-"""
+"""Does foo.svg --> foo.pdf with no change to the file."""
from __future__ import annotations
diff --git a/tests/roots/test-ext-inheritance_diagram/test.py b/tests/roots/test-ext-inheritance_diagram/test.py
index efb1c2a7f6e..0146c5d0f39 100644
--- a/tests/roots/test-ext-inheritance_diagram/test.py
+++ b/tests/roots/test-ext-inheritance_diagram/test.py
@@ -18,5 +18,5 @@ class DocMainLevel(Foo):
pass
-class Alice(object):
+class Alice(object): # NoQA: UP004
pass
diff --git a/tests/roots/test-ext-math-compat/conf.py b/tests/roots/test-ext-math-compat/conf.py
index 85e3950a5d0..82cc265bc9f 100644
--- a/tests/roots/test-ext-math-compat/conf.py
+++ b/tests/roots/test-ext-math-compat/conf.py
@@ -4,7 +4,7 @@
extensions = ['sphinx.ext.mathjax']
-def my_math_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
+def my_math_role(role, rawtext, text, lineno, inliner, options={}, content=[]): # NoQA: B006
text = 'E = mc^2'
return [nodes.math(text, text)], []
diff --git a/tests/roots/test-ext-math/math.rst b/tests/roots/test-ext-math/math.rst
index c05c3a05367..8a186d1b2cd 100644
--- a/tests/roots/test-ext-math/math.rst
+++ b/tests/roots/test-ext-math/math.rst
@@ -24,7 +24,7 @@ This is inline math: :math:`a^2 + b^2 = c^2`.
n \in \mathbb N
.. math::
- :nowrap:
+ :no-wrap:
a + 1 < b
diff --git a/tests/roots/test-ext-napoleon-paramtype/conf.py b/tests/roots/test-ext-napoleon-paramtype/conf.py
index 70d30ce0a65..1eb7bb0b5c1 100644
--- a/tests/roots/test-ext-napoleon-paramtype/conf.py
+++ b/tests/roots/test-ext-napoleon-paramtype/conf.py
@@ -6,7 +6,7 @@
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
- 'sphinx.ext.intersphinx'
+ 'sphinx.ext.intersphinx',
]
# Python inventory is manually created in the test
diff --git a/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py b/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py
index e1ae794c799..a1560f99141 100644
--- a/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py
+++ b/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py
@@ -1,5 +1,6 @@
class Bar:
"""The bar."""
+
def list(self) -> None:
"""A list method."""
@@ -7,4 +8,3 @@ def list(self) -> None:
def int() -> float:
"""An int method."""
return 1.0
-
diff --git a/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py b/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py
index 6979f9e4a19..a6116f6d619 100644
--- a/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py
+++ b/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py
@@ -1,5 +1,6 @@
class Foo:
"""The foo."""
+
def do(
self,
*,
diff --git a/tests/roots/test-ext-napoleon/mypackage/typehints.py b/tests/roots/test-ext-napoleon/mypackage/typehints.py
index 526b78e40bb..6a2838b1220 100644
--- a/tests/roots/test-ext-napoleon/mypackage/typehints.py
+++ b/tests/roots/test-ext-napoleon/mypackage/typehints.py
@@ -1,6 +1,5 @@
def hello(x: int, *args: int, **kwargs: int) -> None:
- """
- Parameters
+ """Parameters
----------
x
X
diff --git a/tests/roots/test-ext-viewcode-find-package/conf.py b/tests/roots/test-ext-viewcode-find-package/conf.py
new file mode 100644
index 00000000000..cad4c5597de
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find-package/conf.py
@@ -0,0 +1,24 @@
+import os
+import sys
+
+source_dir = os.path.abspath('.')
+if source_dir not in sys.path:
+ sys.path.insert(0, source_dir)
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
+exclude_patterns = ['_build']
+
+
+if 'test_linkcode' in tags: # NoQA: F821 (tags is injected into conf.py)
+ extensions.remove('sphinx.ext.viewcode')
+ extensions.append('sphinx.ext.linkcode')
+
+ def linkcode_resolve(domain, info):
+ if domain == 'py':
+ fn = info['module'].replace('.', '/')
+ return 'http://foobar/source/%s.py' % fn
+ elif domain == 'js':
+ return 'http://foobar/js/' + info['fullname']
+ elif domain in {'c', 'cpp'}:
+ return 'http://foobar/%s/%s' % (domain, ''.join(info['names']))
+ else:
+ raise AssertionError
diff --git a/tests/roots/test-ext-viewcode-find-package/index.rst b/tests/roots/test-ext-viewcode-find-package/index.rst
new file mode 100644
index 00000000000..b40d1cd06c5
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find-package/index.rst
@@ -0,0 +1,10 @@
+viewcode
+========
+
+.. currentmodule:: main_package.subpackage.submodule
+
+.. autofunction:: func1
+
+.. autoclass:: Class1
+
+.. autoclass:: Class3
diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py
new file mode 100644
index 00000000000..7654a53caa9
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py
@@ -0,0 +1 @@
+from main_package import subpackage
diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py
new file mode 100644
index 00000000000..a1e31add516
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py
@@ -0,0 +1,3 @@
+from main_package.subpackage._subpackage2 import submodule
+
+__all__ = ['submodule']
diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/__init__.py
new file mode 100644
index 00000000000..8b137891791
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/__init__.py
@@ -0,0 +1 @@
+
diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py
new file mode 100644
index 00000000000..8aefb9457da
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py
@@ -0,0 +1,24 @@
+"""submodule"""
+# raise RuntimeError('This module should not get imported')
+
+
+def decorator(f):
+ return f
+
+
+@decorator
+def func1(a, b):
+ """this is func1"""
+ return a, b
+
+
+@decorator
+class Class1:
+ """this is Class1"""
+
+
+class Class3:
+ """this is Class3"""
+
+ class_attr = 42
+ """this is the class attribute class_attr"""
diff --git a/tests/roots/test-ext-viewcode-find/not_a_package/submodule.py b/tests/roots/test-ext-viewcode-find/not_a_package/submodule.py
index ba8be78de5f..48171d76ac7 100644
--- a/tests/roots/test-ext-viewcode-find/not_a_package/submodule.py
+++ b/tests/roots/test-ext-viewcode-find/not_a_package/submodule.py
@@ -1,7 +1,8 @@
"""
submodule
-"""
-raise RuntimeError('This module should not get imported')
+""" # NoQA: D212
+
+raise RuntimeError('This module should not get imported') # NoQA: EM101,TRY003
def decorator(f):
@@ -12,20 +13,17 @@ def decorator(f):
def func1(a, b):
"""
this is func1
- """
+ """ # NoQA: D212
return a, b
@decorator
class Class1:
- """
- this is Class1
- """
+ """this is Class1"""
class Class3:
- """
- this is Class3
- """
+ """this is Class3"""
+
class_attr = 42
"""this is the class attribute class_attr"""
diff --git a/tests/roots/test-ext-viewcode/conf.py b/tests/roots/test-ext-viewcode/conf.py
index 0a5d2fcb06b..a15e2eab586 100644
--- a/tests/roots/test-ext-viewcode/conf.py
+++ b/tests/roots/test-ext-viewcode/conf.py
@@ -1,23 +1,30 @@
import sys
from pathlib import Path
+from sphinx.ext.linkcode import add_linkcode_domain
+
sys.path.insert(0, str(Path.cwd().resolve()))
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
exclude_patterns = ['_build']
-if 'test_linkcode' in tags:
+if 'test_linkcode' in tags: # NoQA: F821 (tags is injected into conf.py)
extensions.remove('sphinx.ext.viewcode')
extensions.append('sphinx.ext.linkcode')
def linkcode_resolve(domain, info):
if domain == 'py':
fn = info['module'].replace('.', '/')
- return "https://foobar/source/%s.py" % fn
- elif domain == "js":
- return "https://foobar/js/" + info['fullname']
- elif domain in ("c", "cpp"):
- return f"https://foobar/{domain}/{''.join(info['names'])}"
+ return 'https://foobar/source/%s.py' % fn
+ elif domain == 'js':
+ return 'https://foobar/js/' + info['fullname']
+ elif domain in {'c', 'cpp'}:
+ return f'https://foobar/{domain}/{"".join(info["names"])}'
+ elif domain == 'rst':
+ return 'http://foobar/rst/{fullname}'.format(**info)
else:
- raise AssertionError()
+ raise AssertionError
+
+ def setup(app):
+ add_linkcode_domain('rst', ['fullname'])
diff --git a/tests/roots/test-ext-viewcode/objects.rst b/tests/roots/test-ext-viewcode/objects.rst
index 114adbf2eb1..bf492e2630d 100644
--- a/tests/roots/test-ext-viewcode/objects.rst
+++ b/tests/roots/test-ext-viewcode/objects.rst
@@ -167,3 +167,10 @@ CPP domain
.. cpp:function:: T& operator[]( unsigned j )
const T& operator[]( unsigned j ) const
+
+rST domain
+==========
+
+.. rst:role:: foo
+
+ Foo description.
diff --git a/tests/roots/test-ext-viewcode/spam/mod1.py b/tests/roots/test-ext-viewcode/spam/mod1.py
index a078328c283..bf5c0fc38e0 100644
--- a/tests/roots/test-ext-viewcode/spam/mod1.py
+++ b/tests/roots/test-ext-viewcode/spam/mod1.py
@@ -1,6 +1,4 @@
-"""
-mod1
-"""
+"""mod1"""
def decorator(f):
@@ -9,22 +7,17 @@ def decorator(f):
@decorator
def func1(a, b):
- """
- this is func1
- """
+ """this is func1"""
return a, b
@decorator
class Class1:
- """
- this is Class1
- """
+ """this is Class1"""
class Class3:
- """
- this is Class3
- """
+ """this is Class3"""
+
class_attr = 42
"""this is the class attribute class_attr"""
diff --git a/tests/roots/test-ext-viewcode/spam/mod2.py b/tests/roots/test-ext-viewcode/spam/mod2.py
index 72cb0897815..9a77a10e2be 100644
--- a/tests/roots/test-ext-viewcode/spam/mod2.py
+++ b/tests/roots/test-ext-viewcode/spam/mod2.py
@@ -1,6 +1,4 @@
-"""
-mod2
-"""
+"""mod2"""
def decorator(f):
@@ -9,14 +7,10 @@ def decorator(f):
@decorator
def func2(a, b):
- """
- this is func2
- """
+ """this is func2"""
return a, b
@decorator
class Class2:
- """
- this is Class2
- """
+ """this is Class2"""
diff --git a/tests/roots/test-extensions/read_parallel.py b/tests/roots/test-extensions/read_parallel.py
index a3e052f9570..08770105543 100644
--- a/tests/roots/test-extensions/read_parallel.py
+++ b/tests/roots/test-extensions/read_parallel.py
@@ -1,4 +1,4 @@
def setup(app):
return {
- 'parallel_read_safe': True
+ 'parallel_read_safe': True,
}
diff --git a/tests/roots/test-extensions/read_serial.py b/tests/roots/test-extensions/read_serial.py
index c55570a5c44..4910d4f7faf 100644
--- a/tests/roots/test-extensions/read_serial.py
+++ b/tests/roots/test-extensions/read_serial.py
@@ -1,4 +1,4 @@
def setup(app):
return {
- 'parallel_read_safe': False
+ 'parallel_read_safe': False,
}
diff --git a/tests/roots/test-extensions/write_serial.py b/tests/roots/test-extensions/write_serial.py
index 75494ce7772..6e2910870cb 100644
--- a/tests/roots/test-extensions/write_serial.py
+++ b/tests/roots/test-extensions/write_serial.py
@@ -1,4 +1,4 @@
def setup(app):
return {
- 'parallel_write_safe': False
+ 'parallel_write_safe': False,
}
diff --git a/tests/roots/test-highlight_options/conf.py b/tests/roots/test-highlight_options/conf.py
index 90997d44482..a8ee7730db9 100644
--- a/tests/roots/test-highlight_options/conf.py
+++ b/tests/roots/test-highlight_options/conf.py
@@ -1,4 +1,4 @@
highlight_options = {
'default': {'default_option': True},
- 'python': {'python_option': True}
+ 'python': {'python_option': True},
}
diff --git a/tests/roots/test-html_assets/conf.py b/tests/roots/test-html_assets/conf.py
index 7f94bbbce73..7212d4ac687 100644
--- a/tests/roots/test-html_assets/conf.py
+++ b/tests/roots/test-html_assets/conf.py
@@ -3,10 +3,18 @@
html_static_path = ['static', 'subdir']
html_extra_path = ['extra', 'subdir']
-html_css_files = ['css/style.css',
- ('https://example.com/custom.css',
- {'title': 'title', 'media': 'print', 'priority': 400})]
-html_js_files = ['js/custom.js',
- ('https://example.com/script.js',
- {'async': 'async', 'priority': 400})]
+html_css_files = [
+ 'css/style.css',
+ (
+ 'https://example.com/custom.css',
+ {'title': 'title', 'media': 'print', 'priority': 400},
+ ),
+]
+html_js_files = [
+ 'js/custom.js',
+ (
+ 'https://example.com/script.js',
+ {'async': 'async', 'priority': 400},
+ ),
+]
exclude_patterns = ['**/_build', '**/.htpasswd']
diff --git a/tests/roots/test-image-in-parsed-literal/conf.py b/tests/roots/test-image-in-parsed-literal/conf.py
index 5d06da63366..69ad26aa4e9 100644
--- a/tests/roots/test-image-in-parsed-literal/conf.py
+++ b/tests/roots/test-image-in-parsed-literal/conf.py
@@ -1,9 +1,9 @@
exclude_patterns = ['_build']
-rst_epilog = '''
+rst_epilog = """
.. |picture| image:: pic.png
:height: 1cm
:scale: 200%
:align: middle
:alt: alternative_text
-'''
+"""
diff --git a/tests/roots/test-image-in-section/conf.py b/tests/roots/test-image-in-section/conf.py
index 9cb250c1aef..08b11db4f63 100644
--- a/tests/roots/test-image-in-section/conf.py
+++ b/tests/roots/test-image-in-section/conf.py
@@ -1,8 +1,8 @@
exclude_patterns = ['_build']
-rst_epilog = '''
+rst_epilog = """
.. |picture| image:: pic.png
:width: 15pt
:height: 15pt
:alt: alternative_text
-'''
+"""
diff --git a/tests/roots/test-inheritance/dummy/test.py b/tests/roots/test-inheritance/dummy/test.py
index 12fe8d900f3..e3838e7f07d 100644
--- a/tests/roots/test-inheritance/dummy/test.py
+++ b/tests/roots/test-inheritance/dummy/test.py
@@ -1,6 +1,4 @@
-r"""
-
- Test with a class diagram like this::
+r"""Test with a class diagram like this::
A
/ \
@@ -8,7 +6,7 @@
/ \ / \
E D F
-"""
+""" # NoQA: D208
class A:
diff --git a/tests/roots/test-inheritance/dummy/test_nested.py b/tests/roots/test-inheritance/dummy/test_nested.py
index 4b6801892ec..ccbde523d11 100644
--- a/tests/roots/test-inheritance/dummy/test_nested.py
+++ b/tests/roots/test-inheritance/dummy/test_nested.py
@@ -1,9 +1,8 @@
-"""Test with nested classes.
-"""
+"""Test with nested classes."""
class A:
- class B:
+ class B: # NoQA: D106
pass
diff --git a/tests/roots/test-intl/conf.py b/tests/roots/test-intl/conf.py
index 09c47bb0637..2b49de56e7a 100644
--- a/tests/roots/test-intl/conf.py
+++ b/tests/roots/test-intl/conf.py
@@ -1,6 +1,6 @@
project = 'Sphinx intl '
source_suffix = {
- '.txt': 'restructuredtext'
+ '.txt': 'restructuredtext',
}
keep_warnings = True
templates_path = ['_templates']
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po b/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po
index a19c9d19ff9..9c7ab7d2a38 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po
@@ -1,47 +1,47 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) 2012, foof
-# This file is distributed under the same license as the foo package.
-# FIRST AUTHOR , YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: sphinx 1.0\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-01-01 05:00+0000\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: LANGUAGE \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-msgid "i18n with definition terms"
-msgstr "I18N WITH DEFINITION TERMS"
-
-msgid "Some term"
-msgstr "SOME TERM"
-
-msgid "The corresponding definition"
-msgstr "THE CORRESPONDING DEFINITION"
-
-msgid "Some *term* `with link `__"
-msgstr "SOME *TERM* `WITH LINK `__"
-
-msgid "The corresponding definition #2"
-msgstr "THE CORRESPONDING DEFINITION #2"
-
-msgid "Some **term** with"
-msgstr "SOME **TERM** WITH"
-
-msgid "classifier1"
-msgstr "CLASSIFIER1"
-
-msgid "classifier2"
-msgstr "CLASSIFIER2"
-
-msgid "Some term with"
-msgstr "SOME TERM WITH"
-
-msgid "classifier[]"
-msgstr "CLASSIFIER[]"
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2012, foof
+# This file is distributed under the same license as the foo package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: sphinx 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-01-01 05:00+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "i18n with definition terms"
+msgstr "I18N WITH DEFINITION TERMS"
+
+msgid "Some term"
+msgstr "SOME TERM"
+
+msgid "The corresponding definition"
+msgstr "THE CORRESPONDING DEFINITION"
+
+msgid "Some *term* `with link `__"
+msgstr "SOME *TERM* `WITH LINK `__"
+
+msgid "The corresponding definition #2"
+msgstr "THE CORRESPONDING DEFINITION #2"
+
+msgid "Some **term** with"
+msgstr "SOME **TERM** WITH"
+
+msgid "classifier1"
+msgstr "CLASSIFIER1"
+
+msgid "classifier2"
+msgstr "CLASSIFIER2"
+
+msgid "Some term with"
+msgstr "SOME TERM WITH"
+
+msgid "classifier[]"
+msgstr "CLASSIFIER[]"
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po b/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po
index 048b81f9555..1dfb4bec8dd 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po
@@ -1,26 +1,26 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) 2012, foof
-# This file is distributed under the same license as the foo package.
-# FIRST AUTHOR , YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: sphinx 1.0\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-01-29 14:10+0000\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: LANGUAGE \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-msgid "i18n with glossary terms inconsistency"
-msgstr "I18N WITH GLOSSARY TERMS INCONSISTENCY"
-
-msgid "link to :term:`Some term` and :term:`Some other term`."
-msgstr "LINK TO :term:`SOME NEW TERM`."
-
-msgid "link to :term:`Some term`."
-msgstr "LINK TO :term:`TERM NOT IN GLOSSARY`."
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2012, foof
+# This file is distributed under the same license as the foo package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: sphinx 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-01-29 14:10+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "i18n with glossary terms inconsistency"
+msgstr "I18N WITH GLOSSARY TERMS INCONSISTENCY"
+
+msgid "link to :term:`Some term` and :term:`Some other term`."
+msgstr "LINK TO :term:`SOME NEW TERM`."
+
+msgid "link to :term:`Some term`."
+msgstr "LINK TO :term:`TERM NOT IN GLOSSARY`."
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/rubric.po b/tests/roots/test-intl/xx/LC_MESSAGES/rubric.po
index 91376236d82..e4b097a68dc 100644
--- a/tests/roots/test-intl/xx/LC_MESSAGES/rubric.po
+++ b/tests/roots/test-intl/xx/LC_MESSAGES/rubric.po
@@ -1,29 +1,29 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) 2012, foof
-# This file is distributed under the same license as the foo package.
-# FIRST AUTHOR , YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: sphinx 1.0\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-11-12 07:00+0000\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME \n"
-"Language-Team: LANGUAGE \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-msgid "i18n with rubric"
-msgstr "I18N WITH RUBRIC"
-
-msgid "rubric title"
-msgstr "RUBRIC TITLE"
-
-msgid "rubric in the block"
-msgstr "RUBRIC IN THE BLOCK"
-
-msgid "block"
-msgstr "BLOCK"
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2012, foof
+# This file is distributed under the same license as the foo package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: sphinx 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-11-12 07:00+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "i18n with rubric"
+msgstr "I18N WITH RUBRIC"
+
+msgid "rubric title"
+msgstr "RUBRIC TITLE"
+
+msgid "rubric in the block"
+msgstr "RUBRIC IN THE BLOCK"
+
+msgid "block"
+msgstr "BLOCK"
diff --git a/tests/roots/test-latex-includegraphics/conf.py b/tests/roots/test-latex-includegraphics/conf.py
index 65c19ab859c..86570bdfea3 100644
--- a/tests/roots/test-latex-includegraphics/conf.py
+++ b/tests/roots/test-latex-includegraphics/conf.py
@@ -1,7 +1,7 @@
exclude_patterns = ['_build']
latex_elements = {
- 'preamble': r'''
+ 'preamble': r"""
\makeatletter
\def\dividetwolengths#1#2{\the\dimexpr
\numexpr65536*\dimexpr#1\relax/\dimexpr#2\relax sp}%
@@ -43,5 +43,5 @@
}
\def\sphinxincludegraphics#1#{\tempincludegraphics#1}
\makeatother
-''',
+""",
}
diff --git a/tests/roots/test-latex-labels-before-module/automodule1.py b/tests/roots/test-latex-labels-before-module/automodule1.py
index 0545aa42705..fb1e2b64861 100644
--- a/tests/roots/test-latex-labels-before-module/automodule1.py
+++ b/tests/roots/test-latex-labels-before-module/automodule1.py
@@ -1,2 +1 @@
"""docstring"""
-
diff --git a/tests/roots/test-latex-labels-before-module/automodule2a.py b/tests/roots/test-latex-labels-before-module/automodule2a.py
index 0545aa42705..fb1e2b64861 100644
--- a/tests/roots/test-latex-labels-before-module/automodule2a.py
+++ b/tests/roots/test-latex-labels-before-module/automodule2a.py
@@ -1,2 +1 @@
"""docstring"""
-
diff --git a/tests/roots/test-latex-labels-before-module/automodule2b.py b/tests/roots/test-latex-labels-before-module/automodule2b.py
index 0545aa42705..fb1e2b64861 100644
--- a/tests/roots/test-latex-labels-before-module/automodule2b.py
+++ b/tests/roots/test-latex-labels-before-module/automodule2b.py
@@ -1,2 +1 @@
"""docstring"""
-
diff --git a/tests/roots/test-latex-labels-before-module/automodule3.py b/tests/roots/test-latex-labels-before-module/automodule3.py
index 0545aa42705..fb1e2b64861 100644
--- a/tests/roots/test-latex-labels-before-module/automodule3.py
+++ b/tests/roots/test-latex-labels-before-module/automodule3.py
@@ -1,2 +1 @@
"""docstring"""
-
diff --git a/tests/roots/test-latex-numfig/conf.py b/tests/roots/test-latex-numfig/conf.py
index 287bd1c9b6e..7d92eddc256 100644
--- a/tests/roots/test-latex-numfig/conf.py
+++ b/tests/roots/test-latex-numfig/conf.py
@@ -1,8 +1,6 @@
extensions = ['sphinx.ext.imgmath'] # for math_numfig
latex_documents = [
- ('indexmanual', 'SphinxManual.tex', 'Test numfig manual',
- 'Sphinx', 'manual'),
- ('indexhowto', 'SphinxHowTo.tex', 'Test numfig howto',
- 'Sphinx', 'howto'),
+ ('indexmanual', 'SphinxManual.tex', 'Test numfig manual', 'Sphinx', 'manual'),
+ ('indexhowto', 'SphinxHowTo.tex', 'Test numfig howto', 'Sphinx', 'howto'),
]
diff --git a/tests/roots/test-latex-title/conf.py b/tests/roots/test-latex-title/conf.py
index 64433165b73..2059c9f994d 100644
--- a/tests/roots/test-latex-title/conf.py
+++ b/tests/roots/test-latex-title/conf.py
@@ -1,4 +1,4 @@
# set empty string to the third column to use the first section title to document title
latex_documents = [
- ('index', 'test.tex', '', 'Sphinx', 'report')
+ ('index', 'test.tex', '', 'Sphinx', 'report'),
]
diff --git a/tests/roots/test-local-logo/conf.py b/tests/roots/test-local-logo/conf.py
index 1a166c13058..580424fc77e 100644
--- a/tests/roots/test-local-logo/conf.py
+++ b/tests/roots/test-local-logo/conf.py
@@ -1,4 +1,10 @@
latex_documents = [
- ('index', 'test.tex', 'The basic Sphinx documentation for testing', 'Sphinx', 'report')
+ (
+ 'index',
+ 'test.tex',
+ 'The basic Sphinx documentation for testing',
+ 'Sphinx',
+ 'report',
+ )
]
-html_logo = "images/img.png"
+html_logo = 'images/img.png'
diff --git a/tests/roots/test-markup-citation/conf.py b/tests/roots/test-markup-citation/conf.py
index e274bde806b..facde90889e 100644
--- a/tests/roots/test-markup-citation/conf.py
+++ b/tests/roots/test-markup-citation/conf.py
@@ -1,3 +1,9 @@
latex_documents = [
- ('index', 'test.tex', 'The basic Sphinx documentation for testing', 'Sphinx', 'report')
+ (
+ 'index',
+ 'test.tex',
+ 'The basic Sphinx documentation for testing',
+ 'Sphinx',
+ 'report',
+ )
]
diff --git a/tests/roots/test-markup-rubric/conf.py b/tests/roots/test-markup-rubric/conf.py
index eccdbf78895..78d95a237d6 100644
--- a/tests/roots/test-markup-rubric/conf.py
+++ b/tests/roots/test-markup-rubric/conf.py
@@ -1,4 +1,10 @@
latex_documents = [
- ('index', 'test.tex', 'The basic Sphinx documentation for testing', 'Sphinx', 'report')
+ (
+ 'index',
+ 'test.tex',
+ 'The basic Sphinx documentation for testing',
+ 'Sphinx',
+ 'report',
+ )
]
latex_toplevel_sectioning = 'section'
diff --git a/tests/roots/test-pycode/cp_1251_coded.py b/tests/roots/test-pycode/cp_1251_coded.py
index 43d98f354d0..ca849762241 100644
--- a/tests/roots/test-pycode/cp_1251_coded.py
+++ b/tests/roots/test-pycode/cp_1251_coded.py
@@ -1,4 +1,4 @@
-#!python
-# -*- coding: windows-1251 -*-
-
-X="" #:It MUST look like X=""
\ No newline at end of file
+#!python
+# -*- coding: windows-1251 -*-
+
+X="Х" #:It MUST look like X="Х"
\ No newline at end of file
diff --git a/tests/roots/test-remote-logo/conf.py b/tests/roots/test-remote-logo/conf.py
index 07949ba91fc..b96edae1de7 100644
--- a/tests/roots/test-remote-logo/conf.py
+++ b/tests/roots/test-remote-logo/conf.py
@@ -1,5 +1,11 @@
latex_documents = [
- ('index', 'test.tex', 'The basic Sphinx documentation for testing', 'Sphinx', 'report')
+ (
+ 'index',
+ 'test.tex',
+ 'The basic Sphinx documentation for testing',
+ 'Sphinx',
+ 'report',
+ )
]
-html_logo = "https://www.python.org/static/img/python-logo.png"
-html_favicon = "https://www.python.org/static/favicon.ico"
+html_logo = 'https://www.python.org/static/img/python-logo.png'
+html_favicon = 'https://www.python.org/static/favicon.ico'
diff --git a/tests/roots/test-roles-download/conf.py b/tests/roots/test-roles-download/conf.py
index e274bde806b..facde90889e 100644
--- a/tests/roots/test-roles-download/conf.py
+++ b/tests/roots/test-roles-download/conf.py
@@ -1,3 +1,9 @@
latex_documents = [
- ('index', 'test.tex', 'The basic Sphinx documentation for testing', 'Sphinx', 'report')
+ (
+ 'index',
+ 'test.tex',
+ 'The basic Sphinx documentation for testing',
+ 'Sphinx',
+ 'report',
+ )
]
diff --git a/tests/roots/test-root/autodoc_target.py b/tests/roots/test-root/autodoc_target.py
index 59f6c74d64c..df9e6b853cb 100644
--- a/tests/roots/test-root/autodoc_target.py
+++ b/tests/roots/test-root/autodoc_target.py
@@ -32,7 +32,7 @@ def __get__(self, obj, type=None):
def meth(self):
"""Function."""
- return "The Answer"
+ return 'The Answer'
class CustomDataDescriptorMeta(type):
@@ -41,15 +41,18 @@ class CustomDataDescriptorMeta(type):
class CustomDataDescriptor2(CustomDataDescriptor):
"""Descriptor class with custom metaclass docstring."""
+
__metaclass__ = CustomDataDescriptorMeta
def _funky_classmethod(name, b, c, d, docstring=None):
- """Generates a classmethod for a class from a template by filling out
- some arguments."""
+ """Generates a classmethod for a class from a template by filling out some arguments."""
+
def template(cls, a, b, c, d=4, e=5, f=6):
return a, b, c, d, e, f
+
from functools import partial
+
function = partial(template, b=b, c=c, d=d)
function.__name__ = name
function.__doc__ = docstring
@@ -70,7 +73,7 @@ def inheritedmeth(self):
class Class(Base):
"""Class to document."""
- descr = CustomDataDescriptor("Descriptor instance docstring.")
+ descr = CustomDataDescriptor('Descriptor instance docstring.')
def meth(self):
"""Function."""
@@ -104,10 +107,11 @@ def prop(self):
mdocattr = StringIO()
"""should be documented as well - süß"""
- roger = _funky_classmethod("roger", 2, 3, 4)
+ roger = _funky_classmethod('roger', 2, 3, 4)
- moore = _funky_classmethod("moore", 9, 8, 7,
- docstring="moore(a, e, f) -> happiness")
+ moore = _funky_classmethod(
+ 'moore', 9, 8, 7, docstring='moore(a, e, f) -> happiness'
+ )
def __init__(self, arg):
self.inst_attr_inline = None #: an inline documented instance attr
@@ -117,22 +121,20 @@ def __init__(self, arg):
"""a documented instance attribute"""
self._private_inst_attr = None #: a private instance attribute
- def __special1__(self):
+ def __special1__(self): # NoQA: PLW3201
"""documented special method"""
- def __special2__(self):
+ def __special2__(self): # NoQA: PLW3201
# undocumented special method
pass
-class CustomDict(dict):
+class CustomDict(dict): # NoQA: FURB189
"""Docstring."""
def function(foo, *args, **kwds):
- """
- Return spam.
- """
+ """Return spam."""
pass
@@ -152,7 +154,7 @@ def meth(self):
class DocstringSig:
def meth(self):
"""meth(FOO, BAR=1) -> BAZ
-First line of docstring
+ First line of docstring
rest of docstring
"""
@@ -179,7 +181,7 @@ def prop2(self):
return 456
-class StrRepr(str):
+class StrRepr(str): # NoQA: FURB189,SLOT000
def __repr__(self):
return self
@@ -196,7 +198,7 @@ class InstAttCls:
#: It can have multiple lines.
ca1 = 'a'
- ca2 = 'b' #: Doc comment for InstAttCls.ca2. One line only.
+ ca2 = 'b' #: Doc comment for InstAttCls.ca2. One line only.
ca3 = 'c'
"""Docstring for class attribute InstAttCls.ca3."""
@@ -210,9 +212,7 @@ def __init__(self):
class EnumCls(enum.Enum):
- """
- this is enum class
- """
+ """this is enum class"""
#: doc for val1
val1 = 12
diff --git a/tests/roots/test-root/conf.py b/tests/roots/test-root/conf.py
index 21ec2922e97..0a750789128 100644
--- a/tests/roots/test-root/conf.py
+++ b/tests/roots/test-root/conf.py
@@ -8,10 +8,12 @@
from sphinx import addnodes
-extensions = ['sphinx.ext.autodoc',
- 'sphinx.ext.todo',
- 'sphinx.ext.coverage',
- 'sphinx.ext.extlinks']
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.coverage',
+ 'sphinx.ext.extlinks',
+]
jsmath_path = 'dummy.js'
@@ -34,9 +36,16 @@
show_authors = True
numfig = True
-html_sidebars = {'**': ['localtoc.html', 'relations.html', 'sourcelink.html',
- 'customsb.html', 'searchbox.html'],
- 'index': ['contentssb.html', 'localtoc.html', 'globaltoc.html']}
+html_sidebars = {
+ '**': [
+ 'localtoc.html',
+ 'relations.html',
+ 'sourcelink.html',
+ 'customsb.html',
+ 'searchbox.html',
+ ],
+ 'index': ['contentssb.html', 'localtoc.html', 'globaltoc.html'],
+}
html_last_updated_fmt = '%b %d, %Y'
html_context = {'hckey': 'hcval', 'hckey_co': 'wrong_hcval_co'}
@@ -122,11 +131,13 @@
coverage_c_path = ['special/*.h']
coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'}
-extlinks = {'issue': ('https://bugs.python.org/issue%s', 'issue %s'),
- 'pyurl': ('https://python.org/%s', None)}
+extlinks = {
+ 'issue': ('https://bugs.python.org/issue%s', 'issue %s'),
+ 'pyurl': ('https://python.org/%s', None),
+}
# modify tags from conf.py
-tags.add('confpytag')
+tags.add('confpytag') # NoQA: F821 (tags is injected into conf.py)
# -- extension API
@@ -149,8 +160,9 @@ def setup(app):
import parsermod
app.add_directive('clsdir', ClassDirective)
- app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)',
- userdesc_parse, objname='user desc')
+ app.add_object_type(
+ 'userdesc', 'userdescrole', '%s (userdesc)', userdesc_parse, objname='user desc'
+ )
app.add_js_file('file://moo.js')
app.add_source_suffix('.foo', 'foo')
app.add_source_parser(parsermod.Parser)
diff --git a/tests/roots/test-root/math.txt b/tests/roots/test-root/math.txt
index 5a209bed43b..11664400779 100644
--- a/tests/roots/test-root/math.txt
+++ b/tests/roots/test-root/math.txt
@@ -24,7 +24,7 @@ This is inline math: :math:`a^2 + b^2 = c^2`.
n \in \mathbb N
.. math::
- :nowrap:
+ :no-wrap:
a + 1 < b
diff --git a/tests/roots/test-root/special/code.py b/tests/roots/test-root/special/code.py
index b7934b2312c..624b494459e 100644
--- a/tests/roots/test-root/special/code.py
+++ b/tests/roots/test-root/special/code.py
@@ -1,2 +1,2 @@
-print("line 1")
-print("line 2")
+print('line 1')
+print('line 2')
diff --git a/tests/roots/test-stylesheets/conf.py b/tests/roots/test-stylesheets/conf.py
index fa37130a5c9..d3c6f62d5e0 100644
--- a/tests/roots/test-stylesheets/conf.py
+++ b/tests/roots/test-stylesheets/conf.py
@@ -4,6 +4,6 @@
def setup(app):
app.add_css_file('persistent.css')
- app.add_css_file('default.css', title="Default")
- app.add_css_file('alternate1.css', title="Alternate", rel="alternate stylesheet")
- app.add_css_file('alternate2.css', rel="alternate stylesheet")
+ app.add_css_file('default.css', title='Default')
+ app.add_css_file('alternate1.css', title='Alternate', rel='alternate stylesheet')
+ app.add_css_file('alternate2.css', rel='alternate stylesheet')
diff --git a/tests/roots/test-templating/conf.py b/tests/roots/test-templating/conf.py
index 7a2baeda29e..52c2526911d 100644
--- a/tests/roots/test-templating/conf.py
+++ b/tests/roots/test-templating/conf.py
@@ -1,6 +1,6 @@
project = 'Sphinx templating '
source_suffix = {
- '.txt': 'restructuredtext'
+ '.txt': 'restructuredtext',
}
keep_warnings = True
templates_path = ['_templates']
diff --git a/tests/roots/test-util-copyasset_overwrite/myext.py b/tests/roots/test-util-copyasset_overwrite/myext.py
index 5ef9e69e645..e1e6c8f89b3 100644
--- a/tests/roots/test-util-copyasset_overwrite/myext.py
+++ b/tests/roots/test-util-copyasset_overwrite/myext.py
@@ -6,14 +6,16 @@
def _copy_asset_overwrite_hook(app):
css = app.outdir / '_static' / 'custom-styles.css'
# html_static_path is copied by default
- assert css.read_text(encoding='utf-8') == '/* html_static_path */\n', 'invalid default text'
+ css_content = css.read_text(encoding='utf-8')
+ assert css_content == '/* html_static_path */\n', 'invalid default text'
# warning generated by here
copy_asset(
Path(__file__).resolve().parent.joinpath('myext_static', 'custom-styles.css'),
app.outdir / '_static',
)
# This demonstrates that no overwriting occurs
- assert css.read_text(encoding='utf-8') == '/* html_static_path */\n', 'file overwritten!'
+ css_content = css.read_text(encoding='utf-8')
+ assert css_content == '/* html_static_path */\n', 'file overwritten!'
return []
diff --git a/tests/roots/test-versioning/conf.py b/tests/roots/test-versioning/conf.py
index d52d1f2746c..5520b516eee 100644
--- a/tests/roots/test-versioning/conf.py
+++ b/tests/roots/test-versioning/conf.py
@@ -1,5 +1,5 @@
project = 'versioning test root'
source_suffix = {
- '.txt': 'restructuredtext'
+ '.txt': 'restructuredtext',
}
exclude_patterns = ['_build']
diff --git a/tests/roots/test-warnings/autodoc_fodder.py b/tests/roots/test-warnings/autodoc_fodder.py
index 59e4e210f3a..eb48b9be81f 100644
--- a/tests/roots/test-warnings/autodoc_fodder.py
+++ b/tests/roots/test-warnings/autodoc_fodder.py
@@ -1,6 +1,5 @@
class MarkupError:
- """
- .. note:: This is a docstring with a
+ """.. note:: This is a docstring with a
small markup error which should have
correct location information.
"""
diff --git a/tests/test__cli/__init__.py b/tests/test__cli/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/test_util/test_util_console.py b/tests/test__cli/test__cli_util_errors.py
similarity index 58%
rename from tests/test_util/test_util_console.py
rename to tests/test__cli/test__cli_util_errors.py
index fb48e189f30..cfe4ad8afa6 100644
--- a/tests/test_util/test_util_console.py
+++ b/tests/test__cli/test__cli_util_errors.py
@@ -4,47 +4,38 @@
import operator
from typing import TYPE_CHECKING
-import pytest
-
-from sphinx.util.console import blue, reset, strip_colors, strip_escape_sequences
+from sphinx._cli.util.colour import blue, reset
+from sphinx._cli.util.errors import strip_escape_sequences
if TYPE_CHECKING:
- from collections.abc import Callable, Sequence
- from typing import Final, TypeVar
-
- _T = TypeVar('_T')
+ from collections.abc import Sequence
+ from typing import Final
CURSOR_UP: Final[str] = '\x1b[2A' # ignored ANSI code
ERASE_LINE: Final[str] = '\x1b[2K' # supported ANSI code
-TEXT: Final[str] = '\x07 Hello world!'
-
-
-@pytest.mark.parametrize(
- ('strip_function', 'ansi_base_blocks', 'text_base_blocks'),
- [
- (
- strip_colors,
- # double ERASE_LINE so that the tested strings may have 2 of them
- [TEXT, blue(TEXT), reset(TEXT), ERASE_LINE, ERASE_LINE, CURSOR_UP],
- # :func:`strip_colors` removes color codes but keeps ERASE_LINE and CURSOR_UP
- [TEXT, TEXT, TEXT, ERASE_LINE, ERASE_LINE, CURSOR_UP],
- ),
- (
- strip_escape_sequences,
- # double ERASE_LINE so that the tested strings may have 2 of them
- [TEXT, blue(TEXT), reset(TEXT), ERASE_LINE, ERASE_LINE, CURSOR_UP],
- # :func:`strip_escape_sequences` strips ANSI codes known by Sphinx
- [TEXT, TEXT, TEXT, '', '', CURSOR_UP],
- ),
- ],
- ids=[strip_colors.__name__, strip_escape_sequences.__name__],
-)
-def test_strip_ansi(
- strip_function: Callable[[str], str],
- ansi_base_blocks: Sequence[str],
- text_base_blocks: Sequence[str],
-) -> None:
- assert callable(strip_function)
+TEXT: Final[str] = '\x07 ß Hello world!'
+
+
+def test_strip_escape_sequences() -> None:
+ # double ERASE_LINE so that the tested strings may have 2 of them
+ ansi_base_blocks = [
+ TEXT,
+ blue(TEXT),
+ reset(TEXT),
+ ERASE_LINE,
+ ERASE_LINE,
+ CURSOR_UP,
+ ]
+ # :func:`strip_escape_sequences` strips ANSI codes known by Sphinx
+ text_base_blocks = [
+ TEXT,
+ TEXT,
+ TEXT,
+ '',
+ '',
+ CURSOR_UP,
+ ]
+
assert len(text_base_blocks) == len(ansi_base_blocks)
N = len(ansi_base_blocks)
@@ -69,7 +60,7 @@ def next_ansi_blocks(choices: Sequence[str], n: int) -> Sequence[str]:
ansi_string = glue.join(ansi_strings)
text_string = glue.join(text_strings)
- assert strip_function(ansi_string) == text_string
+ assert strip_escape_sequences(ansi_string) == text_string
def test_strip_ansi_short_forms():
@@ -78,7 +69,7 @@ def test_strip_ansi_short_forms():
# some messages use '\x1b[0m' instead of ``reset(s)``, so we
# test whether this alternative form is supported or not.
- for strip_function in strip_colors, strip_escape_sequences:
+ for strip_function in strip_escape_sequences, strip_escape_sequences:
# \x1b[m and \x1b[0m are equivalent to \x1b[00m
assert strip_function('\x1b[m') == ''
assert strip_function('\x1b[0m') == ''
diff --git a/tests/test_application.py b/tests/test_application.py
index c4b11b64127..8cfde31ff39 100644
--- a/tests/test_application.py
+++ b/tests/test_application.py
@@ -13,10 +13,10 @@
from docutils import nodes
import sphinx.application
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.errors import ExtensionError
from sphinx.testing.util import SphinxTestApp
from sphinx.util import logging
-from sphinx.util.console import strip_colors
if TYPE_CHECKING:
import os
@@ -84,14 +84,14 @@ def test_emit_with_nonascii_name_node(app):
@pytest.mark.sphinx('html', testroot='root')
def test_extensions(app):
app.setup_extension('shutil')
- warning = strip_colors(app.warning.getvalue())
+ warning = strip_escape_sequences(app.warning.getvalue())
assert "extension 'shutil' has no setup() function" in warning
@pytest.mark.sphinx('html', testroot='root')
def test_extension_in_blacklist(app):
app.setup_extension('sphinxjp.themecore')
- msg = strip_colors(app.warning.getvalue())
+ msg = strip_escape_sequences(app.warning.getvalue())
assert msg.startswith("WARNING: the extension 'sphinxjp.themecore' was")
diff --git a/tests/test_builders/test_build.py b/tests/test_builders/test_build.py
index 5f396cc48af..d4ab77b2516 100644
--- a/tests/test_builders/test_build.py
+++ b/tests/test_builders/test_build.py
@@ -1,5 +1,7 @@
"""Test all builders."""
+from __future__ import annotations
+
import os
import shutil
from contextlib import contextmanager
@@ -69,7 +71,7 @@ def test_build_all(requests_head, make_app, nonascii_srcdir, buildername):
def test_root_doc_not_found(tmp_path, make_app):
(tmp_path / 'conf.py').touch()
- assert os.listdir(tmp_path) == ['conf.py']
+ assert [p.name for p in tmp_path.iterdir()] == ['conf.py']
app = make_app('dummy', srcdir=tmp_path)
with pytest.raises(SphinxError):
diff --git a/tests/test_builders/test_build_changes.py b/tests/test_builders/test_build_changes.py
index 69db01ba3e5..f252b3ce3a6 100644
--- a/tests/test_builders/test_build_changes.py
+++ b/tests/test_builders/test_build_changes.py
@@ -1,5 +1,7 @@
"""Test the ChangesBuilder class."""
+from __future__ import annotations
+
import pytest
diff --git a/tests/test_builders/test_build_dirhtml.py b/tests/test_builders/test_build_dirhtml.py
index 3e78f1ed0c8..fb945f3cda7 100644
--- a/tests/test_builders/test_build_dirhtml.py
+++ b/tests/test_builders/test_build_dirhtml.py
@@ -1,10 +1,12 @@
"""Test dirhtml builder."""
+from __future__ import annotations
+
import posixpath
import pytest
-from sphinx.util.inventory import InventoryFile
+from sphinx.util.inventory import InventoryFile, _InventoryItem
@pytest.mark.sphinx('dirhtml', testroot='builder-dirhtml')
@@ -28,28 +30,33 @@ def test_dirhtml(app):
invdata = InventoryFile.load(f, 'path/to', posixpath.join)
assert 'index' in invdata.get('std:doc', {})
- assert invdata['std:doc']['index'] == ('Project name not set', '', 'path/to/', '-')
+ assert invdata['std:doc']['index'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='path/to/',
+ display_name='-',
+ )
assert 'foo/index' in invdata.get('std:doc', {})
- assert invdata['std:doc']['foo/index'] == (
- 'Project name not set',
- '',
- 'path/to/foo/',
- '-',
+ assert invdata['std:doc']['foo/index'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='path/to/foo/',
+ display_name='-',
)
assert 'index' in invdata.get('std:label', {})
- assert invdata['std:label']['index'] == (
- 'Project name not set',
- '',
- 'path/to/#index',
- '-',
+ assert invdata['std:label']['index'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='path/to/#index',
+ display_name='-',
)
assert 'foo' in invdata.get('std:label', {})
- assert invdata['std:label']['foo'] == (
- 'Project name not set',
- '',
- 'path/to/foo/#foo',
- 'foo/index',
+ assert invdata['std:label']['foo'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='path/to/foo/#foo',
+ display_name='foo/index',
)
diff --git a/tests/test_builders/test_build_epub.py b/tests/test_builders/test_build_epub.py
index 3c2be6323d4..c025b2f8fc0 100644
--- a/tests/test_builders/test_build_epub.py
+++ b/tests/test_builders/test_build_epub.py
@@ -1,5 +1,7 @@
"""Test the HTML builder and check output against XPath."""
+from __future__ import annotations
+
import os
import subprocess
import xml.etree.ElementTree as ET
diff --git a/tests/test_builders/test_build_gettext.py b/tests/test_builders/test_build_gettext.py
index 00d7f826ecc..d78f1e71b00 100644
--- a/tests/test_builders/test_build_gettext.py
+++ b/tests/test_builders/test_build_gettext.py
@@ -1,5 +1,7 @@
"""Test the build process with gettext builder with the test root."""
+from __future__ import annotations
+
import gettext
import re
import subprocess
diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py
index 097f895d7c8..035ee90baa1 100644
--- a/tests/test_builders/test_build_html.py
+++ b/tests/test_builders/test_build_html.py
@@ -9,10 +9,11 @@
import pytest
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.builders.html import validate_html_extra_path, validate_html_static_path
from sphinx.errors import ConfigError
-from sphinx.util.console import strip_colors
-from sphinx.util.inventory import InventoryFile
+from sphinx.testing.util import etree_parse
+from sphinx.util.inventory import InventoryFile, _InventoryItem
from tests.test_builders.xpath_data import FIGURE_CAPTION
from tests.test_builders.xpath_util import check_xpath
@@ -233,36 +234,36 @@ def test_html_inventory(app):
'genindex',
'search',
}
- assert invdata['std:label']['modindex'] == (
- 'Project name not set',
- '',
- 'https://www.google.com/py-modindex.html',
- 'Module Index',
+ assert invdata['std:label']['modindex'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='https://www.google.com/py-modindex.html',
+ display_name='Module Index',
)
- assert invdata['std:label']['py-modindex'] == (
- 'Project name not set',
- '',
- 'https://www.google.com/py-modindex.html',
- 'Python Module Index',
+ assert invdata['std:label']['py-modindex'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='https://www.google.com/py-modindex.html',
+ display_name='Python Module Index',
)
- assert invdata['std:label']['genindex'] == (
- 'Project name not set',
- '',
- 'https://www.google.com/genindex.html',
- 'Index',
+ assert invdata['std:label']['genindex'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='https://www.google.com/genindex.html',
+ display_name='Index',
)
- assert invdata['std:label']['search'] == (
- 'Project name not set',
- '',
- 'https://www.google.com/search.html',
- 'Search Page',
+ assert invdata['std:label']['search'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='https://www.google.com/search.html',
+ display_name='Search Page',
)
assert set(invdata['std:doc'].keys()) == {'index'}
- assert invdata['std:doc']['index'] == (
- 'Project name not set',
- '',
- 'https://www.google.com/index.html',
- 'The basic Sphinx documentation for testing',
+ assert invdata['std:doc']['index'] == _InventoryItem(
+ project_name='Project name not set',
+ project_version='',
+ uri='https://www.google.com/index.html',
+ display_name='The basic Sphinx documentation for testing',
)
@@ -512,7 +513,7 @@ def test_validate_html_extra_path(app):
validate_html_extra_path(app, app.config)
assert app.config.html_extra_path == ['_static']
- warnings = strip_colors(app.warning.getvalue()).splitlines()
+ warnings = strip_escape_sequences(app.warning.getvalue()).splitlines()
assert "html_extra_path entry '/path/to/not_found' does not exist" in warnings[0]
assert warnings[1].endswith(' is placed inside outdir')
assert warnings[2].endswith(' does not exist')
@@ -536,7 +537,7 @@ def test_validate_html_static_path(app):
validate_html_static_path(app, app.config)
assert app.config.html_static_path == ['_static']
- warnings = strip_colors(app.warning.getvalue()).splitlines()
+ warnings = strip_escape_sequences(app.warning.getvalue()).splitlines()
assert "html_static_path entry '/path/to/not_found' does not exist" in warnings[0]
assert warnings[1].endswith(' is placed inside outdir')
assert warnings[2].endswith(' does not exist')
@@ -598,7 +599,7 @@ def handler(app):
app.build()
assert not target.exists()
- ws = strip_colors(app.warning.getvalue()).splitlines()
+ ws = strip_escape_sequences(app.warning.getvalue()).splitlines()
assert len(ws) >= 1
file = os.fsdecode(target)
@@ -657,3 +658,60 @@ def __call__(self, nodes):
r'.//dt[@id="MyList"][1]',
chk('class MyList[\nT,\n](list[T])'),
)
+
+
+@pytest.mark.sphinx(
+ 'html',
+ testroot='domain-py-python_maximum_signature_line_length',
+ confoverrides={
+ 'python_maximum_signature_line_length': 1,
+ 'python_trailing_comma_in_multi_line_signatures': False,
+ },
+)
+def test_html_pep_695_trailing_comma_in_multi_line_signatures(app):
+ app.build()
+ fname = app.outdir / 'index.html'
+ etree = etree_parse(fname)
+
+ class chk:
+ def __init__(self, expect: str) -> None:
+ self.expect = expect
+
+ def __call__(self, nodes):
+ assert len(nodes) == 1, nodes
+ objnode = ''.join(nodes[0].itertext()).replace('\n\n', '')
+ objnode = objnode.rstrip(chr(182)) # remove '¶' symbol
+ objnode = objnode.strip('\n') # remove surrounding new lines
+ assert objnode == self.expect
+
+ # each signature has a dangling ',' at the end of its parameters lists
+ check_xpath(
+ etree,
+ fname,
+ r'.//dt[@id="generic_foo"][1]',
+ chk('generic_foo[\nT\n]()'),
+ )
+ check_xpath(
+ etree,
+ fname,
+ r'.//dt[@id="generic_bar"][1]',
+ chk('generic_bar[\nT\n](\nx: list[T]\n)'),
+ )
+ check_xpath(
+ etree,
+ fname,
+ r'.//dt[@id="generic_ret"][1]',
+ chk('generic_ret[\nR\n]() → R'),
+ )
+ check_xpath(
+ etree,
+ fname,
+ r'.//dt[@id="MyGenericClass"][1]',
+ chk('class MyGenericClass[\nX\n]'),
+ )
+ check_xpath(
+ etree,
+ fname,
+ r'.//dt[@id="MyList"][1]',
+ chk('class MyList[\nT\n](list[T])'),
+ )
diff --git a/tests/test_builders/test_build_html_assets.py b/tests/test_builders/test_build_html_assets.py
index f7d21fe61b6..60ee923f7a9 100644
--- a/tests/test_builders/test_build_html_assets.py
+++ b/tests/test_builders/test_build_html_assets.py
@@ -1,5 +1,7 @@
"""Test the HTML builder and check output against XPath."""
+from __future__ import annotations
+
import re
from pathlib import Path
@@ -138,22 +140,26 @@ def test_file_checksum(app):
def test_file_checksum_query_string():
with pytest.raises(
- ThemeError, match='Local asset file paths must not contain query strings'
+ ThemeError,
+ match='Local asset file paths must not contain query strings',
):
_file_checksum(Path(), 'with_query_string.css?dead_parrots=1')
with pytest.raises(
- ThemeError, match='Local asset file paths must not contain query strings'
+ ThemeError,
+ match='Local asset file paths must not contain query strings',
):
_file_checksum(Path(), 'with_query_string.js?dead_parrots=1')
with pytest.raises(
- ThemeError, match='Local asset file paths must not contain query strings'
+ ThemeError,
+ match='Local asset file paths must not contain query strings',
):
_file_checksum(Path.cwd(), '_static/with_query_string.css?dead_parrots=1')
with pytest.raises(
- ThemeError, match='Local asset file paths must not contain query strings'
+ ThemeError,
+ match='Local asset file paths must not contain query strings',
):
_file_checksum(Path.cwd(), '_static/with_query_string.js?dead_parrots=1')
diff --git a/tests/test_builders/test_build_html_code.py b/tests/test_builders/test_build_html_code.py
index d32c0b3e2b9..02684b22e04 100644
--- a/tests/test_builders/test_build_html_code.py
+++ b/tests/test_builders/test_build_html_code.py
@@ -1,3 +1,6 @@
+from __future__ import annotations
+
+import pygments
import pytest
@@ -32,11 +35,16 @@ def test_html_codeblock_linenos_style_inline(app):
@pytest.mark.sphinx('html', testroot='reST-code-role')
def test_html_code_role(app):
+ if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19):
+ sp = ' '
+ else:
+ sp = ' '
+
app.build()
content = (app.outdir / 'index.html').read_text(encoding='utf8')
common_content = (
- 'def foo'
+ f'def{sp}foo'
'('
'1 '
'+ '
diff --git a/tests/test_builders/test_build_html_copyright.py b/tests/test_builders/test_build_html_copyright.py
index 8e017ede4d7..f91ebbdb888 100644
--- a/tests/test_builders/test_build_html_copyright.py
+++ b/tests/test_builders/test_build_html_copyright.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import time
import pytest
@@ -7,6 +9,17 @@
LOCALTIME_2009 = type(LT)(LT_NEW)
+@pytest.fixture
+def no_source_date_year(monkeypatch):
+ """Explicitly clear SOURCE_DATE_EPOCH from the environment; this
+ fixture can be used to ensure that copyright substitution logic
+ does not occur during selected test cases.
+ """
+ with monkeypatch.context() as m:
+ m.delenv('SOURCE_DATE_EPOCH', raising=False)
+ yield
+
+
@pytest.fixture(
params=[
1199145600, # 2008-01-01 00:00:00
@@ -22,7 +35,7 @@ def source_date_year(request, monkeypatch):
@pytest.mark.sphinx('html', testroot='copyright-multiline')
-def test_html_multi_line_copyright(app):
+def test_html_multi_line_copyright(no_source_date_year, app):
app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf-8')
diff --git a/tests/test_builders/test_build_html_download.py b/tests/test_builders/test_build_html_download.py
index 4855949cd60..f3f5a1fd43b 100644
--- a/tests/test_builders/test_build_html_download.py
+++ b/tests/test_builders/test_build_html_download.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import hashlib
import re
diff --git a/tests/test_builders/test_build_html_highlight.py b/tests/test_builders/test_build_html_highlight.py
index 07d5e3e9887..0d713c7e871 100644
--- a/tests/test_builders/test_build_html_highlight.py
+++ b/tests/test_builders/test_build_html_highlight.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from unittest.mock import ANY, call, patch
import pytest
diff --git a/tests/test_builders/test_build_html_image.py b/tests/test_builders/test_build_html_image.py
index 5a061ed4dec..bb4baa5b643 100644
--- a/tests/test_builders/test_build_html_image.py
+++ b/tests/test_builders/test_build_html_image.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import re
from pathlib import Path
diff --git a/tests/test_builders/test_build_html_maths.py b/tests/test_builders/test_build_html_maths.py
index 0f776917e48..f3a96fccfa1 100644
--- a/tests/test_builders/test_build_html_maths.py
+++ b/tests/test_builders/test_build_html_maths.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from sphinx.errors import ConfigError
@@ -35,7 +37,7 @@ def test_html_math_renderer_is_duplicated(make_app, app_params):
args, kwargs = app_params
with pytest.raises(
ConfigError,
- match='Many math_renderers are registered. But no math_renderer is selected.',
+ match=r'Many math_renderers are registered\. But no math_renderer is selected\.',
):
make_app(*args, **kwargs)
@@ -72,5 +74,8 @@ def test_html_math_renderer_is_chosen(app):
)
def test_html_math_renderer_is_mismatched(make_app, app_params):
args, kwargs = app_params
- with pytest.raises(ConfigError, match="Unknown math_renderer 'imgmath' is given."):
+ with pytest.raises(
+ ConfigError,
+ match=r"Unknown math_renderer 'imgmath' is given\.",
+ ):
make_app(*args, **kwargs)
diff --git a/tests/test_builders/test_build_html_numfig.py b/tests/test_builders/test_build_html_numfig.py
index 833918fccdd..e1291fc1767 100644
--- a/tests/test_builders/test_build_html_numfig.py
+++ b/tests/test_builders/test_build_html_numfig.py
@@ -1,6 +1,7 @@
"""Test the HTML builder and check output against XPath."""
-import os
+from __future__ import annotations
+
import re
import pytest
@@ -307,7 +308,7 @@ def test_numfig_without_numbered_toctree(
index = re.sub(':numbered:.*', '', index)
(app.srcdir / 'index.rst').write_text(index, encoding='utf8')
- if not os.listdir(app.outdir):
+ if not list(app.outdir.iterdir()):
app.build()
check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found)
diff --git a/tests/test_builders/test_build_html_tocdepth.py b/tests/test_builders/test_build_html_tocdepth.py
index 7d6afc4c1c3..702aab8c69f 100644
--- a/tests/test_builders/test_build_html_tocdepth.py
+++ b/tests/test_builders/test_build_html_tocdepth.py
@@ -1,5 +1,7 @@
"""Test the HTML builder and check output against XPath."""
+from __future__ import annotations
+
import pytest
from tests.test_builders.xpath_html_util import _intradocument_hyperlink_check
diff --git a/tests/test_builders/test_build_html_toctree.py b/tests/test_builders/test_build_html_toctree.py
index a59de6328e6..08a6268cf4c 100644
--- a/tests/test_builders/test_build_html_toctree.py
+++ b/tests/test_builders/test_build_html_toctree.py
@@ -1,5 +1,7 @@
"""Test the HTML builder and check output against XPath."""
+from __future__ import annotations
+
import re
from unittest.mock import patch
diff --git a/tests/test_builders/test_build_latex.py b/tests/test_builders/test_build_latex.py
index d95c5e4c286..0a67e55bc4f 100644
--- a/tests/test_builders/test_build_latex.py
+++ b/tests/test_builders/test_build_latex.py
@@ -1,5 +1,7 @@
"""Test the build process with LaTeX builder with the test root."""
+from __future__ import annotations
+
import http.server
import os
import re
@@ -9,13 +11,14 @@
from shutil import copyfile
from subprocess import CalledProcessError
+import pygments
import pytest
from sphinx.builders.latex import default_latex_documents
from sphinx.config import Config
from sphinx.errors import SphinxError
-from sphinx.ext.intersphinx import load_mappings, validate_intersphinx_mapping
from sphinx.ext.intersphinx import setup as intersphinx_setup
+from sphinx.ext.intersphinx._load import load_mappings, validate_intersphinx_mapping
from sphinx.util.osutil import ensuredir
from sphinx.writers.latex import LaTeXTranslator
@@ -98,7 +101,7 @@ def do_GET(self):
if self.path == '/sphinx.png':
with open('tests/roots/test-local-logo/images/img.png', 'rb') as f:
content = f.read()
- content_type = 'image/png'
+ content_type = 'image/png'
if content:
self.send_response(200, 'OK')
@@ -2113,12 +2116,16 @@ def test_latex_container(app):
@pytest.mark.sphinx('latex', testroot='reST-code-role')
def test_latex_code_role(app):
+ if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19):
+ sp = r'\PYG{+w}{ }'
+ else:
+ sp = ' '
+
app.build()
content = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8')
common_content = (
- r'\PYG{k}{def} '
- r'\PYG{n+nf}{foo}'
+ r'\PYG{k}{def}' + sp + r'\PYG{n+nf}{foo}'
r'\PYG{p}{(}'
r'\PYG{l+m+mi}{1} '
r'\PYG{o}{+} '
@@ -2134,10 +2141,7 @@ def test_latex_code_role(app):
r'\PYG{k}{pass}'
)
assert (
- r'Inline \sphinxcode{\sphinxupquote{%' # NoQA: ISC003
- + '\n'
- + common_content
- + '%\n}} code block'
+ 'Inline \\sphinxcode{\\sphinxupquote{%\n' + common_content + '%\n}} code block'
) in content
assert (
r'\begin{sphinxVerbatim}[commandchars=\\\{\}]'
@@ -2210,7 +2214,6 @@ def test_one_parameter_per_line(app):
app.build(force_all=True)
result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8')
- # TODO: should these asserts check presence or absence of a final \sphinxparamcomma?
# signature of 23 characters is too short to trigger one-param-per-line mark-up
assert (
'\\pysiglinewithargsret\n'
@@ -2223,6 +2226,7 @@ def test_one_parameter_per_line(app):
'{\\sphinxbfcode{\\sphinxupquote{foo}}}\n'
'{\\sphinxoptional{\\sphinxparam{' in result
)
+ assert r'\sphinxparam{\DUrole{n}{f}}\sphinxparamcomma' in result
# generic_arg[T]
assert (
@@ -2279,6 +2283,22 @@ def test_one_parameter_per_line(app):
)
+@pytest.mark.sphinx(
+ 'latex',
+ testroot='domain-py-python_maximum_signature_line_length',
+ confoverrides={
+ 'python_maximum_signature_line_length': 23,
+ 'python_trailing_comma_in_multi_line_signatures': False,
+ },
+)
+def test_one_parameter_per_line_without_trailing_comma(app):
+ app.build(force_all=True)
+ result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8')
+
+ assert r'\sphinxparam{\DUrole{n}{f}}\sphinxparamcomma' not in result
+ assert r'\sphinxparam{\DUrole{n}{f}}}}' in result
+
+
@pytest.mark.sphinx('latex', testroot='markup-rubric')
def test_latex_rubric(app):
app.build()
diff --git a/tests/test_builders/test_build_linkcheck.py b/tests/test_builders/test_build_linkcheck.py
index c27ad189f3e..56d42afea45 100644
--- a/tests/test_builders/test_build_linkcheck.py
+++ b/tests/test_builders/test_build_linkcheck.py
@@ -19,9 +19,9 @@
from urllib3.poolmanager import PoolManager
import sphinx.util.http_date
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.builders.linkcheck import (
CheckRequest,
- CheckResult,
Hyperlink,
HyperlinkAvailabilityCheckWorker,
RateLimit,
@@ -29,7 +29,6 @@
)
from sphinx.util import requests
from sphinx.util._pathlib import _StrPath
-from sphinx.util.console import strip_colors
from tests.utils import CERT_FILE, serve_application
@@ -41,6 +40,9 @@
from urllib3 import HTTPConnectionPool
+ from sphinx.builders.linkcheck import (
+ CheckResult,
+ )
from sphinx.testing.util import SphinxTestApp
@@ -486,8 +488,7 @@ def custom_handler(
valid_credentials: tuple[str, str] | None = None,
success_criteria: Callable[[Any], bool] = lambda _: True,
) -> type[BaseHTTPRequestHandler]:
- """
- Returns an HTTP request handler that authenticates the client and then determines
+ """Returns an HTTP request handler that authenticates the client and then determines
an appropriate HTTP response code, based on caller-provided credentials and optional
success criteria, respectively.
"""
@@ -500,7 +501,7 @@ def custom_handler(
def authenticated(
method: Callable[[CustomHandler], None],
) -> Callable[[CustomHandler], None]:
- def method_if_authenticated(self):
+ def method_if_authenticated(self: CustomHandler) -> None:
if expected_token is None:
return method(self)
elif not self.headers['Authorization']:
@@ -512,6 +513,7 @@ def method_if_authenticated(self):
self.send_response(403, 'Forbidden')
self.send_header('Content-Length', '0')
self.end_headers()
+ return None
return method_if_authenticated
@@ -773,7 +775,7 @@ def test_linkcheck_allowed_redirects(app: SphinxTestApp) -> None:
assert (
f'index.rst:3: WARNING: redirect http://{address}/path2 - with Found to '
f'http://{address}/?redirected=1\n'
- ) in strip_colors(app.warning.getvalue())
+ ) in strip_escape_sequences(app.warning.getvalue())
assert len(app.warning.getvalue().splitlines()) == 1
@@ -925,7 +927,7 @@ class InfiniteRedirectOnHeadHandler(BaseHTTPRequestHandler):
def do_HEAD(self):
self.send_response(302, 'Found')
- self.send_header('Location', '/')
+ self.send_header('Location', '/redirected')
self.send_header('Content-Length', '0')
self.end_headers()
@@ -965,6 +967,55 @@ def test_TooManyRedirects_on_HEAD(app, monkeypatch):
}
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver')
+def test_ignore_local_redirection(app):
+ with serve_application(app, InfiniteRedirectOnHeadHandler) as address:
+ app.config.linkcheck_ignore = [f'http://{address}/redirected']
+ app.build()
+
+ with open(app.outdir / 'output.json', encoding='utf-8') as fp:
+ content = json.load(fp)
+ assert content == {
+ 'code': 302,
+ 'status': 'ignored',
+ 'filename': 'index.rst',
+ 'lineno': 1,
+ 'uri': f'http://{address}/',
+ 'info': f'ignored redirect: http://{address}/redirected',
+ }
+
+
+class RemoteDomainRedirectHandler(InfiniteRedirectOnHeadHandler):
+ protocol_version = 'HTTP/1.1'
+
+ def do_GET(self):
+ self.send_response(301, 'Found')
+ if self.path == '/':
+ self.send_header('Location', '/local')
+ elif self.path == '/local':
+ self.send_header('Location', 'http://example.test/migrated')
+ self.send_header('Content-Length', '0')
+ self.end_headers()
+
+
+@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver')
+def test_ignore_remote_redirection(app):
+ with serve_application(app, RemoteDomainRedirectHandler) as address:
+ app.config.linkcheck_ignore = ['http://example.test']
+ app.build()
+
+ with open(app.outdir / 'output.json', encoding='utf-8') as fp:
+ content = json.load(fp)
+ assert content == {
+ 'code': 301,
+ 'status': 'ignored',
+ 'filename': 'index.rst',
+ 'lineno': 1,
+ 'uri': f'http://{address}/',
+ 'info': 'ignored redirect: http://example.test/migrated',
+ }
+
+
def make_retry_after_handler(
responses: list[tuple[int, str | None]],
) -> type[BaseHTTPRequestHandler]:
@@ -1010,7 +1061,7 @@ def test_too_many_requests_retry_after_int_delay(app, capsys):
'info': '',
}
rate_limit_log = f'-rate limited- http://{address}/ | sleeping...\n'
- assert rate_limit_log in strip_colors(app.status.getvalue())
+ assert rate_limit_log in strip_escape_sequences(app.status.getvalue())
_stdout, stderr = capsys.readouterr()
assert stderr == textwrap.dedent(
"""\
diff --git a/tests/test_builders/test_build_manpage.py b/tests/test_builders/test_build_manpage.py
index 17ef00e612d..d80a067504f 100644
--- a/tests/test_builders/test_build_manpage.py
+++ b/tests/test_builders/test_build_manpage.py
@@ -1,5 +1,7 @@
"""Test the build process with manpage builder with the test root."""
+from __future__ import annotations
+
import docutils
import pytest
diff --git a/tests/test_builders/test_build_texinfo.py b/tests/test_builders/test_build_texinfo.py
index e77306c3c52..5a8ce2d5e02 100644
--- a/tests/test_builders/test_build_texinfo.py
+++ b/tests/test_builders/test_build_texinfo.py
@@ -1,5 +1,7 @@
"""Test the build process with Texinfo builder with the test root."""
+from __future__ import annotations
+
import re
import subprocess
from pathlib import Path
diff --git a/tests/test_builders/test_build_warnings.py b/tests/test_builders/test_build_warnings.py
index c89e33b93d5..7178abb646b 100644
--- a/tests/test_builders/test_build_warnings.py
+++ b/tests/test_builders/test_build_warnings.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os
import re
import sys
@@ -5,8 +7,8 @@
import pytest
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.errors import SphinxError
-from sphinx.util.console import strip_colors
ENV_WARNINGS = """\
{root}/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \
@@ -53,7 +55,7 @@
def _check_warnings(expected_warnings: str, warning: str) -> None:
- warnings = strip_colors(re.sub(re.escape(os.sep) + '{1,2}', '/', warning))
+ warnings = strip_escape_sequences(re.sub(re.escape(os.sep) + '{1,2}', '/', warning))
assert re.match(f'{expected_warnings}$', warnings), (
"Warnings don't match:\n"
f'--- Expected (regex):\n{expected_warnings}\n'
@@ -125,7 +127,7 @@ def setup(app):
tmp_path.joinpath('index.rst').write_text('Test\n====\n', encoding='utf-8')
app = make_app(srcdir=tmp_path)
app.build()
- assert strip_colors(app.warning.getvalue()).strip() == (
+ assert strip_escape_sequences(app.warning.getvalue()).strip() == (
"WARNING: cannot cache unpickable configuration value: 'my_config' "
'(because it contains a function, class, or module object) [config.cache]'
)
diff --git a/tests/test_builders/test_incremental_reading.py b/tests/test_builders/test_incremental_reading.py
index e1ecda7f4af..4ae1b77e1b1 100644
--- a/tests/test_builders/test_incremental_reading.py
+++ b/tests/test_builders/test_incremental_reading.py
@@ -1,5 +1,7 @@
"""Test the Builder class."""
+from __future__ import annotations
+
import sys
import pytest
diff --git a/tests/test_command_line.py b/tests/test_command_line.py
index e346784b5dd..3f35a495fcc 100644
--- a/tests/test_command_line.py
+++ b/tests/test_command_line.py
@@ -2,7 +2,7 @@
import sys
from pathlib import Path
-from typing import Any
+from typing import TYPE_CHECKING
import pytest
@@ -10,6 +10,9 @@
from sphinx.cmd.build import get_parser
from sphinx.cmd.make_mode import run_make_mode
+if TYPE_CHECKING:
+ from typing import Any
+
broken_argparse = (
sys.version_info[:3] <= (3, 12, 6)
or sys.version_info[:3] == (3, 13, 0)
diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py
index 78b35b83ab1..4b8321583e5 100644
--- a/tests/test_config/test_config.py
+++ b/tests/test_config/test_config.py
@@ -162,7 +162,7 @@ def test_config_pickle_circular_reference_in_list():
config = Config()
config.add('a', [], '', types=list)
- config.add('b', [], '', types=list)
+ config.add('b', [], '', types=frozenset({list}))
config.a, config.b = a, b
actual = pickle.loads(pickle.dumps(config))
@@ -216,7 +216,7 @@ def check(
assert v.__class__ is u.__class__
for u_i, v_i in zip(u, v, strict=True):
counter[type(u)] += 1
- check(u_i, v_i, counter=counter, guard=guard | {id(u), id(v)})
+ check(u_i, v_i, counter=counter, guard=guard | {id(u), id(v)}) # type: ignore[arg-type]
return counter
@@ -244,7 +244,7 @@ def test_config_pickle_circular_reference_in_dict():
check_is_serializable(x, circular=True)
config = Config()
- config.add('x', [], '', types=dict)
+ config.add('x', [], '', types=frozenset({dict}))
config.x = x
actual = pickle.loads(pickle.dumps(config))
@@ -280,7 +280,7 @@ def check(
assert v.__class__ is u.__class__
for u_i, v_i in zip(u, v, strict=True):
counter[type(u)] += 1
- check(u[u_i], v[v_i], counter=counter, guard=guard | {id(u), id(v)})
+ check(u[u_i], v[v_i], counter=counter, guard=guard | {id(u), id(v)}) # type: ignore[arg-type]
return counter
counters = check(actual.x, x, counter=Counter())
diff --git a/tests/test_config/test_copyright.py b/tests/test_config/test_copyright.py
index 72cef60709b..fdabcc08123 100644
--- a/tests/test_config/test_copyright.py
+++ b/tests/test_config/test_copyright.py
@@ -1,5 +1,7 @@
"""Test copyright year adjustment"""
+from __future__ import annotations
+
import time
import pytest
diff --git a/tests/test_directives/test_directive_code.py b/tests/test_directives/test_directive_code.py
index 0b81b65c1d8..422de5ee4d3 100644
--- a/tests/test_directives/test_directive_code.py
+++ b/tests/test_directives/test_directive_code.py
@@ -1,5 +1,8 @@
"""Test the code-block directive."""
+from __future__ import annotations
+
+import pygments
import pytest
from docutils import nodes
@@ -104,7 +107,8 @@ def test_LiteralIncludeReader_lines_and_lineno_match2(literal_inc_path, app):
options = {'lines': '0,3,5', 'lineno-match': True}
reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG)
with pytest.raises(
- ValueError, match='Cannot use "lineno-match" with a disjoint set of "lines"'
+ ValueError,
+ match='Cannot use "lineno-match" with a disjoint set of "lines"',
):
reader.read()
@@ -114,7 +118,8 @@ def test_LiteralIncludeReader_lines_and_lineno_match3(literal_inc_path, app):
options = {'lines': '100-', 'lineno-match': True}
reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG)
with pytest.raises(
- ValueError, match="Line spec '100-': no lines pulled from include file"
+ ValueError,
+ match="Line spec '100-': no lines pulled from include file",
):
reader.read()
@@ -356,7 +361,7 @@ def test_code_block_emphasize_latex(app):
.read_text(encoding='utf8')
.replace('\r\n', '\n')
)
- includes = '\\fvset{hllines={, 5, 6, 13, 14, 15, 24, 25, 26,}}%\n'
+ includes = '\\fvset{hllines={, 6, 7, 16, 17, 18, 19, 29, 30, 31,}}%\n'
assert includes in latex
includes = '\\end{sphinxVerbatim}\n\\sphinxresetverbatimhllines\n'
assert includes in latex
@@ -392,6 +397,11 @@ def test_literal_include_block_start_with_comment_or_brank(app):
@pytest.mark.sphinx('html', testroot='directive-code')
def test_literal_include_linenos(app):
+ if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19):
+ sp = ' '
+ else:
+ sp = ' '
+
app.build(filenames=[app.srcdir / 'linenos.rst'])
html = (app.outdir / 'linenos.html').read_text(encoding='utf8')
@@ -409,7 +419,7 @@ def test_literal_include_linenos(app):
# :lines: 5-9
assert (
- '5class '
+ f'5class{sp}'
'Foo:'
) in html
@@ -554,12 +564,17 @@ def test_code_block_highlighted(app):
@pytest.mark.sphinx('html', testroot='directive-code')
def test_linenothreshold(app):
+ if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19):
+ sp = ' '
+ else:
+ sp = ' '
+
app.build(filenames=[app.srcdir / 'linenothreshold.rst'])
html = (app.outdir / 'linenothreshold.html').read_text(encoding='utf8')
# code-block using linenothreshold
assert (
- '1class '
+ f'1class{sp}'
'Foo:'
) in html
diff --git a/tests/test_directives/test_directive_object_description.py b/tests/test_directives/test_directive_object_description.py
index 7f3ffba633a..501a6c9772a 100644
--- a/tests/test_directives/test_directive_object_description.py
+++ b/tests/test_directives/test_directive_object_description.py
@@ -14,21 +14,24 @@
from sphinx.util.docutils import sphinx_domains
if TYPE_CHECKING:
- from sphinx.builders import Builder
+ from sphinx.application import Sphinx
+ from sphinx.environment import BuildEnvironment
-def _doctree_for_test(builder: Builder, docname: str) -> nodes.document:
- builder.env.prepare_settings(docname)
- publisher = create_publisher(builder.app, 'restructuredtext')
- with sphinx_domains(builder.env):
- publisher.set_source(source_path=str(builder.env.doc2path(docname)))
+def _doctree_for_test(
+ app: Sphinx, env: BuildEnvironment, docname: str
+) -> nodes.document:
+ env.prepare_settings(docname)
+ publisher = create_publisher(app, 'restructuredtext')
+ with sphinx_domains(env):
+ publisher.set_source(source_path=str(env.doc2path(docname)))
publisher.publish()
return publisher.document
@pytest.mark.sphinx('text', testroot='object-description-sections')
def test_object_description_sections(app):
- doctree = _doctree_for_test(app.builder, 'index')
+ doctree = _doctree_for_test(app, app.env, 'index')
#
#
#
diff --git a/tests/test_directives/test_directive_only.py b/tests/test_directives/test_directive_only.py
index 297f304dfdb..7d5f15674c3 100644
--- a/tests/test_directives/test_directive_only.py
+++ b/tests/test_directives/test_directive_only.py
@@ -1,5 +1,7 @@
"""Test the only directive with the test root."""
+from __future__ import annotations
+
import re
import pytest
diff --git a/tests/test_directives/test_directive_option.py b/tests/test_directives/test_directive_option.py
index 8f07e538545..11369a4346d 100644
--- a/tests/test_directives/test_directive_option.py
+++ b/tests/test_directives/test_directive_option.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
diff --git a/tests/test_directives/test_directive_other.py b/tests/test_directives/test_directive_other.py
index 45a236bfd5a..3f09bb83789 100644
--- a/tests/test_directives/test_directive_other.py
+++ b/tests/test_directives/test_directive_other.py
@@ -1,5 +1,7 @@
"""Test the other directives."""
+from __future__ import annotations
+
from pathlib import Path
import pytest
diff --git a/tests/test_directives/test_directive_patch.py b/tests/test_directives/test_directive_patch.py
index a35c3f32e81..a141cc86682 100644
--- a/tests/test_directives/test_directive_patch.py
+++ b/tests/test_directives/test_directive_patch.py
@@ -1,5 +1,7 @@
"""Test the patched directives."""
+from __future__ import annotations
+
import pytest
from docutils import nodes
diff --git a/tests/test_directives/test_directives_no_typesetting.py b/tests/test_directives/test_directives_no_typesetting.py
index 8692cd387fa..68df4fbefcf 100644
--- a/tests/test_directives/test_directives_no_typesetting.py
+++ b/tests/test_directives/test_directives_no_typesetting.py
@@ -1,5 +1,7 @@
"""Tests the directives"""
+from __future__ import annotations
+
import pytest
from docutils import nodes
diff --git a/tests/test_domains/test_domain_c.py b/tests/test_domains/test_domain_c.py
index d90bcd0b7b5..6a5de744faa 100644
--- a/tests/test_domains/test_domain_c.py
+++ b/tests/test_domains/test_domain_c.py
@@ -1,9 +1,11 @@
"""Tests the C Domain"""
+from __future__ import annotations
+
import itertools
import xml.etree.ElementTree as ET
import zlib
-from io import StringIO
+from typing import TYPE_CHECKING
import pytest
@@ -20,20 +22,23 @@
desc_signature_line,
pending_xref,
)
-from sphinx.domains.c._ids import _id_prefix, _macroKeywords, _max_id
+from sphinx.domains.c._ids import _id_prefix, _macro_keywords, _max_id
from sphinx.domains.c._parser import DefinitionParser
from sphinx.domains.c._symbol import Symbol
-from sphinx.ext.intersphinx import load_mappings, validate_intersphinx_mapping
+from sphinx.ext.intersphinx._load import load_mappings, validate_intersphinx_mapping
from sphinx.testing import restructuredtext
from sphinx.testing.util import assert_node
from sphinx.util.cfamily import DefinitionError
from sphinx.writers.text import STDINDENT
+if TYPE_CHECKING:
+ from io import StringIO
+
class Config:
c_id_attributes = ['id_attr', 'LIGHTGBM_C_EXPORT']
c_paren_attributes = ['paren_attr']
- c_extra_keywords = _macroKeywords
+ c_extra_keywords = _macro_keywords
def parse(name, string):
@@ -44,62 +49,62 @@ def parse(name, string):
return ast
-def _check(name, input, idDict, output, key, asTextOutput):
+def _check(name, input, id_dict, output, key, as_text_output):
if key is None:
key = name
key += ' '
if name in {'function', 'member'}:
- inputActual = input
- outputAst = output
- outputAsText = output
+ input_actual = input
+ output_ast = output
+ output_as_text = output
else:
- inputActual = input.format(key='')
- outputAst = output.format(key='')
- outputAsText = output.format(key=key)
- if asTextOutput is not None:
- outputAsText = asTextOutput
+ input_actual = input.format(key='')
+ output_ast = output.format(key='')
+ output_as_text = output.format(key=key)
+ if as_text_output is not None:
+ output_as_text = as_text_output
# first a simple check of the AST
- ast = parse(name, inputActual)
+ ast = parse(name, input_actual)
res = str(ast)
- if res != outputAst:
+ if res != output_ast:
print()
print('Input: ', input)
print('Result: ', res)
- print('Expected: ', outputAst)
+ print('Expected: ', output_ast)
raise DefinitionError
- rootSymbol = Symbol(None, None, None, None, None)
- symbol = rootSymbol.add_declaration(ast, docname='TestDoc', line=42)
- parentNode = addnodes.desc()
+ root_symbol = Symbol(None, None, None, None, None)
+ symbol = root_symbol.add_declaration(ast, docname='TestDoc', line=42)
+ parent_node = addnodes.desc()
signode = addnodes.desc_signature(input, '')
- parentNode += signode
+ parent_node += signode
ast.describe_signature(signode, 'lastIsName', symbol, options={})
- resAsText = parentNode.astext()
- if resAsText != outputAsText:
+ res_as_text = parent_node.astext()
+ if res_as_text != output_as_text:
print()
print('Input: ', input)
- print('astext(): ', resAsText)
- print('Expected: ', outputAsText)
+ print('astext(): ', res_as_text)
+ print('Expected: ', output_as_text)
raise DefinitionError
- idExpected = [None]
+ id_expected = [None]
for i in range(1, _max_id + 1):
- if i in idDict:
- idExpected.append(idDict[i])
+ if i in id_dict:
+ id_expected.append(id_dict[i])
else:
- idExpected.append(idExpected[i - 1])
- idActual = [None]
+ id_expected.append(id_expected[i - 1])
+ id_actual = [None]
for i in range(1, _max_id + 1):
# try:
id = ast.get_id(version=i)
assert id is not None
- idActual.append(id[len(_id_prefix[i]) :])
+ id_actual.append(id[len(_id_prefix[i]) :])
# except NoOldIdError:
- # idActual.append(None)
+ # id_actual.append(None)
res = [True]
for i in range(1, _max_id + 1):
- res.append(idExpected[i] == idActual[i])
+ res.append(id_expected[i] == id_actual[i])
if not all(res):
print('input: %s' % input.rjust(20))
@@ -107,31 +112,31 @@ def _check(name, input, idDict, output, key, asTextOutput):
if res[i]:
continue
print('Error in id version %d.' % i)
- print('result: %s' % idActual[i])
- print('expected: %s' % idExpected[i])
- # print(rootSymbol.dump(0))
+ print('result: %s' % id_actual[i])
+ print('expected: %s' % id_expected[i])
+ # print(root_symbol.dump(0))
raise DefinitionError
-def check(name, input, idDict, output=None, key=None, asTextOutput=None):
+def check(name, input, id_dict, output=None, key=None, as_text_output=None):
if output is None:
output = input
# First, check without semicolon
- _check(name, input, idDict, output, key, asTextOutput)
+ _check(name, input, id_dict, output, key, as_text_output)
if name != 'macro':
# Second, check with semicolon
_check(
name,
input + ' ;',
- idDict,
+ id_dict,
output + ';',
key,
- asTextOutput + ';' if asTextOutput is not None else None,
+ as_text_output + ';' if as_text_output is not None else None,
)
def test_domain_c_ast_expressions():
- def exprCheck(expr, output=None):
+ def expr_check(expr, output=None):
parser = DefinitionParser(expr, location=None, config=Config())
parser.allowFallbackExpressionParsing = False
ast = parser.parse_expression()
@@ -146,30 +151,30 @@ def exprCheck(expr, output=None):
print('Result: ', res)
print('Expected: ', output)
raise DefinitionError
- displayString = ast.get_display_string()
- if res != displayString:
+ display_string = ast.get_display_string()
+ if res != display_string:
# note: if the expression contains an anon name then this will trigger a falsely
print()
print('Input: ', expr)
print('Result: ', res)
- print('Display: ', displayString)
+ print('Display: ', display_string)
raise DefinitionError
# type expressions
- exprCheck('int*')
- exprCheck('int *const*')
- exprCheck('int *volatile*')
- exprCheck('int *restrict*')
- exprCheck('int *(*)(double)')
- exprCheck('const int*')
- exprCheck('__int64')
- exprCheck('unsigned __int64')
+ expr_check('int*')
+ expr_check('int *const*')
+ expr_check('int *volatile*')
+ expr_check('int *restrict*')
+ expr_check('int *(*)(double)')
+ expr_check('const int*')
+ expr_check('__int64')
+ expr_check('unsigned __int64')
# actual expressions
# primary
- exprCheck('true')
- exprCheck('false')
+ expr_check('true')
+ expr_check('false')
ints = [
'5',
'0',
@@ -183,15 +188,15 @@ def exprCheck(expr, output=None):
"0x0'1'2",
"1'2'3",
]
- unsignedSuffix = ['', 'u', 'U']
- longSuffix = ['', 'l', 'L', 'll', 'LL']
+ unsigned_suffix = ['', 'u', 'U']
+ long_suffix = ['', 'l', 'L', 'll', 'LL']
for i in ints:
- for u in unsignedSuffix:
- for l in longSuffix:
+ for u in unsigned_suffix:
+ for l in long_suffix:
expr = i + u + l
- exprCheck(expr)
+ expr_check(expr)
expr = i + l + u
- exprCheck(expr)
+ expr_check(expr)
for suffix in ('', 'f', 'F', 'l', 'L'):
for e in (
'5e42',
@@ -215,7 +220,7 @@ def exprCheck(expr, output=None):
"1'2'3.4'5'6e7'8'9",
):
expr = e + suffix
- exprCheck(expr)
+ expr_check(expr)
for e in (
'ApF',
'Ap+F',
@@ -238,94 +243,97 @@ def exprCheck(expr, output=None):
"A'B'C.D'E'Fp1'2'3",
):
expr = '0x' + e + suffix
- exprCheck(expr)
- exprCheck('"abc\\"cba"') # string
+ expr_check(expr)
+ expr_check('"abc\\"cba"') # string
# character literals
for p in ('', 'u8', 'u', 'U', 'L'):
- exprCheck(p + "'a'")
- exprCheck(p + "'\\n'")
- exprCheck(p + "'\\012'")
- exprCheck(p + "'\\0'")
- exprCheck(p + "'\\x0a'")
- exprCheck(p + "'\\x0A'")
- exprCheck(p + "'\\u0a42'")
- exprCheck(p + "'\\u0A42'")
- exprCheck(p + "'\\U0001f34c'")
- exprCheck(p + "'\\U0001F34C'")
-
- exprCheck('(5)')
- exprCheck('C')
+ expr_check(p + "'a'")
+ expr_check(p + "'\\n'")
+ expr_check(p + "'\\012'")
+ expr_check(p + "'\\0'")
+ expr_check(p + "'\\x0a'")
+ expr_check(p + "'\\x0A'")
+ expr_check(p + "'\\u0a42'")
+ expr_check(p + "'\\u0A42'")
+ expr_check(p + "'\\U0001f34c'")
+ expr_check(p + "'\\U0001F34C'")
+
+ expr_check('(5)')
+ expr_check('C')
# postfix
- exprCheck('A(2)')
- exprCheck('A[2]')
- exprCheck('a.b.c')
- exprCheck('a->b->c')
- exprCheck('i++')
- exprCheck('i--')
+ expr_check('A(2)')
+ expr_check('A[2]')
+ expr_check('a.b.c')
+ expr_check('a->b->c')
+ expr_check('i++')
+ expr_check('i--')
# unary
- exprCheck('++5')
- exprCheck('--5')
- exprCheck('*5')
- exprCheck('&5')
- exprCheck('+5')
- exprCheck('-5')
- exprCheck('!5')
- exprCheck('not 5')
- exprCheck('~5')
- exprCheck('compl 5')
- exprCheck('sizeof(T)')
- exprCheck('sizeof -42')
- exprCheck('alignof(T)')
+ expr_check('++5')
+ expr_check('--5')
+ expr_check('*5')
+ expr_check('&5')
+ expr_check('+5')
+ expr_check('-5')
+ expr_check('!5')
+ expr_check('not 5')
+ expr_check('~5')
+ expr_check('compl 5')
+ expr_check('sizeof(T)')
+ expr_check('sizeof -42')
+ expr_check('alignof(T)')
# cast
- exprCheck('(int)2')
+ expr_check('(int)2')
# binary op
- exprCheck('5 || 42')
- exprCheck('5 or 42')
- exprCheck('5 && 42')
- exprCheck('5 and 42')
- exprCheck('5 | 42')
- exprCheck('5 bitor 42')
- exprCheck('5 ^ 42')
- exprCheck('5 xor 42')
- exprCheck('5 & 42')
- exprCheck('5 bitand 42')
+ expr_check('5 || 42')
+ expr_check('5 or 42')
+ expr_check('5 && 42')
+ expr_check('5 and 42')
+ expr_check('5 | 42')
+ expr_check('5 bitor 42')
+ expr_check('5 ^ 42')
+ expr_check('5 xor 42')
+ expr_check('5 & 42')
+ expr_check('5 bitand 42')
# ['==', '!=']
- exprCheck('5 == 42')
- exprCheck('5 != 42')
- exprCheck('5 not_eq 42')
+ expr_check('5 == 42')
+ expr_check('5 != 42')
+ expr_check('5 not_eq 42')
# ['<=', '>=', '<', '>']
- exprCheck('5 <= 42')
- exprCheck('5 >= 42')
- exprCheck('5 < 42')
- exprCheck('5 > 42')
+ expr_check('5 <= 42')
+ expr_check('5 >= 42')
+ expr_check('5 < 42')
+ expr_check('5 > 42')
# ['<<', '>>']
- exprCheck('5 << 42')
- exprCheck('5 >> 42')
+ expr_check('5 << 42')
+ expr_check('5 >> 42')
# ['+', '-']
- exprCheck('5 + 42')
- exprCheck('5 - 42')
+ expr_check('5 + 42')
+ expr_check('5 - 42')
# ['*', '/', '%']
- exprCheck('5 * 42')
- exprCheck('5 / 42')
- exprCheck('5 % 42')
+ expr_check('5 * 42')
+ expr_check('5 / 42')
+ expr_check('5 % 42')
# ['.*', '->*']
+ expr_check('5 .* 42')
+ expr_check('5 ->* 42')
+ # TODO: conditional is unimplemented
# conditional
- # TODO
+ # expr_check('5 ? 7 : 3')
# assignment
- exprCheck('a = 5')
- exprCheck('a *= 5')
- exprCheck('a /= 5')
- exprCheck('a %= 5')
- exprCheck('a += 5')
- exprCheck('a -= 5')
- exprCheck('a >>= 5')
- exprCheck('a <<= 5')
- exprCheck('a &= 5')
- exprCheck('a and_eq 5')
- exprCheck('a ^= 5')
- exprCheck('a xor_eq 5')
- exprCheck('a |= 5')
- exprCheck('a or_eq 5')
+ expr_check('a = 5')
+ expr_check('a *= 5')
+ expr_check('a /= 5')
+ expr_check('a %= 5')
+ expr_check('a += 5')
+ expr_check('a -= 5')
+ expr_check('a >>= 5')
+ expr_check('a <<= 5')
+ expr_check('a &= 5')
+ expr_check('a and_eq 5')
+ expr_check('a ^= 5')
+ expr_check('a xor_eq 5')
+ expr_check('a |= 5')
+ expr_check('a or_eq 5')
def test_domain_c_ast_fundamental_types():
@@ -583,29 +591,29 @@ def test_domain_c_ast_enum_definitions():
def test_domain_c_ast_anon_definitions():
- check('struct', '@a', {1: '@a'}, asTextOutput='struct [anonymous]')
- check('union', '@a', {1: '@a'}, asTextOutput='union [anonymous]')
- check('enum', '@a', {1: '@a'}, asTextOutput='enum [anonymous]')
- check('struct', '@1', {1: '@1'}, asTextOutput='struct [anonymous]')
- check('struct', '@a.A', {1: '@a.A'}, asTextOutput='struct [anonymous].A')
+ check('struct', '@a', {1: '@a'}, as_text_output='struct [anonymous]')
+ check('union', '@a', {1: '@a'}, as_text_output='union [anonymous]')
+ check('enum', '@a', {1: '@a'}, as_text_output='enum [anonymous]')
+ check('struct', '@1', {1: '@1'}, as_text_output='struct [anonymous]')
+ check('struct', '@a.A', {1: '@a.A'}, as_text_output='struct [anonymous].A')
def test_domain_c_ast_initializers():
- idsMember = {1: 'v'}
- idsFunction = {1: 'f'}
+ ids_member = {1: 'v'}
+ ids_function = {1: 'f'}
# no init
- check('member', 'T v', idsMember)
- check('function', 'void f(T v)', idsFunction)
+ check('member', 'T v', ids_member)
+ check('function', 'void f(T v)', ids_function)
# with '=', assignment-expression
- check('member', 'T v = 42', idsMember)
- check('function', 'void f(T v = 42)', idsFunction)
+ check('member', 'T v = 42', ids_member)
+ check('function', 'void f(T v = 42)', ids_function)
# with '=', braced-init
- check('member', 'T v = {}', idsMember)
- check('function', 'void f(T v = {})', idsFunction)
- check('member', 'T v = {42, 42, 42}', idsMember)
- check('function', 'void f(T v = {42, 42, 42})', idsFunction)
- check('member', 'T v = {42, 42, 42,}', idsMember)
- check('function', 'void f(T v = {42, 42, 42,})', idsFunction)
+ check('member', 'T v = {}', ids_member)
+ check('function', 'void f(T v = {})', ids_function)
+ check('member', 'T v = {42, 42, 42}', ids_member)
+ check('function', 'void f(T v = {42, 42, 42})', ids_function)
+ check('member', 'T v = {42, 42, 42,}', ids_member)
+ check('function', 'void f(T v = {42, 42, 42,})', ids_function)
# TODO: designator-list
@@ -718,9 +726,9 @@ def extract_role_links(app, filename):
entries = []
for l in lis:
li = ET.fromstring(l) # NoQA: S314 # using known data in tests
- aList = list(li.iter('a'))
- assert len(aList) == 1
- a = aList[0]
+ a_list = list(li.iter('a'))
+ assert len(a_list) == 1
+ a = a_list[0]
target = a.attrib['href'].lstrip('#')
title = a.attrib['title']
assert len(a) == 1
@@ -803,12 +811,12 @@ def test_domain_c_build_field_role(app):
assert len(ws) == 0
-def _get_obj(app, queryName):
+def _get_obj(app, query_name):
domain = app.env.domains.c_domain
- for name, _dispname, objectType, docname, anchor, _prio in domain.get_objects():
- if name == queryName:
- return docname, anchor, objectType
- return queryName, 'not', 'found'
+ for name, _dispname, object_type, docname, anchor, _prio in domain.get_objects():
+ if name == query_name:
+ return docname, anchor, object_type
+ return query_name, 'not', 'found'
@pytest.mark.sphinx(
@@ -817,7 +825,7 @@ def _get_obj(app, queryName):
def test_domain_c_build_intersphinx(tmp_path, app):
# a splitting of test_ids_vs_tags0 into the primary directives in a remote project,
# and then the references in the test project
- origSource = """\
+ orig_source = """\
.. c:member:: int _member
.. c:var:: int _var
.. c:function:: void _function()
@@ -830,7 +838,7 @@ def test_domain_c_build_intersphinx(tmp_path, app):
.. c:type:: _type
.. c:function:: void _functionParam(int param)
-""" # NoQA: F841
+"""
inv_file = tmp_path / 'inventory'
inv_file.write_bytes(
b"""\
@@ -853,7 +861,7 @@ def test_domain_c_build_intersphinx(tmp_path, app):
_union c:union 1 index.html#c.$ -
_var c:member 1 index.html#c.$ -
""")
- ) # NoQA: W291
+ )
app.config.intersphinx_mapping = {
'local': ('https://localhost/intersphinx/c/', str(inv_file)),
}
@@ -1366,7 +1374,7 @@ def test_domain_c_c_maximum_signature_line_length_in_html(app):
\
str\
\
-name,\
+name\
@@ -1387,6 +1395,6 @@ def test_domain_c_c_maximum_signature_line_length_in_text(app):
content = (app.outdir / 'index.txt').read_text(encoding='utf8')
param_line_fmt = STDINDENT * ' ' + '{}\n'
- expected_parameter_list_hello = '(\n{})'.format(param_line_fmt.format('str name,'))
+ expected_parameter_list_hello = '(\n{})'.format(param_line_fmt.format('str name'))
assert expected_parameter_list_hello in content
diff --git a/tests/test_domains/test_domain_cpp.py b/tests/test_domains/test_domain_cpp.py
index a177991a535..f0de741e925 100644
--- a/tests/test_domains/test_domain_cpp.py
+++ b/tests/test_domains/test_domain_cpp.py
@@ -1,9 +1,11 @@
"""Tests the C++ Domain"""
+from __future__ import annotations
+
import itertools
import re
import zlib
-from io import StringIO
+from typing import TYPE_CHECKING
import pytest
@@ -24,12 +26,15 @@
from sphinx.domains.cpp._ids import _id_prefix, _max_id
from sphinx.domains.cpp._parser import DefinitionParser
from sphinx.domains.cpp._symbol import Symbol
-from sphinx.ext.intersphinx import load_mappings, validate_intersphinx_mapping
+from sphinx.ext.intersphinx._load import load_mappings, validate_intersphinx_mapping
from sphinx.testing import restructuredtext
from sphinx.testing.util import assert_node
from sphinx.util.cfamily import DefinitionError, NoOldIdError
from sphinx.writers.text import STDINDENT
+if TYPE_CHECKING:
+ from io import StringIO
+
def parse(name, string):
class Config:
@@ -46,63 +51,63 @@ class Config:
return ast
-def _check(name, input, idDict, output, key, asTextOutput):
+def _check(name, input, id_dict, output, key, as_text_output):
if key is None:
key = name
key += ' '
if name in {'function', 'member'}:
- inputActual = input
- outputAst = output
- outputAsText = output
+ input_actual = input
+ output_ast = output
+ output_as_text = output
else:
- inputActual = input.format(key='')
- outputAst = output.format(key='')
- outputAsText = output.format(key=key)
- if asTextOutput is not None:
- outputAsText = asTextOutput
+ input_actual = input.format(key='')
+ output_ast = output.format(key='')
+ output_as_text = output.format(key=key)
+ if as_text_output is not None:
+ output_as_text = as_text_output
# first a simple check of the AST
- ast = parse(name, inputActual)
+ ast = parse(name, input_actual)
res = str(ast)
- if res != outputAst:
+ if res != output_ast:
print()
print('Input: ', input)
print('Result: ', res)
- print('Expected: ', outputAst)
+ print('Expected: ', output_ast)
raise DefinitionError
- rootSymbol = Symbol(None, None, None, None, None, None, None)
- symbol = rootSymbol.add_declaration(ast, docname='TestDoc', line=42)
- parentNode = addnodes.desc()
+ root_symbol = Symbol(None, None, None, None, None, None, None)
+ symbol = root_symbol.add_declaration(ast, docname='TestDoc', line=42)
+ parent_node = addnodes.desc()
signode = addnodes.desc_signature(input, '')
- parentNode += signode
+ parent_node += signode
ast.describe_signature(signode, 'lastIsName', symbol, options={})
- resAsText = parentNode.astext()
- if resAsText != outputAsText:
+ res_as_text = parent_node.astext()
+ if res_as_text != output_as_text:
print()
print('Input: ', input)
- print('astext(): ', resAsText)
- print('Expected: ', outputAsText)
- print('Node:', parentNode)
+ print('astext(): ', res_as_text)
+ print('Expected: ', output_as_text)
+ print('Node:', parent_node)
raise DefinitionError
- idExpected = [None]
+ id_expected = [None]
for i in range(1, _max_id + 1):
- if i in idDict:
- idExpected.append(idDict[i])
+ if i in id_dict:
+ id_expected.append(id_dict[i])
else:
- idExpected.append(idExpected[i - 1])
- idActual = [None]
+ id_expected.append(id_expected[i - 1])
+ id_actual = [None]
for i in range(1, _max_id + 1):
try:
id = ast.get_id(version=i)
assert id is not None
- idActual.append(id[len(_id_prefix[i]) :])
+ id_actual.append(id[len(_id_prefix[i]) :])
except NoOldIdError:
- idActual.append(None)
+ id_actual.append(None)
res = [True]
for i in range(1, _max_id + 1):
- res.append(idExpected[i] == idActual[i])
+ res.append(id_expected[i] == id_actual[i])
if not all(res):
print('input: %s' % input.rjust(20))
@@ -110,25 +115,25 @@ def _check(name, input, idDict, output, key, asTextOutput):
if res[i]:
continue
print('Error in id version %d.' % i)
- print('result: %s' % idActual[i])
- print('expected: %s' % idExpected[i])
- print(rootSymbol.dump(0))
+ print('result: %s' % id_actual[i])
+ print('expected: %s' % id_expected[i])
+ print(root_symbol.dump(0))
raise DefinitionError
-def check(name, input, idDict, output=None, key=None, asTextOutput=None):
+def check(name, input, id_dict, output=None, key=None, as_text_output=None):
if output is None:
output = input
# First, check without semicolon
- _check(name, input, idDict, output, key, asTextOutput)
+ _check(name, input, id_dict, output, key, as_text_output)
# Second, check with semicolon
_check(
name,
input + ' ;',
- idDict,
+ id_dict,
output + ';',
key,
- asTextOutput + ';' if asTextOutput is not None else None,
+ as_text_output + ';' if as_text_output is not None else None,
)
@@ -172,13 +177,13 @@ def make_id_v2():
def test_domain_cpp_ast_expressions():
- def exprCheck(expr, id, id4=None):
+ def expr_check(expr, id, id4=None):
ids = 'IE1CIA%s_1aE'
# call .format() on the expr to unescape double curly braces
- idDict = {2: ids % expr.format(), 3: ids % id}
+ id_dict = {2: ids % expr.format(), 3: ids % id}
if id4 is not None:
- idDict[4] = ids % id4
- check('class', 'template<> {key}C' % expr, idDict)
+ id_dict[4] = ids % id4
+ check('class', 'template<> {key}C' % expr, id_dict)
class Config:
cpp_id_attributes = ['id_attr']
@@ -193,19 +198,19 @@ class Config:
print('Input: ', expr)
print('Result: ', res)
raise DefinitionError
- displayString = ast.get_display_string()
- if res != displayString:
+ display_string = ast.get_display_string()
+ if res != display_string:
# note: if the expression contains an anon name then this will trigger a falsely
print()
print('Input: ', expr)
print('Result: ', res)
- print('Display: ', displayString)
+ print('Display: ', display_string)
raise DefinitionError
# primary
- exprCheck('nullptr', 'LDnE')
- exprCheck('true', 'L1E')
- exprCheck('false', 'L0E')
+ expr_check('nullptr', 'LDnE')
+ expr_check('true', 'L1E')
+ expr_check('false', 'L0E')
ints = [
'5',
'0',
@@ -219,16 +224,16 @@ class Config:
"0x0'1'2",
"1'2'3",
]
- unsignedSuffix = ['', 'u', 'U']
- longSuffix = ['', 'l', 'L', 'll', 'LL']
+ unsigned_suffix = ['', 'u', 'U']
+ long_suffix = ['', 'l', 'L', 'll', 'LL']
for i in ints:
- for u in unsignedSuffix:
- for l in longSuffix:
+ for u in unsigned_suffix:
+ for l in long_suffix:
expr = i + u + l
- exprCheck(expr, 'L' + expr.replace("'", '') + 'E')
+ expr_check(expr, 'L' + expr.replace("'", '') + 'E')
expr = i + l + u
- exprCheck(expr, 'L' + expr.replace("'", '') + 'E')
- decimalFloats = [
+ expr_check(expr, 'L' + expr.replace("'", '') + 'E')
+ decimal_floats = [
'5e42',
'5e+42',
'5e-42',
@@ -249,7 +254,7 @@ class Config:
".4'5'6e7'8'9",
"1'2'3.4'5'6e7'8'9",
]
- hexFloats = [
+ hex_floats = [
'ApF',
'Ap+F',
'Ap-F',
@@ -271,16 +276,16 @@ class Config:
"A'B'C.D'E'Fp1'2'3",
]
for suffix in ('', 'f', 'F', 'l', 'L'):
- for e in decimalFloats:
+ for e in decimal_floats:
expr = e + suffix
- exprCheck(expr, 'L' + expr.replace("'", '') + 'E')
- for e in hexFloats:
+ expr_check(expr, 'L' + expr.replace("'", '') + 'E')
+ for e in hex_floats:
expr = '0x' + e + suffix
- exprCheck(expr, 'L' + expr.replace("'", '') + 'E')
- exprCheck('"abc\\"cba"', 'LA8_KcE') # string
- exprCheck('this', 'fpT')
+ expr_check(expr, 'L' + expr.replace("'", '') + 'E')
+ expr_check('"abc\\"cba"', 'LA8_KcE') # string
+ expr_check('this', 'fpT')
# character literals
- charPrefixAndIds = [('', 'c'), ('u8', 'c'), ('u', 'Ds'), ('U', 'Di'), ('L', 'w')]
+ char_prefix_and_ids = [('', 'c'), ('u8', 'c'), ('u', 'Ds'), ('U', 'Di'), ('L', 'w')]
chars = [
('a', '97'),
('\\n', '10'),
@@ -293,156 +298,156 @@ class Config:
('\\U0001f34c', '127820'),
('\\U0001F34C', '127820'),
]
- for p, t in charPrefixAndIds:
+ for p, t in char_prefix_and_ids:
for c, val in chars:
- exprCheck(f"{p}'{c}'", t + val)
+ expr_check(f"{p}'{c}'", t + val)
# user-defined literals
for i in ints:
- exprCheck(i + '_udl', 'clL_Zli4_udlEL' + i.replace("'", '') + 'EE')
- exprCheck(i + 'uludl', 'clL_Zli5uludlEL' + i.replace("'", '') + 'EE')
- for f in decimalFloats:
- exprCheck(f + '_udl', 'clL_Zli4_udlEL' + f.replace("'", '') + 'EE')
- exprCheck(f + 'fudl', 'clL_Zli4fudlEL' + f.replace("'", '') + 'EE')
- for f in hexFloats:
- exprCheck('0x' + f + '_udl', 'clL_Zli4_udlEL0x' + f.replace("'", '') + 'EE')
- for p, t in charPrefixAndIds:
+ expr_check(i + '_udl', 'clL_Zli4_udlEL' + i.replace("'", '') + 'EE')
+ expr_check(i + 'uludl', 'clL_Zli5uludlEL' + i.replace("'", '') + 'EE')
+ for f in decimal_floats:
+ expr_check(f + '_udl', 'clL_Zli4_udlEL' + f.replace("'", '') + 'EE')
+ expr_check(f + 'fudl', 'clL_Zli4fudlEL' + f.replace("'", '') + 'EE')
+ for f in hex_floats:
+ expr_check('0x' + f + '_udl', 'clL_Zli4_udlEL0x' + f.replace("'", '') + 'EE')
+ for p, t in char_prefix_and_ids:
for c, val in chars:
- exprCheck(f"{p}'{c}'_udl", 'clL_Zli4_udlE' + t + val + 'E')
- exprCheck('"abc"_udl', 'clL_Zli4_udlELA3_KcEE')
+ expr_check(f"{p}'{c}'_udl", 'clL_Zli4_udlE' + t + val + 'E')
+ expr_check('"abc"_udl', 'clL_Zli4_udlELA3_KcEE')
# from issue #7294
- exprCheck('6.62607015e-34q_J', 'clL_Zli3q_JEL6.62607015e-34EE')
+ expr_check('6.62607015e-34q_J', 'clL_Zli3q_JEL6.62607015e-34EE')
# fold expressions, paren, name
- exprCheck('(... + Ns)', '(... + Ns)', id4='flpl2Ns')
- exprCheck('(Ns + ...)', '(Ns + ...)', id4='frpl2Ns')
- exprCheck('(Ns + ... + 0)', '(Ns + ... + 0)', id4='fLpl2NsL0E')
- exprCheck('(5)', 'L5E')
- exprCheck('C', '1C')
+ expr_check('(... + Ns)', '(... + Ns)', id4='flpl2Ns')
+ expr_check('(Ns + ...)', '(Ns + ...)', id4='frpl2Ns')
+ expr_check('(Ns + ... + 0)', '(Ns + ... + 0)', id4='fLpl2NsL0E')
+ expr_check('(5)', 'L5E')
+ expr_check('C', '1C')
# postfix
- exprCheck('A(2)', 'cl1AL2EE')
- exprCheck('A[2]', 'ix1AL2E')
- exprCheck('a.b.c', 'dtdt1a1b1c')
- exprCheck('a->b->c', 'ptpt1a1b1c')
- exprCheck('i++', 'pp1i')
- exprCheck('i--', 'mm1i')
- exprCheck('dynamic_cast(i)++', 'ppdcR1T1i')
- exprCheck('static_cast(i)++', 'ppscR1T1i')
- exprCheck('reinterpret_cast(i)++', 'pprcR1T1i')
- exprCheck('const_cast(i)++', 'ppccR1T1i')
- exprCheck('typeid(T).name', 'dtti1T4name')
- exprCheck('typeid(a + b).name', 'dttepl1a1b4name')
+ expr_check('A(2)', 'cl1AL2EE')
+ expr_check('A[2]', 'ix1AL2E')
+ expr_check('a.b.c', 'dtdt1a1b1c')
+ expr_check('a->b->c', 'ptpt1a1b1c')
+ expr_check('i++', 'pp1i')
+ expr_check('i--', 'mm1i')
+ expr_check('dynamic_cast(i)++', 'ppdcR1T1i')
+ expr_check('static_cast(i)++', 'ppscR1T1i')
+ expr_check('reinterpret_cast(i)++', 'pprcR1T1i')
+ expr_check('const_cast(i)++', 'ppccR1T1i')
+ expr_check('typeid(T).name', 'dtti1T4name')
+ expr_check('typeid(a + b).name', 'dttepl1a1b4name')
# unary
- exprCheck('++5', 'pp_L5E')
- exprCheck('--5', 'mm_L5E')
- exprCheck('*5', 'deL5E')
- exprCheck('&5', 'adL5E')
- exprCheck('+5', 'psL5E')
- exprCheck('-5', 'ngL5E')
- exprCheck('!5', 'ntL5E')
- exprCheck('not 5', 'ntL5E')
- exprCheck('~5', 'coL5E')
- exprCheck('compl 5', 'coL5E')
- exprCheck('sizeof...(a)', 'sZ1a')
- exprCheck('sizeof(T)', 'st1T')
- exprCheck('sizeof -42', 'szngL42E')
- exprCheck('alignof(T)', 'at1T')
- exprCheck('noexcept(-42)', 'nxngL42E')
+ expr_check('++5', 'pp_L5E')
+ expr_check('--5', 'mm_L5E')
+ expr_check('*5', 'deL5E')
+ expr_check('&5', 'adL5E')
+ expr_check('+5', 'psL5E')
+ expr_check('-5', 'ngL5E')
+ expr_check('!5', 'ntL5E')
+ expr_check('not 5', 'ntL5E')
+ expr_check('~5', 'coL5E')
+ expr_check('compl 5', 'coL5E')
+ expr_check('sizeof...(a)', 'sZ1a')
+ expr_check('sizeof(T)', 'st1T')
+ expr_check('sizeof -42', 'szngL42E')
+ expr_check('alignof(T)', 'at1T')
+ expr_check('noexcept(-42)', 'nxngL42E')
# new-expression
- exprCheck('new int', 'nw_iE')
- exprCheck('new volatile int', 'nw_ViE')
- exprCheck('new int[42]', 'nw_AL42E_iE')
- exprCheck('new int()', 'nw_ipiE')
- exprCheck('new int(5, 42)', 'nw_ipiL5EL42EE')
- exprCheck('::new int', 'nw_iE')
- exprCheck('new int{{}}', 'nw_iilE')
- exprCheck('new int{{5, 42}}', 'nw_iilL5EL42EE')
+ expr_check('new int', 'nw_iE')
+ expr_check('new volatile int', 'nw_ViE')
+ expr_check('new int[42]', 'nw_AL42E_iE')
+ expr_check('new int()', 'nw_ipiE')
+ expr_check('new int(5, 42)', 'nw_ipiL5EL42EE')
+ expr_check('::new int', 'nw_iE')
+ expr_check('new int{{}}', 'nw_iilE')
+ expr_check('new int{{5, 42}}', 'nw_iilL5EL42EE')
# delete-expression
- exprCheck('delete p', 'dl1p')
- exprCheck('delete [] p', 'da1p')
- exprCheck('::delete p', 'dl1p')
- exprCheck('::delete [] p', 'da1p')
+ expr_check('delete p', 'dl1p')
+ expr_check('delete [] p', 'da1p')
+ expr_check('::delete p', 'dl1p')
+ expr_check('::delete [] p', 'da1p')
# cast
- exprCheck('(int)2', 'cviL2E')
+ expr_check('(int)2', 'cviL2E')
# binary op
- exprCheck('5 || 42', 'ooL5EL42E')
- exprCheck('5 or 42', 'ooL5EL42E')
- exprCheck('5 && 42', 'aaL5EL42E')
- exprCheck('5 and 42', 'aaL5EL42E')
- exprCheck('5 | 42', 'orL5EL42E')
- exprCheck('5 bitor 42', 'orL5EL42E')
- exprCheck('5 ^ 42', 'eoL5EL42E')
- exprCheck('5 xor 42', 'eoL5EL42E')
- exprCheck('5 & 42', 'anL5EL42E')
- exprCheck('5 bitand 42', 'anL5EL42E')
+ expr_check('5 || 42', 'ooL5EL42E')
+ expr_check('5 or 42', 'ooL5EL42E')
+ expr_check('5 && 42', 'aaL5EL42E')
+ expr_check('5 and 42', 'aaL5EL42E')
+ expr_check('5 | 42', 'orL5EL42E')
+ expr_check('5 bitor 42', 'orL5EL42E')
+ expr_check('5 ^ 42', 'eoL5EL42E')
+ expr_check('5 xor 42', 'eoL5EL42E')
+ expr_check('5 & 42', 'anL5EL42E')
+ expr_check('5 bitand 42', 'anL5EL42E')
# ['==', '!=']
- exprCheck('5 == 42', 'eqL5EL42E')
- exprCheck('5 != 42', 'neL5EL42E')
- exprCheck('5 not_eq 42', 'neL5EL42E')
+ expr_check('5 == 42', 'eqL5EL42E')
+ expr_check('5 != 42', 'neL5EL42E')
+ expr_check('5 not_eq 42', 'neL5EL42E')
# ['<=', '>=', '<', '>', '<=>']
- exprCheck('5 <= 42', 'leL5EL42E')
- exprCheck('A <= 42', 'le1AL42E')
- exprCheck('5 >= 42', 'geL5EL42E')
- exprCheck('5 < 42', 'ltL5EL42E')
- exprCheck('A < 42', 'lt1AL42E')
- exprCheck('5 > 42', 'gtL5EL42E')
- exprCheck('A > 42', 'gt1AL42E')
- exprCheck('5 <=> 42', 'ssL5EL42E')
- exprCheck('A <=> 42', 'ss1AL42E')
+ expr_check('5 <= 42', 'leL5EL42E')
+ expr_check('A <= 42', 'le1AL42E')
+ expr_check('5 >= 42', 'geL5EL42E')
+ expr_check('5 < 42', 'ltL5EL42E')
+ expr_check('A < 42', 'lt1AL42E')
+ expr_check('5 > 42', 'gtL5EL42E')
+ expr_check('A > 42', 'gt1AL42E')
+ expr_check('5 <=> 42', 'ssL5EL42E')
+ expr_check('A <=> 42', 'ss1AL42E')
# ['<<', '>>']
- exprCheck('5 << 42', 'lsL5EL42E')
- exprCheck('A << 42', 'ls1AL42E')
- exprCheck('5 >> 42', 'rsL5EL42E')
+ expr_check('5 << 42', 'lsL5EL42E')
+ expr_check('A << 42', 'ls1AL42E')
+ expr_check('5 >> 42', 'rsL5EL42E')
# ['+', '-']
- exprCheck('5 + 42', 'plL5EL42E')
- exprCheck('5 - 42', 'miL5EL42E')
+ expr_check('5 + 42', 'plL5EL42E')
+ expr_check('5 - 42', 'miL5EL42E')
# ['*', '/', '%']
- exprCheck('5 * 42', 'mlL5EL42E')
- exprCheck('5 / 42', 'dvL5EL42E')
- exprCheck('5 % 42', 'rmL5EL42E')
+ expr_check('5 * 42', 'mlL5EL42E')
+ expr_check('5 / 42', 'dvL5EL42E')
+ expr_check('5 % 42', 'rmL5EL42E')
# ['.*', '->*']
- exprCheck('5 .* 42', 'dsL5EL42E')
- exprCheck('5 ->* 42', 'pmL5EL42E')
+ expr_check('5 .* 42', 'dsL5EL42E')
+ expr_check('5 ->* 42', 'pmL5EL42E')
# conditional
- exprCheck('5 ? 7 : 3', 'quL5EL7EL3E')
+ expr_check('5 ? 7 : 3', 'quL5EL7EL3E')
# assignment
- exprCheck('a = 5', 'aS1aL5E')
- exprCheck('a *= 5', 'mL1aL5E')
- exprCheck('a /= 5', 'dV1aL5E')
- exprCheck('a %= 5', 'rM1aL5E')
- exprCheck('a += 5', 'pL1aL5E')
- exprCheck('a -= 5', 'mI1aL5E')
- exprCheck('a >>= 5', 'rS1aL5E')
- exprCheck('a <<= 5', 'lS1aL5E')
- exprCheck('a &= 5', 'aN1aL5E')
- exprCheck('a and_eq 5', 'aN1aL5E')
- exprCheck('a ^= 5', 'eO1aL5E')
- exprCheck('a xor_eq 5', 'eO1aL5E')
- exprCheck('a |= 5', 'oR1aL5E')
- exprCheck('a or_eq 5', 'oR1aL5E')
- exprCheck('a = {{1, 2, 3}}', 'aS1ailL1EL2EL3EE')
+ expr_check('a = 5', 'aS1aL5E')
+ expr_check('a *= 5', 'mL1aL5E')
+ expr_check('a /= 5', 'dV1aL5E')
+ expr_check('a %= 5', 'rM1aL5E')
+ expr_check('a += 5', 'pL1aL5E')
+ expr_check('a -= 5', 'mI1aL5E')
+ expr_check('a >>= 5', 'rS1aL5E')
+ expr_check('a <<= 5', 'lS1aL5E')
+ expr_check('a &= 5', 'aN1aL5E')
+ expr_check('a and_eq 5', 'aN1aL5E')
+ expr_check('a ^= 5', 'eO1aL5E')
+ expr_check('a xor_eq 5', 'eO1aL5E')
+ expr_check('a |= 5', 'oR1aL5E')
+ expr_check('a or_eq 5', 'oR1aL5E')
+ expr_check('a = {{1, 2, 3}}', 'aS1ailL1EL2EL3EE')
# complex assignment and conditional
- exprCheck('5 = 6 = 7', 'aSL5EaSL6EL7E')
- exprCheck('5 = 6 ? 7 = 8 : 3', 'aSL5EquL6EaSL7EL8EL3E')
+ expr_check('5 = 6 = 7', 'aSL5EaSL6EL7E')
+ expr_check('5 = 6 ? 7 = 8 : 3', 'aSL5EquL6EaSL7EL8EL3E')
# comma operator
- exprCheck('a, 5', 'cm1aL5E')
+ expr_check('a, 5', 'cm1aL5E')
# Additional tests
# a < expression that starts with something that could be a template
- exprCheck('A < 42', 'lt1AL42E')
+ expr_check('A < 42', 'lt1AL42E')
check(
'function',
'template<> void f(A &v)',
{2: 'IE1fR1AI1BX2EE', 3: 'IE1fR1AI1BXL2EEE', 4: 'IE1fvR1AI1BXL2EEE'},
)
- exprCheck('A<1>::value', 'N1AIXL1EEE5valueE')
+ expr_check('A<1>::value', 'N1AIXL1EEE5valueE')
check('class', 'template {key}A', {2: 'I_iE1A'})
check('enumerator', '{key}A = std::numeric_limits::max()', {2: '1A'})
- exprCheck('operator()()', 'clclE')
- exprCheck('operator()()', 'clclIiEE')
+ expr_check('operator()()', 'clclE')
+ expr_check('operator()()', 'clclIiEE')
# pack expansion
- exprCheck('a(b(c, 1 + d...)..., e(f..., g))', 'cl1aspcl1b1cspplL1E1dEcl1esp1f1gEE')
+ expr_check('a(b(c, 1 + d...)..., e(f..., g))', 'cl1aspcl1b1cspplL1E1dEcl1esp1f1gEE')
def test_domain_cpp_ast_type_definitions():
@@ -1057,17 +1062,17 @@ def test_domain_cpp_ast_enum_definitions():
def test_domain_cpp_ast_anon_definitions():
- check('class', '@a', {3: 'Ut1_a'}, asTextOutput='class [anonymous]')
- check('union', '@a', {3: 'Ut1_a'}, asTextOutput='union [anonymous]')
- check('enum', '@a', {3: 'Ut1_a'}, asTextOutput='enum [anonymous]')
- check('class', '@1', {3: 'Ut1_1'}, asTextOutput='class [anonymous]')
- check('class', '@a::A', {3: 'NUt1_a1AE'}, asTextOutput='class [anonymous]::A')
+ check('class', '@a', {3: 'Ut1_a'}, as_text_output='class [anonymous]')
+ check('union', '@a', {3: 'Ut1_a'}, as_text_output='union [anonymous]')
+ check('enum', '@a', {3: 'Ut1_a'}, as_text_output='enum [anonymous]')
+ check('class', '@1', {3: 'Ut1_1'}, as_text_output='class [anonymous]')
+ check('class', '@a::A', {3: 'NUt1_a1AE'}, as_text_output='class [anonymous]::A')
check(
'function',
'int f(int @a)',
{1: 'f__i', 2: '1fi'},
- asTextOutput='int f(int [anonymous])',
+ as_text_output='int f(int [anonymous])',
)
@@ -1365,37 +1370,37 @@ def test_domain_cpp_ast_template_args():
def test_domain_cpp_ast_initializers():
- idsMember = {1: 'v__T', 2: '1v'}
- idsFunction = {1: 'f__T', 2: '1f1T'}
- idsTemplate = {2: 'I_1TE1fv', 4: 'I_1TE1fvv'}
+ ids_member = {1: 'v__T', 2: '1v'}
+ ids_function = {1: 'f__T', 2: '1f1T'}
+ ids_template = {2: 'I_1TE1fv', 4: 'I_1TE1fvv'}
# no init
- check('member', 'T v', idsMember)
- check('function', 'void f(T v)', idsFunction)
- check('function', 'template void f()', idsTemplate)
+ check('member', 'T v', ids_member)
+ check('function', 'void f(T v)', ids_function)
+ check('function', 'template void f()', ids_template)
# with '=', assignment-expression
- check('member', 'T v = 42', idsMember)
- check('function', 'void f(T v = 42)', idsFunction)
- check('function', 'template void f()', idsTemplate)
+ check('member', 'T v = 42', ids_member)
+ check('function', 'void f(T v = 42)', ids_function)
+ check('function', 'template void f()', ids_template)
# with '=', braced-init
- check('member', 'T v = {}', idsMember)
- check('function', 'void f(T v = {})', idsFunction)
- check('function', 'template void f()', idsTemplate)
- check('member', 'T v = {42, 42, 42}', idsMember)
- check('function', 'void f(T v = {42, 42, 42})', idsFunction)
- check('function', 'template void f()', idsTemplate)
- check('member', 'T v = {42, 42, 42,}', idsMember)
- check('function', 'void f(T v = {42, 42, 42,})', idsFunction)
- check('function', 'template void f()', idsTemplate)
- check('member', 'T v = {42, 42, args...}', idsMember)
- check('function', 'void f(T v = {42, 42, args...})', idsFunction)
- check('function', 'template void f()', idsTemplate)
+ check('member', 'T v = {}', ids_member)
+ check('function', 'void f(T v = {})', ids_function)
+ check('function', 'template void f()', ids_template)
+ check('member', 'T v = {42, 42, 42}', ids_member)
+ check('function', 'void f(T v = {42, 42, 42})', ids_function)
+ check('function', 'template void f()', ids_template)
+ check('member', 'T v = {42, 42, 42,}', ids_member)
+ check('function', 'void f(T v = {42, 42, 42,})', ids_function)
+ check('function', 'template void f()', ids_template)
+ check('member', 'T v = {42, 42, args...}', ids_member)
+ check('function', 'void f(T v = {42, 42, args...})', ids_function)
+ check('function', 'template void f()', ids_template)
# without '=', braced-init
- check('member', 'T v{}', idsMember)
- check('member', 'T v{42, 42, 42}', idsMember)
- check('member', 'T v{42, 42, 42,}', idsMember)
- check('member', 'T v{42, 42, args...}', idsMember)
+ check('member', 'T v{}', ids_member)
+ check('member', 'T v{42, 42, 42}', ids_member)
+ check('member', 'T v{42, 42, 42,}', ids_member)
+ check('member', 'T v{42, 42, args...}', ids_member)
# other
- check('member', 'T v = T{}', idsMember)
+ check('member', 'T v = T{}', ids_member)
def test_domain_cpp_ast_attributes():
@@ -1601,7 +1606,7 @@ def test_domain_cpp_build_misuse_of_roles(app):
ws = filter_warnings(app.warning, 'roles-targets-warn')
# the roles that should be able to generate warnings:
- allRoles = [
+ all_roles = [
'class',
'struct',
'union',
@@ -1613,7 +1618,7 @@ def test_domain_cpp_build_misuse_of_roles(app):
'enum',
'enumerator',
]
- ok = [ # targetType, okRoles
+ ok = [ # target_type, ok_roles
('class', ['class', 'struct', 'type']),
('union', ['union', 'type']),
('func', ['func', 'type']),
@@ -1626,14 +1631,16 @@ def test_domain_cpp_build_misuse_of_roles(app):
('templateParam', ['class', 'struct', 'union', 'member', 'var', 'type']),
]
warn = []
- for targetType, roles in ok:
- txtTargetType = 'function' if targetType == 'func' else targetType
- for r in allRoles:
+ for target_type, roles in ok:
+ txt_target_type = 'function' if target_type == 'func' else target_type
+ for r in all_roles:
if r not in roles:
- warn.append(f'WARNING: cpp:{r} targets a {txtTargetType} (')
- if targetType == 'templateParam':
- warn.append(f'WARNING: cpp:{r} targets a {txtTargetType} (')
- warn.append(f'WARNING: cpp:{r} targets a {txtTargetType} (')
+ warn.append(f'WARNING: cpp:{r} targets a {txt_target_type} (')
+ if target_type == 'templateParam':
+ warn.extend((
+ f'WARNING: cpp:{r} targets a {txt_target_type} (',
+ f'WARNING: cpp:{r} targets a {txt_target_type} (',
+ ))
warn = sorted(warn)
for w in ws:
assert 'targets a' in w
@@ -1660,14 +1667,14 @@ def test_domain_cpp_build_misuse_of_roles(app):
def test_domain_cpp_build_with_add_function_parentheses_is_True(app):
app.build(force_all=True)
- rolePatterns = [
+ role_patterns = [
'Sphinx',
'Sphinx::version',
'version',
'List',
'MyEnum',
]
- parenPatterns = [
+ paren_patterns = [
('ref function without parens ', r'paren_1\(\)'),
('ref function with parens ', r'paren_2\(\)'),
('ref function without parens, explicit title ', 'paren_3_title'),
@@ -1679,19 +1686,19 @@ def test_domain_cpp_build_with_add_function_parentheses_is_True(app):
]
text = (app.outdir / 'roles.html').read_text(encoding='utf8')
- for ref_text in rolePatterns:
+ for ref_text in role_patterns:
pattern = (
f'{ref_text}
'
)
match = re.search(pattern, text)
assert match is not None, f'Pattern not found in roles.html:\n\t{pattern}'
- for desc_text, ref_text in parenPatterns:
+ for desc_text, ref_text in paren_patterns:
pattern = f'{desc_text}{ref_text}
'
match = re.search(pattern, text)
assert match is not None, f'Pattern not found in roles.html:\n\t{pattern}'
text = (app.outdir / 'any-role.html').read_text(encoding='utf8')
- for desc_text, ref_text in parenPatterns:
+ for desc_text, ref_text in paren_patterns:
pattern = f'{desc_text}{ref_text}
'
match = re.search(pattern, text)
assert match is not None, f'Pattern not found in any-role.html:\n\t{pattern}'
@@ -1705,14 +1712,14 @@ def test_domain_cpp_build_with_add_function_parentheses_is_True(app):
def test_domain_cpp_build_with_add_function_parentheses_is_False(app):
app.build(force_all=True)
- rolePatterns = [
+ role_patterns = [
'Sphinx',
'Sphinx::version',
'version',
'List',
'MyEnum',
]
- parenPatterns = [
+ paren_patterns = [
('ref function without parens ', 'paren_1'),
('ref function with parens ', 'paren_2'),
('ref function without parens, explicit title ', 'paren_3_title'),
@@ -1724,19 +1731,19 @@ def test_domain_cpp_build_with_add_function_parentheses_is_False(app):
]
text = (app.outdir / 'roles.html').read_text(encoding='utf8')
- for ref_text in rolePatterns:
+ for ref_text in role_patterns:
pattern = (
f'{ref_text}
'
)
match = re.search(pattern, text)
assert match is not None, f'Pattern not found in roles.html:\n\t{pattern}'
- for desc_text, ref_text in parenPatterns:
+ for desc_text, ref_text in paren_patterns:
pattern = f'{desc_text}{ref_text}
'
match = re.search(pattern, text)
assert match is not None, f'Pattern not found in roles.html:\n\t{pattern}'
text = (app.outdir / 'any-role.html').read_text(encoding='utf8')
- for desc_text, ref_text in parenPatterns:
+ for desc_text, ref_text in paren_patterns:
pattern = f'{desc_text}{ref_text}
'
match = re.search(pattern, text)
assert match is not None, f'Pattern not found in any-role.html:\n\t{pattern}'
@@ -1819,7 +1826,7 @@ def test_domain_cpp_build_field_role(app):
@pytest.mark.sphinx('html', testroot='domain-cpp', confoverrides={'nitpicky': True})
def test_domain_cpp_build_operator_lookup(app):
- app.builder.build_all()
+ app.build(force_all=True)
ws = filter_warnings(app.warning, 'operator-lookup')
assert len(ws) == 5
# TODO: the first one should not happen
@@ -1838,7 +1845,7 @@ def test_domain_cpp_build_operator_lookup(app):
'html', testroot='domain-cpp-intersphinx', confoverrides={'nitpicky': True}
)
def test_domain_cpp_build_intersphinx(tmp_path, app):
- origSource = """\
+ orig_source = """\
.. cpp:class:: _class
.. cpp:struct:: _struct
.. cpp:union:: _union
@@ -1858,7 +1865,7 @@ def test_domain_cpp_build_intersphinx(tmp_path, app):
.. cpp:enum-class:: _enumClass
.. cpp:function:: void _functionParam(int param)
.. cpp:function:: template void _templateParam()
-""" # NoQA: F841
+"""
inv_file = tmp_path / 'inventory'
inv_file.write_bytes(
b"""\
@@ -1888,7 +1895,7 @@ def test_domain_cpp_build_intersphinx(tmp_path, app):
_union cpp:union 1 index.html#_CPPv46$ -
_var cpp:member 1 index.html#_CPPv44$ -
""")
- ) # NoQA: W291
+ )
app.config.intersphinx_mapping = {
'test': ('https://localhost/intersphinx/cpp/', str(inv_file)),
}
@@ -2419,7 +2426,7 @@ def test_domain_cpp_cpp_maximum_signature_line_length_in_html(app):
\
str\
\
-name,\
+name\
@@ -2438,6 +2445,6 @@ def test_domain_cpp_cpp_maximum_signature_line_length_in_text(app):
content = (app.outdir / 'index.txt').read_text(encoding='utf8')
param_line_fmt = STDINDENT * ' ' + '{}\n'
- expected_parameter_list_hello = '(\n{})'.format(param_line_fmt.format('str name,'))
+ expected_parameter_list_hello = '(\n{})'.format(param_line_fmt.format('str name'))
assert expected_parameter_list_hello in content
diff --git a/tests/test_domains/test_domain_js.py b/tests/test_domains/test_domain_js.py
index a51dd13be58..7354a157bbe 100644
--- a/tests/test_domains/test_domain_js.py
+++ b/tests/test_domains/test_domain_js.py
@@ -1,5 +1,7 @@
"""Tests the JavaScript Domain"""
+from __future__ import annotations
+
from unittest.mock import Mock
import docutils.utils
@@ -324,6 +326,25 @@ def test_no_index_entry(app):
)
assert_node(doctree[2], addnodes.index, entries=[])
+ text = '.. js:class:: f\n.. js:class:: g\n :no-index-entry:\n'
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index, desc, addnodes.index, desc))
+ assert_node(
+ doctree[0],
+ addnodes.index,
+ entries=[('single', 'f() (class)', 'f', '', None)],
+ )
+ assert_node(doctree[2], addnodes.index, entries=[])
+
+ text = '.. js:module:: f\n.. js:module:: g\n :no-index-entry:\n'
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index, nodes.target, nodes.target))
+ assert_node(
+ doctree[0],
+ addnodes.index,
+ entries=[('single', 'f (module)', 'module-f', '', None)],
+ )
+
@pytest.mark.sphinx('html', testroot='root')
def test_module_content_line_number(app):
@@ -753,3 +774,121 @@ def test_domain_js_javascript_maximum_signature_line_length_in_text(app):
expected_f,
)
assert expected_parameter_list_foo in content
+
+
+@pytest.mark.sphinx(
+ 'html',
+ testroot='domain-js-javascript_maximum_signature_line_length',
+ confoverrides={'javascript_trailing_comma_in_multi_line_signatures': False},
+)
+def test_domain_js_javascript_trailing_comma_in_multi_line_signatures_in_html(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ expected_parameter_list_hello = """\
+
+
+- \
+\
+name\
+\
+
+
+
+)\
+\
+\
+"""
+ assert expected_parameter_list_hello in content
+
+ param_line_fmt = '{}\n'
+ param_name_fmt = (
+ '{}'
+ )
+ optional_fmt = '{}'
+
+ expected_a = param_line_fmt.format(
+ optional_fmt.format('[')
+ + param_name_fmt.format('a')
+ + ','
+ + optional_fmt.format('['),
+ )
+ assert expected_a in content
+
+ expected_b = param_line_fmt.format(
+ param_name_fmt.format('b')
+ + ','
+ + optional_fmt.format(']')
+ + optional_fmt.format(']'),
+ )
+ assert expected_b in content
+
+ expected_c = param_line_fmt.format(param_name_fmt.format('c') + ',')
+ assert expected_c in content
+
+ expected_d = param_line_fmt.format(
+ param_name_fmt.format('d') + optional_fmt.format('[') + ','
+ )
+ assert expected_d in content
+
+ expected_e = param_line_fmt.format(param_name_fmt.format('e') + ',')
+ assert expected_e in content
+
+ expected_f = param_line_fmt.format(
+ param_name_fmt.format('f') + optional_fmt.format(']')
+ )
+ assert expected_f in content
+
+ expected_parameter_list_foo = """\
+
+
+{}{}{}{}{}{}
+
+)\
+\
+\
+""".format(expected_a, expected_b, expected_c, expected_d, expected_e, expected_f)
+ assert expected_parameter_list_foo in content
+
+
+@pytest.mark.sphinx(
+ 'text',
+ testroot='domain-js-javascript_maximum_signature_line_length',
+ freshenv=True,
+ confoverrides={'javascript_trailing_comma_in_multi_line_signatures': False},
+)
+def test_domain_js_javascript_trailing_comma_in_multi_line_signatures_in_text(app):
+ app.build()
+ content = (app.outdir / 'index.txt').read_text(encoding='utf8')
+ param_line_fmt = STDINDENT * ' ' + '{}\n'
+
+ expected_parameter_list_hello = '(\n{})'.format(param_line_fmt.format('name'))
+
+ assert expected_parameter_list_hello in content
+
+ expected_a = param_line_fmt.format('[a,[')
+ assert expected_a in content
+
+ expected_b = param_line_fmt.format('b,]]')
+ assert expected_b in content
+
+ expected_c = param_line_fmt.format('c,')
+ assert expected_c in content
+
+ expected_d = param_line_fmt.format('d[,')
+ assert expected_d in content
+
+ expected_e = param_line_fmt.format('e,')
+ assert expected_e in content
+
+ expected_f = param_line_fmt.format('f]')
+ assert expected_f in content
+
+ expected_parameter_list_foo = '(\n{}{}{}{}{}{})'.format(
+ expected_a,
+ expected_b,
+ expected_c,
+ expected_d,
+ expected_e,
+ expected_f,
+ )
+ assert expected_parameter_list_foo in content
diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py
index 9f19f1da34c..1e6d86194a3 100644
--- a/tests/test_domains/test_domain_py.py
+++ b/tests/test_domains/test_domain_py.py
@@ -836,6 +836,15 @@ def test_no_index_entry(app):
)
assert_node(doctree[2], addnodes.index, entries=[])
+ text = '.. py:module:: f\n.. py:module:: g\n :no-index-entry:\n'
+ doctree = restructuredtext.parse(app, text)
+ assert_node(doctree, (addnodes.index, nodes.target, nodes.target))
+ assert_node(
+ doctree[0],
+ addnodes.index,
+ entries=[('pair', 'module; f', 'module-f', '', None)],
+ )
+
@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names')
def test_python_python_use_unqualified_type_names(app):
@@ -1073,6 +1082,133 @@ def test_domain_py_python_maximum_signature_line_length_in_text(app):
assert expected_parameter_list_foo in content
+@pytest.mark.sphinx(
+ 'html',
+ testroot='domain-py-python_maximum_signature_line_length',
+ confoverrides={'python_trailing_comma_in_multi_line_signatures': False},
+)
+def test_domain_py_python_trailing_comma_in_multi_line_signatures_in_html(app):
+ app.build()
+ content = (app.outdir / 'index.html').read_text(encoding='utf8')
+ expected_parameter_list_hello = """\
+
+
+- \
+\
+name\
+:\
+ \
+str\
+\
+
+
+
+) \
+\
+→ \
+str\
+\
+\
+\
+"""
+ assert expected_parameter_list_hello in content
+
+ param_line_fmt = '{}\n'
+ param_name_fmt = (
+ '{}'
+ )
+ optional_fmt = '{}'
+
+ expected_a = param_line_fmt.format(
+ optional_fmt.format('[')
+ + param_name_fmt.format('a')
+ + ','
+ + optional_fmt.format('['),
+ )
+ assert expected_a in content
+
+ expected_b = param_line_fmt.format(
+ param_name_fmt.format('b')
+ + ','
+ + optional_fmt.format(']')
+ + optional_fmt.format(']'),
+ )
+ assert expected_b in content
+
+ expected_c = param_line_fmt.format(param_name_fmt.format('c') + ',')
+ assert expected_c in content
+
+ expected_d = param_line_fmt.format(
+ param_name_fmt.format('d') + optional_fmt.format('[') + ','
+ )
+ assert expected_d in content
+
+ expected_e = param_line_fmt.format(param_name_fmt.format('e') + ',')
+ assert expected_e in content
+
+ expected_f = param_line_fmt.format(
+ param_name_fmt.format('f') + optional_fmt.format(']')
+ )
+ assert expected_f in content
+
+ expected_parameter_list_foo = """\
+
+
+{}{}{}{}{}{}
+
+)\
+\
+\
+""".format(expected_a, expected_b, expected_c, expected_d, expected_e, expected_f)
+ assert expected_parameter_list_foo in content
+
+
+@pytest.mark.sphinx(
+ 'text',
+ testroot='domain-py-python_maximum_signature_line_length',
+ freshenv=True,
+ confoverrides={'python_trailing_comma_in_multi_line_signatures': False},
+)
+def test_domain_py_python_trailing_comma_in_multi_line_signatures_in_text(app):
+ app.build()
+ content = (app.outdir / 'index.txt').read_text(encoding='utf8')
+ param_line_fmt = STDINDENT * ' ' + '{}\n'
+
+ expected_parameter_list_hello = '(\n{}) -> str'.format(
+ param_line_fmt.format('name: str')
+ )
+
+ assert expected_parameter_list_hello in content
+
+ expected_a = param_line_fmt.format('[a,[')
+ assert expected_a in content
+
+ expected_b = param_line_fmt.format('b,]]')
+ assert expected_b in content
+
+ expected_c = param_line_fmt.format('c,')
+ assert expected_c in content
+
+ expected_d = param_line_fmt.format('d[,')
+ assert expected_d in content
+
+ expected_e = param_line_fmt.format('e,')
+ assert expected_e in content
+
+ expected_f = param_line_fmt.format('f]')
+ assert expected_f in content
+
+ expected_parameter_list_foo = '(\n{}{}{}{}{}{})'.format(
+ expected_a,
+ expected_b,
+ expected_c,
+ expected_d,
+ expected_e,
+ expected_f,
+ )
+ assert expected_parameter_list_foo in content
+
+
@pytest.mark.sphinx('html', testroot='root')
def test_module_content_line_number(app):
text = '.. py:module:: foo\n\n Some link here: :ref:`abc`\n'
diff --git a/tests/test_domains/test_domain_rst.py b/tests/test_domains/test_domain_rst.py
index dc61792dd0a..9f1eec12c28 100644
--- a/tests/test_domains/test_domain_rst.py
+++ b/tests/test_domains/test_domain_rst.py
@@ -1,5 +1,7 @@
"""Tests the reStructuredText domain."""
+from __future__ import annotations
+
import pytest
from sphinx import addnodes
diff --git a/tests/test_domains/test_domain_std.py b/tests/test_domains/test_domain_std.py
index 4e5d88b63de..b253f28e3d7 100644
--- a/tests/test_domains/test_domain_std.py
+++ b/tests/test_domains/test_domain_std.py
@@ -1,5 +1,7 @@
"""Tests the std domain"""
+from __future__ import annotations
+
from unittest import mock
import pytest
@@ -412,7 +414,7 @@ def test_cmdoption(app):
entries=[('pair', 'ls command line option; -l', 'cmdoption-ls-l', '', None)],
)
assert ('ls', '-l') in domain.progoptions
- assert domain.progoptions[('ls', '-l')] == ('index', 'cmdoption-ls-l')
+ assert domain.progoptions['ls', '-l'] == ('index', 'cmdoption-ls-l')
@pytest.mark.sphinx('html', testroot='root')
@@ -439,7 +441,7 @@ def test_cmdoption_for_None(app):
entries=[('pair', 'command line option; -l', 'cmdoption-l', '', None)],
)
assert (None, '-l') in domain.progoptions
- assert domain.progoptions[(None, '-l')] == ('index', 'cmdoption-l')
+ assert domain.progoptions[None, '-l'] == ('index', 'cmdoption-l')
@pytest.mark.sphinx('html', testroot='root')
@@ -479,8 +481,8 @@ def test_multiple_cmdoptions(app):
)
assert ('cmd', '-o') in domain.progoptions
assert ('cmd', '--output') in domain.progoptions
- assert domain.progoptions[('cmd', '-o')] == ('index', 'cmdoption-cmd-o')
- assert domain.progoptions[('cmd', '--output')] == ('index', 'cmdoption-cmd-o')
+ assert domain.progoptions['cmd', '-o'] == ('index', 'cmdoption-cmd-o')
+ assert domain.progoptions['cmd', '--output'] == ('index', 'cmdoption-cmd-o')
@pytest.mark.sphinx('html', testroot='productionlist')
@@ -502,22 +504,26 @@ def test_productionlist(app):
ul = nodes[2]
cases = []
for li in list(ul):
- assert len(list(li)) == 1
- p = list(li)[0]
+ li_list = list(li)
+ assert len(li_list) == 1
+ p = li_list[0]
assert p.tag == 'p'
text = str(p.text).strip(' :')
- assert len(list(p)) == 1
- a = list(p)[0]
+ p_list = list(p)
+ assert len(p_list) == 1
+ a = p_list[0]
assert a.tag == 'a'
link = a.get('href')
- assert len(list(a)) == 1
- code = list(a)[0]
+ a_list = list(a)
+ assert len(a_list) == 1
+ code = a_list[0]
assert code.tag == 'code'
- assert len(list(code)) == 1
- span = list(code)[0]
+ code_list = list(code)
+ assert len(code_list) == 1
+ span = code_list[0]
assert span.tag == 'span'
- linkText = span.text.strip()
- cases.append((text, link, linkText))
+ link_text = span.text.strip()
+ cases.append((text, link, link_text))
assert cases == [
('A', 'Bare.html#grammar-token-A', 'A'),
('B', 'Bare.html#grammar-token-B', 'B'),
diff --git a/tests/test_environment/test_environment.py b/tests/test_environment/test_environment.py
index 10e98584342..a5059e3e930 100644
--- a/tests/test_environment/test_environment.py
+++ b/tests/test_environment/test_environment.py
@@ -1,10 +1,13 @@
"""Test the BuildEnvironment class."""
+from __future__ import annotations
+
import shutil
from pathlib import Path
import pytest
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.builders.latex import LaTeXBuilder
from sphinx.config import Config
@@ -15,7 +18,6 @@
CONFIG_OK,
_differing_config_keys,
)
-from sphinx.util.console import strip_colors
@pytest.mark.sphinx('dummy', testroot='basic')
@@ -26,7 +28,7 @@ def test_config_status(make_app, app_params):
app1 = make_app(*args, freshenv=True, **kwargs)
assert app1.env.config_status == CONFIG_NEW
app1.build()
- output = strip_colors(app1.status.getvalue())
+ output = strip_escape_sequences(app1.status.getvalue())
# assert 'The configuration has changed' not in output
assert '[new config] 1 added' in output
@@ -34,7 +36,7 @@ def test_config_status(make_app, app_params):
app2 = make_app(*args, **kwargs)
assert app2.env.config_status == CONFIG_OK
app2.build()
- output = strip_colors(app2.status.getvalue())
+ output = strip_escape_sequences(app2.status.getvalue())
assert 'The configuration has changed' not in output
assert '0 added, 0 changed, 0 removed' in output
@@ -47,7 +49,7 @@ def test_config_status(make_app, app_params):
assert app3.env.config_status == CONFIG_CHANGED
app3.build()
shutil.move(other_fname, fname)
- output = strip_colors(app3.status.getvalue())
+ output = strip_escape_sequences(app3.status.getvalue())
assert 'The configuration has changed' in output
assert "[config changed ('master_doc')] 1 added," in output
@@ -58,7 +60,7 @@ def test_config_status(make_app, app_params):
assert app4.env.config_status == CONFIG_EXTENSIONS_CHANGED
app4.build()
want_str = "[extensions changed ('sphinx.ext.autodoc')] 1 added"
- output = strip_colors(app4.status.getvalue())
+ output = strip_escape_sequences(app4.status.getvalue())
assert 'The configuration has changed' not in output
assert want_str in output
@@ -183,14 +185,14 @@ def test_env_relfn2path(app):
assert absfn == str(app.srcdir / 'logo.jpg')
# omit docname (w/ current docname)
- app.env.temp_data['docname'] = 'subdir/document'
+ app.env.current_document.docname = 'subdir/document'
relfn, absfn = app.env.relfn2path('images/logo.jpg')
assert Path(relfn) == Path('subdir/images/logo.jpg')
assert absfn == str(app.srcdir / 'subdir' / 'images' / 'logo.jpg')
# omit docname (w/o current docname)
- app.env.temp_data.clear()
- with pytest.raises(KeyError):
+ app.env.current_document.clear()
+ with pytest.raises(KeyError, match=r"^'docname'$"):
app.env.relfn2path('images/logo.jpg')
diff --git a/tests/test_environment/test_environment_indexentries.py b/tests/test_environment/test_environment_indexentries.py
index 1de861abc49..e54e41e6f64 100644
--- a/tests/test_environment/test_environment_indexentries.py
+++ b/tests/test_environment/test_environment_indexentries.py
@@ -1,5 +1,7 @@
"""Test the sphinx.environment.adapters.indexentries."""
+from __future__ import annotations
+
import pytest
from sphinx.environment.adapters.indexentries import IndexEntries
diff --git a/tests/test_environment/test_environment_record_dependencies.py b/tests/test_environment/test_environment_record_dependencies.py
index 0a17253c091..f5d80ca2f74 100644
--- a/tests/test_environment/test_environment_record_dependencies.py
+++ b/tests/test_environment/test_environment_record_dependencies.py
@@ -1,5 +1,7 @@
"""Tests for ``record_dependencies``."""
+from __future__ import annotations
+
import pytest
diff --git a/tests/test_environment/test_environment_toctree.py b/tests/test_environment/test_environment_toctree.py
index 7f283c42231..890fd596bf8 100644
--- a/tests/test_environment/test_environment_toctree.py
+++ b/tests/test_environment/test_environment_toctree.py
@@ -1,5 +1,7 @@
"""Test the sphinx.environment.adapters.toctree."""
+from __future__ import annotations
+
import pytest
from docutils import nodes
from docutils.nodes import bullet_list, list_item, literal, reference, title
@@ -450,7 +452,7 @@ def test_domain_objects_document_scoping(app):
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
def test_document_toc(app):
app.build()
- toctree = document_toc(app.env, 'index', app.builder.tags)
+ toctree = document_toc(app.env, 'index', app.tags)
assert_node(
toctree,
@@ -500,8 +502,8 @@ def test_document_toc(app):
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
def test_document_toc_only(app):
app.build()
- builder = StandaloneHTMLBuilder(app, app.env)
- toctree = document_toc(app.env, 'index', builder.tags)
+ StandaloneHTMLBuilder(app, app.env) # adds format/builder tags
+ toctree = document_toc(app.env, 'index', app.tags)
assert_node(
toctree,
@@ -559,7 +561,7 @@ def test_document_toc_only(app):
@pytest.mark.test_params(shared_result='test_environment_toctree_basic')
def test_document_toc_tocdepth(app):
app.build()
- toctree = document_toc(app.env, 'tocdepth', app.builder.tags)
+ toctree = document_toc(app.env, 'tocdepth', app.tags)
assert_node(
toctree,
diff --git a/tests/test_errors.py b/tests/test_errors.py
index d551a6ef1e8..e1ef0d389ae 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from sphinx.errors import ExtensionError
diff --git a/tests/test_events.py b/tests/test_events.py
index df175787a17..d601298918f 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -1,5 +1,9 @@
"""Test the EventManager class."""
+from __future__ import annotations
+
+from types import SimpleNamespace
+
import pytest
from sphinx.errors import ExtensionError
@@ -22,16 +26,11 @@ def test_event_priority():
assert result == [3, 1, 2, 5, 4]
-class FakeApp:
- def __init__(self, pdb: bool = False):
- self.pdb = pdb
-
-
def test_event_allowed_exceptions():
def raise_error(app):
raise RuntimeError
- app = FakeApp() # pass a dummy object as an app
+ app = SimpleNamespace(pdb=False) # pass a dummy object as an app
events = EventManager(app) # type: ignore[arg-type]
events.connect('builder-inited', raise_error, priority=500)
@@ -48,7 +47,7 @@ def test_event_pdb():
def raise_error(app):
raise RuntimeError
- app = FakeApp(pdb=True) # pass a dummy object as an app
+ app = SimpleNamespace(pdb=True) # pass a dummy object as an app
events = EventManager(app) # type: ignore[arg-type]
events.connect('builder-inited', raise_error, priority=500)
diff --git a/tests/test_extensions/autodoc_util.py b/tests/test_extensions/autodoc_util.py
index 7c4da07970e..3d08c739300 100644
--- a/tests/test_extensions/autodoc_util.py
+++ b/tests/test_extensions/autodoc_util.py
@@ -22,7 +22,8 @@ def do_autodoc(
options: dict[str, Any] | None = None,
) -> StringList:
options = {} if options is None else options.copy()
- app.env.temp_data.setdefault('docname', 'index') # set dummy docname
+ if not app.env.current_document.docname:
+ app.env.current_document.docname = 'index' # set dummy docname
doccls = app.registry.documenters[objtype]
docoptions = process_documenter_options(doccls, app.config, options)
state = Mock()
diff --git a/tests/test_extensions/ext_napoleon_pep526_data_google.py b/tests/test_extensions/ext_napoleon_pep526_data_google.py
index d0692e0bab1..f42dfcfc442 100644
--- a/tests/test_extensions/ext_napoleon_pep526_data_google.py
+++ b/tests/test_extensions/ext_napoleon_pep526_data_google.py
@@ -1,5 +1,7 @@
"""Test module for napoleon PEP 526 compatibility with google style"""
+from __future__ import annotations
+
module_level_var: int = 99
"""This is an example module level variable"""
diff --git a/tests/test_extensions/ext_napoleon_pep526_data_numpy.py b/tests/test_extensions/ext_napoleon_pep526_data_numpy.py
index eff7746ebf2..9738f84c76b 100644
--- a/tests/test_extensions/ext_napoleon_pep526_data_numpy.py
+++ b/tests/test_extensions/ext_napoleon_pep526_data_numpy.py
@@ -1,12 +1,13 @@
"""Test module for napoleon PEP 526 compatibility with numpy style"""
+from __future__ import annotations
+
module_level_var: int = 99
"""This is an example module level variable"""
class PEP526NumpyClass:
- """
- Sample class with PEP 526 annotations and numpy docstring
+ """Sample class with PEP 526 annotations and numpy docstring
Attributes
----------
diff --git a/tests/test_extensions/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py
index 7e4edbdfce8..61550bea529 100644
--- a/tests/test_extensions/test_ext_apidoc.py
+++ b/tests/test_extensions/test_ext_apidoc.py
@@ -1,12 +1,19 @@
"""Test the sphinx.apidoc module."""
+from __future__ import annotations
+
from collections import namedtuple
-from pathlib import Path
+from typing import TYPE_CHECKING
import pytest
-import sphinx.ext.apidoc
-from sphinx.ext.apidoc import main as apidoc_main
+import sphinx.ext.apidoc._generate
+from sphinx.ext.apidoc._cli import main as apidoc_main
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+_apidoc = namedtuple('_apidoc', 'coderoot,outdir') # NoQA: PYI024
@pytest.fixture
@@ -24,7 +31,7 @@ def apidoc(rootdir, tmp_path, apidoc_params):
*kwargs.get('options', []),
]
apidoc_main(args)
- return namedtuple('apidoc', 'coderoot,outdir')(coderoot, outdir)
+ return _apidoc(coderoot, outdir)
@pytest.fixture
@@ -53,10 +60,10 @@ def test_simple(make_app, apidoc):
@pytest.mark.apidoc(
- coderoot='test-apidoc-custom-templates',
+ coderoot='test-ext-apidoc-custom-templates',
options=[
'--separate',
- '--templatedir=tests/roots/test-apidoc-custom-templates/_templates',
+ '--templatedir=tests/roots/test-ext-apidoc-custom-templates/_templates',
],
)
def test_custom_templates(make_app, apidoc):
@@ -79,16 +86,16 @@ def test_custom_templates(make_app, apidoc):
# Assert that the legacy filename is discovered
with open(builddir / 'mypackage.txt', encoding='utf-8') as f:
txt = f.read()
- assert 'The legacy package template was found!' in txt
+ assert 'The legacy package template was found!' in txt
# Assert that the new filename is preferred
with open(builddir / 'mypackage.mymodule.txt', encoding='utf-8') as f:
txt = f.read()
- assert 'The Jinja module template was found!' in txt
+ assert 'The Jinja module template was found!' in txt
@pytest.mark.apidoc(
- coderoot='test-apidoc-pep420/a',
+ coderoot='test-ext-apidoc-pep420/a',
options=['--implicit-namespaces'],
)
def test_pep_0420_enabled(make_app, apidoc):
@@ -100,17 +107,17 @@ def test_pep_0420_enabled(make_app, apidoc):
with open(outdir / 'a.b.c.rst', encoding='utf-8') as f:
rst = f.read()
- assert 'automodule:: a.b.c.d\n' in rst
- assert 'automodule:: a.b.c\n' in rst
+ assert 'automodule:: a.b.c.d\n' in rst
+ assert 'automodule:: a.b.c\n' in rst
with open(outdir / 'a.b.e.rst', encoding='utf-8') as f:
rst = f.read()
- assert 'automodule:: a.b.e.f\n' in rst
+ assert 'automodule:: a.b.e.f\n' in rst
with open(outdir / 'a.b.x.rst', encoding='utf-8') as f:
rst = f.read()
- assert 'automodule:: a.b.x.y\n' in rst
- assert 'automodule:: a.b.x\n' not in rst
+ assert 'automodule:: a.b.x.y\n' in rst
+ assert 'automodule:: a.b.x\n' not in rst
app = make_app('text', srcdir=outdir)
app.build()
@@ -124,19 +131,19 @@ def test_pep_0420_enabled(make_app, apidoc):
with open(builddir / 'a.b.c.txt', encoding='utf-8') as f:
txt = f.read()
- assert 'a.b.c package\n' in txt
+ assert 'a.b.c package\n' in txt
with open(builddir / 'a.b.e.txt', encoding='utf-8') as f:
txt = f.read()
- assert 'a.b.e.f module\n' in txt
+ assert 'a.b.e.f module\n' in txt
with open(builddir / 'a.b.x.txt', encoding='utf-8') as f:
txt = f.read()
- assert 'a.b.x namespace\n' in txt
+ assert 'a.b.x namespace\n' in txt
@pytest.mark.apidoc(
- coderoot='test-apidoc-pep420/a',
+ coderoot='test-ext-apidoc-pep420/a',
options=['--implicit-namespaces', '--separate'],
)
def test_pep_0420_enabled_separate(make_app, apidoc):
@@ -150,15 +157,15 @@ def test_pep_0420_enabled_separate(make_app, apidoc):
with open(outdir / 'a.b.c.rst', encoding='utf-8') as f:
rst = f.read()
- assert '.. toctree::\n :maxdepth: 4\n\n a.b.c.d\n' in rst
+ assert '.. toctree::\n :maxdepth: 4\n\n a.b.c.d\n' in rst
with open(outdir / 'a.b.e.rst', encoding='utf-8') as f:
rst = f.read()
- assert '.. toctree::\n :maxdepth: 4\n\n a.b.e.f\n' in rst
+ assert '.. toctree::\n :maxdepth: 4\n\n a.b.e.f\n' in rst
with open(outdir / 'a.b.x.rst', encoding='utf-8') as f:
rst = f.read()
- assert '.. toctree::\n :maxdepth: 4\n\n a.b.x.y\n' in rst
+ assert '.. toctree::\n :maxdepth: 4\n\n a.b.x.y\n' in rst
app = make_app('text', srcdir=outdir)
app.build()
@@ -174,18 +181,18 @@ def test_pep_0420_enabled_separate(make_app, apidoc):
with open(builddir / 'a.b.c.txt', encoding='utf-8') as f:
txt = f.read()
- assert 'a.b.c package\n' in txt
+ assert 'a.b.c package\n' in txt
with open(builddir / 'a.b.e.f.txt', encoding='utf-8') as f:
txt = f.read()
- assert 'a.b.e.f module\n' in txt
+ assert 'a.b.e.f module\n' in txt
with open(builddir / 'a.b.x.txt', encoding='utf-8') as f:
txt = f.read()
- assert 'a.b.x namespace\n' in txt
+ assert 'a.b.x namespace\n' in txt
-@pytest.mark.apidoc(coderoot='test-apidoc-pep420/a')
+@pytest.mark.apidoc(coderoot='test-ext-apidoc-pep420/a')
def test_pep_0420_disabled(make_app, apidoc):
outdir = apidoc.outdir
assert (outdir / 'conf.py').is_file()
@@ -198,7 +205,7 @@ def test_pep_0420_disabled(make_app, apidoc):
print(app._warning.getvalue())
-@pytest.mark.apidoc(coderoot='test-apidoc-pep420/a/b')
+@pytest.mark.apidoc(coderoot='test-ext-apidoc-pep420/a/b')
def test_pep_0420_disabled_top_level_verify(make_app, apidoc):
outdir = apidoc.outdir
assert (outdir / 'conf.py').is_file()
@@ -207,9 +214,9 @@ def test_pep_0420_disabled_top_level_verify(make_app, apidoc):
with open(outdir / 'c.rst', encoding='utf-8') as f:
rst = f.read()
- assert 'c package\n' in rst
- assert 'automodule:: c.d\n' in rst
- assert 'automodule:: c\n' in rst
+ assert 'c package\n' in rst
+ assert 'automodule:: c.d\n' in rst
+ assert 'automodule:: c\n' in rst
app = make_app('text', srcdir=outdir)
app.build()
@@ -217,7 +224,7 @@ def test_pep_0420_disabled_top_level_verify(make_app, apidoc):
print(app._warning.getvalue())
-@pytest.mark.apidoc(coderoot='test-apidoc-trailing-underscore')
+@pytest.mark.apidoc(coderoot='test-ext-apidoc-trailing-underscore')
def test_trailing_underscore(make_app, apidoc):
outdir = apidoc.outdir
assert (outdir / 'conf.py').is_file()
@@ -231,12 +238,12 @@ def test_trailing_underscore(make_app, apidoc):
builddir = outdir / '_build' / 'text'
with open(builddir / 'package_.txt', encoding='utf-8') as f:
rst = f.read()
- assert 'package_ package\n' in rst
- assert 'package_.module_ module\n' in rst
+ assert 'package_ package\n' in rst
+ assert 'package_.module_ module\n' in rst
@pytest.mark.apidoc(
- coderoot='test-apidoc-pep420/a',
+ coderoot='test-ext-apidoc-pep420/a',
excludes=['b/c/d.py', 'b/e/f.py', 'b/e/__init__.py'],
options=['--implicit-namespaces', '--separate'],
)
@@ -254,7 +261,7 @@ def test_excludes(apidoc):
@pytest.mark.apidoc(
- coderoot='test-apidoc-pep420/a',
+ coderoot='test-ext-apidoc-pep420/a',
excludes=['b/e'],
options=['--implicit-namespaces', '--separate'],
)
@@ -271,7 +278,7 @@ def test_excludes_subpackage_should_be_skipped(apidoc):
@pytest.mark.apidoc(
- coderoot='test-apidoc-pep420/a',
+ coderoot='test-ext-apidoc-pep420/a',
excludes=['b/e/f.py'],
options=['--implicit-namespaces', '--separate'],
)
@@ -288,7 +295,7 @@ def test_excludes_module_should_be_skipped(apidoc):
@pytest.mark.apidoc(
- coderoot='test-apidoc-pep420/a',
+ coderoot='test-ext-apidoc-pep420/a',
excludes=[],
options=['--implicit-namespaces', '--separate'],
)
@@ -342,11 +349,11 @@ def test_extension_parsed(apidoc):
with open(outdir / 'conf.py', encoding='utf-8') as f:
rst = f.read()
- assert 'sphinx.ext.mathjax' in rst
+ assert 'sphinx.ext.mathjax' in rst
@pytest.mark.apidoc(
- coderoot='test-apidoc-toc/mypackage',
+ coderoot='test-ext-apidoc-toc/mypackage',
options=['--implicit-namespaces'],
)
def test_toc_all_references_should_exist_pep420_enabled(apidoc):
@@ -378,7 +385,7 @@ def test_toc_all_references_should_exist_pep420_enabled(apidoc):
@pytest.mark.apidoc(
- coderoot='test-apidoc-toc/mypackage',
+ coderoot='test-ext-apidoc-toc/mypackage',
)
def test_toc_all_references_should_exist_pep420_disabled(apidoc):
"""All references in toc should exist. This test doesn't say if
@@ -425,7 +432,7 @@ def extract_toc(path):
@pytest.mark.apidoc(
- coderoot='test-apidoc-subpackage-in-toc',
+ coderoot='test-ext-apidoc-subpackage-in-toc',
options=['--separate'],
)
def test_subpackage_in_toc(apidoc):
@@ -500,8 +507,8 @@ def test_module_file(tmp_path):
'\n'
'.. automodule:: example\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
@@ -515,8 +522,8 @@ def test_module_file_noheadings(tmp_path):
assert content == (
'.. automodule:: example\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
@@ -553,24 +560,24 @@ def test_package_file(tmp_path):
'\n'
'.. automodule:: testpkg.hello\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
'\n'
'testpkg.world module\n'
'--------------------\n'
'\n'
'.. automodule:: testpkg.world\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
'\n'
'Module contents\n'
'---------------\n'
'\n'
'.. automodule:: testpkg\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
content = (outdir / 'testpkg.subpkg.rst').read_text(encoding='utf8')
@@ -583,8 +590,8 @@ def test_package_file(tmp_path):
'\n'
'.. automodule:: testpkg.subpkg\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
@@ -615,8 +622,8 @@ def test_package_file_separate(tmp_path):
'\n'
'.. automodule:: testpkg\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
content = (outdir / 'testpkg.example.rst').read_text(encoding='utf8')
@@ -626,8 +633,8 @@ def test_package_file_separate(tmp_path):
'\n'
'.. automodule:: testpkg.example\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
@@ -645,8 +652,8 @@ def test_package_file_module_first(tmp_path):
'\n'
'.. automodule:: testpkg\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
'\n'
'Submodules\n'
'----------\n'
@@ -656,8 +663,8 @@ def test_package_file_module_first(tmp_path):
'\n'
'.. automodule:: testpkg.example\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
@@ -678,8 +685,8 @@ def test_package_file_without_submodules(tmp_path):
'\n'
'.. automodule:: testpkg\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
@@ -710,8 +717,8 @@ def test_namespace_package_file(tmp_path):
'\n'
'.. automodule:: testpkg.example\n'
' :members:\n'
- ' :undoc-members:\n'
' :show-inheritance:\n'
+ ' :undoc-members:\n'
)
@@ -721,12 +728,12 @@ def test_no_duplicates(rootdir, tmp_path):
We can't use pytest.mark.apidoc here as we use a different set of arguments
to apidoc_main
"""
- original_suffixes = sphinx.ext.apidoc.PY_SUFFIXES
+ original_suffixes = sphinx.ext.apidoc._generate.PY_SUFFIXES
try:
# Ensure test works on Windows
- sphinx.ext.apidoc.PY_SUFFIXES += ('.so',)
+ sphinx.ext.apidoc._generate.PY_SUFFIXES += ('.so',)
- package = rootdir / 'test-apidoc-duplicates' / 'fish_licence'
+ package = rootdir / 'test-ext-apidoc-duplicates' / 'fish_licence'
outdir = tmp_path / 'out'
apidoc_main(['-o', str(outdir), '-T', str(package), '--implicit-namespaces'])
@@ -739,7 +746,7 @@ def test_no_duplicates(rootdir, tmp_path):
assert count_submodules == 1
finally:
- sphinx.ext.apidoc.PY_SUFFIXES = original_suffixes
+ sphinx.ext.apidoc._generate.PY_SUFFIXES = original_suffixes
def test_remove_old_files(tmp_path: Path):
diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py
index dbe0fbe1d71..fe7ff9a907b 100644
--- a/tests/test_extensions/test_ext_autodoc.py
+++ b/tests/test_extensions/test_ext_autodoc.py
@@ -10,17 +10,18 @@
import itertools
import operator
import sys
-from types import SimpleNamespace
from typing import TYPE_CHECKING
from unittest.mock import Mock
from warnings import catch_warnings
import pytest
-from docutils.statemachine import ViewList
from sphinx import addnodes
from sphinx.ext.autodoc import ALL, ModuleLevelDocumenter, Options
+# NEVER import these objects from sphinx.ext.autodoc directly
+from sphinx.ext.autodoc.directive import DocumenterBridge
+
from tests.test_extensions.autodoc_util import do_autodoc
try:
@@ -34,8 +35,10 @@
if TYPE_CHECKING:
from typing import Any
+ from sphinx.environment import BuildEnvironment
+
-def make_directive_bridge(env):
+def make_directive_bridge(env: BuildEnvironment) -> DocumenterBridge:
options = Options(
inherited_members=False,
undoc_members=False,
@@ -54,11 +57,11 @@ def make_directive_bridge(env):
ignore_module_all=False,
)
- directive = SimpleNamespace(
+ directive = DocumenterBridge(
env=env,
- genopt=options,
- result=ViewList(),
- record_dependencies=set(),
+ reporter=None,
+ options=options,
+ lineno=0,
state=Mock(),
)
directive.state.document.settings.tab_width = 8
@@ -95,9 +98,10 @@ def verify(objtype, name, result):
'test_ext_autodoc.raises(exc) -> None',
('test_ext_autodoc', ['raises'], 'exc', 'None'),
)
- directive.env.temp_data['autodoc:module'] = 'test_ext_autodoc'
+ directive.env.current_document.autodoc_module = 'test_ext_autodoc'
verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None))
- del directive.env.temp_data['autodoc:module']
+ directive.env.current_document.autodoc_module = ''
+
directive.env.ref_context['py:module'] = 'test_ext_autodoc'
verify('function', 'raises', ('test_ext_autodoc', ['raises'], None, None))
verify('class', 'Base', ('test_ext_autodoc', ['Base'], None, None))
@@ -111,7 +115,7 @@ def verify(objtype, name, result):
)
directive.env.ref_context['py:module'] = 'sphinx.testing.util'
directive.env.ref_context['py:class'] = 'Foo'
- directive.env.temp_data['autodoc:class'] = 'SphinxTestApp'
+ directive.env.current_document.autodoc_class = 'SphinxTestApp'
verify(
'method',
'cleanup',
@@ -216,7 +220,7 @@ class GMeta(FMeta):
assert formatsig('class', 'C', C, None, None) == '(a, b=None)'
assert formatsig('class', 'C', D, 'a, b', 'X') == '(a, b) -> X'
- class ListSubclass(list):
+ class ListSubclass(list): # NoQA: FURB189
pass
# only supported if the python implementation decides to document it
@@ -239,8 +243,7 @@ class F2:
"""some docstring for F2."""
def __init__(self, *args, **kw):
- """
- __init__(a1, a2, kw1=True, kw2=False)
+ """__init__(a1, a2, kw1=True, kw2=False)
some docstring for __init__.
"""
@@ -256,7 +259,7 @@ class H:
def foo1(self, b, *c):
pass
- def foo2(b, *c):
+ def foo2(b, *c): # NoQA: N805
pass
def foo3(self, d='\n'):
@@ -317,7 +320,7 @@ def process_signature(*args):
app.connect('autodoc-process-signature', process_signature)
- def func(x: int, y: int) -> int:
+ def func(x: int, y: int) -> int: # type: ignore[empty-body]
pass
directive = make_directive_bridge(app.env)
@@ -359,9 +362,7 @@ def f():
"""Docstring"""
def g():
- """
- Docstring
- """
+ """Docstring"""
for func in (f, g):
assert getdocl('function', func) == ['Docstring']
@@ -526,7 +527,7 @@ def test_autodoc_exception(app):
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_warnings(app):
- app.env.temp_data['docname'] = 'dummy'
+ app.env.current_document.docname = 'dummy'
# can't import module
do_autodoc(app, 'module', 'unknown')
@@ -727,7 +728,9 @@ def test_autodoc_undoc_members(app):
actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
+ ' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr',
+ ' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment',
@@ -749,7 +752,9 @@ def test_autodoc_undoc_members(app):
actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
+ ' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr',
+ ' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment',
@@ -920,7 +925,9 @@ def test_autodoc_special_members(app):
' .. py:method:: Class.__special1__()',
' .. py:method:: Class.__special2__()',
' .. py:attribute:: Class.__weakref__',
+ ' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr',
+ ' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment',
@@ -1199,6 +1206,8 @@ def test_autodoc_member_order(app):
' .. py:attribute:: Class.mdocattr',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)',
' .. py:method:: Class.moore(a, e, f) -> happiness',
+ ' .. py:method:: Class.b_staticmeth()',
+ ' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.inst_attr_inline',
' .. py:attribute:: Class.inst_attr_comment',
' .. py:attribute:: Class.inst_attr_string',
@@ -1215,10 +1224,15 @@ def test_autodoc_member_order(app):
actual = do_autodoc(app, 'class', 'target.Class', options)
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
- ' .. py:method:: Class.excludemeth()',
- ' .. py:method:: Class.meth()',
+ # class methods
' .. py:method:: Class.moore(a, e, f) -> happiness',
' .. py:method:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)',
+ # static methods
+ ' .. py:method:: Class.a_staticmeth()',
+ ' .. py:method:: Class.b_staticmeth()',
+ # regular methods
+ ' .. py:method:: Class.excludemeth()',
+ ' .. py:method:: Class.meth()',
' .. py:method:: Class.skipmeth()',
' .. py:method:: Class.undocmeth()',
' .. py:attribute:: Class._private_inst_attr',
@@ -1242,7 +1256,9 @@ def test_autodoc_member_order(app):
assert list(filter(lambda l: '::' in l, actual)) == [
'.. py:class:: Class(arg)',
' .. py:attribute:: Class._private_inst_attr',
+ ' .. py:method:: Class.a_staticmeth()',
' .. py:attribute:: Class.attr',
+ ' .. py:method:: Class.b_staticmeth()',
' .. py:attribute:: Class.docattr',
' .. py:method:: Class.excludemeth()',
' .. py:attribute:: Class.inst_attr_comment',
@@ -1299,7 +1315,7 @@ def test_autodoc_module_member_order(app):
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_module_scope(app):
- app.env.temp_data['autodoc:module'] = 'target'
+ app.env.current_document.autodoc_module = 'target'
actual = do_autodoc(app, 'attribute', 'Class.mdocattr')
assert list(actual) == [
'',
@@ -1314,8 +1330,8 @@ def test_autodoc_module_scope(app):
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_class_scope(app):
- app.env.temp_data['autodoc:module'] = 'target'
- app.env.temp_data['autodoc:class'] = 'Class'
+ app.env.current_document.autodoc_module = 'target'
+ app.env.current_document.autodoc_class = 'Class'
actual = do_autodoc(app, 'attribute', 'mdocattr')
assert list(actual) == [
'',
@@ -1527,7 +1543,7 @@ def _node(
tab = ' ' * 3
def rst_option(name: str, value: Any) -> str:
- value = '' if value in {1, True} else value
+ value = '' if value == 1 else value # note True == 1.
return f'{prefix}{tab}:{name}: {value!s}'.rstrip()
lines = [
@@ -2572,7 +2588,7 @@ def test_autodoc_TYPE_CHECKING(app):
'',
' .. py:attribute:: Foo.attr1',
' :module: target.TYPE_CHECKING',
- ' :type: ~_io.StringIO',
+ ' :type: ~io.StringIO',
'',
'',
'.. py:function:: spam(ham: ~collections.abc.Iterable[str]) -> tuple[~gettext.NullTranslations, bool]',
@@ -3164,3 +3180,32 @@ def function_rst(name, sig):
*function_rst('bar', 'x: typing.Literal[1234]'),
*function_rst('foo', 'x: typing.Literal[target.literal.MyEnum.a]'),
]
+
+
+@pytest.mark.sphinx('html', testroot='ext-autodoc')
+def test_no_index_entry(app):
+ # modules can use no-index-entry
+ options = {'no-index-entry': None}
+ actual = do_autodoc(app, 'module', 'target.module', options)
+ assert ' :no-index-entry:' in list(actual)
+
+ # classes can use no-index-entry
+ actual = do_autodoc(app, 'class', 'target.classes.Foo', options)
+ assert ' :no-index-entry:' in list(actual)
+
+ # functions can use no-index-entry
+ actual = do_autodoc(app, 'function', 'target.functions.func', options)
+ assert ' :no-index-entry:' in list(actual)
+
+ # modules respect no-index-entry in autodoc_default_options
+ app.config.autodoc_default_options = {'no-index-entry': True}
+ actual = do_autodoc(app, 'module', 'target.module')
+ assert ' :no-index-entry:' in list(actual)
+
+ # classes respect config-level no-index-entry
+ actual = do_autodoc(app, 'class', 'target.classes.Foo')
+ assert ' :no-index-entry:' in list(actual)
+
+ # functions respect config-level no-index-entry
+ actual = do_autodoc(app, 'function', 'target.functions.func')
+ assert ' :no-index-entry:' in list(actual)
diff --git a/tests/test_extensions/test_ext_autodoc_autoattribute.py b/tests/test_extensions/test_ext_autodoc_autoattribute.py
index 41fcc99011b..51358302a6e 100644
--- a/tests/test_extensions/test_ext_autodoc_autoattribute.py
+++ b/tests/test_extensions/test_ext_autodoc_autoattribute.py
@@ -4,6 +4,8 @@
source file translated by test_build.
"""
+from __future__ import annotations
+
import pytest
from tests.test_extensions.autodoc_util import do_autodoc
diff --git a/tests/test_extensions/test_ext_autodoc_autodata.py b/tests/test_extensions/test_ext_autodoc_autodata.py
index b794666e97e..796e29e1d45 100644
--- a/tests/test_extensions/test_ext_autodoc_autodata.py
+++ b/tests/test_extensions/test_ext_autodoc_autodata.py
@@ -4,6 +4,8 @@
source file translated by test_build.
"""
+from __future__ import annotations
+
import pytest
from tests.test_extensions.autodoc_util import do_autodoc
diff --git a/tests/test_extensions/test_ext_autodoc_autofunction.py b/tests/test_extensions/test_ext_autodoc_autofunction.py
index e70069bfd61..1899e5a8a0c 100644
--- a/tests/test_extensions/test_ext_autodoc_autofunction.py
+++ b/tests/test_extensions/test_ext_autodoc_autofunction.py
@@ -4,7 +4,12 @@
source file translated by test_build.
"""
-from typing import Any
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any
import pytest
diff --git a/tests/test_extensions/test_ext_autodoc_automodule.py b/tests/test_extensions/test_ext_autodoc_automodule.py
index 2f502f12336..c9503a765c3 100644
--- a/tests/test_extensions/test_ext_autodoc_automodule.py
+++ b/tests/test_extensions/test_ext_autodoc_automodule.py
@@ -4,6 +4,8 @@
source file translated by test_build.
"""
+from __future__ import annotations
+
import inspect
import sys
import typing
diff --git a/tests/test_extensions/test_ext_autodoc_autoproperty.py b/tests/test_extensions/test_ext_autodoc_autoproperty.py
index 9c9178d3b01..b6d2125e25f 100644
--- a/tests/test_extensions/test_ext_autodoc_autoproperty.py
+++ b/tests/test_extensions/test_ext_autodoc_autoproperty.py
@@ -4,6 +4,8 @@
source file translated by test_build.
"""
+from __future__ import annotations
+
import pytest
from tests.test_extensions.autodoc_util import do_autodoc
diff --git a/tests/test_extensions/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py
index c3911b8533c..f56948c3d23 100644
--- a/tests/test_extensions/test_ext_autodoc_configs.py
+++ b/tests/test_extensions/test_ext_autodoc_configs.py
@@ -1,10 +1,11 @@
"""Test the autodoc extension. This tests mainly for config variables"""
+from __future__ import annotations
+
import platform
import sys
-from collections.abc import Iterator
from contextlib import contextmanager
-from pathlib import Path
+from typing import TYPE_CHECKING
import pytest
@@ -12,10 +13,9 @@
from tests.test_extensions.autodoc_util import do_autodoc
-skip_py314_segfault = pytest.mark.skipif(
- sys.version_info[:2] >= (3, 14),
- reason='Segmentation fault: https://github.com/python/cpython/issues/125017',
-)
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+ from pathlib import Path
IS_PYPY = platform.python_implementation() == 'PyPy'
@@ -189,7 +189,6 @@ def test_autodoc_class_signature_separated_init(app):
]
-@skip_py314_segfault
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_class_signature_separated_new(app):
app.config.autodoc_class_signature = 'separated'
@@ -373,7 +372,6 @@ def test_autodoc_inherit_docstrings_for_inherited_members(app):
]
-@skip_py314_segfault
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_docstring_signature(app):
options = {'members': None, 'special-members': '__init__, __new__'}
@@ -450,8 +448,7 @@ def test_autodoc_docstring_signature(app):
' __init__(self, a, b=1) -> None',
' First line of docstring',
'',
- ' rest of docstring',
- '',
+ ' rest of docstring',
'',
'',
' .. py:method:: DocstringSig.__new__(cls, *new_args, **new_kwargs)',
@@ -461,8 +458,7 @@ def test_autodoc_docstring_signature(app):
' __new__(cls, d, e=1) -> DocstringSig',
' First line of docstring',
'',
- ' rest of docstring',
- '',
+ ' rest of docstring',
'',
'',
' .. py:method:: DocstringSig.meth()',
@@ -471,8 +467,7 @@ def test_autodoc_docstring_signature(app):
' meth(FOO, BAR=1) -> BAZ',
' First line of docstring',
'',
- ' rest of docstring',
- '',
+ ' rest of docstring',
'',
'',
' .. py:method:: DocstringSig.meth2()',
@@ -635,7 +630,7 @@ def test_mocked_module_imports(app):
sys.modules.pop('target', None) # unload target module to clear the module cache
# no autodoc_mock_imports
- options = {'members': 'TestAutodoc,decoratedFunction,func,Alias'}
+ options = {'members': 'TestAutodoc,decorated_function,func,Alias'}
actual = do_autodoc(app, 'module', 'target.need_mocks', options)
assert list(actual) == []
assert "autodoc: failed to import module 'need_mocks'" in app.warning.getvalue()
@@ -674,16 +669,16 @@ def test_mocked_module_imports(app):
' docstring',
'',
'',
- ' .. py:method:: TestAutodoc.decoratedMethod()',
+ ' .. py:method:: TestAutodoc.decorated_method()',
' :module: target.need_mocks',
'',
- ' TestAutodoc::decoratedMethod docstring',
+ ' TestAutodoc::decorated_method docstring',
'',
'',
- '.. py:function:: decoratedFunction()',
+ '.. py:function:: decorated_function()',
' :module: target.need_mocks',
'',
- ' decoratedFunction docstring',
+ ' decorated_function docstring',
'',
'',
'.. py:function:: func(arg: missing_module.Class)',
@@ -701,11 +696,6 @@ def test_mocked_module_imports(app):
confoverrides={'autodoc_typehints': 'signature'},
)
def test_autodoc_typehints_signature(app):
- if sys.version_info[:2] >= (3, 13):
- type_ppp = 'pathlib._local.PurePosixPath'
- else:
- type_ppp = 'pathlib.PurePosixPath'
-
options = {
'members': None,
'undoc-members': None,
@@ -731,7 +721,7 @@ def test_autodoc_typehints_signature(app):
'',
'.. py:data:: CONST3',
' :module: target.typehints',
- f' :type: ~{type_ppp}',
+ ' :type: ~pathlib.PurePosixPath',
" :value: PurePosixPath('/a/b/c')",
'',
' docstring',
@@ -754,7 +744,7 @@ def test_autodoc_typehints_signature(app):
'',
' .. py:attribute:: Math.CONST3',
' :module: target.typehints',
- f' :type: ~{type_ppp}',
+ ' :type: ~pathlib.PurePosixPath',
" :value: PurePosixPath('/a/b/c')",
'',
'',
@@ -776,7 +766,7 @@ def test_autodoc_typehints_signature(app):
'',
' .. py:property:: Math.path',
' :module: target.typehints',
- f' :type: ~{type_ppp}',
+ ' :type: ~pathlib.PurePosixPath',
'',
'',
' .. py:property:: Math.prop',
@@ -801,7 +791,7 @@ def test_autodoc_typehints_signature(app):
'',
' docstring',
'',
- f" alias of TypeVar('T', bound=\\ :py:class:`~{type_ppp}`)",
+ " alias of TypeVar('T', bound=\\ :py:class:`~pathlib.PurePosixPath`)",
'',
'',
'.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, '
@@ -833,10 +823,6 @@ def test_autodoc_typehints_signature(app):
confoverrides={'autodoc_typehints': 'none'},
)
def test_autodoc_typehints_none(app):
- if sys.version_info[:2] >= (3, 13):
- type_ppp = 'pathlib._local.PurePosixPath'
- else:
- type_ppp = 'pathlib.PurePosixPath'
options = {
'members': None,
'undoc-members': None,
@@ -924,7 +910,7 @@ def test_autodoc_typehints_none(app):
'',
' docstring',
'',
- f" alias of TypeVar('T', bound=\\ :py:class:`~{type_ppp}`)",
+ " alias of TypeVar('T', bound=\\ :py:class:`~pathlib.PurePosixPath`)",
'',
'',
'.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)',
@@ -1370,7 +1356,7 @@ def test_autodoc_type_aliases(app):
' docstring',
'',
'',
- '.. py:function:: read(r: ~_io.BytesIO) -> ~_io.StringIO',
+ '.. py:function:: read(r: ~io.BytesIO) -> ~io.StringIO',
' :module: target.autodoc_type_aliases',
'',
' docstring',
@@ -1443,7 +1429,7 @@ def test_autodoc_type_aliases(app):
' docstring',
'',
'',
- '.. py:function:: read(r: ~_io.BytesIO) -> my.module.StringIO',
+ '.. py:function:: read(r: ~io.BytesIO) -> my.module.StringIO',
' :module: target.autodoc_type_aliases',
'',
' docstring',
@@ -1516,10 +1502,6 @@ def test_autodoc_typehints_description_and_type_aliases(app):
confoverrides={'autodoc_typehints_format': 'fully-qualified'},
)
def test_autodoc_typehints_format_fully_qualified(app):
- if sys.version_info[:2] >= (3, 13):
- type_ppp = 'pathlib._local.PurePosixPath'
- else:
- type_ppp = 'pathlib.PurePosixPath'
options = {
'members': None,
'undoc-members': None,
@@ -1545,7 +1527,7 @@ def test_autodoc_typehints_format_fully_qualified(app):
'',
'.. py:data:: CONST3',
' :module: target.typehints',
- f' :type: {type_ppp}',
+ ' :type: pathlib.PurePosixPath',
" :value: PurePosixPath('/a/b/c')",
'',
' docstring',
@@ -1568,7 +1550,7 @@ def test_autodoc_typehints_format_fully_qualified(app):
'',
' .. py:attribute:: Math.CONST3',
' :module: target.typehints',
- f' :type: {type_ppp}',
+ ' :type: pathlib.PurePosixPath',
" :value: PurePosixPath('/a/b/c')",
'',
'',
@@ -1590,7 +1572,7 @@ def test_autodoc_typehints_format_fully_qualified(app):
'',
' .. py:property:: Math.path',
' :module: target.typehints',
- f' :type: {type_ppp}',
+ ' :type: pathlib.PurePosixPath',
'',
'',
' .. py:property:: Math.prop',
@@ -1615,7 +1597,7 @@ def test_autodoc_typehints_format_fully_qualified(app):
'',
' docstring',
'',
- f" alias of TypeVar('T', bound=\\ :py:class:`{type_ppp}`)",
+ " alias of TypeVar('T', bound=\\ :py:class:`pathlib.PurePosixPath`)",
'',
'',
'.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, '
@@ -1700,7 +1682,7 @@ def test_autodoc_default_options(app):
if (3, 11, 7) <= sys.version_info < (3, 12) or sys.version_info >= (3, 12, 1):
list_of_weak_references = ' list of weak references to the object'
else:
- list_of_weak_references = " list of weak references to the object (if defined)" # fmt: skip
+ list_of_weak_references = ' list of weak references to the object (if defined)' # fmt: skip
# no settings
actual = do_autodoc(app, 'class', 'target.enums.EnumCls')
@@ -1779,7 +1761,7 @@ def test_autodoc_default_options_with_values(app):
if (3, 11, 7) <= sys.version_info < (3, 12) or sys.version_info >= (3, 12, 1):
list_of_weak_references = ' list of weak references to the object'
else:
- list_of_weak_references = " list of weak references to the object (if defined)" # fmt: skip
+ list_of_weak_references = ' list of weak references to the object (if defined)' # fmt: skip
# with :members:
app.config.autodoc_default_options = {'members': 'val1,val2'}
diff --git a/tests/test_extensions/test_ext_autodoc_events.py b/tests/test_extensions/test_ext_autodoc_events.py
index 8ff8630ad7e..c2e9940a92b 100644
--- a/tests/test_extensions/test_ext_autodoc_events.py
+++ b/tests/test_extensions/test_ext_autodoc_events.py
@@ -1,5 +1,7 @@
"""Test the autodoc extension. This tests mainly for autodoc events"""
+from __future__ import annotations
+
import pytest
from sphinx.ext.autodoc import between, cut_lines
@@ -69,7 +71,7 @@ def test_cut_lines_no_objtype():
]
process = cut_lines(2)
- process(None, 'function', 'func', None, {}, docstring_lines) # type: ignore[arg-type]
+ process(None, 'function', 'func', None, {}, docstring_lines)
assert docstring_lines == [
'second line',
'---',
diff --git a/tests/test_extensions/test_ext_autodoc_importer.py b/tests/test_extensions/test_ext_autodoc_importer.py
new file mode 100644
index 00000000000..f14b8256c14
--- /dev/null
+++ b/tests/test_extensions/test_ext_autodoc_importer.py
@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+from sphinx.ext.autodoc.importer import import_module
+
+
+def test_import_native_module_stubs(rootdir: Path) -> None:
+ fish_licence_root = rootdir / 'test-ext-apidoc-duplicates'
+
+ sys_path = list(sys.path)
+ sys.path.insert(0, str(fish_licence_root))
+ halibut = import_module('fish_licence.halibut')
+ sys.path[:] = sys_path
+
+ assert halibut.__file__.endswith('halibut.pyi')
+ assert halibut.__spec__.origin.endswith('halibut.pyi')
+
+ halibut_path = Path(halibut.__file__).resolve()
+ assert halibut_path.is_file()
+ assert halibut_path == fish_licence_root / 'fish_licence' / 'halibut.pyi'
diff --git a/tests/test_extensions/test_ext_autodoc_preserve_defaults.py b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py
index 1f8b5554c6e..b7b4fcf7027 100644
--- a/tests/test_extensions/test_ext_autodoc_preserve_defaults.py
+++ b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py
@@ -1,5 +1,7 @@
"""Test the autodoc extension."""
+from __future__ import annotations
+
import pytest
from tests.test_extensions.autodoc_util import do_autodoc
diff --git a/tests/test_extensions/test_ext_autodoc_private_members.py b/tests/test_extensions/test_ext_autodoc_private_members.py
index 2915c74d7af..d5f8df9f03d 100644
--- a/tests/test_extensions/test_ext_autodoc_private_members.py
+++ b/tests/test_extensions/test_ext_autodoc_private_members.py
@@ -1,5 +1,7 @@
"""Test the autodoc extension. This tests mainly for private-members option."""
+from __future__ import annotations
+
import pytest
from tests.test_extensions.autodoc_util import do_autodoc
diff --git a/tests/test_extensions/test_ext_autosectionlabel.py b/tests/test_extensions/test_ext_autosectionlabel.py
index 2133f64bfaf..fdd0267ab69 100644
--- a/tests/test_extensions/test_ext_autosectionlabel.py
+++ b/tests/test_extensions/test_ext_autosectionlabel.py
@@ -1,12 +1,14 @@
"""Test sphinx.ext.autosectionlabel extension."""
+from __future__ import annotations
+
import re
import pytest
@pytest.mark.sphinx('html', testroot='ext-autosectionlabel')
-def test_autosectionlabel_html(app, skipped_labels=False):
+def test_autosectionlabel_html(app):
app.build(force_all=True)
content = (app.outdir / 'index.html').read_text(encoding='utf8')
diff --git a/tests/test_extensions/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py
index 5a96afdd3e3..47589718fdc 100644
--- a/tests/test_extensions/test_ext_autosummary.py
+++ b/tests/test_extensions/test_ext_autosummary.py
@@ -1,10 +1,12 @@
"""Test the autosummary extension."""
+from __future__ import annotations
+
import sys
from contextlib import chdir
from io import StringIO
+from typing import TYPE_CHECKING
from unittest.mock import Mock, patch
-from xml.etree.ElementTree import Element
import pytest
from docutils import nodes
@@ -26,6 +28,9 @@
from sphinx.testing.util import assert_node, etree_parse
from sphinx.util.docutils import new_document
+if TYPE_CHECKING:
+ from xml.etree.ElementTree import Element
+
html_warnfile = StringIO()
@@ -142,7 +147,11 @@ def test_extract_summary(capsys):
assert err == ''
-@pytest.mark.sphinx('dummy', testroot='autosummary', confoverrides=defaults.copy())
+@pytest.mark.sphinx(
+ 'dummy',
+ testroot='ext-autosummary-ext',
+ confoverrides=defaults.copy(),
+)
def test_get_items_summary(make_app, app_params):
import sphinx.ext.autosummary
import sphinx.ext.autosummary.generate
@@ -181,9 +190,9 @@ def handler(app, what, name, obj, options, lines):
assert html_warnings == ''
expected_values = {
- 'withSentence': 'I have a sentence which spans multiple lines.',
- 'noSentence': "this doesn't start with a capital.",
- 'emptyLine': 'This is the real summary',
+ 'with_sentence': 'I have a sentence which spans multiple lines.',
+ 'no_sentence': "this doesn't start with a capital.",
+ 'empty_line': 'This is the real summary',
'module_attr': 'This is a module attribute',
'C.class_attr': 'This is a class attribute',
'C.instance_attr': 'This is an instance attribute',
@@ -214,7 +223,11 @@ def str_content(elem: Element) -> str:
return ''.join(str_content(e) for e in elem)
-@pytest.mark.sphinx('xml', testroot='autosummary', confoverrides=defaults.copy())
+@pytest.mark.sphinx(
+ 'xml',
+ testroot='ext-autosummary-ext',
+ confoverrides=defaults.copy(),
+)
def test_escaping(app):
app.build(force_all=True)
@@ -227,7 +240,7 @@ def test_escaping(app):
@pytest.mark.sphinx('html', testroot='ext-autosummary')
def test_autosummary_generate_content_for_module(app):
- import autosummary_dummy_module
+ import autosummary_dummy_module # type: ignore[import-not-found]
template = Mock()
@@ -444,7 +457,7 @@ def test_autosummary_generate_content_for_module_imported_members(app):
@pytest.mark.sphinx('html', testroot='ext-autosummary')
def test_autosummary_generate_content_for_module_imported_members_inherited_module(app):
- import autosummary_dummy_inherited_module
+ import autosummary_dummy_inherited_module # type: ignore[import-not-found]
template = Mock()
@@ -739,7 +752,11 @@ def test_autosummary_filename_map(app):
assert html_warnings == ''
-@pytest.mark.sphinx('latex', testroot='autosummary', confoverrides=defaults.copy())
+@pytest.mark.sphinx(
+ 'latex',
+ testroot='ext-autosummary-ext',
+ confoverrides=defaults.copy(),
+)
def test_autosummary_latex_table_colspec(app):
app.build(force_all=True)
result = (app.outdir / 'projectnamenotset.tex').read_text(encoding='utf8')
@@ -809,9 +826,8 @@ def test_autosummary_module_all(app):
app.build()
# generated/foo is generated successfully
assert app.env.get_doctree('generated/autosummary_dummy_package_all')
- module = (
- app.srcdir / 'generated' / 'autosummary_dummy_package_all.rst'
- ).read_text(encoding='utf8')
+ path = app.srcdir / 'generated' / 'autosummary_dummy_package_all.rst'
+ module = path.read_text(encoding='utf8')
assert ' .. autosummary::\n \n PublicBar\n \n' in module
assert (
' .. autosummary::\n \n public_foo\n public_baz\n \n'
@@ -823,6 +839,30 @@ def test_autosummary_module_all(app):
sys.modules.pop('autosummary_dummy_package_all', None)
+@pytest.mark.sphinx('dummy', testroot='ext-autosummary-module_empty_all')
+def test_autosummary_module_empty_all(app):
+ try:
+ app.build()
+ # generated/foo is generated successfully
+ assert app.env.get_doctree('generated/autosummary_dummy_package_empty_all')
+ path = app.srcdir / 'generated' / 'autosummary_dummy_package_empty_all.rst'
+ module = path.read_text(encoding='utf8')
+ assert '.. automodule:: autosummary_dummy_package_empty_all' in module
+ # for __all__ = (), the output should not contain any variables
+ assert '__all__' not in module
+ assert '__builtins__' not in module
+ assert '__cached__' not in module
+ assert '__doc__' not in module
+ assert '__file__' not in module
+ assert '__loader__' not in module
+ assert '__name__' not in module
+ assert '__package__' not in module
+ assert '__path__' not in module
+ assert '__spec__' not in module
+ finally:
+ sys.modules.pop('autosummary_dummy_package_all', None)
+
+
@pytest.mark.sphinx(
'html',
testroot='ext-autodoc',
diff --git a/tests/test_extensions/test_ext_autosummary_imports.py b/tests/test_extensions/test_ext_autosummary_imports.py
index 12a0d4624b0..7abee757e3c 100644
--- a/tests/test_extensions/test_ext_autosummary_imports.py
+++ b/tests/test_extensions/test_ext_autosummary_imports.py
@@ -1,5 +1,7 @@
"""Test autosummary for import cycles."""
+from __future__ import annotations
+
import pytest
from docutils import nodes
diff --git a/tests/test_extensions/test_ext_coverage.py b/tests/test_extensions/test_ext_coverage.py
index 563fd7eb61b..5dc2f95a8bc 100644
--- a/tests/test_extensions/test_ext_coverage.py
+++ b/tests/test_extensions/test_ext_coverage.py
@@ -1,5 +1,7 @@
"""Test the coverage builder."""
+from __future__ import annotations
+
import pickle
import pytest
@@ -37,7 +39,7 @@ def test_build(app):
)
assert len(undoc_c) == 1
# the key is the full path to the header file, which isn't testable
- assert list(undoc_c.values())[0] == {('function', 'Py_SphinxTest')}
+ assert next(iter(undoc_c.values())) == {('function', 'Py_SphinxTest')}
assert 'autodoc_target' in undoc_py
assert 'funcs' in undoc_py['autodoc_target']
diff --git a/tests/test_extensions/test_ext_doctest.py b/tests/test_extensions/test_ext_doctest.py
index 0f141f276d2..d075ca0485b 100644
--- a/tests/test_extensions/test_ext_doctest.py
+++ b/tests/test_extensions/test_ext_doctest.py
@@ -1,5 +1,7 @@
"""Test the doctest extension."""
+from __future__ import annotations
+
import os
from collections import Counter
@@ -73,7 +75,7 @@ def cleanup_call():
cleanup_called += 1
-recorded_calls = Counter()
+recorded_calls: Counter[tuple[str, str, int]] = Counter()
@pytest.mark.sphinx('doctest', testroot='ext-doctest-skipif')
@@ -120,7 +122,7 @@ def test_skipif(app):
def record(directive, part, should_skip):
- recorded_calls[(directive, part, should_skip)] += 1
+ recorded_calls[directive, part, should_skip] += 1
return f'Recorded {directive} {part} {should_skip}'
diff --git a/tests/test_extensions/test_ext_duration.py b/tests/test_extensions/test_ext_duration.py
index 535786de892..c8984de02e3 100644
--- a/tests/test_extensions/test_ext_duration.py
+++ b/tests/test_extensions/test_ext_duration.py
@@ -1,5 +1,7 @@
"""Test sphinx.ext.duration extension."""
+from __future__ import annotations
+
import re
import pytest
diff --git a/tests/test_extensions/test_ext_extlinks.py b/tests/test_extensions/test_ext_extlinks.py
index 38614329802..55b80e84e4d 100644
--- a/tests/test_extensions/test_ext_extlinks.py
+++ b/tests/test_extensions/test_ext_extlinks.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
diff --git a/tests/test_extensions/test_ext_githubpages.py b/tests/test_extensions/test_ext_githubpages.py
index 156b9a3916a..ddf54d96e6f 100644
--- a/tests/test_extensions/test_ext_githubpages.py
+++ b/tests/test_extensions/test_ext_githubpages.py
@@ -1,5 +1,7 @@
"""Test sphinx.ext.githubpages extension."""
+from __future__ import annotations
+
import pytest
diff --git a/tests/test_extensions/test_ext_graphviz.py b/tests/test_extensions/test_ext_graphviz.py
index 4be01caf023..b4e0a167316 100644
--- a/tests/test_extensions/test_ext_graphviz.py
+++ b/tests/test_extensions/test_ext_graphviz.py
@@ -1,5 +1,7 @@
"""Test sphinx.ext.graphviz extension."""
+from __future__ import annotations
+
import re
import sys
diff --git a/tests/test_extensions/test_ext_ifconfig.py b/tests/test_extensions/test_ext_ifconfig.py
index 318c56a5741..7c07405b2f8 100644
--- a/tests/test_extensions/test_ext_ifconfig.py
+++ b/tests/test_extensions/test_ext_ifconfig.py
@@ -1,5 +1,7 @@
"""Test sphinx.ext.ifconfig extension."""
+from __future__ import annotations
+
import docutils.utils
import pytest
diff --git a/tests/test_extensions/test_ext_imgconverter.py b/tests/test_extensions/test_ext_imgconverter.py
index a37c98c172d..b44fd5782a2 100644
--- a/tests/test_extensions/test_ext_imgconverter.py
+++ b/tests/test_extensions/test_ext_imgconverter.py
@@ -1,5 +1,7 @@
"""Test sphinx.ext.imgconverter extension."""
+from __future__ import annotations
+
import subprocess
import pytest
diff --git a/tests/test_extensions/test_ext_imgmockconverter.py b/tests/test_extensions/test_ext_imgmockconverter.py
index fc34c24c65e..7d10a522e93 100644
--- a/tests/test_extensions/test_ext_imgmockconverter.py
+++ b/tests/test_extensions/test_ext_imgmockconverter.py
@@ -1,5 +1,7 @@
"""Test image converter with identical basenames"""
+from __future__ import annotations
+
import pytest
diff --git a/tests/test_extensions/test_ext_inheritance_diagram.py b/tests/test_extensions/test_ext_inheritance_diagram.py
index 2aa4ff99188..20c2dc6a8fe 100644
--- a/tests/test_extensions/test_ext_inheritance_diagram.py
+++ b/tests/test_extensions/test_ext_inheritance_diagram.py
@@ -1,5 +1,7 @@
"""Test sphinx.ext.inheritance_diagram extension."""
+from __future__ import annotations
+
import re
import sys
import zlib
@@ -12,7 +14,7 @@
InheritanceException,
import_classes,
)
-from sphinx.ext.intersphinx import load_mappings, validate_intersphinx_mapping
+from sphinx.ext.intersphinx._load import load_mappings, validate_intersphinx_mapping
@pytest.mark.sphinx('html', testroot='inheritance')
@@ -325,7 +327,7 @@ def test_import_classes(rootdir):
saved_path = sys.path.copy()
sys.path.insert(0, str(rootdir / 'test-ext-inheritance_diagram'))
try:
- from example.sphinx import DummyClass
+ from example.sphinx import DummyClass # type: ignore[import-not-found]
# got exception for unknown class or module
with pytest.raises(InheritanceException):
diff --git a/tests/test_extensions/test_ext_intersphinx.py b/tests/test_extensions/test_ext_intersphinx.py
index ffc99032695..f5c83ac4eb5 100644
--- a/tests/test_extensions/test_ext_intersphinx.py
+++ b/tests/test_extensions/test_ext_intersphinx.py
@@ -11,24 +11,24 @@
from docutils import nodes
from sphinx import addnodes
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.builders.html import INVENTORY_FILENAME
from sphinx.config import Config
from sphinx.errors import ConfigError
-from sphinx.ext.intersphinx import (
- inspect_main,
- load_mappings,
- missing_reference,
- validate_intersphinx_mapping,
-)
from sphinx.ext.intersphinx import setup as intersphinx_setup
+from sphinx.ext.intersphinx._cli import inspect_main
from sphinx.ext.intersphinx._load import (
_fetch_inventory,
_fetch_inventory_group,
_get_safe_url,
+ _InvConfig,
_strip_basic_auth,
+ load_mappings,
+ validate_intersphinx_mapping,
)
+from sphinx.ext.intersphinx._resolve import missing_reference
from sphinx.ext.intersphinx._shared import _IntersphinxProject
-from sphinx.util.console import strip_colors
+from sphinx.util.inventory import _InventoryItem
from tests.test_util.intersphinx_data import (
INVENTORY_V2,
@@ -37,11 +37,8 @@
)
from tests.utils import http_server
-if TYPE_CHECKING:
- from typing import NoReturn
-
-class FakeList(list):
+class FakeList(list[str]):
def __iter__(self) -> NoReturn:
raise NotImplementedError
@@ -67,35 +64,37 @@ def set_config(app, mapping):
app.config.intersphinx_mapping = mapping.copy()
app.config.intersphinx_cache_limit = 0
app.config.intersphinx_disabled_reftypes = []
+ app.config.intersphinx_timeout = None
@mock.patch('sphinx.ext.intersphinx._load.InventoryFile')
-@mock.patch('sphinx.ext.intersphinx._load._read_from_url')
+@mock.patch('sphinx.ext.intersphinx._load.requests.get')
@pytest.mark.sphinx('html', testroot='root')
-def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app): # NoQA: PT019
+def test_fetch_inventory_redirection(get_request, InventoryFile, app):
+ mocked_get = get_request.return_value.__enter__.return_value
intersphinx_setup(app)
- _read_from_url().readline.return_value = b'# Sphinx inventory version 2'
+ mocked_get.content = b'# Sphinx inventory version 2'
# same uri and inv, not redirected
- _read_from_url().url = 'https://hostname/' + INVENTORY_FILENAME
+ mocked_get.url = 'https://hostname/' + INVENTORY_FILENAME
_fetch_inventory(
target_uri='https://hostname/',
inv_location='https://hostname/' + INVENTORY_FILENAME,
- config=app.config,
+ config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
)
assert 'intersphinx inventory has moved' not in app.status.getvalue()
- assert InventoryFile.load.call_args[0][1] == 'https://hostname/'
+ assert InventoryFile.loads.call_args[1]['uri'] == 'https://hostname/'
# same uri and inv, redirected
app.status.seek(0)
app.status.truncate(0)
- _read_from_url().url = 'https://hostname/new/' + INVENTORY_FILENAME
+ mocked_get.url = 'https://hostname/new/' + INVENTORY_FILENAME
_fetch_inventory(
target_uri='https://hostname/',
inv_location='https://hostname/' + INVENTORY_FILENAME,
- config=app.config,
+ config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
)
assert app.status.getvalue() == (
@@ -103,31 +102,31 @@ def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app): # NoQ
'https://hostname/%s -> https://hostname/new/%s\n'
% (INVENTORY_FILENAME, INVENTORY_FILENAME)
)
- assert InventoryFile.load.call_args[0][1] == 'https://hostname/new'
+ assert InventoryFile.loads.call_args[1]['uri'] == 'https://hostname/new'
# different uri and inv, not redirected
app.status.seek(0)
app.status.truncate(0)
- _read_from_url().url = 'https://hostname/new/' + INVENTORY_FILENAME
+ mocked_get.url = 'https://hostname/new/' + INVENTORY_FILENAME
_fetch_inventory(
target_uri='https://hostname/',
inv_location='https://hostname/new/' + INVENTORY_FILENAME,
- config=app.config,
+ config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
)
assert 'intersphinx inventory has moved' not in app.status.getvalue()
- assert InventoryFile.load.call_args[0][1] == 'https://hostname/'
+ assert InventoryFile.loads.call_args[1]['uri'] == 'https://hostname/'
# different uri and inv, redirected
app.status.seek(0)
app.status.truncate(0)
- _read_from_url().url = 'https://hostname/other/' + INVENTORY_FILENAME
+ mocked_get.url = 'https://hostname/other/' + INVENTORY_FILENAME
_fetch_inventory(
target_uri='https://hostname/',
inv_location='https://hostname/new/' + INVENTORY_FILENAME,
- config=app.config,
+ config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
)
assert app.status.getvalue() == (
@@ -135,7 +134,7 @@ def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app): # NoQ
'https://hostname/new/%s -> https://hostname/other/%s\n'
% (INVENTORY_FILENAME, INVENTORY_FILENAME)
)
- assert InventoryFile.load.call_args[0][1] == 'https://hostname/'
+ assert InventoryFile.loads.call_args[1]['uri'] == 'https://hostname/'
@pytest.mark.sphinx('html', testroot='root')
@@ -157,11 +156,11 @@ def test_missing_reference(tmp_path, app):
load_mappings(app)
inv = app.env.intersphinx_inventory
- assert inv['py:module']['module2'] == (
- 'foo',
- '2.0',
- 'https://docs.python.org/foo.html#module-module2',
- '-',
+ assert inv['py:module']['module2'] == _InventoryItem(
+ project_name='foo',
+ project_version='2.0',
+ uri='https://docs.python.org/foo.html#module-module2',
+ display_name='-',
)
# check resolution when a target is found
@@ -532,7 +531,7 @@ def test_validate_intersphinx_mapping_warnings(app):
match=r'^Invalid `intersphinx_mapping` configuration \(16 errors\).$',
):
validate_intersphinx_mapping(app, app.config)
- warnings = strip_colors(app.warning.getvalue()).splitlines()
+ warnings = strip_escape_sequences(app.warning.getvalue()).splitlines()
assert len(warnings) == len(bad_intersphinx_mapping) - 3
assert warnings == [
"ERROR: Invalid intersphinx project identifier `''` in intersphinx_mapping. Project identifiers must be non-empty strings.",
@@ -707,7 +706,7 @@ def test_intersphinx_role(app):
app.build()
content = (app.outdir / 'index.html').read_text(encoding='utf8')
- warnings = strip_colors(app.warning.getvalue()).splitlines()
+ warnings = strip_escape_sequences(app.warning.getvalue()).splitlines()
index_path = app.srcdir / 'index.rst'
assert warnings == [
f"{index_path}:21: WARNING: role for external cross-reference not found in domain 'py': 'nope' [intersphinx.external]",
@@ -741,6 +740,8 @@ def test_intersphinx_role(app):
if TYPE_CHECKING:
+ from typing import NoReturn
+
from sphinx.ext.intersphinx._shared import InventoryCacheEntry
@@ -760,6 +761,7 @@ def test_intersphinx_cache_limit(app, monkeypatch, cache_limit, expected_expired
app.config.intersphinx_mapping = {
'inv': (url, None),
}
+ app.config.intersphinx_timeout = None
# load the inventory and check if it's done correctly
intersphinx_cache: dict[str, InventoryCacheEntry] = {
url: ('inv', 0, {}), # Timestamp of last cache write is zero.
@@ -784,7 +786,7 @@ def test_intersphinx_cache_limit(app, monkeypatch, cache_limit, expected_expired
project=project,
cache=intersphinx_cache,
now=now,
- config=app.config,
+ config=_InvConfig.from_config(app.config),
srcdir=app.srcdir,
)
# If we hadn't mocked `_fetch_inventory`, it would've made
diff --git a/tests/test_extensions/test_ext_intersphinx_cache.py b/tests/test_extensions/test_ext_intersphinx_cache.py
index 047589b7fd5..91f3cdcc01e 100644
--- a/tests/test_extensions/test_ext_intersphinx_cache.py
+++ b/tests/test_extensions/test_ext_intersphinx_cache.py
@@ -9,8 +9,9 @@
from io import BytesIO
from typing import TYPE_CHECKING
-from sphinx.ext.intersphinx import InventoryAdapter
+from sphinx.ext.intersphinx._shared import InventoryAdapter
from sphinx.testing.util import SphinxTestApp
+from sphinx.util.inventory import _InventoryItem
from tests.utils import http_server
@@ -18,7 +19,6 @@
from collections.abc import Iterable
from typing import BinaryIO
- from sphinx.util.typing import InventoryItem
BASE_CONFIG = {
'extensions': ['sphinx.ext.intersphinx'],
@@ -109,10 +109,14 @@ def record(self) -> dict[str, tuple[str | None, str | None]]:
"""The :confval:`intersphinx_mapping` record for this project."""
return {self.name: (self.url, self.file)}
- def normalise(self, entry: InventoryEntry) -> tuple[str, InventoryItem]:
+ def normalise(self, entry: InventoryEntry) -> tuple[str, _InventoryItem]:
"""Format an inventory entry as if it were part of this project."""
- url = posixpath.join(self.url, entry.uri)
- return entry.name, (self.safe_name, self.safe_version, url, entry.display_name)
+ return entry.name, _InventoryItem(
+ project_name=self.safe_name,
+ project_version=self.safe_version,
+ uri=posixpath.join(self.url, entry.uri),
+ display_name=entry.display_name,
+ )
class FakeInventory:
diff --git a/tests/test_extensions/test_ext_math.py b/tests/test_extensions/test_ext_math.py
index 5a866520afb..e0445318840 100644
--- a/tests/test_extensions/test_ext_math.py
+++ b/tests/test_extensions/test_ext_math.py
@@ -1,5 +1,7 @@
"""Test math extensions."""
+from __future__ import annotations
+
import re
import shutil
import subprocess
diff --git a/tests/test_extensions/test_ext_napoleon.py b/tests/test_extensions/test_ext_napoleon.py
index a14cb55dda2..53f64530260 100644
--- a/tests/test_extensions/test_ext_napoleon.py
+++ b/tests/test_extensions/test_ext_napoleon.py
@@ -1,5 +1,7 @@
"""Tests for :mod:`sphinx.ext.napoleon.__init__` module."""
+from __future__ import annotations
+
import functools
from collections import namedtuple
from unittest import mock
@@ -11,9 +13,7 @@
def simple_decorator(f):
- """
- A simple decorator that does nothing, for tests to use.
- """
+ """A simple decorator that does nothing, for tests to use."""
@functools.wraps(f)
def wrapper(*args, **kwargs):
@@ -31,12 +31,12 @@ def _private_undoc():
pass
-def __special_doc__():
+def __special_doc__(): # NoQA: N807
"""module.__special_doc__.DOCSTRING"""
pass
-def __special_undoc__():
+def __special_undoc__(): # NoQA: N807
pass
@@ -77,7 +77,7 @@ def __special_undoc__(self): # NoQA: PLW3201
pass
-SampleNamedTuple = namedtuple('SampleNamedTuple', 'user_id block_type def_id')
+SampleNamedTuple = namedtuple('SampleNamedTuple', 'user_id block_type def_id') # NoQA: PYI024
class TestProcessDocstring:
diff --git a/tests/test_extensions/test_ext_napoleon_docstring.py b/tests/test_extensions/test_ext_napoleon_docstring.py
index f47f2ef7dbb..74dd625692a 100644
--- a/tests/test_extensions/test_ext_napoleon_docstring.py
+++ b/tests/test_extensions/test_ext_napoleon_docstring.py
@@ -1,4 +1,6 @@
-"""Tests for :mod:`sphinx.ext.napoleon.docstring` module."""
+"""Tests for :py:mod:`sphinx.ext.napoleon.docstring` module."""
+
+from __future__ import annotations
import re
import zlib
@@ -10,12 +12,12 @@
import pytest
-from sphinx.ext.intersphinx import load_mappings, validate_intersphinx_mapping
+from sphinx.ext.intersphinx._load import load_mappings, validate_intersphinx_mapping
from sphinx.ext.napoleon import Config
from sphinx.ext.napoleon.docstring import (
GoogleDocstring,
NumpyDocstring,
- _convert_numpy_type_spec,
+ _convert_type_spec,
_recombine_set_tokens,
_token_type,
_tokenize_type_spec,
@@ -26,7 +28,7 @@
from tests.test_extensions.ext_napoleon_pep526_data_numpy import PEP526NumpyClass
-class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))):
+class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))): # NoQA: PYI024
"""Sample namedtuple subclass
Attributes
@@ -118,9 +120,9 @@ def test_class_data_member_inline_no_type(self):
assert actual == [source]
def test_class_data_member_inline_ref_in_type(self):
- source = f':class:`int`: {self.inline_google_docstring}'
+ source = f':py:class:`int`: {self.inline_google_docstring}'
actual = self._docstring(source).splitlines()
- assert actual == [self.inline_google_docstring, '', ':type: :class:`int`']
+ assert actual == [self.inline_google_docstring, '', ':type: :py:class:`int`']
class TestGoogleDocstring:
@@ -465,14 +467,14 @@ def test_parameters_with_class_reference(self):
This class should only be used by runtimes.
Arguments:
- runtime (:class:`~typing.Dict`\\[:class:`int`,:class:`str`\\]): Use it to
+ runtime (:py:class:`~typing.Dict`\\[:py:class:`int`,:py:class:`str`\\]): Use it to
access the environment. It is available in XBlock code
as ``self.runtime``.
- field_data (:class:`FieldData`): Interface used by the XBlock
+ field_data (:py:class:`FieldData`): Interface used by the XBlock
fields to access their data from wherever it is persisted.
- scope_ids (:class:`ScopeIds`): Identifiers needed to resolve scopes.
+ scope_ids (:py:class:`ScopeIds`): Identifiers needed to resolve scopes.
"""
@@ -485,19 +487,19 @@ def test_parameters_with_class_reference(self):
:param runtime: Use it to
access the environment. It is available in XBlock code
as ``self.runtime``.
-:type runtime: :class:`~typing.Dict`\\[:class:`int`,:class:`str`\\]
+:type runtime: :py:class:`~typing.Dict`\\[:py:class:`int`,:py:class:`str`\\]
:param field_data: Interface used by the XBlock
fields to access their data from wherever it is persisted.
-:type field_data: :class:`FieldData`
+:type field_data: :py:class:`FieldData`
:param scope_ids: Identifiers needed to resolve scopes.
-:type scope_ids: :class:`ScopeIds`
+:type scope_ids: :py:class:`ScopeIds`
"""
assert str(actual) == expected
def test_attributes_with_class_reference(self):
docstring = """\
Attributes:
- in_attr(:class:`numpy.ndarray`): super-dooper attribute
+ in_attr(:py:class:`numpy.ndarray`): super-dooper attribute
"""
actual = GoogleDocstring(docstring)
@@ -506,7 +508,7 @@ def test_attributes_with_class_reference(self):
super-dooper attribute
- :type: :class:`numpy.ndarray`
+ :type: :py:class:`numpy.ndarray`
"""
assert str(actual) == expected
@@ -581,14 +583,14 @@ def test_xrefs_in_return_type(self):
docstring = """Example Function
Returns:
- :class:`numpy.ndarray`: A :math:`n \\times 2` array containing
+ :py:class:`numpy.ndarray`: A :math:`n \\times 2` array containing
a bunch of math items
"""
expected = """Example Function
:returns: A :math:`n \\times 2` array containing
a bunch of math items
-:rtype: :class:`numpy.ndarray`
+:rtype: :py:class:`numpy.ndarray`
"""
actual = GoogleDocstring(docstring)
assert str(actual) == expected
@@ -610,7 +612,7 @@ def test_raises_types(self):
If the dimensions couldn't be parsed.
`InvalidArgumentsError`
If the arguments are invalid.
- :exc:`~ValueError`
+ :py:exc:`~ValueError`
If the arguments are wrong.
""",
@@ -721,7 +723,7 @@ def test_raises_types(self):
Example Function
Raises:
- :class:`exc.InvalidDimensionsError`
+ :py:class:`exc.InvalidDimensionsError`
""",
"""
@@ -736,7 +738,7 @@ def test_raises_types(self):
Example Function
Raises:
- :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed.
+ :py:class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed.
""",
"""
@@ -751,15 +753,15 @@ def test_raises_types(self):
Example Function
Raises:
- :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed,
- then a :class:`exc.InvalidDimensionsError` will be raised.
+ :py:class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed,
+ then a :py:class:`exc.InvalidDimensionsError` will be raised.
""",
"""
Example Function
:raises exc.InvalidDimensionsError: If the dimensions couldn't be parsed,
- then a :class:`exc.InvalidDimensionsError` will be raised.
+ then a :py:class:`exc.InvalidDimensionsError` will be raised.
""",
),
################################
@@ -768,8 +770,8 @@ def test_raises_types(self):
Example Function
Raises:
- :class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed.
- :class:`exc.InvalidArgumentsError`: If the arguments are invalid.
+ :py:class:`exc.InvalidDimensionsError`: If the dimensions couldn't be parsed.
+ :py:class:`exc.InvalidArgumentsError`: If the arguments are invalid.
""",
"""
@@ -785,8 +787,8 @@ def test_raises_types(self):
Example Function
Raises:
- :class:`exc.InvalidDimensionsError`
- :class:`exc.InvalidArgumentsError`
+ :py:class:`exc.InvalidDimensionsError`
+ :py:class:`exc.InvalidArgumentsError`
""",
"""
@@ -1219,7 +1221,7 @@ def test_custom_generic_sections(self):
),
)
- testConfig = Config(
+ test_config = Config(
napoleon_custom_sections=[
'Really Important Details',
('Sooper Warning', 'warns'),
@@ -1229,7 +1231,7 @@ def test_custom_generic_sections(self):
)
for docstring, expected in docstrings:
- actual = GoogleDocstring(docstring, testConfig)
+ actual = GoogleDocstring(docstring, test_config)
assert str(actual) == expected
def test_noindex(self):
@@ -1367,7 +1369,7 @@ class TestNumpyDocstring:
"""
Single line summary
- :Parameters: **arg1** (:class:`str`) -- Extended
+ :Parameters: **arg1** (:py:class:`str`) -- Extended
description of arg1
""",
),
@@ -1396,14 +1398,14 @@ class TestNumpyDocstring:
"""
Single line summary
- :Parameters: * **arg1** (:class:`str`) -- Extended
+ :Parameters: * **arg1** (:py:class:`str`) -- Extended
description of arg1
- * **arg2** (:class:`int`) -- Extended
+ * **arg2** (:py:class:`int`) -- Extended
description of arg2
- :Keyword Arguments: * **kwarg1** (:class:`str`) -- Extended
+ :Keyword Arguments: * **kwarg1** (:py:class:`str`) -- Extended
description of kwarg1
- * **kwarg2** (:class:`int`) -- Extended
+ * **kwarg2** (:py:class:`int`) -- Extended
description of kwarg2
""",
),
@@ -1420,7 +1422,7 @@ class TestNumpyDocstring:
"""
Single line summary
- :returns: :class:`str` -- Extended
+ :returns: :py:class:`str` -- Extended
description of return value
""",
),
@@ -1437,7 +1439,7 @@ class TestNumpyDocstring:
"""
Single line summary
- :returns: :class:`str` -- Extended
+ :returns: :py:class:`str` -- Extended
description of return value
""",
),
@@ -1457,7 +1459,7 @@ class TestNumpyDocstring:
"""
Single line summary
- :Parameters: * **arg1** (:class:`str`) -- Extended description of arg1
+ :Parameters: * **arg1** (:py:class:`str`) -- Extended description of arg1
* **\\*args** -- Variable length argument list.
* **\\*\\*kwargs** -- Arbitrary keyword arguments.
""",
@@ -1476,7 +1478,7 @@ class TestNumpyDocstring:
"""
Single line summary
- :Parameters: * **arg1** (:class:`str`) -- Extended description of arg1
+ :Parameters: * **arg1** (:py:class:`str`) -- Extended description of arg1
* **\\*args, \\*\\*kwargs** -- Variable length argument list and arbitrary keyword arguments.
""",
),
@@ -1496,9 +1498,9 @@ class TestNumpyDocstring:
"""
Single line summary
- :Receives: * **arg1** (:class:`str`) -- Extended
+ :Receives: * **arg1** (:py:class:`str`) -- Extended
description of arg1
- * **arg2** (:class:`int`) -- Extended
+ * **arg2** (:py:class:`int`) -- Extended
description of arg2
""",
),
@@ -1518,9 +1520,9 @@ class TestNumpyDocstring:
"""
Single line summary
- :Receives: * **arg1** (:class:`str`) -- Extended
+ :Receives: * **arg1** (:py:class:`str`) -- Extended
description of arg1
- * **arg2** (:class:`int`) -- Extended
+ * **arg2** (:py:class:`int`) -- Extended
description of arg2
""",
),
@@ -1537,7 +1539,7 @@ class TestNumpyDocstring:
"""
Single line summary
- :Yields: :class:`str` -- Extended
+ :Yields: :py:class:`str` -- Extended
description of yielded value
""",
),
@@ -1554,7 +1556,7 @@ class TestNumpyDocstring:
"""
Single line summary
- :Yields: :class:`str` -- Extended
+ :Yields: :py:class:`str` -- Extended
description of yielded value
""",
),
@@ -1735,9 +1737,9 @@ def test_see_also_refs(self):
.. seealso::
- :obj:`some`, :obj:`other`, :obj:`funcs`
+ :py:obj:`some`, :py:obj:`other`, :py:obj:`funcs`
\n\
- :obj:`otherfunc`
+ :py:obj:`otherfunc`
relationship
"""
assert str(actual) == expected
@@ -1761,9 +1763,9 @@ def test_see_also_refs(self):
.. seealso::
- :obj:`some`, :obj:`other`, :obj:`funcs`
+ :py:obj:`some`, :py:obj:`other`, :py:obj:`funcs`
\n\
- :obj:`otherfunc`
+ :py:obj:`otherfunc`
relationship
"""
assert str(actual) == expected
@@ -1790,7 +1792,7 @@ def test_see_also_refs(self):
.. seealso::
- :obj:`some`, :obj:`MyClass.other`, :func:`funcs`
+ :py:obj:`some`, :py:obj:`MyClass.other`, :func:`funcs`
\n\
:func:`~my_package.otherfunc`
relationship
@@ -1870,7 +1872,7 @@ def test_return_types(self):
""")
expected = dedent("""
:returns: a dataframe
- :rtype: :class:`~pandas.DataFrame`
+ :rtype: :py:class:`~pandas.DataFrame`
""")
translations = {
'DataFrame': '~pandas.DataFrame',
@@ -1896,11 +1898,11 @@ def test_yield_types(self):
expected = dedent("""
Example Function
- :Yields: :term:`scalar` or :class:`array-like ` -- The result of the computation
+ :Yields: :term:`scalar` or :py:class:`array-like ` -- The result of the computation
""")
translations = {
'scalar': ':term:`scalar`',
- 'array-like': ':class:`array-like `',
+ 'array-like': ':py:class:`array-like `',
}
config = Config(
napoleon_type_aliases=translations, napoleon_preprocess_types=True
@@ -2452,60 +2454,60 @@ def test_list_in_parameter_description(self):
expected = """One line summary.
-:Parameters: * **no_list** (:class:`int`)
- * **one_bullet_empty** (:class:`int`) --
+:Parameters: * **no_list** (:py:class:`int`)
+ * **one_bullet_empty** (:py:class:`int`) --
*
- * **one_bullet_single_line** (:class:`int`) --
+ * **one_bullet_single_line** (:py:class:`int`) --
- first line
- * **one_bullet_two_lines** (:class:`int`) --
+ * **one_bullet_two_lines** (:py:class:`int`) --
+ first line
continued
- * **two_bullets_single_line** (:class:`int`) --
+ * **two_bullets_single_line** (:py:class:`int`) --
- first line
- second line
- * **two_bullets_two_lines** (:class:`int`) --
+ * **two_bullets_two_lines** (:py:class:`int`) --
* first line
continued
* second line
continued
- * **one_enumeration_single_line** (:class:`int`) --
+ * **one_enumeration_single_line** (:py:class:`int`) --
1. first line
- * **one_enumeration_two_lines** (:class:`int`) --
+ * **one_enumeration_two_lines** (:py:class:`int`) --
1) first line
continued
- * **two_enumerations_one_line** (:class:`int`) --
+ * **two_enumerations_one_line** (:py:class:`int`) --
(iii) first line
(iv) second line
- * **two_enumerations_two_lines** (:class:`int`) --
+ * **two_enumerations_two_lines** (:py:class:`int`) --
a. first line
continued
b. second line
continued
- * **one_definition_one_line** (:class:`int`) --
+ * **one_definition_one_line** (:py:class:`int`) --
item 1
first line
- * **one_definition_two_lines** (:class:`int`) --
+ * **one_definition_two_lines** (:py:class:`int`) --
item 1
first line
continued
- * **two_definitions_one_line** (:class:`int`) --
+ * **two_definitions_one_line** (:py:class:`int`) --
item 1
first line
item 2
second line
- * **two_definitions_two_lines** (:class:`int`) --
+ * **two_definitions_two_lines** (:py:class:`int`) --
item 1
first line
@@ -2513,14 +2515,14 @@ def test_list_in_parameter_description(self):
item 2
second line
continued
- * **one_definition_blank_line** (:class:`int`) --
+ * **one_definition_blank_line** (:py:class:`int`) --
item 1
first line
extra first line
- * **two_definitions_blank_lines** (:class:`int`) --
+ * **two_definitions_blank_lines** (:py:class:`int`) --
item 1
@@ -2533,7 +2535,7 @@ def test_list_in_parameter_description(self):
second line
extra second line
- * **definition_after_normal_text** (:class:`int`) -- text line
+ * **definition_after_normal_text** (:py:class:`int`) -- text line
item 1
first line
@@ -2664,18 +2666,18 @@ def test_convert_numpy_type_spec(self):
converted = (
'',
'*optional*',
- ':class:`str`, *optional*',
- ':class:`int` or :class:`float` or :obj:`None`, *default*: :obj:`None`',
- ':class:`list` of :class:`tuple` of :class:`str`, *optional*',
- ':class:`int`, *default* :obj:`None`',
+ ':py:class:`str`, *optional*',
+ ':py:class:`int` or :py:class:`float` or :py:obj:`None`, *default*: :py:obj:`None`',
+ ':py:class:`list` of :py:class:`tuple` of :py:class:`str`, *optional*',
+ ':py:class:`int`, *default* :py:obj:`None`',
'``{"F", "C", "N"}``',
"``{'F', 'C', 'N'}``, *default*: ``'N'``",
"``{'F', 'C', 'N'}``, *default* ``'N'``",
- ':class:`pandas.DataFrame`, *optional*',
+ ':py:class:`pandas.DataFrame`, *optional*',
)
for spec, expected in zip(specs, converted, strict=True):
- actual = _convert_numpy_type_spec(spec, translations=translations)
+ actual = _convert_type_spec(spec, translations=translations)
assert actual == expected
def test_parameter_types(self):
@@ -2703,23 +2705,23 @@ def test_parameter_types(self):
""")
expected = dedent("""\
:param param1: the data to work on
- :type param1: :class:`DataFrame`
+ :type param1: :py:class:`DataFrame`
:param param2: a parameter with different types
- :type param2: :class:`int` or :class:`float` or :obj:`None`, *optional*
+ :type param2: :py:class:`int` or :py:class:`float` or :py:obj:`None`, *optional*
:param param3: a optional mapping
:type param3: :term:`dict-like `, *optional*
:param param4: a optional parameter with different types
- :type param4: :class:`int` or :class:`float` or :obj:`None`, *optional*
+ :type param4: :py:class:`int` or :py:class:`float` or :py:obj:`None`, *optional*
:param param5: a optional parameter with fixed values
:type param5: ``{"F", "C", "N"}``, *optional*
:param param6: different default format
- :type param6: :class:`int`, *default* :obj:`None`
+ :type param6: :py:class:`int`, *default* :py:obj:`None`
:param param7: a optional mapping
- :type param7: :term:`mapping` of :term:`hashable` to :class:`str`, *optional*
+ :type param7: :term:`mapping` of :term:`hashable` to :py:class:`str`, *optional*
:param param8: ellipsis
- :type param8: :obj:`... ` or :obj:`Ellipsis`
+ :type param8: :py:obj:`... ` or :py:obj:`Ellipsis`
:param param9: a parameter with tuple of list of int
- :type param9: :class:`tuple` of :class:`list` of :class:`int`
+ :type param9: :py:class:`tuple` of :py:class:`list` of :py:class:`int`
""")
translations = {
'dict-like': ':term:`dict-like `',
@@ -2879,7 +2881,7 @@ def test_napoleon_keyword_and_paramtype(app, tmp_path):
list py:class 1 list.html -
int py:class 1 int.html -
""")
- ) # NoQA: W291
+ )
app.config.intersphinx_mapping = {'python': ('127.0.0.1:5555', str(inv_file))}
validate_intersphinx_mapping(app, app.config)
load_mappings(app)
diff --git a/tests/test_extensions/test_ext_todo.py b/tests/test_extensions/test_ext_todo.py
index ed7f8d60963..27a92100b13 100644
--- a/tests/test_extensions/test_ext_todo.py
+++ b/tests/test_extensions/test_ext_todo.py
@@ -1,5 +1,7 @@
"""Test sphinx.ext.todo extension."""
+from __future__ import annotations
+
import re
import pytest
@@ -92,8 +94,7 @@ def on_todo_defined(app, node):
confoverrides={'todo_include_todos': True},
)
def test_todo_valid_link(app):
- """
- Test that the inserted "original entry" links for todo items have a target
+ """Test that the inserted "original entry" links for todo items have a target
that exists in the LaTeX output. The target was previously incorrectly
omitted (GitHub issue #1020).
"""
diff --git a/tests/test_extensions/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py
index eeef391c1e4..01d68cd3ccf 100644
--- a/tests/test_extensions/test_ext_viewcode.py
+++ b/tests/test_extensions/test_ext_viewcode.py
@@ -6,6 +6,7 @@
import shutil
from typing import TYPE_CHECKING
+import pygments
import pytest
if TYPE_CHECKING:
@@ -13,6 +14,11 @@
def check_viewcode_output(app: SphinxTestApp) -> str:
+ if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19):
+ sp = ' '
+ else:
+ sp = ' '
+
warnings = re.sub(r'\\+', '/', app.warning.getvalue())
assert re.findall(
r"index.rst:\d+: WARNING: Object named 'func1' not found in include "
@@ -41,10 +47,11 @@ def check_viewcode_output(app: SphinxTestApp) -> str:
'[docs]\n'
) in result
assert '@decorator\n' in result
- assert 'class Class1:\n' in result
- assert ' """\n' in result
- assert ' this is Class1\n' in result
- assert ' """\n' in result
+ assert f'class{sp}Class1:\n' in result
+ assert (
+ ' '
+ '"""this is Class1""" \n'
+ ) in result
return result
@@ -116,6 +123,7 @@ def test_linkcode(app):
assert 'https://foobar/js/' in stuff
assert 'https://foobar/c/' in stuff
assert 'https://foobar/cpp/' in stuff
+ assert 'http://foobar/rst/' in stuff
@pytest.mark.sphinx('html', testroot='ext-viewcode-find', freshenv=True)
@@ -161,3 +169,24 @@ def find_source(app, modname):
'This is the class attribute class_attr',
):
assert result.count(needle) == 1
+
+
+@pytest.mark.sphinx('html', testroot='ext-viewcode-find-package', freshenv=True)
+def test_find_local_package_import_path(app, status, warning):
+ app.build(force_all=True)
+ result = (app.outdir / 'index.html').read_text(encoding='utf8')
+
+ count_func1 = result.count(
+ 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#func1"'
+ )
+ assert count_func1 == 1
+
+ count_class1 = result.count(
+ 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class1"'
+ )
+ assert count_class1 == 1
+
+ count_class3 = result.count(
+ 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class3"'
+ )
+ assert count_class3 == 1
diff --git a/tests/test_extensions/test_extension.py b/tests/test_extensions/test_extension.py
index a84ca30b81f..7c44360ae26 100644
--- a/tests/test_extensions/test_extension.py
+++ b/tests/test_extensions/test_extension.py
@@ -1,5 +1,7 @@
"""Test sphinx.extension module."""
+from __future__ import annotations
+
import pytest
from sphinx.errors import VersionRequirementError
diff --git a/tests/test_highlighting.py b/tests/test_highlighting.py
index 430d569c0e6..141de97020d 100644
--- a/tests/test_highlighting.py
+++ b/tests/test_highlighting.py
@@ -1,5 +1,7 @@
"""Test the Pygments highlighting bridge."""
+from __future__ import annotations
+
from unittest import mock
import pygments
@@ -10,7 +12,7 @@
from sphinx.highlighting import PygmentsBridge
-if tuple(map(int, pygments.__version__.split('.')))[:2] < (2, 18):
+if tuple(map(int, pygments.__version__.split('.')[:2])) < (2, 18):
from pygments.formatter import Formatter
Formatter.__class_getitem__ = classmethod(lambda cls, name: cls) # type: ignore[attr-defined]
diff --git a/tests/test_intl/test_catalogs.py b/tests/test_intl/test_catalogs.py
index 5e28df680ec..1a6f3f425df 100644
--- a/tests/test_intl/test_catalogs.py
+++ b/tests/test_intl/test_catalogs.py
@@ -1,5 +1,7 @@
"""Test the base build process."""
+from __future__ import annotations
+
import shutil
from pathlib import Path
diff --git a/tests/test_intl/test_intl.py b/tests/test_intl/test_intl.py
index 19808f6d538..398d9d18b88 100644
--- a/tests/test_intl/test_intl.py
+++ b/tests/test_intl/test_intl.py
@@ -3,22 +3,30 @@
Runs the text builder in the test root.
"""
+from __future__ import annotations
+
import os
import re
import shutil
import time
-from io import StringIO
+from typing import TYPE_CHECKING
+import pygments
import pytest
from babel.messages import mofile, pofile
from babel.messages.catalog import Catalog
from docutils import nodes
from sphinx import locale
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.testing.util import assert_node, etree_parse
-from sphinx.util.console import strip_colors
from sphinx.util.nodes import NodeMatcher
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+ from io import StringIO
+ from pathlib import Path
+
_CATALOG_LOCALE = 'xx'
sphinx_intl = pytest.mark.sphinx(
@@ -41,9 +49,9 @@ def write_mo(pathname, po):
return mofile.write_mo(f, po)
-def _set_mtime_ns(target: str | os.PathLike[str], value: int) -> int:
+def _set_mtime_ns(target: Path, value: int) -> int:
os.utime(target, ns=(value, value))
- return os.stat(target).st_mtime_ns
+ return target.stat().st_mtime_ns
def _get_bom_intl_path(app):
@@ -818,7 +826,7 @@ def sleep(self, ds: float) -> None:
@pytest.fixture
-def mock_time_and_i18n() -> tuple[pytest.MonkeyPatch, _MockClock]:
+def mock_time_and_i18n() -> Iterator[tuple[pytest.MonkeyPatch, _MockClock]]:
from sphinx.util.i18n import CatalogInfo
# save the 'original' definition
@@ -831,6 +839,7 @@ def mock_write_mo(self, locale, use_fuzzy=False):
# see: https://github.com/pytest-dev/pytest/issues/363
with pytest.MonkeyPatch.context() as mock:
+ clock: _MockClock
if os.name == 'posix':
clock = _MockUnixClock()
else:
@@ -1482,6 +1491,11 @@ def test_xml_strange_markup(app):
@pytest.mark.sphinx('html', testroot='intl')
@pytest.mark.test_params(shared_result='test_intl_basic')
def test_additional_targets_should_not_be_translated(app):
+ if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19):
+ sp = ' '
+ else:
+ sp = ' '
+
app.build()
# [literalblock.txt]
result = (app.outdir / 'literalblock.html').read_text(encoding='utf8')
@@ -1520,7 +1534,7 @@ def test_additional_targets_should_not_be_translated(app):
# doctest block should not be translated but be highlighted
expected_expr = (
""">>> """
- """import sys """
+ f"""import{sp}sys """
"""# sys importing"""
)
assert_count(expected_expr, result, 1)
@@ -1565,6 +1579,11 @@ def test_additional_targets_should_not_be_translated(app):
},
)
def test_additional_targets_should_be_translated(app):
+ if tuple(map(int, pygments.__version__.split('.')[:2])) >= (2, 19):
+ sp = ' '
+ else:
+ sp = ' '
+
app.build()
# [literalblock.txt]
result = (app.outdir / 'literalblock.html').read_text(encoding='utf8')
@@ -1614,7 +1633,7 @@ def test_additional_targets_should_be_translated(app):
# doctest block should not be translated but be highlighted
expected_expr = (
""">>> """
- """import sys """
+ f"""import{sp}sys """
"""# SYS IMPORTING"""
)
assert_count(expected_expr, result, 1)
@@ -1876,7 +1895,7 @@ def test_image_glob_intl_using_figure_language_filename(app):
def getwarning(warnings: StringIO) -> str:
- return strip_colors(warnings.getvalue().replace(os.sep, '/'))
+ return strip_escape_sequences(warnings.getvalue().replace(os.sep, '/'))
@pytest.mark.sphinx(
diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py
index 9eb0b83fa89..d6dcbe384d9 100644
--- a/tests/test_markup/test_markup.py
+++ b/tests/test_markup/test_markup.py
@@ -1,5 +1,7 @@
"""Test various Sphinx-specific markup extensions."""
+from __future__ import annotations
+
import re
import warnings
from types import SimpleNamespace
@@ -9,7 +11,6 @@
from docutils.parsers.rst import Parser as RstParser
from sphinx import addnodes
-from sphinx.builders.html.transforms import KeyboardTransform
from sphinx.builders.latex import LaTeXBuilder
from sphinx.environment import default_settings
from sphinx.roles import XRefRole
@@ -24,6 +25,7 @@
@pytest.fixture
def settings(app):
+ env = app.env
texescape.init() # otherwise done by the latex builder
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning)
@@ -35,10 +37,10 @@ def settings(app):
)
settings = optparser.get_default_values()
settings.smart_quotes = True
- settings.env = app.builder.env
- settings.env.temp_data['docname'] = 'dummy'
+ settings.env = env
+ settings.env.current_document.docname = 'dummy'
settings.contentsname = 'dummy'
- domain_context = sphinx_domains(settings.env)
+ domain_context = sphinx_domains(env)
domain_context.enable()
yield settings
domain_context.disable()
@@ -97,7 +99,6 @@ class ForgivingLaTeXTranslator(LaTeXTranslator, ForgivingTranslator):
def verify_re_html(app, parse):
def verify(rst, html_expected):
document = parse(rst)
- KeyboardTransform(document).apply()
html_translator = ForgivingHTMLTranslator(document, app.builder)
document.walkabout(html_translator)
html_translated = ''.join(html_translator.fragment).strip()
@@ -354,28 +355,35 @@ def get(name):
'verify',
':kbd:`Control+X`',
(
- ''
+ ''
'Control'
'+'
'X'
- '
'
+ ''
+ ),
+ (
+ '\\sphinxAtStartPar\n'
+ '\\sphinxkeyboard{\\sphinxupquote{Control}}'
+ '+'
+ '\\sphinxkeyboard{\\sphinxupquote{X}}'
),
- '\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{Control+X}}',
),
(
# kbd role
'verify',
':kbd:`Alt+^`',
(
- ''
+ ''
'Alt'
'+'
'^'
- '
'
+ ''
),
(
'\\sphinxAtStartPar\n'
- '\\sphinxkeyboard{\\sphinxupquote{Alt+\\textasciicircum{}}}'
+ '\\sphinxkeyboard{\\sphinxupquote{Alt}}'
+ '+'
+ '\\sphinxkeyboard{\\sphinxupquote{\\textasciicircum{}}}'
),
),
(
@@ -383,7 +391,7 @@ def get(name):
'verify',
':kbd:`M-x M-s`',
(
- ''
+ ''
'M'
'-'
'x'
@@ -391,11 +399,17 @@ def get(name):
'M'
'-'
's'
- '
'
+ ''
),
(
'\\sphinxAtStartPar\n'
- '\\sphinxkeyboard{\\sphinxupquote{M\\sphinxhyphen{}x M\\sphinxhyphen{}s}}'
+ '\\sphinxkeyboard{\\sphinxupquote{M}}'
+ '\\sphinxhyphen{}'
+ '\\sphinxkeyboard{\\sphinxupquote{x}}'
+ ' '
+ '\\sphinxkeyboard{\\sphinxupquote{M}}'
+ '\\sphinxhyphen{}'
+ '\\sphinxkeyboard{\\sphinxupquote{s}}'
),
),
(
@@ -419,6 +433,28 @@ def get(name):
'sys rq
',
'\\sphinxAtStartPar\n\\sphinxkeyboard{\\sphinxupquote{sys rq}}',
),
+ (
+ # kbd role
+ 'verify',
+ ':kbd:`⌘+⇧+M`',
+ (
+ ''
+ '⌘'
+ '+'
+ '⇧'
+ '+'
+ 'M'
+ '
'
+ ),
+ (
+ '\\sphinxAtStartPar\n'
+ '\\sphinxkeyboard{\\sphinxupquote{⌘}}'
+ '+'
+ '\\sphinxkeyboard{\\sphinxupquote{⇧}}'
+ '+'
+ '\\sphinxkeyboard{\\sphinxupquote{M}}'
+ ),
+ ),
(
# non-interpolation of dashes in option role
'verify_re',
diff --git a/tests/test_markup/test_metadata.py b/tests/test_markup/test_metadata.py
index b3fe3e678ee..d10dcbc5804 100644
--- a/tests/test_markup/test_metadata.py
+++ b/tests/test_markup/test_metadata.py
@@ -2,14 +2,14 @@
# adapted from an example of bibliographic metadata at
# https://docutils.sourceforge.io/docs/user/rst/demo.txt
+from __future__ import annotations
import pytest
@pytest.mark.sphinx('dummy', testroot='metadata')
def test_docinfo(app):
- """
- Inspect the 'docinfo' metadata stored in the first node of the document.
+ """Inspect the 'docinfo' metadata stored in the first node of the document.
Note this doesn't give us access to data stored in subsequence blocks
that might be considered document metadata, such as 'abstract' or
'dedication' blocks, or the 'meta' role. Doing otherwise is probably more
diff --git a/tests/test_markup/test_parser.py b/tests/test_markup/test_parser.py
index a047923c720..eb8ccf24f1d 100644
--- a/tests/test_markup/test_parser.py
+++ b/tests/test_markup/test_parser.py
@@ -1,5 +1,7 @@
"""Tests parsers module."""
+from __future__ import annotations
+
from unittest.mock import Mock, patch
import pytest
@@ -27,7 +29,7 @@ def test_RSTParser_prolog_epilog(RSTStateMachine, app):
]
# with rst_prolog
- app.env.config.rst_prolog = 'this is rst_prolog\nhello reST!'
+ app.config.rst_prolog = 'this is rst_prolog\nhello reST!'
parser.parse(text, document)
(content, _), _ = RSTStateMachine().run.call_args
assert list(content.xitems()) == [
@@ -39,8 +41,8 @@ def test_RSTParser_prolog_epilog(RSTStateMachine, app):
]
# with rst_epilog
- app.env.config.rst_prolog = None
- app.env.config.rst_epilog = 'this is rst_epilog\ngood-bye reST!'
+ app.config.rst_prolog = None
+ app.config.rst_epilog = 'this is rst_epilog\ngood-bye reST!'
parser.parse(text, document)
(content, _), _ = RSTStateMachine().run.call_args
assert list(content.xitems()) == [
@@ -52,8 +54,8 @@ def test_RSTParser_prolog_epilog(RSTStateMachine, app):
]
# expandtabs / convert whitespaces
- app.env.config.rst_prolog = None
- app.env.config.rst_epilog = None
+ app.config.rst_prolog = None
+ app.config.rst_epilog = None
text = '\thello Sphinx world\n\v\fSphinx is a document generator'
parser.parse(text, document)
(content, _), _ = RSTStateMachine().run.call_args
diff --git a/tests/test_markup/test_smartquotes.py b/tests/test_markup/test_smartquotes.py
index b57a8a81017..2f34f9f1a21 100644
--- a/tests/test_markup/test_smartquotes.py
+++ b/tests/test_markup/test_smartquotes.py
@@ -1,5 +1,7 @@
"""Test smart quotes."""
+from __future__ import annotations
+
import pytest
from sphinx.testing.util import etree_parse
diff --git a/tests/test_project.py b/tests/test_project.py
index aa8b04b1f06..0a7688b2cfe 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -1,5 +1,7 @@
"""Tests project module."""
+from __future__ import annotations
+
from pathlib import Path
import pytest
diff --git a/tests/test_pycode/test_pycode.py b/tests/test_pycode/test_pycode.py
index 3a34d6f3a0f..2d5ec66d32f 100644
--- a/tests/test_pycode/test_pycode.py
+++ b/tests/test_pycode/test_pycode.py
@@ -1,5 +1,7 @@
"""Test pycode."""
+from __future__ import annotations
+
import sys
from pathlib import Path
@@ -149,15 +151,15 @@ def test_ModuleAnalyzer_find_attr_docs():
('Foo', 'attr8'),
('Foo', 'attr9'),
}
- assert docs[('Foo', 'attr1')] == ['comment before attr1', '']
- assert docs[('Foo', 'attr3')] == ['attribute comment for attr3', '']
- assert docs[('Foo', 'attr4')] == ['long attribute comment', '']
- assert docs[('Foo', 'attr4')] == ['long attribute comment', '']
- assert docs[('Foo', 'attr5')] == ['attribute comment for attr5', '']
- assert docs[('Foo', 'attr6')] == ['this comment is ignored', '']
- assert docs[('Foo', 'attr7')] == ['this comment is ignored', '']
- assert docs[('Foo', 'attr8')] == ['attribute comment for attr8', '']
- assert docs[('Foo', 'attr9')] == ['string after attr9', '']
+ assert docs['Foo', 'attr1'] == ['comment before attr1', '']
+ assert docs['Foo', 'attr3'] == ['attribute comment for attr3', '']
+ assert docs['Foo', 'attr4'] == ['long attribute comment', '']
+ assert docs['Foo', 'attr4'] == ['long attribute comment', '']
+ assert docs['Foo', 'attr5'] == ['attribute comment for attr5', '']
+ assert docs['Foo', 'attr6'] == ['this comment is ignored', '']
+ assert docs['Foo', 'attr7'] == ['this comment is ignored', '']
+ assert docs['Foo', 'attr8'] == ['attribute comment for attr8', '']
+ assert docs['Foo', 'attr9'] == ['string after attr9', '']
assert analyzer.tagorder == {
'Foo': 0,
'Foo.__init__': 8,
@@ -187,5 +189,5 @@ def test_ModuleAnalyzer_find_attr_docs_for_posonlyargs_method():
analyzer = ModuleAnalyzer.for_string(code, 'module')
docs = analyzer.find_attr_docs()
assert set(docs) == {('Foo', 'attr')}
- assert docs[('Foo', 'attr')] == ['attribute comment', '']
+ assert docs['Foo', 'attr'] == ['attribute comment', '']
assert analyzer.tagorder == {'Foo': 0, 'Foo.__init__': 1, 'Foo.attr': 2}
diff --git a/tests/test_pycode/test_pycode_ast.py b/tests/test_pycode/test_pycode_ast.py
index c62600a82dc..bc734e737dc 100644
--- a/tests/test_pycode/test_pycode_ast.py
+++ b/tests/test_pycode/test_pycode_ast.py
@@ -1,5 +1,7 @@
"""Test pycode.ast"""
+from __future__ import annotations
+
import ast
import pytest
diff --git a/tests/test_pycode/test_pycode_parser.py b/tests/test_pycode/test_pycode_parser.py
index 7883a8de3cf..813aa3a413e 100644
--- a/tests/test_pycode/test_pycode_parser.py
+++ b/tests/test_pycode/test_pycode_parser.py
@@ -1,5 +1,7 @@
"""Test pycode.parser."""
+from __future__ import annotations
+
from sphinx.pycode.parser import Parser
from sphinx.util.inspect import signature_from_str
diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py
index 07a61689575..c9c205db8c0 100644
--- a/tests/test_quickstart.py
+++ b/tests/test_quickstart.py
@@ -1,22 +1,27 @@
"""Test the sphinx.quickstart module."""
+from __future__ import annotations
+
import time
-from collections.abc import Callable
from io import StringIO
-from pathlib import Path
-from typing import Any
+from typing import TYPE_CHECKING
import pytest
+from sphinx._cli.util.colour import disable_colour, enable_colour
from sphinx.cmd import quickstart as qs
from sphinx.testing.util import SphinxTestApp
-from sphinx.util.console import coloron, nocolor
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from pathlib import Path
+ from typing import Any
warnfile = StringIO()
def setup_module():
- nocolor()
+ disable_colour()
def mock_input(
@@ -45,7 +50,7 @@ def input_(prompt: str) -> str:
def teardown_module():
qs.term_input = real_input
- coloron()
+ enable_colour()
def test_do_prompt():
diff --git a/tests/test_roles.py b/tests/test_roles.py
index 127f671a145..11b0a13cf4c 100644
--- a/tests/test_roles.py
+++ b/tests/test_roles.py
@@ -1,5 +1,7 @@
"""Test sphinx.roles"""
+from __future__ import annotations
+
from unittest.mock import Mock
import pytest
diff --git a/tests/test_search.py b/tests/test_search.py
index 600f66cb9f6..46d841e5a4d 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -16,7 +16,7 @@
from tests.utils import TESTS_ROOT
if TYPE_CHECKING:
- from collections.abc import Iterator
+ from collections.abc import Iterable, Iterator
from pathlib import Path
from typing import Any
@@ -53,12 +53,14 @@ def __str__(self) -> str:
class DummyDomain:
- def __init__(self, name: str, data: dict) -> None:
+ def __init__(
+ self, name: str, data: Iterable[tuple[str, str, str, str, str, int]]
+ ) -> None:
self.name = name
self.data = data
self.object_types: dict[str, ObjType] = {}
- def get_objects(self) -> list[tuple[str, str, str, str, str, int]]:
+ def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]:
return self.data
@@ -425,8 +427,7 @@ def test_search_index_is_deterministic(app):
def is_title_tuple_type(item: list[int | str]) -> bool:
- """
- In the search index, titles inside .alltitles are stored as a tuple of
+ """In the search index, titles inside .alltitles are stored as a tuple of
(document_idx, title_anchor). Tuples are represented as lists in JSON,
but their contents must not be sorted. We cannot sort them anyway, as
document_idx is an int and title_anchor is a str.
diff --git a/tests/test_theming/test_html_theme.py b/tests/test_theming/test_html_theme.py
index bdfca9886d8..7f2d34a93e6 100644
--- a/tests/test_theming/test_html_theme.py
+++ b/tests/test_theming/test_html_theme.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
diff --git a/tests/test_theming/test_templating.py b/tests/test_theming/test_templating.py
index 6eb2fdffa78..c508716b7c7 100644
--- a/tests/test_theming/test_templating.py
+++ b/tests/test_theming/test_templating.py
@@ -1,5 +1,7 @@
"""Test templating."""
+from __future__ import annotations
+
import pytest
from sphinx.ext.autosummary.generate import setup_documenters
diff --git a/tests/test_theming/test_theming.py b/tests/test_theming/test_theming.py
index afd15f838b4..bf3025473d0 100644
--- a/tests/test_theming/test_theming.py
+++ b/tests/test_theming/test_theming.py
@@ -1,5 +1,7 @@
"""Test the Theme class."""
+from __future__ import annotations
+
import shutil
from pathlib import Path
from xml.etree.ElementTree import ParseError
diff --git a/tests/test_transforms/test_transforms_move_module_targets.py b/tests/test_transforms/test_transforms_move_module_targets.py
index df72d9c33a5..f64b7d6a500 100644
--- a/tests/test_transforms/test_transforms_move_module_targets.py
+++ b/tests/test_transforms/test_transforms_move_module_targets.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from docutils import nodes
diff --git a/tests/test_transforms/test_transforms_post_transforms.py b/tests/test_transforms/test_transforms_post_transforms.py
index edd28749431..962202d22b0 100644
--- a/tests/test_transforms/test_transforms_post_transforms.py
+++ b/tests/test_transforms/test_transforms_post_transforms.py
@@ -173,7 +173,7 @@ def mark_node(self, node: nodes.Node) -> NoReturn:
visitor_methods = {f'visit_{tp.__name__}' for tp in desc_sig_elements_list}
visitor_methods.update(f'visit_{name}' for name in add_visitor_method_for)
class_dict = dict.fromkeys(visitor_methods, BaseCustomTranslatorClass.mark_node)
- return type('CustomTranslatorClass', (BaseCustomTranslatorClass,), class_dict) # type: ignore[return-value]
+ return type('CustomTranslatorClass', (BaseCustomTranslatorClass,), class_dict)
@pytest.mark.parametrize(
'add_visitor_method_for',
@@ -266,7 +266,7 @@ def test_custom_implementation(
strict=True,
):
assert_node(node, node_type)
- assert not node.hasattr('_sig_node_type')
+ assert not hasattr(node, '_sig_node_type')
assert mess == f'mark: {node_type.__name__!r}'
else:
# desc_sig_* nodes are converted into inline nodes
diff --git a/tests/test_transforms/test_transforms_post_transforms_code.py b/tests/test_transforms/test_transforms_post_transforms_code.py
index 57e9bb054cc..73bfcf49a8b 100644
--- a/tests/test_transforms/test_transforms_post_transforms_code.py
+++ b/tests/test_transforms/test_transforms_post_transforms_code.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
@@ -13,6 +15,8 @@ def test_trim_doctest_flags_html(app):
assert 'QUUX' not in result
assert 'CORGE' not in result
assert 'GRAULT' in result
+ assert 'now() \n' not in result
+ assert 'now()\n' in result
@pytest.mark.sphinx(
diff --git a/tests/test_transforms/test_transforms_post_transforms_images.py b/tests/test_transforms/test_transforms_post_transforms_images.py
index bb5d076f071..800fb3b986b 100644
--- a/tests/test_transforms/test_transforms_post_transforms_images.py
+++ b/tests/test_transforms/test_transforms_post_transforms_images.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from types import SimpleNamespace
from docutils import nodes
diff --git a/tests/test_transforms/test_transforms_reorder_nodes.py b/tests/test_transforms/test_transforms_reorder_nodes.py
index 22fdcf742cf..6afa6ab5b9c 100644
--- a/tests/test_transforms/test_transforms_reorder_nodes.py
+++ b/tests/test_transforms/test_transforms_reorder_nodes.py
@@ -1,5 +1,7 @@
"""Tests the transformations"""
+from __future__ import annotations
+
import pytest
from docutils import nodes
diff --git a/tests/test_transforms/test_unreferenced_footnotes.py b/tests/test_transforms/test_unreferenced_footnotes.py
index b1cf08f783a..c9810ad607b 100644
--- a/tests/test_transforms/test_unreferenced_footnotes.py
+++ b/tests/test_transforms/test_unreferenced_footnotes.py
@@ -1,9 +1,15 @@
"""Test the ``UnreferencedFootnotesDetector`` transform."""
-from pathlib import Path
+from __future__ import annotations
-from sphinx.testing.util import SphinxTestApp
-from sphinx.util.console import strip_colors
+from typing import TYPE_CHECKING
+
+from sphinx._cli.util.errors import strip_escape_sequences
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+ from sphinx.testing.util import SphinxTestApp
def test_warnings(make_app: type[SphinxTestApp], tmp_path: Path) -> None:
@@ -29,7 +35,7 @@ def test_warnings(make_app: type[SphinxTestApp], tmp_path: Path) -> None:
)
app = make_app(srcdir=tmp_path)
app.build()
- warnings = strip_colors(app.warning.getvalue()).lstrip()
+ warnings = strip_escape_sequences(app.warning.getvalue()).lstrip()
warnings = warnings.replace(str(tmp_path / 'index.rst'), 'source/index.rst')
assert (
warnings
diff --git a/tests/test_util/test_util.py b/tests/test_util/test_util.py
index b04c30e7aa8..a78e4151fda 100644
--- a/tests/test_util/test_util.py
+++ b/tests/test_util/test_util.py
@@ -1,6 +1,33 @@
"""Tests util functions."""
-from sphinx.util.osutil import ensuredir
+from __future__ import annotations
+
+import pytest
+
+import sphinx.util
+from sphinx._cli.util.errors import strip_escape_sequences
+from sphinx.deprecation import RemovedInSphinx10Warning, RemovedInSphinx90Warning
+from sphinx.errors import ExtensionError
+from sphinx.util._files import DownloadFiles, FilenameUniqDict
+from sphinx.util._importer import import_object
+from sphinx.util._lines import parse_line_num_spec
+from sphinx.util._uri import encode_uri, is_url
+from sphinx.util.index_entries import _split_into, split_index_msg
+from sphinx.util.matching import patfilter
+from sphinx.util.nodes import (
+ caption_ref_re,
+ explicit_title_re,
+ nested_parse_with_titles,
+ split_explicit_title,
+)
+from sphinx.util.osutil import (
+ SEP,
+ copyfile,
+ ensuredir,
+ make_filename,
+ os_path,
+ relative_uri,
+)
def test_ensuredir(tmp_path):
@@ -10,3 +37,55 @@ def test_ensuredir(tmp_path):
path = tmp_path / 'a' / 'b' / 'c'
ensuredir(path)
assert path.is_dir()
+
+
+def test_exported_attributes():
+ # RemovedInSphinx90Warning
+ with pytest.warns(
+ RemovedInSphinx90Warning,
+ match=r"deprecated, use 'sphinx.util.index_entries.split_index_msg' instead.",
+ ):
+ assert sphinx.util.split_index_msg is split_index_msg
+ with pytest.warns(RemovedInSphinx90Warning, match=r'deprecated.'):
+ assert sphinx.util.split_into is _split_into
+ with pytest.warns(
+ RemovedInSphinx90Warning,
+ match=r"deprecated, use 'sphinx.errors.ExtensionError' instead.",
+ ):
+ assert sphinx.util.ExtensionError is ExtensionError
+ with pytest.warns(
+ RemovedInSphinx90Warning,
+ match=r"deprecated, use 'hashlib.md5' instead.",
+ ):
+ _ = sphinx.util.md5
+ with pytest.warns(
+ RemovedInSphinx90Warning,
+ match=r"deprecated, use 'hashlib.sha1' instead.",
+ ):
+ _ = sphinx.util.sha1
+
+ # RemovedInSphinx10Warning
+ with pytest.warns(RemovedInSphinx10Warning, match=r'deprecated.'):
+ assert sphinx.util.FilenameUniqDict is FilenameUniqDict
+ with pytest.warns(RemovedInSphinx10Warning, match=r'deprecated.'):
+ assert sphinx.util.DownloadFiles is DownloadFiles
+ with pytest.warns(RemovedInSphinx10Warning, match=r'deprecated.'):
+ assert sphinx.util.import_object is import_object
+
+ # Re-exported for backwards compatibility,
+ # but not currently deprecated
+ assert sphinx.util.encode_uri is encode_uri
+ assert sphinx.util.isurl is is_url
+ assert sphinx.util.parselinenos is parse_line_num_spec
+ assert sphinx.util.patfilter is patfilter
+ assert sphinx.util.strip_escape_sequences is strip_escape_sequences
+ assert sphinx.util.caption_ref_re is caption_ref_re
+ assert sphinx.util.explicit_title_re is explicit_title_re
+ assert sphinx.util.nested_parse_with_titles is nested_parse_with_titles
+ assert sphinx.util.split_explicit_title is split_explicit_title
+ assert sphinx.util.SEP is SEP
+ assert sphinx.util.copyfile is copyfile
+ assert sphinx.util.ensuredir is ensuredir
+ assert sphinx.util.make_filename is make_filename
+ assert sphinx.util.os_path is os_path
+ assert sphinx.util.relative_uri is relative_uri
diff --git a/tests/test_util/test_util_display.py b/tests/test_util/test_util_display.py
index afad5a34d90..0428a6c7c99 100644
--- a/tests/test_util/test_util_display.py
+++ b/tests/test_util/test_util_display.py
@@ -1,9 +1,11 @@
"""Tests util functions."""
+from __future__ import annotations
+
import pytest
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.util import logging
-from sphinx.util.console import strip_colors
from sphinx.util.display import (
SkipProgressMessage,
display_chunk,
@@ -28,7 +30,7 @@ def test_status_iterator_length_0(app):
app.status.seek(0)
app.status.truncate(0)
yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... '))
- output = strip_colors(app.status.getvalue())
+ output = strip_escape_sequences(app.status.getvalue())
assert 'testing ... hello sphinx world \n' in output
assert yields == ['hello', 'sphinx', 'world']
@@ -45,7 +47,7 @@ def test_status_iterator_verbosity_0(app, monkeypatch):
['hello', 'sphinx', 'world'], 'testing ... ', length=3, verbosity=0
)
assert list(yields) == ['hello', 'sphinx', 'world']
- output = strip_colors(app.status.getvalue())
+ output = strip_escape_sequences(app.status.getvalue())
assert 'testing ... [ 33%] hello\r' in output
assert 'testing ... [ 67%] sphinx\r' in output
assert 'testing ... [100%] world\r\n' in output
@@ -63,7 +65,7 @@ def test_status_iterator_verbosity_1(app, monkeypatch):
['hello', 'sphinx', 'world'], 'testing ... ', length=3, verbosity=1
)
assert list(yields) == ['hello', 'sphinx', 'world']
- output = strip_colors(app.status.getvalue())
+ output = strip_escape_sequences(app.status.getvalue())
assert 'testing ... [ 33%] hello\n' in output
assert 'testing ... [ 67%] sphinx\n' in output
assert 'testing ... [100%] world\n\n' in output
@@ -78,24 +80,24 @@ def test_progress_message(app):
with progress_message('testing'):
logger.info('blah ', nonl=True)
- output = strip_colors(app.status.getvalue())
+ output = strip_escape_sequences(app.status.getvalue())
assert 'testing... blah done\n' in output
# skipping case
with progress_message('testing'):
- raise SkipProgressMessage('Reason: %s', 'error') # NoQA: EM101
+ raise SkipProgressMessage('Reason: %s', 'error') # NoQA: EM101,TRY003
- output = strip_colors(app.status.getvalue())
+ output = strip_escape_sequences(app.status.getvalue())
assert 'testing... skipped\nReason: error\n' in output
# error case
try:
with progress_message('testing'):
- raise RuntimeError
+ raise RuntimeError # NoQA: TRY301
except Exception:
pass
- output = strip_colors(app.status.getvalue())
+ output = strip_escape_sequences(app.status.getvalue())
assert 'testing... failed\n' in output
# decorator
@@ -104,5 +106,5 @@ def func():
logger.info('in func ', nonl=True)
func()
- output = strip_colors(app.status.getvalue())
+ output = strip_escape_sequences(app.status.getvalue())
assert 'testing... in func done\n' in output
diff --git a/tests/test_util/test_util_docstrings.py b/tests/test_util/test_util_docstrings.py
index 6d416a0754e..9fde0cb14eb 100644
--- a/tests/test_util/test_util_docstrings.py
+++ b/tests/test_util/test_util_docstrings.py
@@ -1,5 +1,7 @@
"""Test sphinx.util.docstrings."""
+from __future__ import annotations
+
from sphinx.util.docstrings import (
prepare_commentdoc,
prepare_docstring,
diff --git a/tests/test_util/test_util_docutils.py b/tests/test_util/test_util_docutils.py
index 6d04b5886fd..caf5c9d0093 100644
--- a/tests/test_util/test_util_docutils.py
+++ b/tests/test_util/test_util_docutils.py
@@ -40,32 +40,32 @@ class custom_node(nodes.Element):
assert not hasattr(nodes.SparseNodeVisitor, 'depart_custom_node')
-def test_SphinxFileOutput(tmpdir):
+def test_SphinxFileOutput(tmp_path):
content = 'Hello Sphinx World'
# write test.txt at first
- filename = str(tmpdir / 'test.txt')
- output = SphinxFileOutput(destination_path=filename)
+ filename = tmp_path / 'test.txt'
+ output = SphinxFileOutput(destination_path=str(filename))
output.write(content)
os.utime(filename, ns=(0, 0))
# overwrite it again
output.write(content)
- assert os.stat(filename).st_mtime_ns != 0 # updated
+ assert filename.stat().st_mtime_ns != 0 # updated
# write test2.txt at first
- filename = str(tmpdir / 'test2.txt')
- output = SphinxFileOutput(destination_path=filename, overwrite_if_changed=True)
+ filename = tmp_path / 'test2.txt'
+ output = SphinxFileOutput(destination_path=str(filename), overwrite_if_changed=True)
output.write(content)
os.utime(filename, ns=(0, 0))
# overwrite it again
output.write(content)
- assert os.stat(filename).st_mtime_ns == 0 # not updated
+ assert filename.stat().st_mtime_ns == 0 # not updated
# overwrite it again (content changed)
output.write(content + '; content change')
- assert os.stat(filename).st_mtime_ns != 0 # updated
+ assert filename.stat().st_mtime_ns != 0 # updated
@pytest.mark.sphinx('html', testroot='root')
diff --git a/tests/test_util/test_util_fileutil.py b/tests/test_util/test_util_fileutil.py
index 7ffa83fc223..32a20de1bbe 100644
--- a/tests/test_util/test_util_fileutil.py
+++ b/tests/test_util/test_util_fileutil.py
@@ -1,12 +1,14 @@
"""Tests sphinx.util.fileutil functions."""
+from __future__ import annotations
+
import re
from unittest import mock
import pytest
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.jinja2glue import BuiltinTemplateLoader
-from sphinx.util.console import strip_colors
from sphinx.util.fileutil import _template_basename, copy_asset, copy_asset_file
@@ -125,7 +127,7 @@ def test_copy_asset_template(app):
app.build(force_all=True)
expected_msg = r'^Writing evaluated template result to [^\n]*\bAPI.html$'
- output = strip_colors(app.status.getvalue())
+ output = strip_escape_sequences(app.status.getvalue())
assert re.findall(expected_msg, output, flags=re.MULTILINE)
@@ -134,7 +136,7 @@ def test_copy_asset_overwrite(app):
app.build()
src = app.srcdir / 'myext_static' / 'custom-styles.css'
dst = app.outdir / '_static' / 'custom-styles.css'
- assert strip_colors(app.warning.getvalue()) == (
+ assert strip_escape_sequences(app.warning.getvalue()) == (
f'WARNING: Aborted attempted copy from {src} to {dst} '
'(the destination path has existing data). '
'[misc.copy_overwrite]\n'
diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py
index d5ee52fb1f8..e0283ecb8bc 100644
--- a/tests/test_util/test_util_i18n.py
+++ b/tests/test_util/test_util_i18n.py
@@ -1,19 +1,18 @@
"""Test i18n util."""
+from __future__ import annotations
+
import datetime
import os
import time
from pathlib import Path
-import babel
import pytest
from babel.messages.mofile import read_mo
from sphinx.errors import SphinxError
from sphinx.util import i18n
-BABEL_VERSION = tuple(map(int, babel.__version__.split('.')))
-
def test_catalog_info_for_file_and_path():
cat = i18n.CatalogInfo('path', 'domain', 'utf-8')
@@ -40,7 +39,7 @@ def test_catalog_outdated(tmp_path):
mo_file.write_text('#', encoding='utf8')
assert not cat.is_outdated() # if mo is exist and newer than po
- new_mtime = os.stat(mo_file).st_mtime_ns - 10_000_000_000
+ new_mtime = mo_file.stat().st_mtime_ns - 10_000_000_000
os.utime(mo_file, ns=(new_mtime, new_mtime)) # to be outdated
assert cat.is_outdated() # if mo is exist and older than po
@@ -99,7 +98,7 @@ def test_format_date():
def test_format_date_timezone():
dt = datetime.datetime(2016, 8, 7, 5, 11, 17, 0, tzinfo=datetime.UTC)
if time.localtime(dt.timestamp()).tm_gmtoff == 0:
- raise pytest.skip('Local time zone is GMT') # NoQA: EM101
+ raise pytest.skip('Local time zone is GMT') # NoQA: EM101,TRY003
fmt = '%Y-%m-%d %H:%M:%S'
@@ -108,7 +107,7 @@ def test_format_date_timezone():
assert fd_gmt == '2016-08-07 05:11:17'
assert fd_gmt == iso_gmt
- iso_local = dt.astimezone().isoformat(' ').split('+')[0]
+ iso_local = dt.astimezone().isoformat(' ')[:19] # strip the timezone
fd_local = i18n.format_date(fmt, date=dt, language='en', local_time=True)
assert fd_local == iso_local
assert fd_local != fd_gmt
@@ -117,10 +116,10 @@ def test_format_date_timezone():
@pytest.mark.sphinx('html', testroot='root')
def test_get_filename_for_language(app):
get_filename = i18n.get_image_filename_for_language
- app.env.temp_data['docname'] = 'index'
+ app.env.current_document.docname = 'index'
# language is en
- app.env.config.language = 'en'
+ app.config.language = 'en'
assert get_filename('foo.png', app.env) == 'foo.en.png'
assert get_filename('foo.bar.png', app.env) == 'foo.bar.en.png'
assert get_filename('dir/foo.png', app.env) == 'dir/foo.en.png'
@@ -128,8 +127,8 @@ def test_get_filename_for_language(app):
assert get_filename('foo', app.env) == 'foo.en'
# modify figure_language_filename and language is 'en'
- app.env.config.language = 'en'
- app.env.config.figure_language_filename = 'images/{language}/{root}{ext}'
+ app.config.language = 'en'
+ app.config.figure_language_filename = 'images/{language}/{root}{ext}'
assert get_filename('foo.png', app.env) == 'images/en/foo.png'
assert get_filename('foo.bar.png', app.env) == 'images/en/foo.bar.png'
assert get_filename('subdir/foo.png', app.env) == 'images/en/subdir/foo.png'
@@ -137,8 +136,8 @@ def test_get_filename_for_language(app):
assert get_filename('foo', app.env) == 'images/en/foo'
# new path and basename tokens
- app.env.config.language = 'en'
- app.env.config.figure_language_filename = '{path}{language}/{basename}{ext}'
+ app.config.language = 'en'
+ app.config.figure_language_filename = '{path}{language}/{basename}{ext}'
assert get_filename('foo.png', app.env) == 'en/foo.png'
assert get_filename('foo.bar.png', app.env) == 'en/foo.bar.png'
assert get_filename('subdir/foo.png', app.env) == 'subdir/en/foo.png'
@@ -146,17 +145,17 @@ def test_get_filename_for_language(app):
assert get_filename('foo', app.env) == 'en/foo'
# invalid figure_language_filename
- app.env.config.figure_language_filename = '{root}.{invalid}{ext}'
+ app.config.figure_language_filename = '{root}.{invalid}{ext}'
with pytest.raises(SphinxError):
get_filename('foo.png', app.env)
# docpath (for a document in the top of source directory)
- app.env.config.language = 'en'
- app.env.config.figure_language_filename = '/{docpath}{language}/{basename}{ext}'
+ app.config.language = 'en'
+ app.config.figure_language_filename = '/{docpath}{language}/{basename}{ext}'
assert get_filename('foo.png', app.env) == '/en/foo.png'
# docpath (for a document in the sub directory)
- app.env.temp_data['docname'] = 'subdir/index'
+ app.env.current_document.docname = 'subdir/index'
assert get_filename('foo.png', app.env) == '/subdir/en/foo.png'
diff --git a/tests/test_util/test_util_images.py b/tests/test_util/test_util_images.py
index afc8587fa30..006d28003ce 100644
--- a/tests/test_util/test_util_images.py
+++ b/tests/test_util/test_util_images.py
@@ -1,5 +1,7 @@
"""Test images util."""
+from __future__ import annotations
+
import pytest
from sphinx.util.images import (
diff --git a/tests/test_util/test_util_importer.py b/tests/test_util/test_util_importer.py
index 57fd6e46fb6..af0503c6899 100644
--- a/tests/test_util/test_util_importer.py
+++ b/tests/test_util/test_util_importer.py
@@ -1,5 +1,7 @@
"""Test sphinx.util._importer."""
+from __future__ import annotations
+
import pytest
from sphinx.errors import ExtensionError
diff --git a/tests/test_util/test_util_inspect.py b/tests/test_util/test_util_inspect.py
index 5c868efa6e9..81eb3af2398 100644
--- a/tests/test_util/test_util_inspect.py
+++ b/tests/test_util/test_util_inspect.py
@@ -54,6 +54,21 @@ class Inherited(Base):
pass
+class MyInt(int):
+ @classmethod
+ def classmeth(cls):
+ pass
+
+
+class MyIntOverride(MyInt):
+ @classmethod
+ def from_bytes(cls, *a, **kw):
+ return super().from_bytes(*a, **kw)
+
+ def conjugate(self):
+ return super().conjugate()
+
+
def func():
pass
@@ -62,7 +77,7 @@ async def coroutinefunc():
pass
-async def asyncgenerator():
+async def asyncgenerator(): # NoQA: RUF029
yield
@@ -96,7 +111,7 @@ def test_TypeAliasForwardRef():
sig_str = stringify_annotation(alias, 'fully-qualified-except-typing')
assert sig_str == "TypeAliasForwardRef('example')"
- alias = Optional[alias] # NoQA: UP007
+ alias = Optional[alias] # NoQA: UP045
sig_str = stringify_annotation(alias, 'fully-qualified-except-typing')
assert sig_str == "TypeAliasForwardRef('example') | None"
@@ -696,11 +711,97 @@ class Qux:
inspect.getslots(Bar())
-def test_isclassmethod():
- assert inspect.isclassmethod(Base.classmeth)
- assert not inspect.isclassmethod(Base.meth)
- assert inspect.isclassmethod(Inherited.classmeth)
- assert not inspect.isclassmethod(Inherited.meth)
+@pytest.mark.parametrize(
+ ('expect', 'klass', 'name'),
+ [
+ # class methods
+ (True, Base, 'classmeth'),
+ (True, Inherited, 'classmeth'),
+ (True, MyInt, 'classmeth'),
+ (True, MyIntOverride, 'from_bytes'),
+ # non class methods
+ (False, Base, 'meth'),
+ (False, Inherited, 'meth'),
+ (False, MyInt, 'conjugate'),
+ (False, MyIntOverride, 'conjugate'),
+ ],
+)
+def test_isclassmethod(expect, klass, name):
+ subject = getattr(klass, name)
+ assert inspect.isclassmethod(subject) is expect
+ assert inspect.isclassmethod(None, klass, name) is expect
+
+
+@pytest.mark.parametrize(
+ ('expect', 'klass', 'dict_key'),
+ [
+ # int.from_bytes is not a class method descriptor
+ # but int.__dict__['from_bytes'] is one.
+ (True, int, 'from_bytes'),
+ (True, MyInt, 'from_bytes'), # inherited
+ # non class method descriptors
+ (False, Base, 'classmeth'),
+ (False, Inherited, 'classmeth'),
+ (False, int, '__init__'),
+ (False, int, 'conjugate'),
+ (False, MyInt, 'classmeth'),
+ (False, MyIntOverride, 'from_bytes'), # overridden in pure Python
+ ],
+)
+def test_is_classmethod_descriptor(expect, klass, dict_key):
+ in_dict = dict_key in klass.__dict__
+ subject = klass.__dict__.get(dict_key)
+ assert inspect.is_classmethod_descriptor(subject) is (in_dict and expect)
+ assert inspect.is_classmethod_descriptor(None, klass, dict_key) is expect
+
+ method = getattr(klass, dict_key)
+ assert not inspect.is_classmethod_descriptor(method)
+
+
+@pytest.mark.parametrize(
+ ('expect', 'klass', 'dict_key'),
+ [
+ # class method descriptors
+ (True, int, 'from_bytes'),
+ (True, bytes, 'fromhex'),
+ (True, MyInt, 'from_bytes'), # in C only
+ # non class method descriptors
+ (False, Base, 'classmeth'),
+ (False, Inherited, 'classmeth'),
+ (False, int, '__init__'),
+ (False, int, 'conjugate'),
+ (False, MyInt, 'classmeth'),
+ (False, MyIntOverride, 'from_bytes'), # overridden in pure Python
+ ],
+)
+def test_is_builtin_classmethod_like(expect, klass, dict_key):
+ method = getattr(klass, dict_key)
+ assert inspect.is_builtin_classmethod_like(method) is expect
+ assert inspect.is_builtin_classmethod_like(None, klass, dict_key) is expect
+
+
+@pytest.mark.parametrize(
+ ('expect', 'klass', 'name'),
+ [
+ # regular class methods
+ (True, Base, 'classmeth'),
+ (True, Inherited, 'classmeth'),
+ (True, MyInt, 'classmeth'),
+ # inherited C class method
+ (True, MyIntOverride, 'from_bytes'),
+ # class method descriptors
+ (True, int, 'from_bytes'),
+ (True, bytes, 'fromhex'),
+ (True, MyInt, 'from_bytes'), # in C only
+ # not classmethod-like
+ (False, int, '__init__'),
+ (False, int, 'conjugate'),
+ (False, MyIntOverride, 'conjugate'), # overridden in pure Python
+ ],
+)
+def test_is_classmethod_like(expect, klass, name):
+ subject = getattr(klass, name)
+ assert inspect.is_classmethod_like(subject) is expect
def test_isstaticmethod():
@@ -789,7 +890,7 @@ def test_isattributedescriptor():
try:
# _testcapi module cannot be importable in some distro
# refs: https://github.com/sphinx-doc/sphinx/issues/9868
- import _testcapi
+ import _testcapi # type: ignore[import-not-found]
# instancemethod (C-API)
testinstancemethod = _testcapi.instancemethod(str.__repr__)
@@ -811,7 +912,7 @@ def test_isproperty():
def test_isgenericalias():
#: A list of int
T = List[int] # NoQA: UP006
- S = list[Union[str, None]] # NoQA: UP006, UP007
+ S = list[Union[str, None]] # NoQA: UP007
C = Callable[[int], None] # a generic alias not having a doccomment
@@ -841,15 +942,14 @@ def func1(a, b, c):
def test_getdoc_inherited_classmethod():
class Foo:
@classmethod
- def meth(self):
- """
- docstring
- indented text
+ def meth(cls):
+ """Docstring
+ indented text
"""
class Bar(Foo):
@classmethod
- def meth(self):
+ def meth(cls):
# inherited classmethod
pass
@@ -860,9 +960,8 @@ def meth(self):
def test_getdoc_inherited_decorated_method():
class Foo:
def meth(self):
- """
- docstring
- indented text
+ """Docstring
+ indented text
"""
class Bar(Foo):
diff --git a/tests/test_util/test_util_inventory.py b/tests/test_util/test_util_inventory.py
index b09c2ec2dd8..3a2069344ed 100644
--- a/tests/test_util/test_util_inventory.py
+++ b/tests/test_util/test_util_inventory.py
@@ -2,16 +2,13 @@
from __future__ import annotations
-import os
-import posixpath
-from io import BytesIO
from typing import TYPE_CHECKING
import pytest
import sphinx.locale
from sphinx.testing.util import SphinxTestApp
-from sphinx.util.inventory import InventoryFile
+from sphinx.util.inventory import InventoryFile, _InventoryItem
from tests.test_util.intersphinx_data import (
INVENTORY_V1,
@@ -25,64 +22,60 @@
def test_read_inventory_v1():
- f = BytesIO(INVENTORY_V1)
- invdata = InventoryFile.load(f, '/util', posixpath.join)
- assert invdata['py:module']['module'] == (
- 'foo',
- '1.0',
- '/util/foo.html#module-module',
- '-',
+ invdata = InventoryFile.loads(INVENTORY_V1, uri='/util')
+ assert invdata['py:module']['module'] == _InventoryItem(
+ project_name='foo',
+ project_version='1.0',
+ uri='/util/foo.html#module-module',
+ display_name='-',
)
- assert invdata['py:class']['module.cls'] == (
- 'foo',
- '1.0',
- '/util/foo.html#module.cls',
- '-',
+ assert invdata['py:class']['module.cls'] == _InventoryItem(
+ project_name='foo',
+ project_version='1.0',
+ uri='/util/foo.html#module.cls',
+ display_name='-',
)
def test_read_inventory_v2():
- f = BytesIO(INVENTORY_V2)
- invdata = InventoryFile.load(f, '/util', posixpath.join)
+ invdata = InventoryFile.loads(INVENTORY_V2, uri='/util')
assert len(invdata['py:module']) == 2
- assert invdata['py:module']['module1'] == (
- 'foo',
- '2.0',
- '/util/foo.html#module-module1',
- 'Long Module desc',
+ assert invdata['py:module']['module1'] == _InventoryItem(
+ project_name='foo',
+ project_version='2.0',
+ uri='/util/foo.html#module-module1',
+ display_name='Long Module desc',
)
- assert invdata['py:module']['module2'] == (
- 'foo',
- '2.0',
- '/util/foo.html#module-module2',
- '-',
+ assert invdata['py:module']['module2'] == _InventoryItem(
+ project_name='foo',
+ project_version='2.0',
+ uri='/util/foo.html#module-module2',
+ display_name='-',
)
- assert invdata['py:function']['module1.func'][2] == (
+ assert invdata['py:function']['module1.func'].uri == (
'/util/sub/foo.html#module1.func'
)
- assert invdata['c:function']['CFunc'][2] == '/util/cfunc.html#CFunc'
- assert invdata['std:term']['a term'][2] == '/util/glossary.html#term-a-term'
- assert invdata['std:term']['a term including:colon'][2] == (
+ assert invdata['c:function']['CFunc'].uri == '/util/cfunc.html#CFunc'
+ assert invdata['std:term']['a term'].uri == '/util/glossary.html#term-a-term'
+ assert invdata['std:term']['a term including:colon'].uri == (
'/util/glossary.html#term-a-term-including-colon'
)
def test_read_inventory_v2_not_having_version():
- f = BytesIO(INVENTORY_V2_NO_VERSION)
- invdata = InventoryFile.load(f, '/util', posixpath.join)
- assert invdata['py:module']['module1'] == (
- 'foo',
- '',
- '/util/foo.html#module-module1',
- 'Long Module desc',
+ invdata = InventoryFile.loads(INVENTORY_V2_NO_VERSION, uri='/util')
+ assert invdata['py:module']['module1'] == _InventoryItem(
+ project_name='foo',
+ project_version='',
+ uri='/util/foo.html#module-module1',
+ display_name='Long Module desc',
)
@pytest.mark.sphinx('html', testroot='root')
def test_ambiguous_definition_warning(app):
- f = BytesIO(INVENTORY_V2_AMBIGUOUS_TERMS)
- InventoryFile.load(f, '/util', posixpath.join)
+ InventoryFile.loads(INVENTORY_V2_AMBIGUOUS_TERMS, uri='/util')
def _multiple_defs_notice_for(entity: str) -> str:
return f'contains multiple definitions for {entity}'
@@ -100,10 +93,10 @@ def _multiple_defs_notice_for(entity: str) -> str:
def _write_appconfig(dir: Path, language: str, prefix: str | None = None) -> Path:
prefix = prefix or language
- os.makedirs(dir / prefix, exist_ok=True)
+ (dir / prefix).mkdir(parents=True, exist_ok=True)
(dir / prefix / 'conf.py').write_text(f'language = "{language}"', encoding='utf8')
(dir / prefix / 'index.rst').write_text('index.rst', encoding='utf8')
- assert sorted(os.listdir(dir / prefix)) == ['conf.py', 'index.rst']
+ assert sorted(p.name for p in (dir / prefix).iterdir()) == ['conf.py', 'index.rst']
assert (dir / prefix / 'index.rst').exists()
return dir / prefix
diff --git a/tests/test_util/test_util_lines.py b/tests/test_util/test_util_lines.py
index a4ab41641de..5c9db412d88 100644
--- a/tests/test_util/test_util_lines.py
+++ b/tests/test_util/test_util_lines.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import pytest
from sphinx.util._lines import parse_line_num_spec
diff --git a/tests/test_util/test_util_logging.py b/tests/test_util/test_util_logging.py
index 5a9e0be5ada..37c6c6b0015 100644
--- a/tests/test_util/test_util_logging.py
+++ b/tests/test_util/test_util_logging.py
@@ -1,5 +1,7 @@
"""Test logging util."""
+from __future__ import annotations
+
import codecs
import os
from pathlib import Path
@@ -7,8 +9,9 @@
import pytest
from docutils import nodes
+from sphinx._cli.util.errors import strip_escape_sequences
from sphinx.util import logging
-from sphinx.util.console import colorize, strip_colors
+from sphinx.util.console import colorize
from sphinx.util.logging import is_suppressed_warning, prefixed_warnings
from sphinx.util.parallel import ParallelTasks
@@ -113,7 +116,7 @@ def test_once_warning_log(app):
logger.warning('message: %d', 1, once=True)
logger.warning('message: %d', 2, once=True)
- warnings = strip_colors(app.warning.getvalue())
+ warnings = strip_escape_sequences(app.warning.getvalue())
assert 'WARNING: message: 1\nWARNING: message: 2\n' in warnings
@@ -269,7 +272,7 @@ def test_pending_warnings(app):
assert 'WARNING: message3' not in app.warning.getvalue()
# actually logged as ordered
- warnings = strip_colors(app.warning.getvalue())
+ warnings = strip_escape_sequences(app.warning.getvalue())
assert 'WARNING: message2\nWARNING: message3' in warnings
@@ -379,7 +382,7 @@ def test_show_warning_types(app):
logger.warning('message3', type='test')
logger.warning('message4', type='test', subtype='logging')
- warnings = strip_colors(app.warning.getvalue()).splitlines()
+ warnings = strip_escape_sequences(app.warning.getvalue()).splitlines()
assert warnings == [
'WARNING: message2',
diff --git a/tests/test_util/test_util_matching.py b/tests/test_util/test_util_matching.py
index 01fb9fde2e1..9a51123ceff 100644
--- a/tests/test_util/test_util_matching.py
+++ b/tests/test_util/test_util_matching.py
@@ -1,5 +1,7 @@
"""Tests sphinx.util.matching functions."""
+from __future__ import annotations
+
from sphinx.util.matching import Matcher, compile_matchers, get_matching_files
diff --git a/tests/test_util/test_util_nodes.py b/tests/test_util/test_util_nodes.py
index a330eff366d..ba8b1d68a06 100644
--- a/tests/test_util/test_util_nodes.py
+++ b/tests/test_util/test_util_nodes.py
@@ -161,8 +161,7 @@ def test_extract_messages(rst, node_cls, count):
def test_extract_messages_without_rawsource():
- """
- Check node.rawsource is fall-backed by using node.astext() value.
+ """Check node.rawsource is fall-backed by using node.astext() value.
`extract_message` which is used from Sphinx i18n feature drop ``not node.rawsource``
nodes. So, all nodes which want to translate must have ``rawsource`` value.
@@ -180,7 +179,7 @@ def test_extract_messages_without_rawsource():
document.append(p)
_transform(document)
assert_node_count(extract_messages(document), nodes.TextElement, 1)
- assert [m for n, m in extract_messages(document)][0], 'text sentence'
+ assert next(m for n, m in extract_messages(document)), 'text sentence'
def test_clean_astext():
diff --git a/tests/test_util/test_util_rst.py b/tests/test_util/test_util_rst.py
index d6e83433af3..4d8d0e09482 100644
--- a/tests/test_util/test_util_rst.py
+++ b/tests/test_util/test_util_rst.py
@@ -1,5 +1,7 @@
"""Tests sphinx.util.rst functions."""
+from __future__ import annotations
+
from docutils.statemachine import StringList
from jinja2 import Environment
diff --git a/tests/test_util/test_util_template.py b/tests/test_util/test_util_template.py
index 51bb225b2c4..19e8b961ccf 100644
--- a/tests/test_util/test_util_template.py
+++ b/tests/test_util/test_util_template.py
@@ -1,5 +1,7 @@
"""Tests sphinx.util.template functions."""
+from __future__ import annotations
+
from sphinx.util.template import ReSTRenderer
diff --git a/tests/test_util/test_util_typing.py b/tests/test_util/test_util_typing.py
index 314639efbe7..c2c1ba1ac2e 100644
--- a/tests/test_util/test_util_typing.py
+++ b/tests/test_util/test_util_typing.py
@@ -1,12 +1,38 @@
"""Tests util.typing functions."""
+from __future__ import annotations
+
+import ctypes
import dataclasses
import sys
import typing as t
+import zipfile
from collections import abc
from contextvars import Context, ContextVar, Token
from enum import Enum
+from io import (
+ BufferedRandom,
+ BufferedReader,
+ BufferedRWPair,
+ BufferedWriter,
+ BytesIO,
+ FileIO,
+ StringIO,
+ TextIOWrapper,
+)
+from json import JSONDecoder, JSONEncoder
+from lzma import LZMACompressor, LZMADecompressor
+from multiprocessing import Process
from numbers import Integral
+from pathlib import (
+ Path,
+ PosixPath,
+ PurePath,
+ PurePosixPath,
+ PureWindowsPath,
+ WindowsPath,
+)
+from pickle import Pickler, Unpickler
from struct import Struct
from types import (
AsyncGeneratorType,
@@ -16,6 +42,7 @@
ClassMethodDescriptorType,
CodeType,
CoroutineType,
+ EllipsisType,
FrameType,
FunctionType,
GeneratorType,
@@ -27,6 +54,8 @@
MethodType,
MethodWrapperType,
ModuleType,
+ NoneType,
+ NotImplementedType,
TracebackType,
WrapperDescriptorType,
)
@@ -44,8 +73,9 @@
TypeVar,
Union,
)
+from weakref import WeakSet
-from sphinx.ext.autodoc import mock
+from sphinx.ext.autodoc.mock import mock
from sphinx.util.typing import _INVALID_BUILTIN_CLASSES, restify, stringify_annotation
@@ -97,6 +127,9 @@ def test_restify():
assert restify(TracebackType) == ':py:class:`types.TracebackType`'
assert restify(TracebackType, 'smart') == ':py:class:`~types.TracebackType`'
+ assert restify(Path) == ':py:class:`pathlib.Path`'
+ assert restify(Path, 'smart') == ':py:class:`~pathlib.Path`'
+
assert restify(Any) == ':py:obj:`~typing.Any`'
assert restify(Any, 'smart') == ':py:obj:`~typing.Any`'
@@ -108,11 +141,38 @@ def test_is_invalid_builtin_class():
# if these tests start failing, it means that the __module__
# of one of these classes has changed, and _INVALID_BUILTIN_CLASSES
# in sphinx.util.typing needs to be updated.
- assert _INVALID_BUILTIN_CLASSES.keys() == {
+ invalid_types = (
+ # contextvars
Context,
ContextVar,
Token,
+ # ctypes
+ ctypes.Array,
+ ctypes.Structure,
+ ctypes.Union,
+ # io
+ FileIO,
+ BytesIO,
+ StringIO,
+ BufferedReader,
+ BufferedWriter,
+ BufferedRWPair,
+ BufferedRandom,
+ TextIOWrapper,
+ # json
+ JSONDecoder,
+ JSONEncoder,
+ # lzma
+ LZMACompressor,
+ LZMADecompressor,
+ # multiprocessing
+ Process,
+ # pickle
+ Pickler,
+ Unpickler,
+ # struct
Struct,
+ # types
AsyncGeneratorType,
BuiltinFunctionType,
BuiltinMethodType,
@@ -120,6 +180,7 @@ def test_is_invalid_builtin_class():
ClassMethodDescriptorType,
CodeType,
CoroutineType,
+ EllipsisType,
FrameType,
FunctionType,
GeneratorType,
@@ -131,30 +192,46 @@ def test_is_invalid_builtin_class():
MethodType,
MethodWrapperType,
ModuleType,
+ NoneType,
+ NotImplementedType,
TracebackType,
WrapperDescriptorType,
- }
- assert Struct.__module__ == '_struct'
- assert AsyncGeneratorType.__module__ == 'builtins'
- assert BuiltinFunctionType.__module__ == 'builtins'
- assert BuiltinMethodType.__module__ == 'builtins'
- assert CellType.__module__ == 'builtins'
- assert ClassMethodDescriptorType.__module__ == 'builtins'
- assert CodeType.__module__ == 'builtins'
- assert CoroutineType.__module__ == 'builtins'
- assert FrameType.__module__ == 'builtins'
- assert FunctionType.__module__ == 'builtins'
- assert GeneratorType.__module__ == 'builtins'
- assert GetSetDescriptorType.__module__ == 'builtins'
- assert LambdaType.__module__ == 'builtins'
- assert MappingProxyType.__module__ == 'builtins'
- assert MemberDescriptorType.__module__ == 'builtins'
- assert MethodDescriptorType.__module__ == 'builtins'
- assert MethodType.__module__ == 'builtins'
- assert MethodWrapperType.__module__ == 'builtins'
- assert ModuleType.__module__ == 'builtins'
- assert TracebackType.__module__ == 'builtins'
- assert WrapperDescriptorType.__module__ == 'builtins'
+ # weakref
+ WeakSet,
+ )
+ if sys.version_info[:2] >= (3, 12):
+ invalid_types += (
+ # zipfile
+ zipfile.Path,
+ zipfile.CompleteDirs,
+ )
+ if sys.version_info[:2] >= (3, 13):
+ invalid_types += (
+ # pathlib
+ Path,
+ PosixPath,
+ PurePath,
+ PurePosixPath,
+ PureWindowsPath,
+ WindowsPath,
+ )
+
+ invalid_names = {(cls.__module__, cls.__qualname__) for cls in invalid_types}
+ if sys.version_info[:2] < (3, 13):
+ invalid_names |= {
+ ('pathlib._local', 'Path'),
+ ('pathlib._local', 'PosixPath'),
+ ('pathlib._local', 'PurePath'),
+ ('pathlib._local', 'PurePosixPath'),
+ ('pathlib._local', 'PureWindowsPath'),
+ ('pathlib._local', 'WindowsPath'),
+ }
+ if sys.version_info[:2] < (3, 12):
+ invalid_names |= {
+ ('zipfile._path', 'Path'),
+ ('zipfile._path', 'CompleteDirs'),
+ }
+ assert _INVALID_BUILTIN_CLASSES.keys() == invalid_names
def test_restify_type_hints_containers():
@@ -350,7 +427,7 @@ def test_restify_type_ForwardRef():
restify(list[ForwardRef('MyInt')]) == ':py:class:`list`\\ [:py:class:`MyInt`]'
)
- ann_rst = restify(Tuple[dict[ForwardRef('MyInt'), str], list[List[int]]]) # type: ignore[attr-defined]
+ ann_rst = restify(Tuple[dict[ForwardRef('MyInt'), str], list[List[int]]])
assert ann_rst == (
':py:class:`~typing.Tuple`\\ [:py:class:`dict`\\ [:py:class:`MyInt`, :py:class:`str`], :py:class:`list`\\ [:py:class:`~typing.List`\\ [:py:class:`int`]]]'
)
@@ -371,14 +448,14 @@ def test_restify_type_Literal():
def test_restify_pep_585():
- assert restify(list[str]) == ':py:class:`list`\\ [:py:class:`str`]' # type: ignore[attr-defined]
- ann_rst = restify(dict[str, str]) # type: ignore[attr-defined]
+ assert restify(list[str]) == ':py:class:`list`\\ [:py:class:`str`]'
+ ann_rst = restify(dict[str, str])
assert ann_rst == ':py:class:`dict`\\ [:py:class:`str`, :py:class:`str`]'
assert restify(tuple[str, ...]) == ':py:class:`tuple`\\ [:py:class:`str`, ...]'
assert restify(tuple[str, str, str]) == (
':py:class:`tuple`\\ [:py:class:`str`, :py:class:`str`, :py:class:`str`]'
)
- ann_rst = restify(dict[str, tuple[int, ...]]) # type: ignore[attr-defined]
+ ann_rst = restify(dict[str, tuple[int, ...]])
assert ann_rst == (
':py:class:`dict`\\ '
'[:py:class:`str`, :py:class:`tuple`\\ '
@@ -426,10 +503,10 @@ class X(t.TypedDict):
def test_restify_type_union_operator():
- assert restify(int | None) == ':py:class:`int` | :py:obj:`None`' # type: ignore[attr-defined]
- assert restify(None | int) == ':py:obj:`None` | :py:class:`int`' # type: ignore[attr-defined]
- assert restify(int | str) == ':py:class:`int` | :py:class:`str`' # type: ignore[attr-defined]
- ann_rst = restify(int | str | None) # type: ignore[attr-defined]
+ assert restify(int | None) == ':py:class:`int` | :py:obj:`None`'
+ assert restify(None | int) == ':py:obj:`None` | :py:class:`int`'
+ assert restify(int | str) == ':py:class:`int` | :py:class:`str`'
+ ann_rst = restify(int | str | None)
assert ann_rst == ':py:class:`int` | :py:class:`str` | :py:obj:`None`'
@@ -442,7 +519,7 @@ def test_restify_broken_type_hints():
def test_restify_mock():
with mock(['unknown']):
- import unknown
+ import unknown # type: ignore[import-not-found]
assert restify(unknown) == ':py:class:`unknown`'
assert restify(unknown.secret.Class) == ':py:class:`unknown.secret.Class`'
@@ -485,6 +562,10 @@ def test_stringify_annotation():
assert ann_str == 'types.TracebackType'
assert stringify_annotation(TracebackType, 'smart') == '~types.TracebackType'
+ ann_str = stringify_annotation(Path, 'fully-qualified-except-typing')
+ assert ann_str == 'pathlib.Path'
+ assert stringify_annotation(Path, 'smart') == '~pathlib.Path'
+
assert stringify_annotation(Any, 'fully-qualified-except-typing') == 'Any'
assert stringify_annotation(Any, 'fully-qualified') == 'typing.Any'
assert stringify_annotation(Any, 'smart') == '~typing.Any'
@@ -856,8 +937,8 @@ def test_stringify_type_hints_alias():
assert stringify_annotation(MyStr, 'fully-qualified-except-typing') == 'str'
assert stringify_annotation(MyStr, 'smart') == 'str'
- assert stringify_annotation(MyTuple) == 'Tuple[str, str]' # type: ignore[attr-defined]
- assert stringify_annotation(MyTuple, 'smart') == '~typing.Tuple[str, str]' # type: ignore[attr-defined]
+ assert stringify_annotation(MyTuple) == 'Tuple[str, str]'
+ assert stringify_annotation(MyTuple, 'smart') == '~typing.Tuple[str, str]'
def test_stringify_type_Literal():
@@ -879,26 +960,26 @@ def test_stringify_type_Literal():
def test_stringify_type_union_operator():
- assert stringify_annotation(int | None) == 'int | None' # type: ignore[attr-defined]
- assert stringify_annotation(int | None, 'smart') == 'int | None' # type: ignore[attr-defined]
+ assert stringify_annotation(int | None) == 'int | None'
+ assert stringify_annotation(int | None, 'smart') == 'int | None'
- assert stringify_annotation(int | str) == 'int | str' # type: ignore[attr-defined]
- assert stringify_annotation(int | str, 'smart') == 'int | str' # type: ignore[attr-defined]
+ assert stringify_annotation(int | str) == 'int | str'
+ assert stringify_annotation(int | str, 'smart') == 'int | str'
- assert stringify_annotation(int | str | None) == 'int | str | None' # type: ignore[attr-defined]
- assert stringify_annotation(int | str | None, 'smart') == 'int | str | None' # type: ignore[attr-defined]
+ assert stringify_annotation(int | str | None) == 'int | str | None'
+ assert stringify_annotation(int | str | None, 'smart') == 'int | str | None'
ann_str = stringify_annotation(
int | tuple[dict[str, int | None], list[int | str]] | None
)
- assert ann_str == 'int | tuple[dict[str, int | None], list[int | str]] | None' # type: ignore[attr-defined]
+ assert ann_str == 'int | tuple[dict[str, int | None], list[int | str]] | None'
ann_str = stringify_annotation(
int | tuple[dict[str, int | None], list[int | str]] | None, 'smart'
)
- assert ann_str == 'int | tuple[dict[str, int | None], list[int | str]] | None' # type: ignore[attr-defined]
+ assert ann_str == 'int | tuple[dict[str, int | None], list[int | str]] | None'
- assert stringify_annotation(int | Struct) == 'int | struct.Struct' # type: ignore[attr-defined]
- assert stringify_annotation(int | Struct, 'smart') == 'int | ~struct.Struct' # type: ignore[attr-defined]
+ assert stringify_annotation(int | Struct) == 'int | struct.Struct'
+ assert stringify_annotation(int | Struct, 'smart') == 'int | ~struct.Struct'
def test_stringify_broken_type_hints():
@@ -932,16 +1013,16 @@ def test_stringify_type_ForwardRef():
ann_str = stringify_annotation(
Tuple[dict[ForwardRef('MyInt'), str], list[List[int]]]
)
- assert ann_str == 'Tuple[dict[MyInt, str], list[List[int]]]' # type: ignore[attr-defined]
+ assert ann_str == 'Tuple[dict[MyInt, str], list[List[int]]]'
ann_str = stringify_annotation(
Tuple[dict[ForwardRef('MyInt'), str], list[List[int]]],
'fully-qualified-except-typing',
)
- assert ann_str == 'Tuple[dict[MyInt, str], list[List[int]]]' # type: ignore[attr-defined]
+ assert ann_str == 'Tuple[dict[MyInt, str], list[List[int]]]'
ann_str = stringify_annotation(
Tuple[dict[ForwardRef('MyInt'), str], list[List[int]]], 'smart'
)
- assert ann_str == '~typing.Tuple[dict[MyInt, str], list[~typing.List[int]]]' # type: ignore[attr-defined]
+ assert ann_str == '~typing.Tuple[dict[MyInt, str], list[~typing.List[int]]]'
def test_stringify_type_hints_paramspec():
diff --git a/tests/test_util/test_util_uri.py b/tests/test_util/test_util_uri.py
index 43ee1b34e48..fdcb3c2ca8e 100644
--- a/tests/test_util/test_util_uri.py
+++ b/tests/test_util/test_util_uri.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from sphinx.util._uri import encode_uri
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
index 14e7520db49..795868ba84f 100644
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -1,5 +1,7 @@
"""Test the versioning implementation."""
+from __future__ import annotations
+
import pickle
import shutil
diff --git a/tests/test_writers/test_api_translator.py b/tests/test_writers/test_api_translator.py
index bdbea0dc578..cd34aba058a 100644
--- a/tests/test_writers/test_api_translator.py
+++ b/tests/test_writers/test_api_translator.py
@@ -1,5 +1,7 @@
"""Test the Sphinx API for translator."""
+from __future__ import annotations
+
import sys
import pytest
diff --git a/tests/test_writers/test_docutilsconf.py b/tests/test_writers/test_docutilsconf.py
index cd74b5bef95..4201c9df831 100644
--- a/tests/test_writers/test_docutilsconf.py
+++ b/tests/test_writers/test_docutilsconf.py
@@ -1,5 +1,7 @@
"""Test docutils.conf support for several writers."""
+from __future__ import annotations
+
import pytest
from docutils import nodes
diff --git a/tests/test_writers/test_writer_latex.py b/tests/test_writers/test_writer_latex.py
index a0ab3ee5915..55130c44d52 100644
--- a/tests/test_writers/test_writer_latex.py
+++ b/tests/test_writers/test_writer_latex.py
@@ -1,5 +1,7 @@
"""Test the LaTeX writer"""
+from __future__ import annotations
+
import pytest
from sphinx.writers.latex import rstdim_to_latexdim
diff --git a/tests/utils.py b/tests/utils.py
index 9ebe62ec2e6..8972740b881 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -28,8 +28,7 @@
class HttpServerThread(Thread):
def __init__(self, handler: type[BaseRequestHandler], *, port: int = 0) -> None:
- """
- Constructs a threaded HTTP server. The default port number of ``0``
+ """Constructs a threaded HTTP server. The default port number of ``0``
delegates selection of a port number to bind to to Python.
Ref: https://docs.python.org/3.11/library/socketserver.html#asynchronous-mixins
@@ -77,8 +76,7 @@ def http_server(
@contextmanager
def rewrite_hyperlinks(app: Sphinx, server: HTTPServer) -> Iterator[None]:
- """
- Rewrite hyperlinks that refer to network location 'localhost:7777',
+ """Rewrite hyperlinks that refer to network location 'localhost:7777',
allowing that location to vary dynamically with the arbitrary test HTTP
server port assigned during unit testing.
@@ -109,8 +107,7 @@ def serve_application(
tls_enabled: bool = False,
port: int = 0,
) -> Iterator[str]:
- """
- Prepare a temporary server to handle HTTP requests related to the links
+ """Prepare a temporary server to handle HTTP requests related to the links
found in a Sphinx application project.
:param app: The Sphinx application.
diff --git a/tox.ini b/tox.ini
index 572647c1196..674013fdc08 100644
--- a/tox.ini
+++ b/tox.ini
@@ -37,7 +37,6 @@ extras =
# GitHub Workflow step
commands =
ruff check . --output-format github
- flake8 .
mypy
pyright
diff --git a/utils/babel_runner.py b/utils/babel_runner.py
index 8c27f20e3e8..45e97d2e682 100644
--- a/utils/babel_runner.py
+++ b/utils/babel_runner.py
@@ -13,6 +13,8 @@
Compile the ".po" catalogue files to ".mo" and ".js" files.
"""
+from __future__ import annotations
+
import json
import logging
import sys
@@ -82,11 +84,12 @@ def run_extract() -> None:
log = _get_logger()
with open('sphinx/__init__.py', encoding='utf-8') as f:
- for line in f.read().splitlines():
- if line.startswith('__version__ = '):
- # remove prefix; strip whitespace; remove quotation marks
- sphinx_version = line[14:].strip()[1:-1]
- break
+ lines = f.readlines()
+ for line in lines:
+ if line.startswith('__version__ = '):
+ # remove prefix; strip whitespace; remove quotation marks
+ sphinx_version = line[14:].strip()[1:-1]
+ break
catalogue = Catalog(project='Sphinx', version=sphinx_version, charset='utf-8')
@@ -160,8 +163,7 @@ def run_update() -> None:
def run_compile() -> None:
- """
- Catalog compilation command.
+ """Catalogue compilation command.
An extended command that writes all message strings that occur in
JavaScript files to a JavaScript file along with the .mo file.
diff --git a/utils/bump_docker.py b/utils/bump_docker.py
index 570e5ecab49..c36d8a88369 100755
--- a/utils/bump_docker.py
+++ b/utils/bump_docker.py
@@ -2,6 +2,8 @@
"""Usage: bump_docker.py [VERSION]"""
+from __future__ import annotations
+
import re
import subprocess
import sys
diff --git a/utils/bump_version.py b/utils/bump_version.py
index 7894ac9f85e..d32708550c2 100755
--- a/utils/bump_version.py
+++ b/utils/bump_version.py
@@ -9,10 +9,12 @@
import time
from contextlib import contextmanager
from pathlib import Path
-from typing import TYPE_CHECKING, Literal
+from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
+ from typing import Literal
+
script_dir = Path(__file__).resolve().parent
package_dir = script_dir.parent
diff --git a/utils/convert_attestations.py b/utils/convert_attestations.py
index d359c1593b3..46697677d34 100644
--- a/utils/convert_attestations.py
+++ b/utils/convert_attestations.py
@@ -3,6 +3,18 @@
See https://github.com/trailofbits/pypi-attestations.
"""
+# resolution fails without betterproto and protobuf-specs
+# /// script
+# requires-python = ">=3.11"
+# dependencies = [
+# "pypi-attestations~=0.0.12",
+# "sigstore-protobuf-specs==0.3.2",
+# "betterproto==2.0.0b6",
+# ]
+# ///
+
+from __future__ import annotations
+
import json
import sys
from base64 import b64decode
diff --git a/utils/generate_js_fixtures.py b/utils/generate_js_fixtures.py
index 4e126899026..ecf13a94741 100755
--- a/utils/generate_js_fixtures.py
+++ b/utils/generate_js_fixtures.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
+from __future__ import annotations
import shutil
import subprocess