From 7981a7a13756bc34acf441ddf04f138571b7076c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= <hugo@lysator.liu.se>
Date: Sat, 3 Jun 2023 22:26:56 +0200
Subject: [PATCH] Setup sphinx documentation.

Documentation can now be generated to HTML through sphinx.
Some docstrings have been updated to be on the correct format.
---
 .gitignore              |  1 +
 Makefile                | 12 ++++++++-
 README.md               | 18 +++++++++++++
 doc/.gitignore          |  2 ++
 doc/conf.py             | 50 ++++++++++++++++++++++++++++++++++
 doc/index.rst           | 20 ++++++++++++++
 muppet/data/__init__.py |  5 ++++
 muppet/data/html.py     |  2 +-
 muppet/format.py        | 11 ++++----
 muppet/lookup.py        | 60 ++++++++++++++++++++++++-----------------
 10 files changed, 150 insertions(+), 31 deletions(-)
 create mode 100644 doc/.gitignore
 create mode 100644 doc/conf.py
 create mode 100644 doc/index.rst

diff --git a/.gitignore b/.gitignore
index cb556ee..8012cbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 .cache
 .yardoc
+doc.rendered
diff --git a/Makefile b/Makefile
index 2cbbe58..a0426b1 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,9 @@
-.PHONY: all output check test
+.PHONY: all output check test sphinx-apidoc documentation
 
 all: output
 
+DOC_OUTPUT = doc.rendered
+
 output:
 	python -m muppet --env ~/puppet/generated-environments/test/modules/
 
@@ -15,5 +17,13 @@ PYTEST_FLAGS = --cov=muppet --cov-branch --cov-report=html
 test:
 	python -m pytest $(PYTEST_FLAGS) tests
 
+sphinx-apidoc:
+	sphinx-apidoc --separate --force -o doc muppet
+
+$(DOC_OUTPUT)/index.html: sphinx-apidoc
+	sphinx-build -b dirhtml doc $(DOC_OUTPUT)
+
+documentation: $(DOC_OUTPUT)/index.html
+
 clean:
 	-rm -r output
diff --git a/README.md b/README.md
index cd2ffdf..3ad4256 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,24 @@ environments.
 Usage
 -----
 
+### Building the example (probably broken for you)
+
 ```bash
 make clean all
 ```
+
+### Running the tests
+
+```bash
+make check  # Run linters
+make test   # Run unit tests
+```
+
+Coverage information will end up in `htmlcov/`.
+
+### Building the documentation
+```bash
+make documentation
+```
+
+Documentation will end up in `doc.rendered/`.
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644
index 0000000..eebb7a8
--- /dev/null
+++ b/doc/.gitignore
@@ -0,0 +1,2 @@
+muppet*.rst
+modules.rst
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..895caba
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,50 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+# from __future__ import annotations
+
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath('..'))
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = 'Muppet Strings'
+copyright = '2023, Hugo Hörnquist'
+author = 'Hugo Hörnquist'
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+# Coverage not included, since flake8 already captures that
+
+extensions = [
+    'sphinx.ext.autodoc',   # Automatically extract source from pypthon files
+    'sphinx.ext.viewcode',  # Adds source viewer to output
+]
+
+# Fancy type aliases.
+# For this to work, each module has to individually run
+#   from __future__ import annotations
+# Which will prevent type aliases from being eagerly evaluated.
+
+autodoc_type_aliases = {
+    'Markup': 'muppet.data.Markup',
+}
+
+# Add type signatures in parameter list of description, instead of in
+# signature. This reduces the clutter quite a bit.
+autodoc_typehints = 'description'
+
+templates_path = ['_templates']
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = 'pyramid'
+html_static_path = ['_static']
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..17dc708
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,20 @@
+.. Muppet Strings documentation master file, created by
+   sphinx-quickstart on Sat Jun  3 20:13:35 2023.
+   You can adapt this file completely to your liking, but it should at least
+   contain the root `toctree` directive.
+
+Welcome to Muppet Strings's documentation!
+==========================================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+Here is some content.
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/muppet/data/__init__.py b/muppet/data/__init__.py
index 92dcbc2..9e685be 100644
--- a/muppet/data/__init__.py
+++ b/muppet/data/__init__.py
@@ -8,6 +8,8 @@ Almost all of the datatypes have "bad" __repr__ implementations. This
 is to allow *much* easier ocular diffs when running pytest.
 """
 
+from __future__ import annotations
+
 from dataclasses import dataclass
 from abc import ABC, abstractmethod
 from collections.abc import Sequence
@@ -24,6 +26,9 @@ Markup: TypeAlias = Union[str,
                           'ID',
                           'Documentation',
                           'Indentation']
+"""
+Documentation of Markup.
+"""
 
 
 @dataclass
diff --git a/muppet/data/html.py b/muppet/data/html.py
index 1b155fe..67edef9 100644
--- a/muppet/data/html.py
+++ b/muppet/data/html.py
@@ -44,7 +44,7 @@ class HTMLRenderer(Renderer):
         The anchor will contain both the item, rendered as normally,
         and a div with class documentation.
 
-        The suggested CSS for this is
+        The suggested CSS for this is::
 
             .documentation-anchor {
                 display: relative;
diff --git a/muppet/format.py b/muppet/format.py
index e8f855e..43eaa7d 100644
--- a/muppet/format.py
+++ b/muppet/format.py
@@ -1,7 +1,7 @@
 """
 Pretty print a complete puppet documentation.
 
-An `output.json`, as per produced by `./merge-json.py` should be
+An ``output.json``, as per produced by ``./merge-json.py`` should be
 provided as the first element. This program goes through every
 definition in it, and outputs a complete index.html.
 """
@@ -101,9 +101,10 @@ def print_var(x: str, dollar: bool = True) -> Link:
     Print the given variable.
 
     If documentation exists, then add that documentation as hoover text.
-    :param: x
+
+    :param x:
         The variable to print
-    :param: dollar
+    :param dollar:
         If there should be a dollar prefix.
     """
     dol = '$' if dollar else ''
@@ -155,9 +156,9 @@ def parse(form: Any, indent: int, context: list[str]) -> Tag:
     """
     Print everything from a puppet parse tree.
 
-    :param: from
+    :param from:
         A puppet AST.
-    :param: indent
+    :param indent:
         How many levels deep in indentation the code is.
         Will get multiplied by the indentation width.
     """
diff --git a/muppet/lookup.py b/muppet/lookup.py
index 0abb644..d51b1af 100644
--- a/muppet/lookup.py
+++ b/muppet/lookup.py
@@ -1,5 +1,5 @@
-"""
-[Jq(1)](https://jqlang.github.io/jq/) like expressions for python.
+r"""
+Jq(1) <https://jqlang.github.io/jq/> like expressions for python.
 
 Something similar to Jq, but built on python objects.
 All procedures eventually return the expecetd value, or a
@@ -8,17 +8,19 @@ user-supplied default value.
 
 Example
 -------
-        lookup(i) \
-                .ref('docstring') \
-                .ref('tags') \
-                .select(Ref('tag_name') == 'summary')) \
-                .idx(0)
-                .ref('text') \
-                .exec()
+::
+
+    lookup(object) \
+            .ref('docstring') \
+            .ref('tags') \
+            .select(Ref('tag_name') == 'summary')) \
+            .idx(0) \
+            .ref('text') \
+            .exec()
 
 TODO
 ----
-- `select`
+- ``select``
   Selects all values from a list which matches a given expression.
   This would however require us to manage multiple values at once.
 """
@@ -26,11 +28,14 @@ TODO
 from typing import Any, Union
 
 
-class _Expression:
+class Expression:
     """
     A test expression.
 
+    ::
+
         x.find(Ref("key") == "summary")
+
     Would focus in on the first list element which has the key "key"
     with a value of "summary".
 
@@ -38,31 +43,39 @@ class _Expression:
     """
 
     def run(self, value: Any) -> bool:
+        """Evaluate expression, returing true if the value matches the predicate."""
         return False
 
 
-class _RefEqExpr(_Expression):
+class RefEqExpr(Expression):
     """
     Equality expression.
 
-    Assumes that the left part is a _RefExpr and the right part is a value.
+    Assumes that the left part is a RefExpr and the right part is a value.
 
     Checks that the left reference exists in the given value, and that
     it's value is equal to the right one.
     """
 
-    def __init__(self, left: '_RefExpr', right: Any):
+    def __init__(self, left: 'RefExpr', right: Any):
         self.left = left
         self.right = right
 
     def run(self, value: Any) -> bool:
+        """
+        Evaluate the expression.
+
+        Returns true if the left hand side key is present in *value*,
+        and if the value under that key in *value* matches the right
+        hand side.
+        """
         if self.left.key not in value:
             return False
         else:
             return bool(value[self.left.key] == self.right)
 
 
-class _RefExpr(_Expression):
+class RefExpr(Expression):
     """
     A key reference expression.
 
@@ -74,12 +87,12 @@ class _RefExpr(_Expression):
     def __init__(self, key: str):
         self.key = key
 
-    def __eq__(self, other: Any) -> '_RefEqExpr':  # type: ignore
+    def __eq__(self, other: Any) -> 'RefEqExpr':  # type: ignore
         """
         Return a new expression checking equality between left and right.
 
         Left side will be ourself, while the right side can in theory
-        be anything (see _RefEqExpr for details).
+        be anything (see RefEqExpr for details).
 
         Typing is removed here, since the base object specifies the type as
             def __eq__(self, x: Any) -> bool:
@@ -90,9 +103,10 @@ class _RefExpr(_Expression):
         there is a president.
 
         """
-        return _RefEqExpr(self, other)
+        return RefEqExpr(self, other)
 
     def run(self, value: Any) -> bool:
+        """Return true if our key is present in *value*."""
         return self.key in value
 
 
@@ -118,7 +132,7 @@ class _NullLookup:
         """Propagate null."""
         return self
 
-    def find(self, _: _Expression) -> '_NullLookup':
+    def find(self, _: Expression) -> '_NullLookup':
         """Propagate null."""
         return self
 
@@ -162,7 +176,7 @@ class _TrueLookup:
             # Index out of range
             return _NullLookup()
 
-    def find(self, expr: _Expression) -> Union['_TrueLookup', '_NullLookup']:
+    def find(self, expr: Expression) -> Union['_TrueLookup', '_NullLookup']:
         """Find the first element in list matching expression."""
         for item in self.object:
             if expr.run(item):
@@ -190,11 +204,9 @@ def lookup(base: Any) -> Lookup:
 
     All queries should start here.
 
-    Parameters
-    ----------
-    base - Can be anything which has meaningful subfields.
+    :param base: Can be anything which has meaningful subfields.
     """
     return _TrueLookup(base)
 
 
-Ref = _RefExpr
+Ref = RefExpr
-- 
GitLab