From dec53da04a2128649c57bafa1c6d8b79d274b52a Mon Sep 17 00:00:00 2001 From: Rambaud Pierrick <12rambau@users.noreply.github.com> Date: Sat, 28 Jan 2023 07:03:54 +0100 Subject: [PATCH] compute size automatically if not set (#26) * get is already defaulting to None * accept any type of parameters * compute size automatically * compute size automatically * fix mypy issues * fix the tests * fix mypy * remove test notebook * Fix typos * gitignore .vscode * Change size to sizes * Update existing test for computed "sizes" --------- Co-authored-by: tcmetzger <39711796+tcmetzger@users.noreply.github.com> --- .gitignore | 3 + docs/source/conf.py | 17 +-- noxfile.py | 7 +- sphinx_favicon/__init__.py | 127 +++++++++++++++--- tests/roots/test-href_and_static/conf.py | 4 +- tests/roots/test-list_of_three_dicts/conf.py | 6 +- .../conf.py | 22 --- .../conf.py | 18 +++ .../index.rst | 0 tests/roots/test-list_of_urls/conf.py | 4 +- tests/roots/test-single_dict/conf.py | 2 +- tests/test_options.py | 10 +- 12 files changed, 153 insertions(+), 67 deletions(-) delete mode 100644 tests/roots/test-list_of_three_dicts_automated_values/conf.py create mode 100644 tests/roots/test-list_of_three_icons_automated_values/conf.py rename tests/roots/{test-list_of_three_dicts_automated_values => test-list_of_three_icons_automated_values}/index.rst (100%) diff --git a/.gitignore b/.gitignore index b6e4761..7be1192 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# VSCode +.vscode/ diff --git a/docs/source/conf.py b/docs/source/conf.py index 457dd61..3492021 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -54,20 +54,21 @@ favicons = [ # generic icons compatible with most browsers { - # "rel": "icon", # automatically set to "icon" if ommitted - "size": "32x32", + # "rel": "icon", # automatically set to "icon" if omitted + # "sizes": "32x32", # automatically set if the file can be read # "type": "image/png", # autogenerated from file type "href": "favicon-32x32.png", }, - {"size": "16x16", "href": "favicon-16x16.png"}, - {"rel": "shortcut icon", "size": "any", "href": "favicon.ico"}, + "favicon-16x16.png", + {"rel": "shortcut icon", "sizes": "any", "href": "favicon.ico"}, # chrome specific - {"size": "192x192", "href": "android-chrome-192x192.png"}, + "android-chrome-192x192.png", # apple icons {"rel": "mask-icon", "color": "#2d89ef", "href": "safari-pinned-tab.svg"}, - {"rel": "apple-touch-icon", "size": "180x180", "href": "apple-touch-icon.png"}, + {"rel": "apple-touch-icon", "href": "apple-touch-icon.png"}, # msapplications {"name": "msapplication-TileColor", "content": "#2d89ef"}, - {"name": "theme-color", "size": "#ffffff"}, - {"href": "mstile-150x150.png"}, + {"name": "theme-color", "content": "#ffffff"}, + "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/mstile-150x150.png" + # to show it works as well with absolute urls ] diff --git a/noxfile.py b/noxfile.py index eeaf51b..8e1d324 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,7 +7,8 @@ def test(session): """Apply the tests on the lib.""" session.install(".[test]") - session.run("pytest", "--color=yes", "tests") + test_files = session.posargs or ["tests"] + session.run("pytest", "--color=yes", *test_files) @nox.session(name="mypy", reuse_venv=True) @@ -29,7 +30,9 @@ def mypy(session): def docs(session): """Build the docs.""" session.install(".[doc]") - session.run("sphinx-build", "-b=html", "docs/source", "docs/build/html") + session.run( + "sphinx-build", "-b=html", *session.posargs, "docs/source", "docs/build/html" + ) @nox.session(reuse_venv=True) diff --git a/sphinx_favicon/__init__.py b/sphinx_favicon/__init__.py index 4ac90c8..d45ca7c 100644 --- a/sphinx_favicon/__init__.py +++ b/sphinx_favicon/__init__.py @@ -7,10 +7,15 @@ The sphinx-favicon extension gives you more flexibility than the standard favicon.ico supported by Sphinx. It provides a quick and easy way to add the most important favicon formats for different browsers and devices. """ +from io import BytesIO +from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Union from urllib.parse import urlparse import docutils.nodes as nodes +import imagesize +import requests +from requests.exceptions import RequestException from sphinx.application import Sphinx from sphinx.util import logging @@ -18,11 +23,13 @@ FaviconsDef = Union[Dict[str, str], List[Dict[str, str]]] -OUTPUT_STATIC_DIR = "_static" -FILE_FIELD = "static-file" -"""field in the ``FaviconsDef`` pointing to file in the ``html_static_path``""" +OUTPUT_STATIC_DIR: str = "_static" +"output folder for static items in the html builder" -SUPPORTED_MIME_TYPES = { +FILE_FIELD: str = "static-file" +"field in the ``FaviconsDef`` pointing to file in the ``html_static_path``" + +SUPPORTED_MIME_TYPES: Dict[str, str] = { "bmp": "image/x-ms-bmp", "gif": "image/gif", "ico": "image/x-icon", @@ -31,6 +38,11 @@ "png": "image/png", "svg": "image/svg+xml", } +"supported mime types of the link tag" + + +SUPPORTED_SIZE_TYPES: List[str] = ["bmp", "gif", "jpeg", "jpg", "png"] +"list of file type that can be used to compute size" def generate_meta(favicon: Dict[str, str]) -> str: @@ -38,7 +50,7 @@ def generate_meta(favicon: Dict[str, str]) -> str: Default behavior: - If favicon data contains no ``rel`` attribute, sets ``rel="icon"`` - - If no ``size`` attribute is provided, ``size`` will be omitted + - If no ``sizes`` attribute is provided, ``sizes`` will be computed from the file - If no favicon MIME type is provided, the value for ``type`` will be based on the favicon's file name extension (for BMP, GIF, ICO, JPG, JPEG, SVG, or PNG files) @@ -52,29 +64,89 @@ def generate_meta(favicon: Dict[str, str]) -> str: # get the tag of the output tag = "meta" if "name" in favicon else "link" - # prepare all the tag parameters and leave them in the favicon dict - # default to "icon" for link elements if tag == "link": favicon.setdefault("rel", "icon") favicon["href"] # to raise an error if not set + extension = favicon["href"].split(".")[-1] # set the type for link elements. - # if type is not set try to guess it from the file extention + # if type is not set, try to guess it from the file extension type_ = favicon.get("type") - if not type_ and tag == "link": - extention = favicon["href"].split(".")[-1] - if extention in SUPPORTED_MIME_TYPES.keys(): - type_ = SUPPORTED_MIME_TYPES[extention] - if type_ is not None: + if not type_ and tag == "link" and extension in SUPPORTED_MIME_TYPES: + type_ = SUPPORTED_MIME_TYPES[extension] favicon["type"] = type_ # build the html element - parameters = [f'{k}="{v}"' for k, v in favicon.items()] + parameters = [f'{k}="{v}"' for k, v in favicon.items() if v is not None] html_element = f" <{tag} {' '.join(parameters)}>" + return html_element +def _sizes( + favicon: Dict[str, str], static_path: List[str], confdir: str +) -> Dict[str, str]: + """Compute the size of the favicon if its size is not explicitly defined. + + If the file is a SUPPORTED_MIME_TYPES, then the size is computed on the fly and added + to the favicon attributes. Don't do anything if the favicon is not a link tag. + + Args: + favicon: The favicon description as set in the conf.py file + static_path: The static_path registered in the application + confdir: The source directory of the documentation + + Returns: + The favicon with a fully qualified size + """ + # exit if the favicon tag has no href (like meta) + if not (FILE_FIELD in favicon or "href" in favicon): + return favicon + + # init the parameters + link: Optional[str] = favicon.get("href") or favicon.get(FILE_FIELD) + extension: Optional[str] = link.split(".")[-1] if link else None + sizes: Optional[str] = favicon.get("sizes") + + # get the size automatically if not supplied + if link and sizes is None and extension in SUPPORTED_SIZE_TYPES: + file: Optional[Union[BytesIO, Path]] = None + if bool(urlparse(link).netloc): + try: + response = requests.get(link) + except RequestException: + response = requests.Response() + response.status_code = -1 + + if response.status_code == 200: + file = BytesIO(response.content) + else: + logger.warning( + f"The provided link ({link}) cannot be read. " + "Size will not be computed." + ) + else: + for folder in static_path: + path = Path(confdir) / folder / link + if path.is_file(): + file = path + break + if file is None: + logger.warning( + f"The provided path ({link}) is not part of any of the static path. " + "Size will not be computed." + ) + + # compute the image size if image file is found + if file is not None: + w, h = imagesize.get(file) + size = f"{int(w)}x{int(h)}" + favicon["sizes"] = size + + return favicon + + def _static_to_href(pathto: Callable, favicon: Dict[str, str]) -> Dict[str, str]: """Replace static ref to fully qualified href. @@ -109,12 +181,16 @@ def _static_to_href(pathto: Callable, favicon: Dict[str, str]) -> Dict[str, str] return favicon -def create_favicons_meta(pathto: Callable, favicons: FaviconsDef) -> Optional[str]: +def create_favicons_meta( + pathto: Callable, favicons: FaviconsDef, static_path: List[str], confdir: str +) -> Optional[str]: """Create ```` elements for favicons defined in configuration. Args: pathto: Sphinx helper_ function to handle relative URLs favicons: Favicon data from configuration. Can be a single dict or a list of dicts. + static_path: the static_path registered in the application + confdir: the source directory of the documentation Returns: ```` elements for all favicons. @@ -138,7 +214,7 @@ def create_favicons_meta(pathto: Callable, favicons: FaviconsDef) -> Optional[st "Custom favicons will not be included in build." ) continue - + favicon = _sizes(favicon, static_path, confdir) tag = generate_meta(_static_to_href(pathto, favicon)) meta_favicons.append(tag) @@ -158,17 +234,24 @@ def html_page_context( app: The sphinx application pagename: the name of the page as string templatename: the name of the template as string - context: the html context dictionnary + context: the html context dictionary doctree: the docutils document tree """ - if doctree and app.config["favicons"]: - pathto: Callable = context["pathto"] # should exist in a HTML context - favicons_meta = create_favicons_meta(pathto, app.config["favicons"]) - context["metatags"] += favicons_meta + # extract parameters from app + favicons: Optional[Dict[str, str]] = app.config["favicons"] + pathto: Callable = context["pathto"] + static_path: List[str] = app.config["html_static_path"] + confdir: str = app.confdir + + if not (doctree and favicons): + return + + favicons_meta = create_favicons_meta(pathto, favicons, static_path, confdir) + context["metatags"] += favicons_meta def setup(app: Sphinx) -> Dict[str, Any]: - """Add custom configuration to shinx app. + """Add custom configuration to sphinx app. Args: app: the Sphinx application diff --git a/tests/roots/test-href_and_static/conf.py b/tests/roots/test-href_and_static/conf.py index cb632f6..6c6be52 100644 --- a/tests/roots/test-href_and_static/conf.py +++ b/tests/roots/test-href_and_static/conf.py @@ -10,11 +10,11 @@ { "sizes": "32x32", "static-file": "square.svg", - "href": "https://secure.example.com/favicon/favicon-32x32.png", + "href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-32x32.png", }, { "sizes": "128x128", "static-file": "nested/triangle.svg", - "href": "https://secure.example.com/favicon/apple-touch-icon-180x180.png", + "href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png", }, ] diff --git a/tests/roots/test-list_of_three_dicts/conf.py b/tests/roots/test-list_of_three_dicts/conf.py index e00358e..9dca4f0 100644 --- a/tests/roots/test-list_of_three_dicts/conf.py +++ b/tests/roots/test-list_of_three_dicts/conf.py @@ -9,19 +9,19 @@ { "rel": "icon", "sizes": "16x16", - "href": "https://secure.example.com/favicon/favicon-16x16.png", + "href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-16x16.png", "type": "image/png", }, { "rel": "icon", "sizes": "32x32", - "href": "https://secure.example.com/favicon/favicon-32x32.png", + "href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-32x32.png", "type": "image/png", }, { "rel": "apple-touch-icon", "sizes": "180x180", - "href": "https://secure.example.com/favicon/apple-touch-icon-180x180.png", + "href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png", "type": "image/png", }, ] diff --git a/tests/roots/test-list_of_three_dicts_automated_values/conf.py b/tests/roots/test-list_of_three_dicts_automated_values/conf.py deleted file mode 100644 index d1ac9fc..0000000 --- a/tests/roots/test-list_of_three_dicts_automated_values/conf.py +++ /dev/null @@ -1,22 +0,0 @@ -extensions = ["sphinx_favicon"] - -master_doc = "index" -exclude_patterns = ["_build"] - -html_theme = "basic" - -favicons = [ - { - "sizes": "16x16", - "href": "https://secure.example.com/favicon/favicon-16x16.png", - }, - { - "sizes": "32x32", - "href": "https://secure.example.com/favicon/favicon-32x32.png", - }, - { - "rel": "apple-touch-icon", - "sizes": "180x180", - "href": "https://secure.example.com/favicon/apple-touch-icon-180x180.png", - }, -] diff --git a/tests/roots/test-list_of_three_icons_automated_values/conf.py b/tests/roots/test-list_of_three_icons_automated_values/conf.py new file mode 100644 index 0000000..27a4605 --- /dev/null +++ b/tests/roots/test-list_of_three_icons_automated_values/conf.py @@ -0,0 +1,18 @@ +extensions = ["sphinx_favicon"] + +master_doc = "index" +exclude_patterns = ["_build"] + +html_theme = "basic" + +favicons = [ + "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-16x16.png", + { + "href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-32x32.png", + }, + { + "rel": "apple-touch-icon", + "sizes": "180x180", + "href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png", + }, +] diff --git a/tests/roots/test-list_of_three_dicts_automated_values/index.rst b/tests/roots/test-list_of_three_icons_automated_values/index.rst similarity index 100% rename from tests/roots/test-list_of_three_dicts_automated_values/index.rst rename to tests/roots/test-list_of_three_icons_automated_values/index.rst diff --git a/tests/roots/test-list_of_urls/conf.py b/tests/roots/test-list_of_urls/conf.py index 28e3530..dd60143 100644 --- a/tests/roots/test-list_of_urls/conf.py +++ b/tests/roots/test-list_of_urls/conf.py @@ -7,6 +7,6 @@ favicons = [ "https://secure.example.com/favicon/favicon-16x16.gif", - "https://secure.example.com/favicon/favicon-32x32.png", - "https://secure.example.com/favicon/apple-touch-icon-180x180.png", + "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-32x32.png", + "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png", ] diff --git a/tests/roots/test-single_dict/conf.py b/tests/roots/test-single_dict/conf.py index 132f957..1dc9cf6 100644 --- a/tests/roots/test-single_dict/conf.py +++ b/tests/roots/test-single_dict/conf.py @@ -8,5 +8,5 @@ favicons = { "rel": "apple-touch-icon", "sizes": "180x180", - "href": "https://secure.example.com/favicon/apple-touch-icon-180x180.png", + "href": "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png", } diff --git a/tests/test_options.py b/tests/test_options.py index 1d5aae9..846448c 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -27,14 +27,14 @@ def test_list_of_three_dicts(favicon_tags): assert favicon_tags[0]["rel"] == ["icon"] assert ( favicon_tags[0]["href"] - == "https://secure.example.com/favicon/favicon-16x16.png" + == "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-16x16.png" ) assert favicon_tags[0]["type"] == "image/png" assert favicon_tags[0]["sizes"] == "16x16" -@pytest.mark.sphinx("html", testroot="list_of_three_dicts_automated_values") -def test_list_of_three_dicts_automated_values(favicon_tags): +@pytest.mark.sphinx("html", testroot="list_of_three_icons_automated_values") +def test_list_of_three_icons_automated_values(favicon_tags): """Run tests on a list of 3 dicts with automated values. Args: @@ -54,7 +54,7 @@ def test_list_of_three_dicts_automated_values(favicon_tags): assert favicon_tags[0]["rel"] == ["icon"] assert ( favicon_tags[0]["href"] - == "https://secure.example.com/favicon/favicon-16x16.png" + == "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/favicon-16x16.png" ) assert favicon_tags[0]["type"] == "image/png" assert favicon_tags[0]["sizes"] == "16x16" @@ -74,7 +74,7 @@ def test_single_dict(favicon_tags): assert favicon_tags[0]["rel"] == ["apple-touch-icon"] assert ( favicon_tags[0]["href"] - == "https://secure.example.com/favicon/apple-touch-icon-180x180.png" + == "https://raw.githubusercontent.com/tcmetzger/sphinx-favicon/main/docs/source/_static/apple-touch-icon.png" ) assert favicon_tags[0]["type"] == "image/png" assert favicon_tags[0]["sizes"] == "180x180"