diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 5af0a95..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 95 -ignore = E116,E241,E251 -exclude = .git,.tox,.venv diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..47a31bc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 40240ea..5166a36 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -17,9 +17,9 @@ jobs: permissions: id-token: write # for PyPI trusted publishing steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3 cache: pip @@ -35,7 +35,7 @@ jobs: - name: Mint PyPI API token id: mint-token - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: # language=JavaScript script: | @@ -75,16 +75,16 @@ jobs: permissions: contents: write # for softprops/action-gh-release to create GitHub release steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get release version id: get_version - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: core.setOutput('version', context.ref.replace("refs/tags/", "")) - name: Create GitHub release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: name: "sphinxcontrib-devhelp ${{ steps.get_version.outputs.version }}" - body: "Changelog: https://www.sphinx-doc.org/en/master/changes.html" + body: "Changelog: https://github.com/sphinx-doc/sphinxcontrib-devhelp/blob/master/CHANGES.rst" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8cd7db1..e337f0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,26 +25,26 @@ jobs: - "3.9" - "3.10" - "3.11" - - "3.12-dev" + - "3.12" - "3.13-dev" fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 if: "!endsWith(matrix.python, '-dev')" with: python-version: ${{ matrix.python }} - name: Set up Python ${{ matrix.python }} (deadsnakes) - uses: deadsnakes/action@v2.1.1 + uses: deadsnakes/action@v3.1.0 if: "endsWith(matrix.python, '-dev')" with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install .[test,standalone] + python -m pip install .[standalone,test] - name: Test with pytest run: python -m pytest -vv --durations 25 @@ -53,9 +53,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3" - name: Install dependencies @@ -71,12 +71,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - env: [flake8, mypy] + env: + - ruff + - mypy steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3" diff --git a/.github/workflows/transifex.yml b/.github/workflows/transifex.yml index c39d381..571fd4c 100644 --- a/.github/workflows/transifex.yml +++ b/.github/workflows/transifex.yml @@ -15,9 +15,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3 - name: Install transifex client @@ -44,9 +44,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3 - name: Install transifex client @@ -67,7 +67,7 @@ jobs: - name: Compile message catalogs run: python utils/babel_runner.py compile - name: Create Pull Request - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v6 with: commit-message: "[internationalisation] Update translations" branch: bot/pull-translations diff --git a/.gitignore b/.gitignore index b018007..10cef94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,17 @@ *.pyc -*.egg -*.so -*.swp .DS_Store +idea/ +.vscode/ + .mypy_cache/ +.pytest_cache/ +.ruff_cache/ .tags .tox/ +.venv/ +venv/ + build/ dist/ -sphinxcontrib_devhelp.egg-info/ diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..4b7dd2a --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,53 @@ +target-version = "py39" # Pin Ruff to Python 3.9 +output-format = "full" +line-length = 95 + +[lint] +preview = true +select = [ +# "ANN", # flake8-annotations + "C4", # flake8-comprehensions + "COM", # flake8-commas + "B", # flake8-bugbear + "DTZ", # flake8-datetimez + "E", # pycodestyle + "EM", # flake8-errmsg + "EXE", # flake8-executable + "F", # pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INT", # flake8-gettext + "LOG", # flake8-logging + "PERF", # perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "TCH", # flake8-type-checking + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] +ignore = [ + "E116", + "E241", + "E251", +] + +[lint.per-file-ignores] +"tests/*" = [ + "ANN", # tests don't need annotations +] + +[lint.isort] +forced-separate = [ + "tests", +] +required-imports = [ + "from __future__ import annotations", +] diff --git a/CHANGES b/CHANGES.rst similarity index 83% rename from CHANGES rename to CHANGES.rst index bcd650b..6c64985 100644 --- a/CHANGES +++ b/CHANGES.rst @@ -1,3 +1,11 @@ +Release 2.0.0 (2024-07-28) +========================== + +* Adopt Ruff +* Tighten MyPy settings +* Update GitHub actions versions +* Avoid storing build time in gzip headers + Release 1.0.6 (2024-01-13) ========================== diff --git a/LICENSE b/LICENCE.rst similarity index 100% rename from LICENSE rename to LICENCE.rst diff --git a/Makefile b/Makefile index 26f411a..438ee54 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ clean-mypyfiles: .PHONY: style-check style-check: - @flake8 + @ruff check .PHONY: type-check type-check: diff --git a/pyproject.toml b/pyproject.toml index f8492ca..53d59b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,11 @@ build-backend = "flit_core.buildapi" name = "sphinxcontrib-devhelp" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" readme = "README.rst" -urls.Changelog = "https://www.sphinx-doc.org/en/master/changes.html" -urls.Code = "https://github.com/sphinx-doc/sphinxcontrib-devhelp" +urls.Changelog = "https://github.com/sphinx-doc/sphinxcontrib-devhelp/blob/master/CHANGES.rst" +urls.Code = "https://github.com/sphinx-doc/sphinxcontrib-devhelp/" urls.Download = "https://pypi.org/project/sphinxcontrib-devhelp/" urls.Homepage = "https://www.sphinx-doc.org/" -urls."Issue tracker" = "https://github.com/sphinx-doc/sphinx/issues" +urls."Issue tracker" = "https://github.com/sphinx-doc/sphinx/issues/" license.text = "BSD-2-Clause" requires-python = ">=3.9" @@ -47,9 +47,9 @@ test = [ "pytest", ] lint = [ - "flake8", + "ruff==0.5.5", "mypy", - "docutils-stubs", + "types-docutils", ] standalone = [ "Sphinx>=5", @@ -64,12 +64,42 @@ name = "sphinxcontrib.devhelp" [tool.flit.sdist] include = [ - "CHANGES", - "LICENSE", + "CHANGES.rst", + "LICENCE.rst", # Tests "tests/", "tox.ini", ] [tool.mypy] -ignore_missing_imports = true +python_version = "3.9" +packages = [ + "sphinxcontrib", + "tests", +] +exclude = [ + "tests/roots", +] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +explicit_package_bases = true +extra_checks = true +no_implicit_reexport = true +show_column_numbers = true +show_error_context = true +strict_optional = true +warn_redundant_casts = true +warn_unused_configs = true +warn_unused_ignores = true +enable_error_code = [ + "type-arg", + "redundant-self", + "truthy-iterable", + "ignore-without-code", + "unused-awaitable", +] diff --git a/sphinxcontrib/devhelp/__init__.py b/sphinxcontrib/devhelp/__init__.py index 0f14258..aaefaec 100644 --- a/sphinxcontrib/devhelp/__init__.py +++ b/sphinxcontrib/devhelp/__init__.py @@ -9,11 +9,10 @@ import os import re from os import path -from typing import Any +from typing import TYPE_CHECKING, Any from docutils import nodes from sphinx import addnodes -from sphinx.application import Sphinx from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.environment.adapters.indexentries import IndexEntries from sphinx.locale import get_translation @@ -21,13 +20,13 @@ from sphinx.util.nodes import NodeMatcher from sphinx.util.osutil import make_filename -try: - import xml.etree.ElementTree as etree -except ImportError: - import lxml.etree as etree # type: ignore +if TYPE_CHECKING: + from sphinx.application import Sphinx -__version__ = '1.0.6' -__version_info__ = (1, 0, 6) +import xml.etree.ElementTree as etree + +__version__ = '2.0.0' +__version_info__ = (2, 0, 0) logger = logging.getLogger(__name__) __ = get_translation(__name__, 'console') @@ -80,8 +79,7 @@ def build_devhelp(self, outdir: str | os.PathLike[str], outname: str) -> None: self.config.master_doc, self, prune_toctrees=False) def write_toc(node: nodes.Node, parent: etree.Element) -> None: - if isinstance(node, addnodes.compact_paragraph) or \ - isinstance(node, nodes.bullet_list): + if isinstance(node, (addnodes.compact_paragraph, nodes.bullet_list)): for subnode in node: write_toc(subnode, parent) elif isinstance(node, nodes.list_item): @@ -93,7 +91,7 @@ def write_toc(node: nodes.Node, parent: etree.Element) -> None: parent.attrib['name'] = node.astext() matcher = NodeMatcher(addnodes.compact_paragraph, toctree=Any) - for node in tocdoc.findall(matcher): # type: addnodes.compact_paragraph + for node in tocdoc.findall(matcher): write_toc(node, chapters) # Index @@ -115,17 +113,17 @@ def write_index(title: str, refs: list[Any], subitems: Any) -> None: if subitems: parent_title = re.sub(r'\s*\(.*\)\s*$', '', title) for subitem in subitems: - write_index("%s %s" % (parent_title, subitem[0]), + write_index(f'{parent_title} {subitem[0]}', subitem[1], []) - for (key, group) in index: - for title, (refs, subitems, key) in group: + for (_group_key, group) in index: + for title, (refs, subitems, _category_key) in group: write_index(title, refs, subitems) # Dump the XML file xmlfile = path.join(outdir, outname + '.devhelp.gz') - with gzip.open(xmlfile, 'w') as f: - tree.write(f, 'utf-8') # type: ignore + with gzip.GzipFile(filename=xmlfile, mode='w', mtime=0) as f: + tree.write(f, 'utf-8') def setup(app: Sphinx) -> dict[str, Any]: diff --git a/tests/conftest.py b/tests/conftest.py index d4b08e5..3934d3f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,17 +1,14 @@ +from __future__ import annotations + from pathlib import Path import pytest -import sphinx - -pytest_plugins = 'sphinx.testing.fixtures' +pytest_plugins = ( + 'sphinx.testing.fixtures', +) @pytest.fixture(scope='session') -def rootdir(): - if sphinx.version_info[:2] < (7, 2): - from sphinx.testing.path import path - - return path(__file__).parent.abspath() / 'roots' - +def rootdir() -> Path: return Path(__file__).resolve().parent / 'roots' diff --git a/tests/test_devhelp.py b/tests/test_devhelp.py index 680f7ac..52452fd 100644 --- a/tests/test_devhelp.py +++ b/tests/test_devhelp.py @@ -1,8 +1,32 @@ """Test for devhelp extension.""" +from __future__ import annotations + +from time import sleep +from typing import TYPE_CHECKING + import pytest +if TYPE_CHECKING: + from sphinx.application import Sphinx + @pytest.mark.sphinx('devhelp', testroot='basic') -def test_basic(app, status, warning): +def test_basic(app: Sphinx) -> None: app.builder.build_all() + + +@pytest.mark.sphinx('devhelp', testroot='basic', freshenv=True) +def test_basic_deterministic_build(app: Sphinx) -> None: + app.config.devhelp_basename, output_filename = 'testing', 'testing.devhelp.gz' + + app.builder.build_all() + output_initial = (app.outdir / output_filename).read_bytes() + + sleep(2) + + app.builder.build_all() + output_repeat = (app.outdir / output_filename).read_bytes() + + msg = f"Content of '{output_filename}' differed between builds." + assert output_repeat == output_initial, msg diff --git a/tox.ini b/tox.ini index e54e565..bbd9ed7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ minversion = 2.4.0 envlist = py{39,310,311,312,313}, - flake8, + ruff, mypy isolated_build = True @@ -17,14 +17,14 @@ setenv = commands= pytest --durations 25 {posargs} -[testenv:flake8] +[testenv:ruff] description = Run style checks. extras = test lint commands= - flake8 + ruff check [testenv:mypy] description = @@ -33,4 +33,4 @@ extras = test lint commands= - mypy sphinxcontrib/ --explicit-package-bases + mypy