From 4388f9eed001d180aba81327fcf90a086d682d6e Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 03:45:18 +0000 Subject: [PATCH 01/19] git mv setup.py pyproject.toml --- setup.py => pyproject.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename setup.py => pyproject.toml (100%) diff --git a/setup.py b/pyproject.toml similarity index 100% rename from setup.py rename to pyproject.toml From 173cfc8c747604647a348983f83cbc75b84dffe3 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 03:52:46 +0000 Subject: [PATCH 02/19] Use declarative metadata - Move to pyproject.toml metadata - Use pypa/build - Update workflows and tooling --- .github/workflows/workflow.yml | 4 +- LICENSE.md | 19 +++--- dev-requirements.txt | 3 +- pyproject.toml | 104 +++++++++++++++++++------------- sphinxext/opengraph/__init__.py | 2 + 5 files changed, 78 insertions(+), 54 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 76dac05..6d1b586 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -47,7 +47,7 @@ jobs: set -xe python -VV python -m site - python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip python -m pip install -r dev-requirements.txt - name: Install package run: | @@ -92,7 +92,7 @@ jobs: run: | python -VV python -m site - python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip python -m pip install -r dev-requirements.txt python -m pip install "sphinx${{ matrix.sphinx-version }}" - name: Download sdist and wheel artifacts diff --git a/LICENSE.md b/LICENSE.md index ef1a952..9d6b64a 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -3,14 +3,15 @@ All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the FIRST nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +* Neither the name of the FIRST nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY FIRST AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED @@ -21,4 +22,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/dev-requirements.txt b/dev-requirements.txt index ab5efb8..dd6b67c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,4 @@ sphinx -wheel==0.43.0 +build>=1 pytest==7.4.4 beautifulsoup4==4.12.3 -setuptools==70.1.0 diff --git a/pyproject.toml b/pyproject.toml index be81e0d..18bd68f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,43 +1,65 @@ -import setuptools +[build-system] +requires = ["flit_core>=3.7"] +build-backend = "flit_core.buildapi" -with open("README.md", encoding="utf-8") as readme: - long_description = readme.read() +# project metadata +[project] +name = "sphinxext-opengraph" +description = "Sphinx Extension to enable OGP support" +readme = "README.md" +urls.Code = "https://github.com/wpilibsuite/sphinxext-opengraph/" +urls.Documentation = "https://sphinxext-opengraph.readthedocs.io/" +urls.Download = "https://pypi.org/project/sphinxext-opengraph/" +urls.Homepage = "https://github.com/wpilibsuite/sphinxext-opengraph/" +urls."Issue tracker" = "https://github.com/wpilibsuite/sphinxext-opengraph/issues" +license.text = "BSD-3-Clause" +requires-python = ">=3.8" -setuptools.setup( - name="sphinxext-opengraph", - use_scm_version=True, - setup_requires=["setuptools_scm"], - author="Itay Ziv", - author_email="itay220204@gmail.com", - description="Sphinx Extension to enable OGP support", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/wpilibsuite/sphinxext-opengraph", - license="LICENSE.md", - install_requires=["sphinx>=5.0"], - packages=["sphinxext/opengraph"], - include_package_data=True, - package_data={"sphinxext.opengraph": ["sphinxext/opengraph/_static/*"]}, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Plugins", - "Environment :: Web Environment", - "Framework :: Sphinx :: Extension", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python", - "Topic :: Documentation :: Sphinx", - "Topic :: Documentation", - "Topic :: Software Development :: Documentation", - "Topic :: Text Processing", - "Topic :: Utilities", - ], - python_requires=">=3.8", -) +# Classifiers list: https://pypi.org/classifiers/ +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Plugins", + "Environment :: Web Environment", + "Framework :: Sphinx :: Extension", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python", + "Topic :: Documentation :: Sphinx", + "Topic :: Documentation", + "Topic :: Software Development :: Documentation", + "Topic :: Text Processing", + "Topic :: Utilities", +] +dependencies = [ + "Sphinx>=5.0", +] +dynamic = ["version"] + +[[project.authors]] +name = "Itay Ziv" +email = "itay220204@gmail.com" + +[tool.flit.module] +name = "sphinxext.opengraph" + +[tool.flit.sdist] +include = [ + "LICENSE.md", + # Documentation + "docs/", + # Resources + "sphinxext/opengraph/_static/", + # Tests + "tests/", + "noxfile.py", +] +exclude = [ + "doc/_build", +] diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 65f29c5..161fc33 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -20,6 +20,8 @@ import os +__version__ = "0.9.1" +version_info = (0, 9, 1) DEFAULT_DESCRIPTION_LENGTH = 200 DEFAULT_DESCRIPTION_LENGTH_SOCIAL_CARDS = 160 From 6cf22f297a78f08f25038f2b0c266120c3c8e91e Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:08:24 +0000 Subject: [PATCH 03/19] Support READTHEDOCS_CANONICAL_URL --- sphinxext/opengraph/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 161fc33..5c7a805 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -1,5 +1,5 @@ from typing import Any, Dict -from urllib.parse import urljoin, urlparse, urlunparse +from urllib.parse import urljoin, urlparse, urlsplit, urlunparse from pathlib import Path import docutils.nodes as nodes @@ -86,12 +86,16 @@ def get_tags( # type tag tags["og:type"] = config["ogp_type"] - if os.getenv("READTHEDOCS") and not config["ogp_site_url"]: - # readthedocs uses html_baseurl for sphinx > 1.8 - parse_result = urlparse(config["html_baseurl"]) - - if config["html_baseurl"] is None: - raise OSError("ReadTheDocs did not provide a valid canonical URL!") + if not config["ogp_site_url"] and os.getenv("READTHEDOCS"): + if config["html_baseurl"] is not None: + # readthedocs uses ``html_baseurl`` for Sphinx > 1.8 + parse_result = urlsplit(config["html_baseurl"]) + else: + # readthedocs addons no longer configures ``html_baseurl`` + if rtd_canonical_url := os.getenv("READTHEDOCS_CANONICAL_URL"): + parse_result = urlsplit(rtd_canonical_url) + else: + raise OSError("ReadTheDocs did not provide a valid canonical URL!") # Grab root url from canonical url config["ogp_site_url"] = urlunparse( From 07ae701cf50dd5f09fe208a38b78c2e9cb7dfa42 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:09:08 +0000 Subject: [PATCH 04/19] Declare support for Python 3.13 --- .github/workflows/workflow.yml | 5 ++++- pyproject.toml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 6d1b586..9bc6b46 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -62,13 +62,14 @@ jobs: name: my-dist path: dist/* + test: needs: build-wheel runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ['pypy3.9', '3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['pypy3.9', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] sphinx-version: ['>=5,<6', '>=6a0,<7', '>=7,<8', '>=8,<9'] os: [windows-latest, macos-latest, ubuntu-latest] exclude: @@ -78,6 +79,8 @@ jobs: sphinx-version: '>=8,<9' - python-version: 'pypy3.9' sphinx-version: '>=8,<9' + - python-version: '3.13' + sphinx-version: '>=5,<6' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index 18bd68f..1eb901b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python", "Topic :: Documentation :: Sphinx", "Topic :: Documentation", From 4be7f53b6c3ef200f576e917e217792fa29166d5 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:09:46 +0000 Subject: [PATCH 05/19] Improve performance of ``get_title()`` --- sphinxext/opengraph/__init__.py | 3 +-- sphinxext/opengraph/titleparser.py | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 5c7a805..71341ea 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -74,8 +74,7 @@ def get_tags( desc_len = DEFAULT_DESCRIPTION_LENGTH # Get the title and parse any html in it - title = get_title(context["title"], skip_html_tags=False) - title_excluding_html = get_title(context["title"], skip_html_tags=True) + title, title_excluding_html = get_title(context["title"]) # Parse/walk doctree for metadata (tag/description) description = get_description(doctree, desc_len, [title, title_excluding_html]) diff --git a/sphinxext/opengraph/titleparser.py b/sphinxext/opengraph/titleparser.py index 9c2cce7..b433269 100644 --- a/sphinxext/opengraph/titleparser.py +++ b/sphinxext/opengraph/titleparser.py @@ -26,12 +26,9 @@ def handle_data(self, data) -> None: self.text_outside_tags += data -def get_title(title: str, skip_html_tags: bool = False): +def get_title(title: str): htp = HTMLTextParser() htp.feed(title) htp.close() - if skip_html_tags: - return htp.text_outside_tags - else: - return htp.text + return htp.text, htp.text_outside_tags From 3f99ead77e5af6635341d947dd2eda0b2e8e5712 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:12:31 +0000 Subject: [PATCH 06/19] Define a ``social_cards`` optional extra --- README.md | 8 ++++++++ pyproject.toml | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/README.md b/README.md index c01653c..bc1c12c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,12 @@ Sphinx extension to generate [Open Graph metadata](https://ogp.me/) for each pag python -m pip install sphinxext-opengraph ``` +The `matplotlib` package is required to generate social cards: + +```sh +python -m pip install sphinxext-opengraph[social_cards] +``` + ## Usage Just add `sphinxext.opengraph` to your extensions list in your `conf.py` @@ -19,6 +25,8 @@ extensions = [ "sphinxext.opengraph", ] ``` + + ## Options These values are placed in the `conf.py` of your Sphinx project. diff --git a/pyproject.toml b/pyproject.toml index 1eb901b..04d6a89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,11 @@ dependencies = [ ] dynamic = ["version"] +[project.optional-dependencies] +social_cards = [ + "matplotlib>=3", +] + [[project.authors]] name = "Itay Ziv" email = "itay220204@gmail.com" From d4007916bcff16ddfe3134e949740e710454ad3c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:15:32 +0000 Subject: [PATCH 07/19] Use pathlib in tests --- tests/conftest.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 57ac73c..ee4b25b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,25 @@ +from pathlib import Path + import pytest +import sphinx from bs4 import BeautifulSoup -from sphinx.testing.path import path - -from sphinx.application import Sphinx - -pytest_plugins = "sphinx.testing.fixtures" +pytest_plugins = ["sphinx.testing.fixtures"] @pytest.fixture(scope="session") def rootdir(): - return path(__file__).parent.abspath() / "roots" + if sphinx.version_info[:2] >= (7, 0): + return Path(__file__).parent.resolve() / "roots" + else: + from sphinx.testing.path import path + + return path(__file__).parent.abspath() / "roots" @pytest.fixture() def content(app): - app.build() + app.build(force_all=True) yield app From c161663955a6eed433e8da276b7119f554647051 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:18:33 +0000 Subject: [PATCH 08/19] Sort imports --- docs/script/generate_social_card_previews.py | 7 ++++--- noxfile.py | 3 ++- sphinxext/opengraph/__init__.py | 16 +++++++++------- sphinxext/opengraph/socialcards.py | 3 ++- tests/test_options.py | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/script/generate_social_card_previews.py b/docs/script/generate_social_card_previews.py index 9d141c0..b0d137d 100644 --- a/docs/script/generate_social_card_previews.py +++ b/docs/script/generate_social_card_previews.py @@ -6,14 +6,15 @@ # %load_ext autoreload # %autoreload 2 +import random from pathlib import Path from textwrap import dedent + from sphinxext.opengraph.socialcards import ( - render_social_card, - MAX_CHAR_PAGE_TITLE, MAX_CHAR_DESCRIPTION, + MAX_CHAR_PAGE_TITLE, + render_social_card, ) -import random here = Path(__file__).parent diff --git a/noxfile.py b/noxfile.py index f06b6d1..20102b4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -11,9 +11,10 @@ ref: https://nox.thea.codes/ """ -import nox from shlex import split +import nox + nox.options.reuse_existing_virtualenvs = True diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 71341ea..3dc1e17 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -1,13 +1,14 @@ +import os +from pathlib import Path from typing import Any, Dict from urllib.parse import urljoin, urlparse, urlsplit, urlunparse -from pathlib import Path import docutils.nodes as nodes from sphinx.application import Sphinx -from .descriptionparser import get_description -from .metaparser import get_meta_description -from .titleparser import get_title +from sphinxext.opengraph.descriptionparser import get_description +from sphinxext.opengraph.metaparser import get_meta_description +from sphinxext.opengraph.titleparser import get_title try: import matplotlib @@ -16,9 +17,10 @@ create_social_card = None DEFAULT_SOCIAL_CONFIG = {} else: - from .socialcards import create_social_card, DEFAULT_SOCIAL_CONFIG - -import os + from sphinxext.opengraph.socialcards import ( + DEFAULT_SOCIAL_CONFIG, + create_social_card, + ) __version__ = "0.9.1" version_info = (0, 9, 1) diff --git a/sphinxext/opengraph/socialcards.py b/sphinxext/opengraph/socialcards.py index abf5ba0..2a2983d 100644 --- a/sphinxext/opengraph/socialcards.py +++ b/sphinxext/opengraph/socialcards.py @@ -2,9 +2,10 @@ import hashlib from pathlib import Path + import matplotlib -from matplotlib import pyplot as plt import matplotlib.image as mpimg +from matplotlib import pyplot as plt from sphinx.util import logging matplotlib.use("agg") diff --git a/tests/test_options.py b/tests/test_options.py index 529d133..b9dc16d 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,6 +1,6 @@ +import conftest import pytest from sphinx.application import Sphinx -import conftest def get_tag(tags, tag_type, kind="property", prefix="og"): From 2cff2969797f8488749c7cf6a8fedd5556c749b6 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:21:26 +0000 Subject: [PATCH 09/19] Improve performance of ``get_description()`` --- sphinxext/opengraph/__init__.py | 2 +- sphinxext/opengraph/descriptionparser.py | 29 +++++++----------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 3dc1e17..de5b51a 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -79,7 +79,7 @@ def get_tags( title, title_excluding_html = get_title(context["title"]) # Parse/walk doctree for metadata (tag/description) - description = get_description(doctree, desc_len, [title, title_excluding_html]) + description = get_description(doctree, desc_len, {title, title_excluding_html}) # title tag tags["og:title"] = title diff --git a/sphinxext/opengraph/descriptionparser.py b/sphinxext/opengraph/descriptionparser.py index f8eea29..a98f1ef 100644 --- a/sphinxext/opengraph/descriptionparser.py +++ b/sphinxext/opengraph/descriptionparser.py @@ -1,5 +1,5 @@ import string -from typing import Iterable +from collections.abc import Set import docutils.nodes as nodes @@ -11,25 +11,11 @@ class DescriptionParser(nodes.NodeVisitor): def __init__( self, + document: nodes.document, + *, desc_len: int, - known_titles: Iterable[str] = None, - document: nodes.document = None, + known_titles: Set[str] = frozenset(), ): - # Hack to prevent requirement for the doctree to be passed in. - # It's only used by doctree.walk(...) to print debug messages. - if document is None: - - class document_cls: - class reporter: - @staticmethod - def debug(*args, **kwaargs): - pass - - document = document_cls() - - if known_titles == None: - known_titles = [] - super().__init__(document) self.description = "" self.desc_len = desc_len @@ -115,9 +101,10 @@ def dispatch_departure(self, node: nodes.Element) -> None: def get_description( doctree: nodes.document, description_length: int, - known_titles: Iterable[str] = None, - document: nodes.document = None, + known_titles: Set[str] = frozenset(), ): - mcv = DescriptionParser(description_length, known_titles, document) + mcv = DescriptionParser( + doctree, desc_len=description_length, known_titles=known_titles + ) doctree.walkabout(mcv) return mcv.description From e416bc5139968b70030a1d99425f04811e11cafc Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:25:42 +0000 Subject: [PATCH 10/19] Drop support for Python 3.8 --- .github/workflows/workflow.yml | 51 ++++++++++++++++++++++------------ pyproject.toml | 3 +- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 9bc6b46..2ede2e1 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,12 +1,10 @@ name: Test and Deploy on: pull_request: - branches: - - main push: env: - FORCE_COLOR: 1 + FORCE_COLOR: "1" jobs: check: @@ -20,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version: "3" cache: pip cache-dependency-path: .github/workflows/workflow.yml - name: Black @@ -37,7 +35,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" cache: pip cache-dependency-path: | .github/workflows/workflow.yml @@ -69,18 +67,35 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['pypy3.9', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] - sphinx-version: ['>=5,<6', '>=6a0,<7', '>=7,<8', '>=8,<9'] - os: [windows-latest, macos-latest, ubuntu-latest] + python-version: + - "pypy3.9" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + sphinx-version: + - ">=5,<6" + - ">=6a0,<7" + - ">=7,<8" + - ">=8,<9" + os: + - windows-latest + - macos-latest + - ubuntu-latest exclude: - - python-version: '3.8' - sphinx-version: '>=8,<9' - - python-version: '3.9' - sphinx-version: '>=8,<9' - - python-version: 'pypy3.9' - sphinx-version: '>=8,<9' - - python-version: '3.13' - sphinx-version: '>=5,<6' + - python-version: "3.9" + sphinx-version: ">=8,<9" + - python-version: "pypy3.9" + sphinx-version: ">=6,<7" + os: "macos-latest" + - python-version: "pypy3.9" + sphinx-version: ">=7,<8" + os: "macos-latest" + - python-version: "pypy3.9" + sphinx-version: ">=8,<9" + - python-version: "3.13" + sphinx-version: ">=5,<6" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -111,7 +126,7 @@ jobs: python -m pytest -vv - name: Install matplotlib run: | - python -m pip install matplotlib + python -m pip install --only-binary :all: matplotlib - name: Run tests with matplotlib for ${{ matrix.python-version }} run: | python -m pytest -vv @@ -125,7 +140,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version: "3" cache: pip cache-dependency-path: docs/requirements.txt - name: Install dependencies diff --git a/pyproject.toml b/pyproject.toml index 04d6a89..f7b481f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ urls.Download = "https://pypi.org/project/sphinxext-opengraph/" urls.Homepage = "https://github.com/wpilibsuite/sphinxext-opengraph/" urls."Issue tracker" = "https://github.com/wpilibsuite/sphinxext-opengraph/issues" license.text = "BSD-3-Clause" -requires-python = ">=3.8" +requires-python = ">=3.9" # Classifiers list: https://pypi.org/classifiers/ classifiers = [ @@ -25,7 +25,6 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From c75cafb23929ed82755956fa5db10e0f51cd12c8 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:26:56 +0000 Subject: [PATCH 11/19] Drop support for Sphinx 5 --- .github/workflows/workflow.yml | 5 +---- docs/requirements.txt | 8 ++++---- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 2ede2e1..0cad7fc 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -75,8 +75,7 @@ jobs: - "3.12" - "3.13" sphinx-version: - - ">=5,<6" - - ">=6a0,<7" + - ">=6,<7" - ">=7,<8" - ">=8,<9" os: @@ -94,8 +93,6 @@ jobs: os: "macos-latest" - python-version: "pypy3.9" sphinx-version: ">=8,<9" - - python-version: "3.13" - sphinx-version: ">=5,<6" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/docs/requirements.txt b/docs/requirements.txt index 83e6de9..3396011 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,6 @@ -myst-parser==0.18.1 -furo==2022.9.29 -sphinx==5.2.3 +myst-parser>=4 +furo>=2024 +sphinx~=8.1.0 sphinx-design ./ -matplotlib \ No newline at end of file +matplotlib diff --git a/pyproject.toml b/pyproject.toml index f7b481f..96f1440 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ classifiers = [ "Topic :: Utilities", ] dependencies = [ - "Sphinx>=5.0", + "Sphinx>=6.0", ] dynamic = ["version"] From 4328637c0f0f16252deaae9e204e43b275b0cec7 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:33:56 +0000 Subject: [PATCH 12/19] Adopt Ruff --- .github/workflows/lint.yml | 38 +++++++++++++++++ .github/workflows/workflow.yml | 19 --------- .ruff.toml | 42 +++++++++++++++++++ README.md | 2 +- docs/script/generate_social_card_previews.py | 2 + docs/source/conf.py | 3 ++ noxfile.py | 2 + sphinxext/opengraph/__init__.py | 2 + sphinxext/opengraph/descriptionparser.py | 2 + sphinxext/opengraph/metaparser.py | 2 + sphinxext/opengraph/socialcards.py | 2 + sphinxext/opengraph/titleparser.py | 2 + tests/conftest.py | 2 + tests/roots/test-arbitrary-tags/conf.py | 2 + tests/roots/test-custom-tags/conf.py | 2 + tests/roots/test-description-length/conf.py | 2 + tests/roots/test-double-spacing/conf.py | 2 + tests/roots/test-first-image-no-image/conf.py | 2 + tests/roots/test-first-image/conf.py | 2 + tests/roots/test-image-rel-paths/conf.py | 2 + tests/roots/test-image/conf.py | 2 + tests/roots/test-list/conf.py | 2 + tests/roots/test-local-image/conf.py | 2 + .../conf.py | 2 + .../conf.py | 2 + .../roots/test-meta-name-description/conf.py | 2 + tests/roots/test-nested-lists/conf.py | 2 + tests/roots/test-overrides-complex/conf.py | 2 + tests/roots/test-overrides-disable/conf.py | 2 + tests/roots/test-overrides-simple/conf.py | 2 + tests/roots/test-quotation-marks/conf.py | 2 + tests/roots/test-rtd-default/conf.py | 2 + tests/roots/test-rtd-invalid/conf.py | 2 + tests/roots/test-simple/conf.py | 2 + .../roots/test-sitename-from-project/conf.py | 2 + tests/roots/test-sitename/conf.py | 2 + tests/roots/test-skip-admonitions/conf.py | 2 + tests/roots/test-skip-code-block/conf.py | 2 + tests/roots/test-skip-comments/conf.py | 2 + tests/roots/test-skip-raw/conf.py | 2 + tests/roots/test-skip-title/conf.py | 2 + tests/roots/test-social-cards-svg/conf.py | 2 + tests/roots/test-type/conf.py | 2 + tests/test_options.py | 2 + 44 files changed, 162 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/lint.yml create mode 100644 .ruff.toml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..5523d9d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,38 @@ +name: Lint source code + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + FORCE_COLOR: "1" + UV_SYSTEM_PYTHON: "1" # make uv do global installs + +jobs: + ruff: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install Ruff + uses: astral-sh/ruff-action@v3 + with: + args: --version + version: 0.9.2 + + - name: Lint with Ruff + run: ruff check --output-format=github + + - name: Format with Ruff + run: ruff format --diff diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 0cad7fc..2fb3430 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -7,25 +7,6 @@ env: FORCE_COLOR: "1" jobs: - check: - runs-on: ubuntu-latest - - # We want to run on external PRs, but not on our own internal PRs as they'll be run - # by the push to the branch. - if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3" - cache: pip - cache-dependency-path: .github/workflows/workflow.yml - - name: Black - run: | - pip install black - black --check --exclude /docs --diff . - build-wheel: runs-on: ubuntu-latest steps: diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..5c19f85 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,42 @@ +target-version = "py39" # Pin Ruff to Python 3.9 +line-length = 88 +output-format = "full" + +[format] +quote-style = "double" +docstring-code-format = true + +[lint] +select = [ +# "C4", # flake8-comprehensions +# "B", # flake8-bugbear + "E", # pycodestyle +# "F", # pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "FURB", # refurb +# "G", # flake8-logging-format + "I", # isort + "LOG", # flake8-logging + "PERF", # perflint +# "PGH", # pygrep-hooks +# "PT", # flake8-pytest-style +# "TC", # flake8-type-checking +# "UP", # pyupgrade + "W", # pycodestyle +] +ignore = [ + "E501", # Ignore line length errors (we use auto-formatting) +] + +[lint.flake8-type-checking] +exempt-modules = [] +strict = true + +[lint.isort] +forced-separate = [ + "tests", +] +required-imports = [ + "from __future__ import annotations", +] diff --git a/README.md b/README.md index bc1c12c..be2b98a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # sphinxext-opengraph [![Build](https://github.com/wpilibsuite/sphinxext-opengraph/workflows/Test%20and%20Deploy/badge.svg)](https://github.com/wpilibsuite/sphinxext-opengraph/actions) -[![Code style: Black](https://img.shields.io/badge/code%20style-Black-000000.svg)](https://github.com/psf/black) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json)](https://github.com/astral-sh/ruff) Sphinx extension to generate [Open Graph metadata](https://ogp.me/) for each page of your documentation. diff --git a/docs/script/generate_social_card_previews.py b/docs/script/generate_social_card_previews.py index b0d137d..494dfd0 100644 --- a/docs/script/generate_social_card_previews.py +++ b/docs/script/generate_social_card_previews.py @@ -6,6 +6,8 @@ # %load_ext autoreload # %autoreload 2 +from __future__ import annotations + import random from pathlib import Path from textwrap import dedent diff --git a/docs/source/conf.py b/docs/source/conf.py index eb612c8..c3144e7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,6 +10,9 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # + +from __future__ import annotations + import os import sys from subprocess import run diff --git a/noxfile.py b/noxfile.py index 20102b4..1e3b304 100644 --- a/noxfile.py +++ b/noxfile.py @@ -11,6 +11,8 @@ ref: https://nox.thea.codes/ """ +from __future__ import annotations + from shlex import split import nox diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index de5b51a..85fbcd4 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from pathlib import Path from typing import Any, Dict diff --git a/sphinxext/opengraph/descriptionparser.py b/sphinxext/opengraph/descriptionparser.py index a98f1ef..a211577 100644 --- a/sphinxext/opengraph/descriptionparser.py +++ b/sphinxext/opengraph/descriptionparser.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import string from collections.abc import Set diff --git a/sphinxext/opengraph/metaparser.py b/sphinxext/opengraph/metaparser.py index 77d06a4..86f7372 100644 --- a/sphinxext/opengraph/metaparser.py +++ b/sphinxext/opengraph/metaparser.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from html.parser import HTMLParser diff --git a/sphinxext/opengraph/socialcards.py b/sphinxext/opengraph/socialcards.py index 2a2983d..694316f 100644 --- a/sphinxext/opengraph/socialcards.py +++ b/sphinxext/opengraph/socialcards.py @@ -1,5 +1,7 @@ """Build a PNG card for each page meant for social media.""" +from __future__ import annotations + import hashlib from pathlib import Path diff --git a/sphinxext/opengraph/titleparser.py b/sphinxext/opengraph/titleparser.py index b433269..33fde4c 100644 --- a/sphinxext/opengraph/titleparser.py +++ b/sphinxext/opengraph/titleparser.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from html.parser import HTMLParser diff --git a/tests/conftest.py b/tests/conftest.py index ee4b25b..5b70325 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pathlib import Path import pytest diff --git a/tests/roots/test-arbitrary-tags/conf.py b/tests/roots/test-arbitrary-tags/conf.py index f7f742a..f9bfe69 100644 --- a/tests/roots/test-arbitrary-tags/conf.py +++ b/tests/roots/test-arbitrary-tags/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-custom-tags/conf.py b/tests/roots/test-custom-tags/conf.py index 1deded7..8a5faf6 100644 --- a/tests/roots/test-custom-tags/conf.py +++ b/tests/roots/test-custom-tags/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-description-length/conf.py b/tests/roots/test-description-length/conf.py index fabe335..88fe65f 100644 --- a/tests/roots/test-description-length/conf.py +++ b/tests/roots/test-description-length/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-double-spacing/conf.py b/tests/roots/test-double-spacing/conf.py index f7f742a..f9bfe69 100644 --- a/tests/roots/test-double-spacing/conf.py +++ b/tests/roots/test-double-spacing/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-first-image-no-image/conf.py b/tests/roots/test-first-image-no-image/conf.py index a492090..dcc8a2d 100644 --- a/tests/roots/test-first-image-no-image/conf.py +++ b/tests/roots/test-first-image-no-image/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-first-image/conf.py b/tests/roots/test-first-image/conf.py index a492090..dcc8a2d 100644 --- a/tests/roots/test-first-image/conf.py +++ b/tests/roots/test-first-image/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-image-rel-paths/conf.py b/tests/roots/test-image-rel-paths/conf.py index 88d2f22..461f3a1 100644 --- a/tests/roots/test-image-rel-paths/conf.py +++ b/tests/roots/test-image-rel-paths/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-image/conf.py b/tests/roots/test-image/conf.py index 8731182..d87074b 100644 --- a/tests/roots/test-image/conf.py +++ b/tests/roots/test-image/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-list/conf.py b/tests/roots/test-list/conf.py index f7f742a..f9bfe69 100644 --- a/tests/roots/test-list/conf.py +++ b/tests/roots/test-list/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-local-image/conf.py b/tests/roots/test-local-image/conf.py index c18a8d1..40c536d 100644 --- a/tests/roots/test-local-image/conf.py +++ b/tests/roots/test-local-image/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-meta-name-description-manual-description/conf.py b/tests/roots/test-meta-name-description-manual-description/conf.py index 8a6134e..27929d7 100644 --- a/tests/roots/test-meta-name-description-manual-description/conf.py +++ b/tests/roots/test-meta-name-description-manual-description/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-meta-name-description-manual-og-description/conf.py b/tests/roots/test-meta-name-description-manual-og-description/conf.py index 8a6134e..27929d7 100644 --- a/tests/roots/test-meta-name-description-manual-og-description/conf.py +++ b/tests/roots/test-meta-name-description-manual-og-description/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-meta-name-description/conf.py b/tests/roots/test-meta-name-description/conf.py index b31eaac..89ebeea 100644 --- a/tests/roots/test-meta-name-description/conf.py +++ b/tests/roots/test-meta-name-description/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-nested-lists/conf.py b/tests/roots/test-nested-lists/conf.py index f7f742a..f9bfe69 100644 --- a/tests/roots/test-nested-lists/conf.py +++ b/tests/roots/test-nested-lists/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-overrides-complex/conf.py b/tests/roots/test-overrides-complex/conf.py index e924f97..30ec7df 100644 --- a/tests/roots/test-overrides-complex/conf.py +++ b/tests/roots/test-overrides-complex/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-overrides-disable/conf.py b/tests/roots/test-overrides-disable/conf.py index 04b6d13..48df71c 100644 --- a/tests/roots/test-overrides-disable/conf.py +++ b/tests/roots/test-overrides-disable/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-overrides-simple/conf.py b/tests/roots/test-overrides-simple/conf.py index 04b6d13..48df71c 100644 --- a/tests/roots/test-overrides-simple/conf.py +++ b/tests/roots/test-overrides-simple/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-quotation-marks/conf.py b/tests/roots/test-quotation-marks/conf.py index 0087204..a0afe4f 100644 --- a/tests/roots/test-quotation-marks/conf.py +++ b/tests/roots/test-quotation-marks/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-rtd-default/conf.py b/tests/roots/test-rtd-default/conf.py index 7f99bb3..d51db6f 100644 --- a/tests/roots/test-rtd-default/conf.py +++ b/tests/roots/test-rtd-default/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-rtd-invalid/conf.py b/tests/roots/test-rtd-invalid/conf.py index 7f99bb3..d51db6f 100644 --- a/tests/roots/test-rtd-invalid/conf.py +++ b/tests/roots/test-rtd-invalid/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-simple/conf.py b/tests/roots/test-simple/conf.py index f7f742a..f9bfe69 100644 --- a/tests/roots/test-simple/conf.py +++ b/tests/roots/test-simple/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-sitename-from-project/conf.py b/tests/roots/test-sitename-from-project/conf.py index dc7805b..04c4bca 100644 --- a/tests/roots/test-sitename-from-project/conf.py +++ b/tests/roots/test-sitename-from-project/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] project = "Project name" diff --git a/tests/roots/test-sitename/conf.py b/tests/roots/test-sitename/conf.py index e3009f7..5827fc0 100644 --- a/tests/roots/test-sitename/conf.py +++ b/tests/roots/test-sitename/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-skip-admonitions/conf.py b/tests/roots/test-skip-admonitions/conf.py index f7f742a..f9bfe69 100644 --- a/tests/roots/test-skip-admonitions/conf.py +++ b/tests/roots/test-skip-admonitions/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-skip-code-block/conf.py b/tests/roots/test-skip-code-block/conf.py index 79ec2e8..c85dd57 100644 --- a/tests/roots/test-skip-code-block/conf.py +++ b/tests/roots/test-skip-code-block/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-skip-comments/conf.py b/tests/roots/test-skip-comments/conf.py index f7f742a..f9bfe69 100644 --- a/tests/roots/test-skip-comments/conf.py +++ b/tests/roots/test-skip-comments/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-skip-raw/conf.py b/tests/roots/test-skip-raw/conf.py index 79ec2e8..c85dd57 100644 --- a/tests/roots/test-skip-raw/conf.py +++ b/tests/roots/test-skip-raw/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-skip-title/conf.py b/tests/roots/test-skip-title/conf.py index f7f742a..f9bfe69 100644 --- a/tests/roots/test-skip-title/conf.py +++ b/tests/roots/test-skip-title/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-social-cards-svg/conf.py b/tests/roots/test-social-cards-svg/conf.py index 8582158..b59bec9 100644 --- a/tests/roots/test-social-cards-svg/conf.py +++ b/tests/roots/test-social-cards-svg/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/roots/test-type/conf.py b/tests/roots/test-type/conf.py index ecb20bd..cceccc0 100644 --- a/tests/roots/test-type/conf.py +++ b/tests/roots/test-type/conf.py @@ -1,3 +1,5 @@ +from __future__ import annotations + extensions = ["sphinxext.opengraph"] master_doc = "index" diff --git a/tests/test_options.py b/tests/test_options.py index b9dc16d..8d5fb9b 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import conftest import pytest from sphinx.application import Sphinx From 7f3c66a59bbb8fa641b00cfa10dc4ecc38691b79 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 04:40:52 +0000 Subject: [PATCH 13/19] Enable Ruff linters --- .ruff.toml | 16 ++++++------ docs/script/generate_social_card_previews.py | 18 +++++++------- sphinxext/opengraph/__init__.py | 26 +++++++++++--------- sphinxext/opengraph/descriptionparser.py | 5 +++- sphinxext/opengraph/socialcards.py | 6 ++--- tests/conftest.py | 10 ++++---- tests/test_options.py | 9 +++++-- 7 files changed, 50 insertions(+), 40 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index 5c19f85..966e04c 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -8,21 +8,21 @@ docstring-code-format = true [lint] select = [ -# "C4", # flake8-comprehensions -# "B", # flake8-bugbear + "C4", # flake8-comprehensions + "B", # flake8-bugbear "E", # pycodestyle -# "F", # pyflakes + "F", # pyflakes "FA", # flake8-future-annotations "FLY", # flynt "FURB", # refurb -# "G", # flake8-logging-format + "G", # flake8-logging-format "I", # isort "LOG", # flake8-logging "PERF", # perflint -# "PGH", # pygrep-hooks -# "PT", # flake8-pytest-style -# "TC", # flake8-type-checking -# "UP", # pyupgrade + "PGH", # pygrep-hooks + "PT", # flake8-pytest-style + "TC", # flake8-type-checking + "UP", # pyupgrade "W", # pycodestyle ] ignore = [ diff --git a/docs/script/generate_social_card_previews.py b/docs/script/generate_social_card_previews.py index 494dfd0..6ce3b78 100644 --- a/docs/script/generate_social_card_previews.py +++ b/docs/script/generate_social_card_previews.py @@ -18,17 +18,17 @@ render_social_card, ) -here = Path(__file__).parent +PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent # Dummy lorem text lorem = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum -""".split() # noqa +""".split() -kwargs_fig = dict( - image=here / "../source/_static/og-logo.png", - image_mini=here / "../../sphinxext/opengraph/_static/sphinx-logo-shadow.png", -) +kwargs_fig = { + "image": PROJECT_ROOT / "docs/source/_static/og-logo.png", + "image_mini": PROJECT_ROOT / "sphinxext/opengraph/_static/sphinx-logo-shadow.png", +} print("Generating previews of social media cards...") plt_objects = None @@ -43,7 +43,7 @@ desc = " ".join(lorem[:100]) desc = desc[: MAX_CHAR_DESCRIPTION - 3] + "..." - path_tmp = Path(here / "../tmp") + path_tmp = Path(PROJECT_ROOT / "docs/tmp") path_tmp.mkdir(exist_ok=True) path_out = Path(path_tmp / f"num_{perm}.png") @@ -57,7 +57,7 @@ kwargs_fig=kwargs_fig, ) - path_examples_page_folder = here / ".." + path_examples_page_folder = PROJECT_ROOT / "docs" embed_text.append( dedent( f""" @@ -79,6 +79,6 @@ """ # Write markdown text that we can use to embed these images in the docs -(here / "../tmp/embed.txt").write_text(embed_text) +(PROJECT_ROOT / "docs/tmp/embed.txt").write_text(embed_text) print("Done generating previews of social media cards...") diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 85fbcd4..45e3469 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -2,27 +2,29 @@ import os from pathlib import Path -from typing import Any, Dict +from typing import TYPE_CHECKING from urllib.parse import urljoin, urlparse, urlsplit, urlunparse import docutils.nodes as nodes -from sphinx.application import Sphinx from sphinxext.opengraph.descriptionparser import get_description from sphinxext.opengraph.metaparser import get_meta_description from sphinxext.opengraph.titleparser import get_title +if TYPE_CHECKING: + from typing import Any + + from sphinx.application import Sphinx + try: - import matplotlib -except ImportError: - print("matplotlib is not installed, social cards will not be generated") - create_social_card = None - DEFAULT_SOCIAL_CONFIG = {} -else: from sphinxext.opengraph.socialcards import ( DEFAULT_SOCIAL_CONFIG, create_social_card, ) +except ImportError: + print("matplotlib is not installed, social cards will not be generated") + create_social_card = None + DEFAULT_SOCIAL_CONFIG = {} __version__ = "0.9.1" version_info = (0, 9, 1) @@ -54,9 +56,9 @@ def make_tag(property: str, content: str, type_: str = "property") -> str: def get_tags( app: Sphinx, - context: Dict[str, Any], + context: dict[str, Any], doctree: nodes.document, - config: Dict[str, Any], + config: dict[str, Any], ) -> str: # Get field lists for per-page overrides fields = context["meta"] @@ -268,14 +270,14 @@ def html_page_context( app: Sphinx, pagename: str, templatename: str, - context: Dict[str, Any], + context: dict[str, Any], doctree: nodes.document, ) -> None: if doctree: context["metatags"] += get_tags(app, context, doctree, app.config) -def setup(app: Sphinx) -> Dict[str, Any]: +def setup(app: Sphinx) -> dict[str, Any]: # ogp_site_url="" allows relative by default, even though it's not # officially supported by OGP. app.add_config_value("ogp_site_url", "", "html") diff --git a/sphinxext/opengraph/descriptionparser.py b/sphinxext/opengraph/descriptionparser.py index a211577..a24d091 100644 --- a/sphinxext/opengraph/descriptionparser.py +++ b/sphinxext/opengraph/descriptionparser.py @@ -1,10 +1,13 @@ from __future__ import annotations import string -from collections.abc import Set +from typing import TYPE_CHECKING import docutils.nodes as nodes +if TYPE_CHECKING: + from collections.abc import Set + class DescriptionParser(nodes.NodeVisitor): """ diff --git a/sphinxext/opengraph/socialcards.py b/sphinxext/opengraph/socialcards.py index 694316f..b916b79 100644 --- a/sphinxext/opengraph/socialcards.py +++ b/sphinxext/opengraph/socialcards.py @@ -57,7 +57,7 @@ def create_social_card( """ # Add a hash to the image path based on metadata to bust caches - # ref: https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/troubleshooting-cards#refreshing_images # noqa + # ref: https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/troubleshooting-cards#refreshing_images hash = hashlib.sha1( (site_name + page_title + description + str(config_social)).encode() ).hexdigest()[:8] @@ -104,12 +104,12 @@ def create_social_card( # If image is an SVG replace it with None if impath.suffix.lower() == ".svg": - LOGGER.warning(f"[Social card] %s cannot be an SVG image, skipping...", img) + LOGGER.warning("[Social card] %s cannot be an SVG image, skipping...", img) kwargs_fig[img] = None # If image doesn't exist, throw a warning and replace with none if not impath.exists(): - LOGGER.warning(f"[Social card]: %s file doesn't exist, skipping...", img) + LOGGER.warning("[Social card]: %s file doesn't exist, skipping...", img) kwargs_fig[img] = None # These are passed directly from the user configuration to our plotting function diff --git a/tests/conftest.py b/tests/conftest.py index 5b70325..a50fee9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,10 +19,10 @@ def rootdir(): return path(__file__).parent.abspath() / "roots" -@pytest.fixture() +@pytest.fixture def content(app): app.build(force_all=True) - yield app + return app def _meta_tags(content, subdir=None): @@ -39,19 +39,19 @@ def _og_meta_tags(content): ] -@pytest.fixture() +@pytest.fixture def meta_tags(content): return _meta_tags(content) -@pytest.fixture() +@pytest.fixture def og_meta_tags(content): return [ tag for tag in _meta_tags(content) if tag.get("property", "").startswith("og:") ] -@pytest.fixture() +@pytest.fixture def og_meta_tags_sub(content): return [ tag diff --git a/tests/test_options.py b/tests/test_options.py index 8d5fb9b..e545c9e 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,8 +1,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import conftest import pytest -from sphinx.application import Sphinx +from sphinx.errors import ExtensionError + +if TYPE_CHECKING: + from sphinx.application import Sphinx def get_tag(tags, tag_type, kind="property", prefix="og"): @@ -315,7 +320,7 @@ def test_rtd_invalid(app: Sphinx, monkeypatch): monkeypatch.setenv("READTHEDOCS", "True") app.config.html_baseurl = None - with pytest.raises(Exception): + with pytest.raises(ExtensionError, match="did not provide a valid canonical URL"): app.build() From 26cfaf46f5cb7a9bdda453368e7ea568abdb17ef Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 05:06:48 +0000 Subject: [PATCH 14/19] Improve type annotations --- docs/script/generate_social_card_previews.py | 4 +- sphinxext/opengraph/__init__.py | 5 +- sphinxext/opengraph/descriptionparser.py | 12 +-- sphinxext/opengraph/metaparser.py | 4 +- sphinxext/opengraph/socialcards.py | 96 ++++++++++---------- sphinxext/opengraph/titleparser.py | 10 +- 6 files changed, 67 insertions(+), 64 deletions(-) diff --git a/docs/script/generate_social_card_previews.py b/docs/script/generate_social_card_previews.py index 6ce3b78..c75b7a2 100644 --- a/docs/script/generate_social_card_previews.py +++ b/docs/script/generate_social_card_previews.py @@ -15,6 +15,7 @@ from sphinxext.opengraph.socialcards import ( MAX_CHAR_DESCRIPTION, MAX_CHAR_PAGE_TITLE, + create_social_card_objects, render_social_card, ) @@ -31,7 +32,7 @@ } print("Generating previews of social media cards...") -plt_objects = None +plt_objects = create_social_card_objects(**kwargs_fig) embed_text = [] for perm in range(20): # Create dummy text description and pagetitle for this iteration @@ -54,7 +55,6 @@ description=desc, siteurl="sphinxext-opengraph.readthedocs.io", plt_objects=plt_objects, - kwargs_fig=kwargs_fig, ) path_examples_page_folder = PROJECT_ROOT / "docs" diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 45e3469..d4616cd 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -15,6 +15,7 @@ from typing import Any from sphinx.application import Sphinx + from sphinx.util.typing import ExtensionMetadata try: from sphinxext.opengraph.socialcards import ( @@ -277,7 +278,7 @@ def html_page_context( context["metatags"] += get_tags(app, context, doctree, app.config) -def setup(app: Sphinx) -> dict[str, Any]: +def setup(app: Sphinx) -> ExtensionMetadata: # ogp_site_url="" allows relative by default, even though it's not # officially supported by OGP. app.add_config_value("ogp_site_url", "", "html") @@ -295,6 +296,8 @@ def setup(app: Sphinx) -> dict[str, Any]: app.connect("html-page-context", html_page_context) return { + "version": __version__, + "env_version": 1, "parallel_read_safe": True, "parallel_write_safe": True, } diff --git a/sphinxext/opengraph/descriptionparser.py b/sphinxext/opengraph/descriptionparser.py index a24d091..4eadd14 100644 --- a/sphinxext/opengraph/descriptionparser.py +++ b/sphinxext/opengraph/descriptionparser.py @@ -20,7 +20,7 @@ def __init__( *, desc_len: int, known_titles: Set[str] = frozenset(), - ): + ) -> None: super().__init__(document) self.description = "" self.desc_len = desc_len @@ -36,12 +36,8 @@ def dispatch_visit(self, node: nodes.Element) -> None: if self.stop: raise nodes.StopTraversal - # Skip comments - if isinstance(node, nodes.Invisible): - raise nodes.SkipNode - - # Skip all admonitions - if isinstance(node, nodes.Admonition): + # Skip comments & all admonitions + if isinstance(node, (nodes.Admonition, nodes.Invisible)): raise nodes.SkipNode # Mark start of nested lists @@ -107,7 +103,7 @@ def get_description( doctree: nodes.document, description_length: int, known_titles: Set[str] = frozenset(), -): +) -> str: mcv = DescriptionParser( doctree, desc_len=description_length, known_titles=known_titles ) diff --git a/sphinxext/opengraph/metaparser.py b/sphinxext/opengraph/metaparser.py index 86f7372..84a1c67 100644 --- a/sphinxext/opengraph/metaparser.py +++ b/sphinxext/opengraph/metaparser.py @@ -8,11 +8,11 @@ class HTMLTextParser(HTMLParser): Parse HTML into text """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.meta_description = None - def handle_starttag(self, tag, attrs) -> None: + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: # For example: # attrs = [("content", "My manual description"), ("name", "description")] if ("name", "description") in attrs: diff --git a/sphinxext/opengraph/socialcards.py b/sphinxext/opengraph/socialcards.py index b916b79..651df0e 100644 --- a/sphinxext/opengraph/socialcards.py +++ b/sphinxext/opengraph/socialcards.py @@ -4,12 +4,23 @@ import hashlib from pathlib import Path +from typing import TYPE_CHECKING import matplotlib +import matplotlib.font_manager import matplotlib.image as mpimg from matplotlib import pyplot as plt from sphinx.util import logging +if TYPE_CHECKING: + from typing import TypeAlias + + from matplotlib.figure import Figure + from matplotlib.text import Text + from sphinx.application import Sphinx + + PltObjects: TypeAlias = tuple[Figure, Text, Text, Text, Text] + matplotlib.use("agg") LOGGER = logging.getLogger(__name__) @@ -38,17 +49,23 @@ # They must be defined here otherwise Sphinx errors when trying to pickle them. # They are dependent on the `multiple` variable defined when the figure is created. # Because they are depending on the figure size and renderer used to generate them. -def _set_page_title_line_width(): +def _set_page_title_line_width() -> int: return 825 -def _set_description_line_width(): +def _set_description_line_width() -> int: return 1000 def create_social_card( - app, config_social, site_name, page_title, description, url_text, page_path -): + app: Sphinx, + config_social: dict[str, bool | str], + site_name: str, + page_title: str, + description: str, + url_text: str, + page_path: str, +) -> Path: """Create a social preview card according to page metadata. This uses page metadata and calls a render function to generate the image. @@ -75,22 +92,20 @@ def create_social_card( # This is because we hash the values of the text + images in the social card. # If the hash doesn't change, it means the output should be the same. if path_image.exists(): - return + return path_images_relative / filename_image # These kwargs are used to generate the base figure image - kwargs_fig = {} + kwargs_fig: dict[str, str | Path | None] = {} # Large image to the top right - if config_social.get("image"): - kwargs_fig["image"] = Path(app.builder.srcdir) / config_social.get("image") + if cs_image := config_social.get("image"): + kwargs_fig["image"] = Path(app.builder.srcdir) / cs_image elif app.config.html_logo: kwargs_fig["image"] = Path(app.builder.srcdir) / app.config.html_logo # Mini image to the bottom right - if config_social.get("image_mini"): - kwargs_fig["image_mini"] = Path(app.builder.srcdir) / config_social.get( - "image_mini" - ) + if cs_image_mini := config_social.get("image_mini"): + kwargs_fig["image_mini"] = Path(app.builder.srcdir) / cs_image_mini else: kwargs_fig["image_mini"] = ( Path(__file__).parent / "_static/sphinx-logo-shadow.png" @@ -119,10 +134,12 @@ def create_social_card( kwargs_fig[config] = config_social.get(config) # Generate the image and store the matplotlib objects so that we can re-use them - if hasattr(app.env, "ogp_social_card_plt_objects"): + try: plt_objects = app.env.ogp_social_card_plt_objects - else: - plt_objects = None + except AttributeError: + # If objects is None it means this is the first time plotting. + # Create the figure objects and return them so that we re-use them later. + plt_objects = create_social_card_objects(**kwargs_fig) plt_objects = render_social_card( path_image, site_name, @@ -130,7 +147,6 @@ def create_social_card( description, url_text, plt_objects, - kwargs_fig, ) app.env.ogp_social_card_plt_objects = plt_objects @@ -140,27 +156,15 @@ def create_social_card( def render_social_card( - path, - site_title=None, - page_title=None, - description=None, - siteurl=None, - plt_objects=None, - kwargs_fig=None, -): + path: Path, + site_title: str, + page_title: str, + description: str, + siteurl: str, + plt_objects: PltObjects, +) -> PltObjects: """Render a social preview card with Matplotlib and write to disk.""" - # If objects is None it means this is the first time plotting. - # Create the figure objects and return them so that we re-use them later. - if plt_objects is None: - ( - fig, - txt_site_title, - txt_page_title, - txt_description, - txt_url, - ) = create_social_card_objects(**kwargs_fig) - else: - fig, txt_site_title, txt_page_title, txt_description, txt_url = plt_objects + fig, txt_site_title, txt_page_title, txt_description, txt_url = plt_objects # Update the matplotlib text objects with new text from this page txt_site_title.set_text(site_title) @@ -174,16 +178,16 @@ def render_social_card( def create_social_card_objects( - image=None, - image_mini=None, - page_title_color="#2f363d", - description_color="#585e63", - site_title_color="#585e63", - site_url_color="#2f363d", - background_color="white", - line_color="#5A626B", - font=None, -): + image: Path | None = None, + image_mini: Path | None = None, + page_title_color: str = "#2f363d", + description_color: str = "#585e63", + site_title_color: str = "#585e63", + site_url_color: str = "#2f363d", + background_color: str = "white", + line_color: str = "#5A626B", + font: str | None = None, +) -> PltObjects: """Create the Matplotlib objects for the first time.""" # If no font specified, load the Roboto Flex font as a fallback if font is None: diff --git a/sphinxext/opengraph/titleparser.py b/sphinxext/opengraph/titleparser.py index 33fde4c..7857cba 100644 --- a/sphinxext/opengraph/titleparser.py +++ b/sphinxext/opengraph/titleparser.py @@ -8,7 +8,7 @@ class HTMLTextParser(HTMLParser): Parse HTML into text """ - def __init__(self): + def __init__(self) -> None: super().__init__() # All text found self.text = "" @@ -16,19 +16,19 @@ def __init__(self): self.text_outside_tags = "" self.level = 0 - def handle_starttag(self, tag, attrs) -> None: + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: self.level += 1 - def handle_endtag(self, tag) -> None: + def handle_endtag(self, tag: str) -> None: self.level -= 1 - def handle_data(self, data) -> None: + def handle_data(self, data: str) -> None: self.text += data if self.level == 0: self.text_outside_tags += data -def get_title(title: str): +def get_title(title: str) -> tuple[str, str]: htp = HTMLTextParser() htp.feed(title) htp.close() From d78b19847235981d184865fb6041d9aa68c47ba4 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 05:19:10 +0000 Subject: [PATCH 15/19] Factor out ``social_card_for_page()`` --- sphinxext/opengraph/__init__.py | 99 ++++++++++++++++++++---------- sphinxext/opengraph/socialcards.py | 23 ++++--- 2 files changed, 79 insertions(+), 43 deletions(-) diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index d4616cd..f734903 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -15,6 +15,8 @@ from typing import Any from sphinx.application import Sphinx + from sphinx.config import Config + from sphinx.environment import BuildEnvironment from sphinx.util.typing import ExtensionMetadata try: @@ -165,34 +167,16 @@ def get_tags( and config_social.get("enable") is not False and create_social_card is not None ): - # Description - description_max_length = config_social.get( - "description_max_length", DEFAULT_DESCRIPTION_LENGTH_SOCIAL_CARDS - 3 - ) - if len(description) > description_max_length: - description = description[:description_max_length].strip() + "..." - - # Page title - pagetitle = title - if len(pagetitle) > DEFAULT_PAGE_LENGTH_SOCIAL_CARDS: - pagetitle = pagetitle[:DEFAULT_PAGE_LENGTH_SOCIAL_CARDS] + "..." - - # Site URL - site_url = config_social.get("site_url", True) - if site_url is True: - url_text = app.config.ogp_site_url.split("://")[-1] - elif isinstance(site_url, str): - url_text = site_url - - # Plot an image with the given metadata to the output path - image_path = create_social_card( - app, - config_social, - site_name, - pagetitle, - description, - url_text, - context["pagename"], + image_url = social_card_for_page( + config_social=config_social, + site_name=site_name, + title=title, + description=description, + pagename=context["pagename"], + srcdir=app.srcdir, + outdir=app.outdir, + config=app.config, + env=app.env, ) ogp_use_first_image = False @@ -202,12 +186,6 @@ def get_tags( else: ogp_image_alt = description - # Link the image in our page metadata - # We use os.path.sep to standardize behavior acros *nix and Windows - url = app.config.ogp_site_url.strip("/") - image_path = str(image_path).replace(os.path.sep, "/").strip("/") - image_url = f"{url}/{image_path}" - # If the social card objects have been added we add special metadata for them # These are the dimensions *in pixels* of the card # They were chosen by looking at the image pixel dimensions on disk @@ -267,6 +245,59 @@ def get_tags( ) +def social_card_for_page( + config_social: dict[str, bool | str], + site_name: str, + title: str, + description: str, + pagename: str, + *, + srcdir: str | Path, + outdir: str | Path, + config: Config, + env: BuildEnvironment, +): + # Description + description_max_length = config_social.get( + "description_max_length", DEFAULT_DESCRIPTION_LENGTH_SOCIAL_CARDS - 3 + ) + if len(description) > description_max_length: + description = description[:description_max_length].strip() + "..." + + # Page title + pagetitle = title + if len(pagetitle) > DEFAULT_PAGE_LENGTH_SOCIAL_CARDS: + pagetitle = pagetitle[:DEFAULT_PAGE_LENGTH_SOCIAL_CARDS] + "..." + + # Site URL + site_url = config_social.get("site_url", True) + if site_url is True: + url_text = config.ogp_site_url.split("://")[-1] + elif isinstance(site_url, str): + url_text = site_url + + # Plot an image with the given metadata to the output path + image_path = create_social_card( + config_social, + site_name, + pagetitle, + description, + url_text, + pagename, + srcdir=srcdir, + outdir=outdir, + config=config, + env=env, + ) + + # Link the image in our page metadata + # We use os.path.sep to standardize behavior acros *nix and Windows + url = config.ogp_site_url.strip("/") + image_path = str(image_path).replace(os.path.sep, "/").strip("/") + image_url = f"{url}/{image_path}" + return image_url + + def html_page_context( app: Sphinx, pagename: str, diff --git a/sphinxext/opengraph/socialcards.py b/sphinxext/opengraph/socialcards.py index 651df0e..d925d2f 100644 --- a/sphinxext/opengraph/socialcards.py +++ b/sphinxext/opengraph/socialcards.py @@ -17,7 +17,8 @@ from matplotlib.figure import Figure from matplotlib.text import Text - from sphinx.application import Sphinx + from sphinx.config import Config + from sphinx.environment import BuildEnvironment PltObjects: TypeAlias = tuple[Figure, Text, Text, Text, Text] @@ -58,13 +59,17 @@ def _set_description_line_width() -> int: def create_social_card( - app: Sphinx, config_social: dict[str, bool | str], site_name: str, page_title: str, description: str, url_text: str, page_path: str, + *, + srcdir: str | Path, + outdir: str | Path, + config: Config, + env: BuildEnvironment, ) -> Path: """Create a social preview card according to page metadata. @@ -84,7 +89,7 @@ def create_social_card( filename_image = f"summary_{page_path.replace('/', '_')}_{hash}.png" # Absolute path used to save the image - path_images_absolute = Path(app.builder.outdir) / path_images_relative + path_images_absolute = Path(outdir) / path_images_relative path_images_absolute.mkdir(exist_ok=True, parents=True) path_image = path_images_absolute / filename_image @@ -99,13 +104,13 @@ def create_social_card( # Large image to the top right if cs_image := config_social.get("image"): - kwargs_fig["image"] = Path(app.builder.srcdir) / cs_image - elif app.config.html_logo: - kwargs_fig["image"] = Path(app.builder.srcdir) / app.config.html_logo + kwargs_fig["image"] = Path(srcdir) / cs_image + elif config.html_logo: + kwargs_fig["image"] = Path(srcdir) / config.html_logo # Mini image to the bottom right if cs_image_mini := config_social.get("image_mini"): - kwargs_fig["image_mini"] = Path(app.builder.srcdir) / cs_image_mini + kwargs_fig["image_mini"] = Path(srcdir) / cs_image_mini else: kwargs_fig["image_mini"] = ( Path(__file__).parent / "_static/sphinx-logo-shadow.png" @@ -135,7 +140,7 @@ def create_social_card( # Generate the image and store the matplotlib objects so that we can re-use them try: - plt_objects = app.env.ogp_social_card_plt_objects + plt_objects = env.ogp_social_card_plt_objects except AttributeError: # If objects is None it means this is the first time plotting. # Create the figure objects and return them so that we re-use them later. @@ -148,7 +153,7 @@ def create_social_card( url_text, plt_objects, ) - app.env.ogp_social_card_plt_objects = plt_objects + env.ogp_social_card_plt_objects = plt_objects # Path relative to build folder will be what we use for linking the URL path_relative_to_build = path_images_relative / filename_image From 117abcbb498be8e93a219e5499efaf9cb752ba51 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 05:22:01 +0000 Subject: [PATCH 16/19] Fix config type in ``get_tags()`` --- sphinxext/opengraph/__init__.py | 40 ++++++++++++++++----------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index f734903..5e0b8f9 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -61,7 +61,7 @@ def get_tags( app: Sphinx, context: dict[str, Any], doctree: nodes.document, - config: dict[str, Any], + config: Config, ) -> str: # Get field lists for per-page overrides fields = context["meta"] @@ -77,7 +77,7 @@ def get_tags( # Set length of description try: desc_len = int( - fields.get("ogp_description_length", config["ogp_description_length"]) + fields.get("ogp_description_length", config.ogp_description_length) ) except ValueError: desc_len = DEFAULT_DESCRIPTION_LENGTH @@ -92,12 +92,12 @@ def get_tags( tags["og:title"] = title # type tag - tags["og:type"] = config["ogp_type"] + tags["og:type"] = config.ogp_type - if not config["ogp_site_url"] and os.getenv("READTHEDOCS"): - if config["html_baseurl"] is not None: + if not config.ogp_site_url and os.getenv("READTHEDOCS"): + if config.html_baseurl is not None: # readthedocs uses ``html_baseurl`` for Sphinx > 1.8 - parse_result = urlsplit(config["html_baseurl"]) + parse_result = urlsplit(config.html_baseurl) else: # readthedocs addons no longer configures ``html_baseurl`` if rtd_canonical_url := os.getenv("READTHEDOCS_CANONICAL_URL"): @@ -106,7 +106,7 @@ def get_tags( raise OSError("ReadTheDocs did not provide a valid canonical URL!") # Grab root url from canonical url - config["ogp_site_url"] = urlunparse( + config.ogp_site_url = urlunparse( ( parse_result.scheme, parse_result.netloc, @@ -120,18 +120,18 @@ def get_tags( # url tag # Get the URL of the specific page page_url = urljoin( - config["ogp_site_url"], app.builder.get_target_uri(context["pagename"]) + config.ogp_site_url, app.builder.get_target_uri(context["pagename"]) ) tags["og:url"] = page_url # site name tag, False disables, default to project if ogp_site_name not # set. - if config["ogp_site_name"] is False: + if config.ogp_site_name is False: site_name = None - elif config["ogp_site_name"] is None: - site_name = config["project"] + elif config.ogp_site_name is None: + site_name = config.project else: - site_name = config["ogp_site_name"] + site_name = config.ogp_site_name if site_name: tags["og:site_name"] = site_name @@ -139,7 +139,7 @@ def get_tags( if description: tags["og:description"] = description - if config["ogp_enable_meta_description"] and not get_meta_description( + if config.ogp_enable_meta_description and not get_meta_description( context["metatags"] ): meta_tags["description"] = description @@ -152,15 +152,15 @@ def get_tags( ogp_image_alt = fields.get("og:image:alt") fields.pop("og:image", None) else: - image_url = config["ogp_image"] - ogp_use_first_image = config["ogp_use_first_image"] - ogp_image_alt = fields.get("og:image:alt", config["ogp_image_alt"]) + image_url = config.ogp_image + ogp_use_first_image = config.ogp_use_first_image + ogp_image_alt = fields.get("og:image:alt", config.ogp_image_alt) # Decide whether to add social media card images for each page. # Only do this as a fallback if the user hasn't given any configuration # to add other images. config_social = DEFAULT_SOCIAL_CONFIG.copy() - social_card_user_options = app.config.ogp_social_cards or {} + social_card_user_options = config.ogp_social_cards or {} config_social.update(social_card_user_options) if ( not (image_url or ogp_use_first_image) @@ -175,7 +175,7 @@ def get_tags( pagename=context["pagename"], srcdir=app.srcdir, outdir=app.outdir, - config=app.config, + config=config, env=app.env, ) ogp_use_first_image = False @@ -219,7 +219,7 @@ def get_tags( else: # ogp_image is set # ogp_image is defined as being relative to the site root. # This workaround is to keep that functionality from breaking. - root = config["ogp_site_url"] + root = config.ogp_site_url image_url = urljoin(root, image_url_parsed.path) tags["og:image"] = image_url @@ -239,7 +239,7 @@ def get_tags( "\n".join( [make_tag(p, c) for p, c in tags.items()] + [make_tag(p, c, "name") for p, c in meta_tags.items()] - + config["ogp_custom_meta_tags"] + + config.ogp_custom_meta_tags ) + "\n" ) From 43d177cab0e828f3dde525a4b118ca4dcf88729a Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 05:24:07 +0000 Subject: [PATCH 17/19] Avoid passing the ``app`` object around --- sphinxext/opengraph/__init__.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 5e0b8f9..8f3537d 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -15,6 +15,7 @@ from typing import Any from sphinx.application import Sphinx + from sphinx.builders import Builder from sphinx.config import Config from sphinx.environment import BuildEnvironment from sphinx.util.typing import ExtensionMetadata @@ -58,10 +59,14 @@ def make_tag(property: str, content: str, type_: str = "property") -> str: def get_tags( - app: Sphinx, context: dict[str, Any], doctree: nodes.document, + *, + srcdir: str | Path, + outdir: str | Path, config: Config, + builder: Builder, + env: BuildEnvironment, ) -> str: # Get field lists for per-page overrides fields = context["meta"] @@ -119,9 +124,7 @@ def get_tags( # url tag # Get the URL of the specific page - page_url = urljoin( - config.ogp_site_url, app.builder.get_target_uri(context["pagename"]) - ) + page_url = urljoin(config.ogp_site_url, builder.get_target_uri(context["pagename"])) tags["og:url"] = page_url # site name tag, False disables, default to project if ogp_site_name not @@ -173,10 +176,10 @@ def get_tags( title=title, description=description, pagename=context["pagename"], - srcdir=app.srcdir, - outdir=app.outdir, + srcdir=srcdir, + outdir=outdir, config=config, - env=app.env, + env=env, ) ogp_use_first_image = False @@ -306,7 +309,15 @@ def html_page_context( doctree: nodes.document, ) -> None: if doctree: - context["metatags"] += get_tags(app, context, doctree, app.config) + context["metatags"] += get_tags( + context, + doctree, + srcdir=app.srcdir, + outdir=app.outdir, + config=app.config, + builder=app.builder, + env=app.env, + ) def setup(app: Sphinx) -> ExtensionMetadata: From a5e22d3a9a955b178d644c0f8b5354814c4ec6e5 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:22:38 +0000 Subject: [PATCH 18/19] Factor out ``read_the_docs_site_url()`` --- sphinxext/opengraph/__init__.py | 41 +++++++++++++++------------------ 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index 8f3537d..b21c5da 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -3,7 +3,7 @@ import os from pathlib import Path from typing import TYPE_CHECKING -from urllib.parse import urljoin, urlparse, urlsplit, urlunparse +from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit import docutils.nodes as nodes @@ -100,27 +100,7 @@ def get_tags( tags["og:type"] = config.ogp_type if not config.ogp_site_url and os.getenv("READTHEDOCS"): - if config.html_baseurl is not None: - # readthedocs uses ``html_baseurl`` for Sphinx > 1.8 - parse_result = urlsplit(config.html_baseurl) - else: - # readthedocs addons no longer configures ``html_baseurl`` - if rtd_canonical_url := os.getenv("READTHEDOCS_CANONICAL_URL"): - parse_result = urlsplit(rtd_canonical_url) - else: - raise OSError("ReadTheDocs did not provide a valid canonical URL!") - - # Grab root url from canonical url - config.ogp_site_url = urlunparse( - ( - parse_result.scheme, - parse_result.netloc, - parse_result.path, - "", - "", - "", - ) - ) + config.ogp_site_url = read_the_docs_site_url(config.html_baseurl) # url tag # Get the URL of the specific page @@ -248,6 +228,23 @@ def get_tags( ) +def read_the_docs_site_url(html_baseurl: str | None) -> str: + # readthedocs addons sets the READTHEDOCS_CANONICAL_URL variable, + # or defines the ``html_baseurl`` variable in conf.py + if rtd_canonical_url := os.getenv("READTHEDOCS_CANONICAL_URL"): + parse_result = urlsplit(rtd_canonical_url) + elif html_baseurl is not None: + parse_result = urlsplit(html_baseurl) + else: + msg = "ReadTheDocs did not provide a valid canonical URL!" + raise RuntimeError(msg) + + # Grab root url from canonical url + return urlunsplit( + (parse_result.scheme, parse_result.netloc, parse_result.path, "", "") + ) + + def social_card_for_page( config_social: dict[str, bool | str], site_name: str, From 8c7a7cfa238684b3babd1d506d48177c067aabb7 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 31 Jan 2025 00:37:59 +0000 Subject: [PATCH 19/19] Enable more linter categories in Ruff --- .ruff.toml | 55 +++++++++++++++++++- docs/script/generate_social_card_previews.py | 4 +- docs/source/conf.py | 18 +++---- noxfile.py | 12 ++--- sphinxext/opengraph/__init__.py | 9 ++-- sphinxext/opengraph/descriptionparser.py | 6 +-- sphinxext/opengraph/metaparser.py | 4 +- sphinxext/opengraph/socialcards.py | 19 +++---- sphinxext/opengraph/titleparser.py | 4 +- tests/conftest.py | 6 +-- tests/test_options.py | 7 ++- 11 files changed, 91 insertions(+), 53 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index 966e04c..67fdfd9 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -8,27 +8,75 @@ docstring-code-format = true [lint] select = [ - "C4", # flake8-comprehensions + "ANN", # flake8-annotations + "ASYNC", # flake8-async "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "D", # pydocstyle + "D212", # Multi-line docstring summary should start at the first line + "D417", # Missing argument description in the docstring for `{definition}`: `{name}` + "DTZ", # flake8-datetimez "E", # pycodestyle + "EM", # flake8-errmsg + "EXE", # flake8-executable "F", # pyflakes "FA", # flake8-future-annotations + "FIX", # flake8-fixme "FLY", # flynt "FURB", # refurb "G", # flake8-logging-format "I", # isort + "ICN", # flake8-import-conventions + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging + "N", # pep8-naming "PERF", # perflint "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PLC", # pylint + "PLE", # pylint + "PLW", # pylint "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T10", # flake8-debugger "TC", # flake8-type-checking + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle + "W", # pycodestyle + "YTT", # flake8-2020 ] ignore = [ + # pydocstyle + "D100", # Missing docstring in public module + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D107", # Missing docstring in `__init__` + "D400", # First line should end with a period + # pycodestyle "E501", # Ignore line length errors (we use auto-formatting) ] +[lint.per-file-ignores] +"tests/*" = [ + "ANN", # tests don't need annotations + "S101", # allow use of assert + "SLF001", # allow private member access +] + [lint.flake8-type-checking] exempt-modules = [] strict = true @@ -40,3 +88,8 @@ forced-separate = [ required-imports = [ "from __future__ import annotations", ] + +[lint.pydocstyle] +convention = "pep257" +ignore-decorators = ["typing.overload"] +ignore-var-parameters = true diff --git a/docs/script/generate_social_card_previews.py b/docs/script/generate_social_card_previews.py index c75b7a2..7d1cfaa 100644 --- a/docs/script/generate_social_card_previews.py +++ b/docs/script/generate_social_card_previews.py @@ -1,5 +1,5 @@ -""" -A helper script to test out what social previews look like. +"""A helper script to test out what social previews look like. + I should remove this when I'm happy with the result. """ diff --git a/docs/source/conf.py b/docs/source/conf.py index c3144e7..f92ab04 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,21 +4,15 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# - from __future__ import annotations -import os import sys +from pathlib import Path from subprocess import run -sys.path.insert(0, os.path.abspath("../..")) +# -- Path setup -------------------------------------------------------------- +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) # -- Project information ----------------------------------------------------- @@ -72,5 +66,7 @@ } # Generate sample social media preview images -path_script = os.path.abspath("../script/generate_social_card_previews.py") -run(f"python {path_script}", shell=True) +path_script = Path( + __file__, "..", "..", "script", "generate_social_card_previews.py" +).resolve() +run(("python", path_script), check=False) # NoQA: S603 diff --git a/noxfile.py b/noxfile.py index 1e3b304..c44f863 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,5 +1,5 @@ -""" -Configuration to automatically run jobs and tests via `nox`. +"""Configuration to automatically run jobs and tests via `nox`. + For example, to build the documentation with a live server: nox -s docs -- live @@ -21,7 +21,7 @@ @nox.session -def docs(session): +def docs(session: nox.Session) -> None: """Build the documentation. Use `-- live` to build with a live server.""" session.install("-r", "docs/requirements.txt") session.install("-e", ".") @@ -31,13 +31,13 @@ def docs(session): session.run(*split("sphinx-autobuild -b html docs/source docs/build/html")) else: session.run( - *split("sphinx-build -nW --keep-going -b html docs/source docs/build/html") + *split("sphinx-build -nW --keep-going -b html docs/source docs/build/html"), ) @nox.session -def test(session): +def test(session: nox.Session) -> None: """Run the test suite.""" session.install("-e", ".") session.install("-r", "dev-requirements.txt") - session.run(*(["pytest"] + session.posargs)) + session.run("pytest", *session.posargs) diff --git a/sphinxext/opengraph/__init__.py b/sphinxext/opengraph/__init__.py index b21c5da..5e84467 100644 --- a/sphinxext/opengraph/__init__.py +++ b/sphinxext/opengraph/__init__.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit -import docutils.nodes as nodes +from docutils import nodes from sphinxext.opengraph.descriptionparser import get_description from sphinxext.opengraph.metaparser import get_meta_description @@ -197,7 +197,7 @@ def get_tags( image_url_parsed = urlparse(image_url) if not image_url_parsed.scheme: # Relative image path detected, relative to the source. Make absolute. - if first_image: + if first_image: # NoQA: SIM108 root = page_url else: # ogp_image is set # ogp_image is defined as being relative to the site root. @@ -256,7 +256,7 @@ def social_card_for_page( outdir: str | Path, config: Config, env: BuildEnvironment, -): +) -> str: # Description description_max_length = config_social.get( "description_max_length", DEFAULT_DESCRIPTION_LENGTH_SOCIAL_CARDS - 3 @@ -294,8 +294,7 @@ def social_card_for_page( # We use os.path.sep to standardize behavior acros *nix and Windows url = config.ogp_site_url.strip("/") image_path = str(image_path).replace(os.path.sep, "/").strip("/") - image_url = f"{url}/{image_path}" - return image_url + return f"{url}/{image_path}" def html_page_context( diff --git a/sphinxext/opengraph/descriptionparser.py b/sphinxext/opengraph/descriptionparser.py index 4eadd14..429f7cb 100644 --- a/sphinxext/opengraph/descriptionparser.py +++ b/sphinxext/opengraph/descriptionparser.py @@ -3,16 +3,14 @@ import string from typing import TYPE_CHECKING -import docutils.nodes as nodes +from docutils import nodes if TYPE_CHECKING: from collections.abc import Set class DescriptionParser(nodes.NodeVisitor): - """ - Finds the title and creates a description from a doctree - """ + """Finds the title and creates a description from a doctree.""" def __init__( self, diff --git a/sphinxext/opengraph/metaparser.py b/sphinxext/opengraph/metaparser.py index 84a1c67..85eed01 100644 --- a/sphinxext/opengraph/metaparser.py +++ b/sphinxext/opengraph/metaparser.py @@ -4,9 +4,7 @@ class HTMLTextParser(HTMLParser): - """ - Parse HTML into text - """ + """Parse HTML into text.""" def __init__(self) -> None: super().__init__() diff --git a/sphinxext/opengraph/socialcards.py b/sphinxext/opengraph/socialcards.py index d925d2f..65e88a3 100644 --- a/sphinxext/opengraph/socialcards.py +++ b/sphinxext/opengraph/socialcards.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import TYPE_CHECKING -import matplotlib +import matplotlib as mpl import matplotlib.font_manager import matplotlib.image as mpimg from matplotlib import pyplot as plt @@ -22,7 +22,7 @@ PltObjects: TypeAlias = tuple[Figure, Text, Text, Text, Text] -matplotlib.use("agg") +mpl.use("agg") LOGGER = logging.getLogger(__name__) HERE = Path(__file__).parent @@ -77,11 +77,11 @@ def create_social_card( It also passes configuration through to the rendering function. If Matplotlib objects are present in the `app` environment, it reuses them. """ - # Add a hash to the image path based on metadata to bust caches # ref: https://developer.twitter.com/en/docs/twitter-for-websites/cards/guides/troubleshooting-cards#refreshing_images hash = hashlib.sha1( - (site_name + page_title + description + str(config_social)).encode() + (site_name + page_title + description + str(config_social)).encode(), + usedforsecurity=False, ).hexdigest()[:8] # Define the file path we'll use for this image @@ -156,8 +156,7 @@ def create_social_card( env.ogp_social_card_plt_objects = plt_objects # Path relative to build folder will be what we use for linking the URL - path_relative_to_build = path_images_relative / filename_image - return path_relative_to_build + return path_images_relative / filename_image def render_social_card( @@ -236,9 +235,7 @@ def create_social_card_objects( left_margin, site_title_y_offset, "Test site title", - { - "size": 24, - }, + {"size": 24}, ha="left", va="top", wrap=True, @@ -260,7 +257,7 @@ def create_social_card_objects( c=page_title_color, ) - txt_page._get_wrap_line_width = _set_page_title_line_width + txt_page._get_wrap_line_width = _set_page_title_line_width # NoQA: SLF001 # description # Just below site title, smallest font and many lines. @@ -280,7 +277,7 @@ def create_social_card_objects( wrap=True, c=description_color, ) - txt_description._get_wrap_line_width = _set_description_line_width + txt_description._get_wrap_line_width = _set_description_line_width # NoQA: SLF001 # url # Aligned to the left of the mini image diff --git a/sphinxext/opengraph/titleparser.py b/sphinxext/opengraph/titleparser.py index 7857cba..a2bcd84 100644 --- a/sphinxext/opengraph/titleparser.py +++ b/sphinxext/opengraph/titleparser.py @@ -4,9 +4,7 @@ class HTMLTextParser(HTMLParser): - """ - Parse HTML into text - """ + """Parse HTML into text.""" def __init__(self) -> None: super().__init__() diff --git a/tests/conftest.py b/tests/conftest.py index a50fee9..a5966d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,13 +11,13 @@ @pytest.fixture(scope="session") def rootdir(): - if sphinx.version_info[:2] >= (7, 0): - return Path(__file__).parent.resolve() / "roots" - else: + if sphinx.version_info[:2] < (7, 2): from sphinx.testing.path import path return path(__file__).parent.abspath() / "roots" + return Path(__file__).parent.resolve() / "roots" + @pytest.fixture def content(app): diff --git a/tests/test_options.py b/tests/test_options.py index e545c9e..87fa8bb 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -11,7 +11,7 @@ def get_tag(tags, tag_type, kind="property", prefix="og"): - return [tag for tag in tags if tag.get(kind) == f"{prefix}:{tag_type}"][0] + return next(tag for tag in tags if tag.get(kind) == f"{prefix}:{tag_type}") def get_tag_content(tags, tag_type, kind="property", prefix="og"): @@ -20,9 +20,8 @@ def get_tag_content(tags, tag_type, kind="property", prefix="og"): def get_meta_description(tags): - return [tag for tag in tags if tag.get("name") == "description"][0].get( - "content", "" - ) + tag = next(tag for tag in tags if tag.get("name") == "description") + return tag.get("content", "") @pytest.mark.sphinx("html", testroot="simple")