Repository: turettn/fastapi_offline (language: python)

Issue:
favicon.png is not hosted offline
When using `fastapi-offline`, I noticed that the favicon for the page is still trying to be pulled from the internet at https://fastapi.tiangolo.com/img/favicon.png.

I believe that by default `fastapi-offline` should bundle this favicon into the wheel and serve it statically with the swagger-ui javascript. Additionally, it would be really nice for used if there was a very simple way to pass a custom favicon into `FastAPIOffline`.

Perhaps something like:

```py
def FastAPIOffline(..., favicon: Optional[Path] = None)
```

Some discussion about how to do this is here: https://github.com/tiangolo/fastapi/issues/2581


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc0b300..aee7012 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,3 +7,8 @@ All versions grab the newest version of the external dependencies (Swagger, ReDo
 
 ## 1.1.0 - 2021-04-27
 - Respects `root_path` to allow for sub-applications
+
+# 1.2.0 - 2021-05-16
+- Bundle default shortcut link
+- Allow configurable shortcut link
+- Redoc: Use local fonts instead of google fonts
diff --git a/README.md b/README.md
index f5cd640..fc27815 100644
--- a/README.md
+++ b/README.md
@@ -46,9 +46,9 @@ async def root():
     return {"message": "Hello World"}
 ```
 
-Any options passed to `FastAPIOffline()` except `docs_url` and `redoc_url` are passed through to `FastAPI()`.
+Any options passed to `FastAPIOffline()` except `docs_url`, `redoc_url`, and `favicon_url` are passed through to `FastAPI()`.
 
-## Disabling Docs Page
+# Disabling Docs Page
 
 If you wish to [disable the docs page](https://fastapi.tiangolo.com/tutorial/metadata/#openapi-url), for 
 example in a sub-application, revert to using the default `FastAPI()` class.
@@ -56,9 +56,19 @@ example in a sub-application, revert to using the default `FastAPI()` class.
 ```py
 app = FastAPI(openapi_url=None)
 ```
+# Using a custom shortcut icon
+
+By default, the FastAPI `favicon.png` is included and used as the shortcut icon on the docs pages.  If you want to use a different one, you can specify it with the `favicon_url` argument:
+
+```py
+app = FastAPIOffline(
+    favicon_url="http://my.cool.site/favicon.png"
+)
+```
 
 # Licensing
 
 * This code is released under the MIT license.
 * Parts of Swagger are included in this package.  The original license ([Apache 2.0](https://swagger.io/license/)) and copyright apply to those files.
 * Parts of Redoc are included in this package.  The original license ([MIT](https://github.com/Redocly/redoc/blob/master/LICENSE)) and copyright apply to those files.
+* The FastAPI `favicon.png` file is included in this package.  The original license ([MIT](https://github.com/Redocly/redoc/blob/master/LICENSE)) and copyright apply to that file.
\ No newline at end of file
diff --git a/fastapi_offline/consts.py b/fastapi_offline/consts.py
new file mode 100644
index 0000000..fff2765
--- /dev/null
+++ b/fastapi_offline/consts.py
@@ -0,0 +1,4 @@
+SWAGGER_JS = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"
+SWAGGER_CSS = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css"
+REDOC_JS = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
+FAVICON = "https://fastapi.tiangolo.com/img/favicon.png"
diff --git a/fastapi_offline/core.py b/fastapi_offline/core.py
index 9db83a7..933f3f8 100644
--- a/fastapi_offline/core.py
+++ b/fastapi_offline/core.py
@@ -21,12 +21,16 @@ def FastAPIOffline(*args: Any, **kwargs: Any) -> FastAPI:
     kwargs["docs_url"] = None
     kwargs["redoc_url"] = None
 
+    # Grab the user specified favicon url (if present)
+    favicon_url = kwargs.pop("favicon_url", None)
+
     # Create the FastAPI object
     app = FastAPI(*args, **kwargs)
 
     # This mostly just keeps mypy happy
     assert isinstance(app.openapi_url, str)
     assert isinstance(app.swagger_ui_oauth2_redirect_url, str)
+
     openapi_url = app.openapi_url
     swagger_ui_oauth2_redirect_url = app.swagger_ui_oauth2_redirect_url
 
@@ -41,12 +45,19 @@ def FastAPIOffline(*args: Any, **kwargs: Any) -> FastAPI:
     @app.get("/docs", include_in_schema=False)
     async def custom_swagger_ui_html(request: Request) -> "Response":
         root = request.scope.get("root_path")
+
+        if favicon_url is None:
+            favicon = f"{root}/static-offline-docs/favicon.png"
+        else:
+            favicon = favicon_url
+
         return get_swagger_ui_html(
             openapi_url=f"{root}{openapi_url}",
             title=app.title + " - Swagger UI",
             oauth2_redirect_url=swagger_ui_oauth2_redirect_url,
             swagger_js_url=f"{root}/static-offline-docs/swagger-ui-bundle.js",
             swagger_css_url=f"{root}/static-offline-docs/swagger-ui.css",
+            swagger_favicon_url=favicon,
         )
 
     @app.get(swagger_ui_oauth2_redirect_url, include_in_schema=False)
@@ -56,11 +67,18 @@ async def swagger_ui_redirect() -> "Response":
     @app.get("/redoc", include_in_schema=False)
     async def redoc_html(request: Request) -> "Response":
         root = request.scope.get("root_path")
+
+        if favicon_url is None:
+            favicon = f"{root}/static-offline-docs/favicon.png"
+        else:
+            favicon = favicon_url
+
         return get_redoc_html(
             openapi_url=f"{root}{openapi_url}",
             title=app.title + " - ReDoc",
             redoc_js_url=f"{root}/static-offline-docs/redoc.standalone.js",
             with_google_fonts=False,
+            redoc_favicon_url=favicon,
         )
 
     # Return the FastAPI object
diff --git a/setup.py b/setup.py
index 79d6de8..d8cce78 100644
--- a/setup.py
+++ b/setup.py
@@ -3,29 +3,31 @@
 from setuptools import setup
 from setuptools.command.sdist import sdist
 
-__version__ = "1.1.0"
+
+__version__ = "1.2.0"
 
 
 BASE_PATH = Path(__file__).parent
 README = (BASE_PATH / "README.md").read_text()
 
-DOWNLOADS = (
-    "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js",
-    "https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css",
-    "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js",
-)
-
 
 class SDistWrapper(sdist):
     def run(self) -> None:
         "Download files into static/, then pass through to normal install"
+        from fastapi_offline.consts import SWAGGER_JS, SWAGGER_CSS, REDOC_JS, FAVICON
+
         # Find ourself
 
         static_path = BASE_PATH / "fastapi_offline" / "static"
         static_path.mkdir(exist_ok=True)
 
         # Download files
-        for download in DOWNLOADS:
+        for download in (
+            SWAGGER_JS,
+            SWAGGER_CSS,
+            REDOC_JS,
+            FAVICON,
+        ):
             urlretrieve(download, static_path / download.split("/")[-1])
 
         sdist.run(self)
@@ -51,5 +53,6 @@ def run(self) -> None:
     python_requires=">=3.6",
     install_requires=["aiofiles", "fastapi"],
     tests_require=["pytest", "requests"],
+    setup_requires=["fastapi", "aiofiles"],
     cmdclass={"sdist": SDistWrapper},
 )


Test patch:
diff --git a/tests/test_consts.py b/tests/test_consts.py
new file mode 100644
index 0000000..96da0a9
--- /dev/null
+++ b/tests/test_consts.py
@@ -0,0 +1,19 @@
+"Check the default FastAPI package to confirm our consts"
+import inspect
+
+from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
+
+from fastapi_offline.consts import *
+
+
+def test_swagger():
+    params = inspect.signature(get_swagger_ui_html).parameters
+    assert params["swagger_js_url"].default == SWAGGER_JS
+    assert params["swagger_css_url"].default == SWAGGER_CSS
+    assert params["swagger_favicon_url"].default == FAVICON
+
+
+def test_redoc():
+    params = inspect.signature(get_redoc_html).parameters
+    assert params["redoc_js_url"].default == REDOC_JS
+    assert params["redoc_favicon_url"].default == FAVICON
diff --git a/tests/test_core.py b/tests/test_core.py
index 6c4462a..2fb2dd8 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -31,6 +31,11 @@ def test_read_docs():
 
 # Check the static pages
 def test_read_statics():
-    for page in ["swagger-ui-bundle.js", "swagger-ui.css", "redoc.standalone.js"]:
+    for page in [
+        "swagger-ui-bundle.js",
+        "swagger-ui.css",
+        "redoc.standalone.js",
+        "favicon.png",
+    ]:
         response = client.get("/static-offline-docs/" + page)
         assert response.status_code == 200
diff --git a/tests/test_favicon.py b/tests/test_favicon.py
new file mode 100644
index 0000000..8ccb3d7
--- /dev/null
+++ b/tests/test_favicon.py
@@ -0,0 +1,46 @@
+from fastapi.testclient import TestClient
+from fastapi_offline import FastAPIOffline
+
+# Create an application
+app1 = FastAPIOffline()
+client1 = TestClient(app1)
+
+FAVICON = "http://fake.site/favicon.png"
+app2 = FastAPIOffline(favicon_url=FAVICON)
+client2 = TestClient(app2)
+
+app2.mount("/app1", app1)
+
+app3 = FastAPIOffline(favicon_url=None)
+client3 = TestClient(app3)
+
+
+def test_normal_redoc():
+    resp = client1.get("/redoc")
+    assert "/static-offline-docs/favicon.png" in resp.text
+
+
+def test_custom_redoc():
+    resp = client2.get("/redoc")
+    assert FAVICON in resp.text
+
+
+def test_normal_swagger():
+    resp = client1.get("/docs")
+    assert "/static-offline-docs/favicon.png" in resp.text
+
+
+def test_custom_swagger():
+    resp = client2.get("/docs")
+    assert FAVICON in resp.text
+
+
+def test_normal_submounted_redoc():
+    resp = client2.get("/app1/redoc")
+    print(resp.text)
+    assert "/app1/static-offline-docs/favicon.png" in resp.text
+
+
+def test_normal_submounted_swagger():
+    resp = client2.get("/app1/docs")
+    assert "/app1/static-offline-docs/favicon.png" in resp.text