diff --git a/.gitignore b/.gitignore index a2bd0196..c59bbbb2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ compile_flags.txt # Sphinx docs/_build/ +docs/generated/ diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 00000000..005e1b35 Binary files /dev/null and b/docs/_static/favicon.ico differ diff --git a/docs/_static/favicon.svg b/docs/_static/favicon.svg deleted file mode 100644 index cce6a9bd..00000000 --- a/docs/_static/favicon.svg +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..502b4c29 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,8 @@ +API Reference +============= + +.. autosummary:: + :toctree: generated + :recursive: + + view diff --git a/docs/conf.py b/docs/conf.py index 1ebdf4fa..4cffdad0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,20 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["sphinx.ext.intersphinx"] +extensions = [ + "sphinx.ext.intersphinx", + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', +] +autosummary_generate = True +add_module_names = False # Cleaner output + +# This is the key part for making detailed pages: +autodoc_default_options = { + 'members': True, + 'undoc-members': True, + 'show-inheritance': True, +} exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} @@ -25,8 +38,18 @@ html_theme = "shibuya" html_theme_options = { "accent_color": "blue", - # "light_logo": "_static/logo_black.svg", - # "dark_logo": "_static/logo_white.svg", + "light_logo": "_static/logo_black.svg", + "dark_logo": "_static/logo_white.svg", + "logo_target": "https://view.zintensity.dev", + "github_url": "https://github.com/ZeroIntensity/view.py", + "announcement": "view.py is currently in alpha and not considered ready for production", } html_static_path = ["_static"] -# html_favicon = "_static/favicon.svg" +html_favicon = "_static/favicon.ico" +html_context = { + "source_type": "github", + "source_user": "ZeroIntensity", + "source_repo": "view.py", + "source_version": "main", + "source_docs_path": "/docs/", +} diff --git a/docs/index.rst b/docs/index.rst index 683abe85..3d69435e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,3 +6,5 @@ Nothing here yet... .. toctree:: :maxdepth: 2 :caption: Contents: + + api diff --git a/requirements.txt b/requirements.txt index 781e4d48..f5a7361b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ # Requirements for Netlify sphinx>=7.0 -shibuya~=2025.8 +shibuya>=2025 diff --git a/src/view/core/app.py b/src/view/core/app.py index dcbd5ecd..5962808b 100644 --- a/src/view/core/app.py +++ b/src/view/core/app.py @@ -123,7 +123,7 @@ def run( This is a sort of magic function that's supposed to "just work". If finer control over the server settings is desired, explicitly use the - server's API with the app's `asgi` or `wsgi` method. + server's API with the app's :meth:`asgi` or :meth:`wsgi` method. """ from view.run.servers import ServerSettings diff --git a/src/view/core/headers.py b/src/view/core/headers.py index 35e3cdc4..3d1fc3ff 100644 --- a/src/view/core/headers.py +++ b/src/view/core/headers.py @@ -82,8 +82,8 @@ def with_new_value(self, key: str, value: str) -> HTTPHeaders: def as_real_headers(headers: HeadersLike | None, /) -> HTTPHeaders: """ - Convenience function for casting a "header-like object" (or `None`) - to a `MultiMap`. + Convenience function for casting a "header-like object" (or ``None``) + to a :class:`MultiMap`. """ if headers is None: return HTTPHeaders() @@ -111,7 +111,7 @@ def as_real_headers(headers: HeadersLike | None, /) -> HTTPHeaders: def wsgi_to_headers(environ: Mapping[str, Any]) -> HTTPHeaders: """ - Convert WSGI headers (from the `environ`) to a case-insensitive multi-map. + Convert WSGI headers (from the ``environ``) to a case-insensitive multi-map. """ values: list[tuple[LowerStr, str]] = [] @@ -126,7 +126,7 @@ def wsgi_to_headers(environ: Mapping[str, Any]) -> HTTPHeaders: return HTTPHeaders(values) -def headers_to_wsgi(headers: HTTPHeaders) -> WSGIHeaders: +def headers_to_wsgi(headers: HTTPHeaders, /) -> WSGIHeaders: """ Convert a case-insensitive multi-map to a WSGI header iterable. """ @@ -148,7 +148,7 @@ def asgi_to_headers(headers: ASGIHeaders, /) -> HTTPHeaders: lower_str = LowerStr(key.decode("utf-8")) values.append((lower_str, value.decode("utf-8"))) - return MultiMap(values) + return HTTPHeaders(values) def headers_to_asgi(headers: HTTPHeaders, /) -> ASGIHeaders: diff --git a/src/view/core/multi_map.py b/src/view/core/multi_map.py index 6fbee710..8aa143cd 100644 --- a/src/view/core/multi_map.py +++ b/src/view/core/multi_map.py @@ -45,7 +45,7 @@ def __init__(self, items: Iterable[tuple[KeyT, ValueT]] = ()) -> None: def __getitem__(self, key: KeyT, /) -> ValueT: """ - Get the first value if it exists, or else raise a `KeyError`. + Get the first value if it exists, or else raise a :exc:`KeyError`. """ return self._values[key][0] @@ -127,7 +127,7 @@ def get_many(self, key: KeyT) -> Sequence[ValueT]: def get_exactly_one(self, key: KeyT) -> ValueT: """ Get precisely one value for a key. If more than one value is present, - then this raises a `HasMultipleValuesError`. + then this raises a :exc:`HasMultipleValuesError`. """ value = self._values[key] if len(value) != 1: diff --git a/src/view/core/request.py b/src/view/core/request.py index e9b3dc2e..ab6a7354 100644 --- a/src/view/core/request.py +++ b/src/view/core/request.py @@ -115,12 +115,12 @@ class Request(BodyMixin): method: Method """ - The HTTP method of the request. See `Method`. + The HTTP method of the request. See :class:`Method`. """ headers: HTTPHeaders """ - A "multi-dictionary" containing the request headers. This is `dict`-like, + A "multi-dictionary" containing the request headers. This is :class:`dict`-like, but if a header has multiple values, it is represented by a list. """ diff --git a/src/view/core/response.py b/src/view/core/response.py index b2e3e4f4..3d0bbf2e 100644 --- a/src/view/core/response.py +++ b/src/view/core/response.py @@ -94,7 +94,7 @@ def from_file( content_type: str | None = None, ) -> FileResponse: """ - Generate a `FileResponse` from a file path. + Generate a :class:`FileResponse` from a file path. """ if __debug__ and not isinstance(chunk_size, int): raise InvalidTypeError(chunk_size, int) @@ -145,7 +145,8 @@ def from_content( headers: HeadersLike | None = None, ) -> TextResponse[AnyStr]: """ - Generate a `TextResponse` from either a `str` or `bytes` object. + Generate a :class:`TextResponse` from either a :class:`str` or + :class:`bytes` object. """ if __debug__ and not isinstance(content, (str, bytes)): @@ -232,7 +233,7 @@ def _wrap_response_tuple(response: _ResponseTuple) -> Response: def _wrap_response(response: ResponseLike, /) -> Response: """ - Wrap a response from a view into a `Response` object. + Wrap a response from a view into a :class:`Response` object. """ logger.debug(f"Got response: {response!r}") if isinstance(response, Response): @@ -266,7 +267,7 @@ async def stream() -> AsyncGenerator[bytes]: async def wrap_view_result(result: ViewResult, /) -> Response: """ Turn the raw result of a view, which might be a coroutine, into a usable - `Response` object. + :class:`Response` object. """ if isinstance(result, Awaitable): result = await result diff --git a/src/view/dom/core.py b/src/view/dom/core.py index 3b97eaf5..f4b2f437 100644 --- a/src/view/dom/core.py +++ b/src/view/dom/core.py @@ -182,7 +182,8 @@ def html_response( function: HTMLView, ) -> RouteView: """ - Return a `Response` object from a function returning HTML. + Return a :class:`~view.core.response.Response` object from a function + returning HTML. """ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> Response: diff --git a/src/view/exceptions.py b/src/view/exceptions.py index fe610a49..928ffe73 100644 --- a/src/view/exceptions.py +++ b/src/view/exceptions.py @@ -17,8 +17,8 @@ def __init__(self, *msg: str) -> None: class InvalidTypeError(ViewError, TypeError): """ Something got a type that it didn't expect. For example, passing a - `str` object in a place where a `bytes` object was expected would raise - this error. + :class:`str` object in a place where a :class:`bytes` object was + expected would raise this error. In order to fix this, please review the documentation of the function you're attempting to call and ensure that you are passing it the correct diff --git a/src/view/javascript.py b/src/view/javascript.py index 4594f615..8fff59a7 100644 --- a/src/view/javascript.py +++ b/src/view/javascript.py @@ -20,7 +20,7 @@ @runtime_checkable class SupportsJavaScript(Protocol): """ - Protocol for objects that want to allow use in `as_javascript_expression()`. + Protocol for objects that want to allow use in :func:`as_javascript_expression`. """ def as_javascript(self) -> str: diff --git a/src/view/run/asgi.py b/src/view/run/asgi.py index 734c6fd3..012419da 100644 --- a/src/view/run/asgi.py +++ b/src/view/run/asgi.py @@ -71,6 +71,9 @@ def asgi_for_app(app: BaseApp, /) -> ASGIProtocol: """ Generate an ASGI-compliant callable for a given app, allowing it to be executed in an ASGI server. + + Don't use this directly; prefer the :meth:`view.core.app.BaseApp.wsgi` + method instead. """ async def asgi( diff --git a/src/view/run/servers.py b/src/view/run/servers.py index bba566d1..b32db0d5 100644 --- a/src/view/run/servers.py +++ b/src/view/run/servers.py @@ -48,7 +48,7 @@ class ServerSettings: def run_uvicorn(self) -> None: """ - Run the app using the `uvicorn` library. + Run the app using the ``uvicorn`` library. """ import uvicorn @@ -56,7 +56,7 @@ def run_uvicorn(self) -> None: def run_hypercorn(self) -> None: """ - Run the app using the `hypercorn` library. + Run the app using the ``hypercorn`` library. """ import asyncio @@ -69,7 +69,7 @@ def run_hypercorn(self) -> None: def run_daphne(self) -> None: """ - Run the app using the `daphne` library. + Run the app using the ``daphne`` library. """ from daphne.endpoints import build_endpoint_description_strings from daphne.server import Server @@ -83,7 +83,7 @@ def run_daphne(self) -> None: def run_gunicorn(self) -> None: """ - Run the app using the `gunicorn` library. + Run the app using the ``gunicorn`` library. """ from gunicorn.app.base import BaseApplication @@ -111,7 +111,7 @@ def load(self): def run_werkzeug(self) -> None: """ - Run the app using the `werkzeug` library. + Run the app using the ``werkzeug`` library. """ from werkzeug.serving import run_simple @@ -119,7 +119,7 @@ def run_werkzeug(self) -> None: def run_wsgiref(self) -> None: """ - Run the app using the built-in `wsgiref` module. + Run the app using the built-in :mod:`wsgiref` module. """ from wsgiref.simple_server import make_server @@ -131,7 +131,7 @@ def run_app_on_any_server(self) -> None: Run the app on the nearest available ASGI or WSGI server. This will always succeed, as it will fall back to the standard - `wsgiref` module if no other server is installed. + :mod:`wsgiref` module if no other server is installed. """ servers: dict[str, StartServer] = { "uvicorn": self.run_uvicorn, diff --git a/src/view/run/wsgi.py b/src/view/run/wsgi.py index c3e9a1c1..5ccfaa1c 100644 --- a/src/view/run/wsgi.py +++ b/src/view/run/wsgi.py @@ -32,6 +32,9 @@ def wsgi_for_app( """ Generate a WSGI-compliant callable for a given app, allowing it to be executed in an ASGI server. + + Don't use this directly; prefer the :meth:`view.core.app.BaseApp.wsgi` + method instead. """ loop = loop or asyncio.new_event_loop() diff --git a/src/view/testing.py b/src/view/testing.py index 4d986e19..24345690 100644 --- a/src/view/testing.py +++ b/src/view/testing.py @@ -21,7 +21,7 @@ async def into_tuple( ) -> tuple[bytes, int, HTTPHeaders]: """ Convenience function for transferring a test client call into a tuple - through a single ``await``. + through a single :keyword:`await`. """ response = await response_coro body = await response.body() @@ -30,7 +30,7 @@ async def into_tuple( def ok(body: str | bytes) -> tuple[bytes, int, dict[str, str]]: """ - Utility function for an OK response from `into_tuple()`. + Utility function for an OK response from :func:`into_tuple`. """ if isinstance(body, str): @@ -40,7 +40,7 @@ def ok(body: str | bytes) -> tuple[bytes, int, dict[str, str]]: def bad(status_code: int) -> tuple[bytes, int, dict[str, str]]: """ - Utility function for an error response from `into_tuple()`. + Utility function for an error response from :func:`into_tuple`. """ body = STATUS_STRINGS[status_code] return (f"{status_code} {body}".encode(), status_code, {}) diff --git a/src/view/utils.py b/src/view/utils.py index b602c6a7..84fdde14 100644 --- a/src/view/utils.py +++ b/src/view/utils.py @@ -18,7 +18,7 @@ def reraise( Context manager to reraise one or many exceptions as a single exception. This is primarily useful for reraising exceptions into HTTP errors, such - as an error 400 (Bad Request). + as a :class:`~view.core.status_codes.BadRequest`. """ target = exceptions or Exception