diff --git a/src/formelsammlung/flask_sphinx_docs.py b/src/formelsammlung/flask_sphinx_docs.py index 67a5c08..c01494f 100644 --- a/src/formelsammlung/flask_sphinx_docs.py +++ b/src/formelsammlung/flask_sphinx_docs.py @@ -8,6 +8,7 @@ :copyright: (c) Christian Riedel :license: GPLv3 """ +from pathlib import Path from typing import Optional from flask import Flask, Response @@ -18,20 +19,25 @@ class SphinxDocServer: # pylint: disable=R0903 .. highlight:: python - You can either include the plugin directly:: - - app = Flask(__name__) - SphinxDocServer(app, doc_dir="../../docs/build/html") - - or you can invoke it in your app factory:: + You can invoke it in your app factory:: sds = SphinxDocServer() def create_app(): app = Flask(__name__) - sds.init_app(app, doc_dir="../../docs/build/html")) + sds.init_app(app) return app + or you can include the plugin directly without setting a ``doc_dir``:: + + app = Flask(__name__) + SphinxDocServer(app) + + or with setting a ``doc_dir``:: + + app = Flask(__name__) + SphinxDocServer(app, doc_dir="../../docs/build/html") + .. highlight:: default """ @@ -40,12 +46,14 @@ def __init__(self, app: Optional[Flask] = None, **kwargs) -> None: if app is not None: self.init_app(app, **kwargs) - @staticmethod - def init_app(app: Flask, doc_dir: str, index_file: str = "index.html") -> None: + def init_app( + self, app: Flask, doc_dir: Optional[str] = None, index_file: str = "index.html" + ) -> None: """Add the `/docs/` route to the `app` object. :param app: Flask object to add the route to. - :param doc_dir: The base directory holding the sphinx docs to serve. + :param doc_dir: The base directory holding the sphinx docs to serve. If not set + the ``doc_dir`` is guessed up to 3 directories above. :param index_file: The html file containing the base toctree. Default: "index.html" """ @@ -58,7 +66,57 @@ def web_docs(filename: str) -> Response: # pylint: disable=W0612 :param filename: File name from URL :return: Requested doc page """ - app.static_folder = doc_dir + app.static_folder = doc_dir or self._find_build_docs(app.root_path) doc_file = app.send_static_file(filename) app.static_folder = "static" return doc_file + + @staticmethod + def _find_build_docs(app_root: str, steps_up_the_tree: int = 3): + """Find build sphinx html docs. + + :param app_root: Root directory of the app. + :param steps_up_the_tree: Amount of steps to go up the file tree, defaults to 3 + :raises IOError: if no 'doc' or 'docs' directory is found. + :raises IOError: if no '_build' or 'build' directory is found in the doc/docs dir. + :raises IOError: if no 'html' directory is found in the _build/build dir. + :return: Path to directory holding the build sphinx docs. + """ + check_dir = file_dir = Path(app_root).parent + + #: Search doc(s) dir up the tree + doc_dir = None + for i in range(0, steps_up_the_tree + 1): + if (check_dir / "doc").is_dir(): + doc_dir = check_dir / "doc" + if (check_dir / "docs").is_dir(): + doc_dir = check_dir / "docs" + + if doc_dir: + break + + check_dir = file_dir.parents[i] + + if not doc_dir: + raise IOError("No 'doc' or 'docs' directory found.") + + #: search for (_)build dir + build_dir = None + if (doc_dir / "_build").is_dir(): + build_dir = doc_dir / "_build" + if (doc_dir / "build").is_dir(): + build_dir = doc_dir / "build" + + if not build_dir: + raise IOError( + f"No '_build' or 'build' directory found in {doc_dir}." + "Maybe you forgot to build the docs." + ) + + #: check for html dir + if Path("html") in build_dir.iterdir(): + return build_dir / "html" + raise IOError( + f"No 'html' directory found in {build_dir}." + "Maybe you forgot to build the HTML docs." + ) diff --git a/tests/test_flask_sphinx_docs.py b/tests/test_flask_sphinx_docs.py index e1c39f4..6b7a3a5 100644 --- a/tests/test_flask_sphinx_docs.py +++ b/tests/test_flask_sphinx_docs.py @@ -8,6 +8,9 @@ :copyright: (c) Christian Riedel :license: GPLv3 """ +from pathlib import Path + +import pytest from flask import Flask from formelsammlung.flask_sphinx_docs import SphinxDocServer @@ -49,3 +52,91 @@ def _create_app(): resp = client.get("/docs/") assert resp.status_code == 200 assert resp.data.decode() == "TEST_CONTENT_2" + + +def test_custom_index_file(tmp_path): + """Test custom index file.""" + test_dir = tmp_path / "docs" + test_dir.mkdir() + test_file = test_dir / "custom.html" + test_file.write_text("TEST_CONTENT") + + app = Flask(__name__) + SphinxDocServer(app, doc_dir=str(test_dir), index_file="custom.html") + client = app.test_client() + + resp = client.get("/docs/") + assert resp.status_code == 200 + assert resp.data.decode() == "TEST_CONTENT" + + +def doc_dir_guessing_option_1(tmp_path, monkeypatch): + """Test guessing of doc dir with 'doc/_build'.""" + test_repo = tmp_path / "testrepo" + test_repo.mkdir() + + py_code_dir = test_repo / "src" / "testrepo" + py_code_dir.mkdir(parents=True) + + monkeypatch.setattr(Path, "parent", py_code_dir) + + with pytest.raises(IOError) as excinfo: + SphinxDocServer._find_build_docs("") + assert "No 'doc' or 'docs'" in str(excinfo.value) + + doc_dir = test_repo / "doc" + doc_dir.mkdir() + + for child in doc_dir.iterdir(): print(child) + + with pytest.raises(IOError) as excinfo: + SphinxDocServer._find_build_docs("") + assert "No '_build' or 'build'" in str(excinfo.value) + + build_dir = doc_dir / "_build" + build_dir.mkdir() + + with pytest.raises(IOError) as excinfo: + SphinxDocServer._find_build_docs("") + assert "No 'html'" in str(excinfo.value) + + html_dir = build_dir / "html" + html_dir.mkdir() + + assert SphinxDocServer._find_build_docs("") == html_dir + + +def doc_dir_guessing_option_2(tmp_path, monkeypatch): + """Test guessing of doc dir with 'docs/build'.""" + test_repo = tmp_path / "testrepo" + test_repo.mkdir() + + py_code_dir = test_repo / "src" / "testrepo" + py_code_dir.mkdir(parents=True) + + monkeypatch.setattr(Path, "parent", py_code_dir) + + with pytest.raises(IOError) as excinfo: + SphinxDocServer._find_build_docs("") + assert "No 'doc' or 'docs'" in str(excinfo.value) + + doc_dir = test_repo / "docs" + doc_dir.mkdir() + + for child in doc_dir.iterdir(): print(child) + + with pytest.raises(IOError) as excinfo: + SphinxDocServer._find_build_docs("") + assert "No '_build' or 'build'" in str(excinfo.value) + + build_dir = doc_dir / "build" + build_dir.mkdir() + + with pytest.raises(IOError) as excinfo: + SphinxDocServer._find_build_docs("") + assert "No 'html'" in str(excinfo.value) + + html_dir = build_dir / "html" + html_dir.mkdir() + + assert SphinxDocServer._find_build_docs("") == html_dir