From f26f95c44f8c773bd2db1a6f346527ae4c22bc33 Mon Sep 17 00:00:00 2001
From: Maurizio Martinelli <maurizio.martinelli@cern.ch>
Date: Wed, 1 Feb 2023 12:57:00 +0100
Subject: [PATCH] Automatic documentation of DaVinciExamples and
 DaVinciTutorials

---
 .../python/DaVinciExamples/debugging.py       |   3 +
 .../option_davinci_v2_composites.py           |   5 +
 .../option_davinci_tupling_from_hlt2.py       |   8 +
 .../DaVinciTutorials/tutorial0_basic_DVjob.py |   3 +
 Phys/DaVinci/python/DaVinci/LbExec.py         |   6 +-
 doc/Makefile                                  |  10 +-
 doc/examples/examples_index.rst               |   5 -
 doc/guide/developing.rst                      |  26 +++
 doc/{tutorials => guide}/running.rst          |   2 +-
 doc/index.rst                                 |   1 +
 doc/make_auto_docs.py                         | 191 ++++++++++++++++++
 doc/tutorials/tutorials_index.rst             |   6 -
 12 files changed, 247 insertions(+), 19 deletions(-)
 delete mode 100644 doc/examples/examples_index.rst
 rename doc/{tutorials => guide}/running.rst (99%)
 create mode 100644 doc/make_auto_docs.py
 delete mode 100644 doc/tutorials/tutorials_index.rst

diff --git a/DaVinciExamples/python/DaVinciExamples/debugging.py b/DaVinciExamples/python/DaVinciExamples/debugging.py
index 8ba031463..3372998d9 100644
--- a/DaVinciExamples/python/DaVinciExamples/debugging.py
+++ b/DaVinciExamples/python/DaVinciExamples/debugging.py
@@ -10,6 +10,9 @@
 ###############################################################################
 """
 Example of a DaVinci job printing decay trees via `PrintDecayTree`.
+
+rst_title: Printing Decay Trees
+rst_desc: This example shows how to print the decay tree of a candidate.
 """
 from PyConf.application import configure, configure_input
 from PyConf.application import make_odin
diff --git a/DaVinciExamples/python/DaVinciExamples/option_davinci_v2_composites.py b/DaVinciExamples/python/DaVinciExamples/option_davinci_v2_composites.py
index 2d779c64d..ebc3a980c 100644
--- a/DaVinciExamples/python/DaVinciExamples/option_davinci_v2_composites.py
+++ b/DaVinciExamples/python/DaVinciExamples/option_davinci_v2_composites.py
@@ -8,6 +8,11 @@
 # granted to it by virtue of its status as an Intergovernmental Organization  #
 # or submit itself to any jurisdiction.                                       #
 ###############################################################################
+"""
+Example of a DaVinci job using v2 functor on composite particles.
+
+rst_title: V2 Composites
+"""
 from PyConf.Algorithms import ChargedBasicsProducer, UniqueIDGeneratorAlg
 from PyConf.Algorithms import ThOrCombiner__2ChargedBasics
 from PyConf.Algorithms import ThOrCombiner__CompositesChargedBasics
diff --git a/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_from_hlt2.py b/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_from_hlt2.py
index 8312777fc..4827ac0f1 100644
--- a/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_from_hlt2.py
+++ b/DaVinciExamples/python/DaVinciExamples/tupling/option_davinci_tupling_from_hlt2.py
@@ -10,6 +10,14 @@
 ###############################################################################
 """
 Read an HLT2 file and create an ntuple with the new DaVinci configuration.
+
+rst_title: Tupling from HLT2
+rst_description: This example shows how to read an HLT2 output file and create a n-tuple.
+It defines various functors operating on the head and daughters of the decay channel
+and creates a FunTuple.
+
+rst_running: lb-run DaVinci/vXXrY lbexec DaVinciExamples.tupling.option_davinci_tupling_from_hlt2
+rst_yaml: ../DaVinciExamples/example_data/FEST_November_2021_dst_newPacking.yaml
 """
 from PyConf.reading import get_particles, get_pvs
 import Functors as F
diff --git a/DaVinciTutorials/python/DaVinciTutorials/tutorial0_basic_DVjob.py b/DaVinciTutorials/python/DaVinciTutorials/tutorial0_basic_DVjob.py
index 2f7792f50..038d39a6e 100644
--- a/DaVinciTutorials/python/DaVinciTutorials/tutorial0_basic_DVjob.py
+++ b/DaVinciTutorials/python/DaVinciTutorials/tutorial0_basic_DVjob.py
@@ -8,6 +8,9 @@
 # granted to it by virtue of its status as an Intergovernmental Organization  #
 # or submit itself to any jurisdiction.                                       #
 ###############################################################################
+'''
+rst_title: Basic DaVinci Job
+'''
 from DaVinci import Options, make_config
 from DaVinci.algorithms import add_filter
 from PyConf.reading import get_particles
diff --git a/Phys/DaVinci/python/DaVinci/LbExec.py b/Phys/DaVinci/python/DaVinci/LbExec.py
index 0fff237c1..473273db8 100644
--- a/Phys/DaVinci/python/DaVinci/LbExec.py
+++ b/Phys/DaVinci/python/DaVinci/LbExec.py
@@ -32,15 +32,13 @@ class Options(DefaultOptions):
   - input_process (str): Input process type, see `GaudiConf.LbExec.options.InputProcessTypes` values.
 
   The optional parameters that need to be set are :
-  - stream (str): Stream name. Default is "default"
-        Note: for `input_process=Hlt2` the stream must be strictly empty. The default value is overwritten in this case.
+  - stream (str): Stream name. Default is "default" (Note: for `input_process=Hlt2` the stream must be strictly empty. The default value is overwritten in this case.)
   - lumi (bool): Flag to store luminosity information. Default is False.
   - evt_pre_filters (dict[str,str]): Event pre-filter code. Default is None.
   - annsvc_config (str): Path to the configuration file from sprucing or Hlt2. Default is None.
   - write_fsr (bool): Flag to write full stream record. Default is True.
   - merge_genfsr (bool): Flag to merge the full stream record. Default is False.
-  - metainfo_additional_tags: (list): Additional central tags for `PyConf.filecontent_metadata.metainfo_repos`.
-    Default is [].
+  - metainfo_additional_tags: (list): Additional central tags for `PyConf.filecontent_metadata.metainfo_repos`. Default is [].
   """
     input_process: InputProcessTypes
     stream: Optional[str] = 'default'
diff --git a/doc/Makefile b/doc/Makefile
index 17f01049e..d81a7dc4e 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -41,7 +41,7 @@ $(SPHINXBUILD): requirements.txt Makefile
 # Catch-all target: route all unknown targets to Sphinx using the new
 # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
 #html pdf: Makefile graphs functor_docs $(SPHINXBUILD)
-html pdf: Makefile graphs $(SPHINXBUILD)
+html pdf: Makefile graphs examples_and_tutorials_doc $(SPHINXBUILD)
 	$(RUNENV) $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 #$(RUNENV) $(SPHINXVERSION) build -r mamartin-docs . _build/html
 #-M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O);
@@ -58,13 +58,17 @@ linkcheck: Makefile graphs $(SPHINXBUILD)
 
 clean: Makefile $(SPHINXBUILD)
 	rm -rf graphviz
+	rm -rf autoapi
 	$(RUNENV) $(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
 
 purge:
-	rm -rf $(SPHINXPREFIX) $(BUILDDIR) graphviz
+	rm -rf $(SPHINXPREFIX) $(BUILDDIR) graphviz autoapi
 
 graphs: $(DATA_GRAPH_SOURCES) $(CONTROL_GRAPH_SOURCES)
 
+examples_and_tutorials_doc:
+	python make_auto_docs.py
+
 # # Generate graphs with names based on the options file that creates them
 # graphviz/%_data_flow.gv graphviz/%_control_flow.gv: scripts/%.py
 # 	mkdir -p graphviz
@@ -78,4 +82,4 @@ graphs: $(DATA_GRAPH_SOURCES) $(CONTROL_GRAPH_SOURCES)
 # 	python make_functor_docs.py "$(GIT_COMMIT)" "master" > "$@"
 
 #.PHONY: help html pdf clean graphs functor_docs purge
-.PHONY: help html pdf clean graphs purge
+.PHONY: help html pdf clean graphs examples_and_tutorials_doc purge
diff --git a/doc/examples/examples_index.rst b/doc/examples/examples_index.rst
deleted file mode 100644
index 01a91bc76..000000000
--- a/doc/examples/examples_index.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Examples
-========
-
-.. toctree::
-
diff --git a/doc/guide/developing.rst b/doc/guide/developing.rst
index a6cd5e0d5..ebd074822 100644
--- a/doc/guide/developing.rst
+++ b/doc/guide/developing.rst
@@ -1,6 +1,26 @@
 Developing DaVinci
 ==================
 
+There are various ways to work on `DaVinci`.
+If you plan to make continous development work, the simplest is probably by building on top of the whole stack using `lb-stack-setup <https://gitlab.cern.ch/rmatev/lb-stack-setup>`_.
+
+.. code-block:: bash
+
+    source /cvmfs/lhcb.cern.ch/lib/LbEnv
+    curl https://gitlab.cern.ch/rmatev/lb-stack-setup/raw/master/setup.py | python3 - stack
+    make DaVinci
+
+
+This will require approximately 7GB of disk, but will guarantee to be up-to-date with all the dependencies.
+
+Alternatively, especially for small changes, a development environment can be built with the commands
+
+.. code-block:: bash
+
+    lb-dev --name DaVinciDev DaVinci/latest
+    cd DaVinciDev
+    git lb-use DaVinci
+    
 
 Coding conventions
 ------------------
@@ -9,6 +29,12 @@ Coding conventions
 Tests and continuous integration
 --------------------------------
 
+Whenever a new commit is made to the DaVinci repository, a `CI pipeline <https://docs.gitlab.com/ee/ci/>`_ runs that performs some style and syntax checks:
+
+ - The LHCb copyright statement should be present at the top of every source file.
+ - The LHCb Python formatting rules must be adhered to.
+ - The Python code must have valid syntax and not raise any `flake8 error codes <https://flake8.pycqa.org/en/2.5.5/warnings.html>`_.
+
 
 Debugging
 ---------
diff --git a/doc/tutorials/running.rst b/doc/guide/running.rst
similarity index 99%
rename from doc/tutorials/running.rst
rename to doc/guide/running.rst
index c711fd3fb..8513f64bd 100644
--- a/doc/tutorials/running.rst
+++ b/doc/guide/running.rst
@@ -50,7 +50,7 @@ Also make a file named ``options.yaml`` containing:
 
 
 .. code-block:: yaml
-
+    
     input_files:
     - root://eoslhcb.cern.ch//eos/lhcb/wg/dpa/wp3/tests/hlt2_passthrough_thor_lines.dst
     annsvc_config: root://eoslhcb.cern.ch//eos/lhcb/wg/dpa/wp3/tests/hlt2_passthrough_thor_lines.tck.json
diff --git a/doc/index.rst b/doc/index.rst
index 50aadd4bb..fcbed5fd8 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -28,6 +28,7 @@ and Turbo output.
 
    configuration/davinci_configuration
    guide/funtuple
+   guide/running
    guide/developing
    guide/documentation
 
diff --git a/doc/make_auto_docs.py b/doc/make_auto_docs.py
new file mode 100644
index 000000000..6ff856f31
--- /dev/null
+++ b/doc/make_auto_docs.py
@@ -0,0 +1,191 @@
+###############################################################################
+# (c) Copyright 2023 CERN for the benefit of the LHCb Collaboration           #
+#                                                                             #
+# This software is distributed under the terms of the GNU General Public      #
+# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   #
+#                                                                             #
+# In applying this licence, CERN does not waive the privileges and immunities #
+# granted to it by virtue of its status as an Intergovernmental Organization  #
+# or submit itself to any jurisdiction.                                       #
+###############################################################################
+'''
+This script is used to automatically create the documentation pages for the scripts in 
+DaVinciExamples and DaVinciTutorials and show them in the section Examples and Tutorials 
+of the DaVinci documentation site.
+
+The script so far creates the documentation pages based on information added on top of 
+the original python files.
+The information is formatted in the form of a comment
+
+"""
+<minimal description of the python script>
+
+rst_title: <documentation page title>
+rst_description: <documentation page description>
+rst_running: <script to run the example/tutorial>
+rst_yaml: <address of the yaml file relative to the 'doc' folder in DaVinci>
+"""
+
+Further sections can be added if needed.
+The information above is decoded in the function GetPageInfo of make_auto_docs.py and used in the functions WritePage to create the page.
+The index for DaVinciExamples or DaVinciTutorials and their respective sub-folders is written by the function WriteIndex that is called recursively when a sub-folder is found.
+'''
+import os, warnings
+
+
+def GetPageInfo(script):
+    """A function to extract the information to write the documentation page from the script
+
+    Args:
+        script (str): the file name of the script
+
+    Raises:
+        Warning: invalid documentation keys
+
+    Returns:
+        dict: a dictionary with information to write the documentation page
+    """
+    info = {'title': None, 'description': None, 'running': None, 'yaml': None}
+    f = open(script, 'r')
+    active_comment = False
+    active_key = None
+    line_start_code = 0
+    for l in f:
+        line_start_code += 1
+        if active_comment and ('"""' in l or "'''" in l):
+            break  # avoid looping on the whole file, stop search after the first comment
+        if '"""' in l or "'''" in l: active_comment = not active_comment
+        if 'rst_' not in l and active_key is None or l == '\n':
+            continue  # ignore lines without sphinx info
+        if 'rst_' in l:
+            active_key = l[l.rfind('rst_') + 4:l.find(':')]  # extract the key
+            if active_key not in info.keys():
+                warnings.warn(
+                    f'The key {active_key} in {script} is not valid!')
+            info[active_key] = l.split(':')[1].lstrip()  # save information
+        else:
+            info[active_key] += l
+    for k in info.keys():
+        if k != 'description' and info[k] is not None:
+            info[k] = info[k].replace('\n', '')
+    for k in ['yaml']:
+        if info[k] is not None and not os.path.exists(info[k]):
+            warnings.warn(f'The {k} path is not valid: {info[k]}')
+    info['line_start_code'] = line_start_code
+    return info
+
+
+def WritePage(script, info, directory='.'):
+    """A function to write the documentation page
+
+    Args:
+        script (str): the file name of the script
+        info (dict): a dictionary with information to write the documentation page
+    """
+    # Title
+    out = info['title'] + '\n'
+    out += '=' * len(info['title']) + '\n\n'
+    # Description
+    if info['description'] is not None:
+        out += info['description'] + '\n\n'
+    # Include script
+    out += f'..  literalinclude:: /{script}\n'
+    out += '    :language: python\n'
+    if info['line_start_code'] != 0:
+        out += f'    :lines: {info["line_start_code"]+1}-\n'
+    out += '    :class: toggle\n\n'
+    # How-to run
+    if info['running'] is not None:
+        out += 'To run the example:\n\n'
+        out += '.. code-block:: sh\n\n'
+        out += f'    {info["running"]}\n\n'
+    # Yaml Options
+    if info['yaml'] is not None:
+        out += 'For reference, these are the options of this example\n\n'
+        out += f'.. literalinclude:: /{info["yaml"]}\n'
+        out += '    :language: yaml\n\n'
+    outname = f'{directory}/{script[script.rfind("/"):script.rfind(".py")]}.rst'
+    with open(outname, 'w') as f:
+        f.write(out)
+    print(f'Wrote file {outname}')
+    return
+
+
+def WriteIndex(directory, doc_directory, title, description=''):
+    """Write the index page for the relevant scripts folder
+
+    Args:
+        directory (str): the folder where the scripts lay
+        doc_directory (str): the name of the folder in the sphinx documentation
+        title (str): the title of the page
+        description (str, optional): a description of the content of the folder. Defaults to ''.
+    """
+    # Create documents' directory if not existing
+    if not os.path.exists(doc_directory):
+        os.makedirs(doc_directory)
+    # Title
+    out = f'{title}\n'
+    out += '=' * len(title) + '\n\n'
+    # Description
+    if description != '':
+        out += description + '\n\n'
+    # Search for scripts in the directory
+    valid_scripts = []
+    valid_folders = []
+    missingDoc = []
+    for f in os.listdir(directory):
+        if not f.endswith('.py'):
+            if os.path.isdir(f'{directory}/{f}'):
+                if not os.path.exists(f'{doc_directory}/{f}'):
+                    os.makedirs(f'{doc_directory}/{f}')
+                missingDoc += WriteIndex(
+                    f'{directory}/{f}', f'{doc_directory}/{f}', f.capitalize())
+                valid_folders += [f'{doc_directory}/{f}']
+            continue
+        if f == '__init__.py': continue
+        valid_scripts += [f'{directory}/{f}']
+    # toctree
+    if len(valid_scripts):
+        wroteTocTree = False
+        if len(valid_folders) and not wroteTocTree:
+            out += '.. toctree::\n\n'
+            wroteTocTree = True
+        for vf in valid_folders:
+            # remove the first folder of the path since the file lives there
+            out += f'   {vf[vf.find("/")+1:]}/{vf[vf.rfind("/")+1:]}_index\n'
+        for sc in valid_scripts:
+            info = GetPageInfo(sc)
+            if info['title'] is None:
+                missingDoc += [sc]
+                warnings.warn(
+                    f'File {sc} is missing the necessary documentation!')
+                continue
+            if not wroteTocTree:
+                out += '.. toctree::\n\n'
+                wroteTocTree = True
+            WritePage(sc, info, doc_directory)
+            out += f'   {sc[sc.rfind("/")+1:].replace(".py","")}\n'
+    # Write filename
+    outname = f'{doc_directory}/{doc_directory[doc_directory.rfind("/")+1:]}_index.rst'
+    with open(outname, 'w') as f:
+        f.write(out)
+    print(f'Wrote file {outname}')
+    return missingDoc
+
+
+def main():
+    missingDoc = []
+    missingDoc += WriteIndex('../DaVinciExamples/python/DaVinciExamples',
+                             'examples', 'DaVinci Examples', '')
+    missingDoc += WriteIndex('../DaVinciTutorials/python/DaVinciTutorials/',
+                             'tutorials', 'DaVinci Tutorials', '')
+    # if len(missingDoc):
+    #     raise ValueError(
+    #         '\033[91mThe following files are missing documentation:\n' +
+    #         '\n'.join(missingDoc) + '\033[0m')
+    return
+
+
+#-------------------------------
+if __name__ == "__main__":
+    main()
diff --git a/doc/tutorials/tutorials_index.rst b/doc/tutorials/tutorials_index.rst
deleted file mode 100644
index e11337e67..000000000
--- a/doc/tutorials/tutorials_index.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Tutorials
-=========
-
-.. toctree::
-
-   running
\ No newline at end of file
-- 
GitLab