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"