From 73dc0ec06ae5ee59a9ede55fa9f6987167621637 Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Mon, 23 Jan 2017 17:59:08 +0000 Subject: [PATCH 1/3] Added Sphinx documenting of rose.(config,macro). * Fixes traceback for a rose macro without a docstring --- .gitignore | 2 + doc/Makefile | 54 ++ doc/rose-api.html | 43 +- doc/rose-config-api.html | 28 + doc/rose.html | 2 + doc/scripts/sphinx-install | 38 + doc/sphinx/Makefile | 177 ++++ doc/sphinx/_static/- | 0 doc/sphinx/conf.py | 268 ++++++ doc/sphinx/config.rst | 5 + doc/sphinx/index.rst | 34 + doc/sphinx/macro.rst | 19 + doc/sphinx/sphinx.rst | 151 +++ lib/python/rose/config.py | 889 +++++++++++++++--- lib/python/rose/macro.py | 209 +++- t/docs/{00-urls.t => 00-basic.t} | 27 +- ...alidator.t => 01-validate-url-validator.t} | 0 17 files changed, 1792 insertions(+), 154 deletions(-) create mode 100644 doc/Makefile create mode 100644 doc/rose-config-api.html create mode 100755 doc/scripts/sphinx-install create mode 100755 doc/sphinx/Makefile create mode 100644 doc/sphinx/_static/- create mode 100644 doc/sphinx/conf.py create mode 100644 doc/sphinx/config.rst create mode 100644 doc/sphinx/index.rst create mode 100644 doc/sphinx/macro.rst create mode 100644 doc/sphinx/sphinx.rst rename t/docs/{00-urls.t => 00-basic.t} (51%) rename t/docs/{01-validate-validator.t => 01-validate-url-validator.t} (100%) diff --git a/.gitignore b/.gitignore index 3440bb5069..a54ad1a2ed 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ etc/rose.conf etc/opt lib/bash/rose_init_site +doc/sphinx/_build +venv diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000000..9c87d63fb6 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,54 @@ +#!/usr/bin/make -f + +# If sphinx-build is not installed or if too early a version is installed make +# will attempt to install latest sphinx-build via a virtualenv. +# +# export SPHINX_DEV_MODE='true' to prevent the virtualenv from being deleted +# after the build completes. + +# Installation of sphinx via a virtualenv. +env-path = ../venv +sphinx-dev-mode := $(shell echo "${SPHINX_DEV_MODE}") +min-sphinx-version = 1.5.3 +sphinx-install := $(shell scripts/sphinx-install $(env-path) $(min-sphinx-version)) + +# Build parameters. +pypath = ../lib/python +build-dir = sphinx/_build + +# Command to tidy the virtualenv if required. +tidy-venv = +ifneq "$(sphinx-dev-mode)" "true" + tidy-venv = rm -rf $(env-path) +endif + +# Command to activate the virtualenv if required. +venv = +ifneq "$(sphinx-install)" "" +venv = . $(env-path)/bin/activate; +endif + +all: clean doctest html tidy + +html: sphinx-build + $(venv) PYTHONPATH=../$(pypath) make -C sphinx html + +doctest: sphinx-build + $(venv) PYTHONPATH=../$(pypath) make -C sphinx doctest + +sphinx-build: $(sphinx-install) + $(venv) PYTHONPATH=$(pypath) sphinx-build -aEW -b dummy sphinx sphinx/_build + +sphinx-install: + $(tidy-venv) + virtualenv --python=python2.7 "$(env-path)" + $(venv) pip install sphinx + +sphinx-activate: + +tidy: + $(tidy-venv) + +clean: + rm -rf $(build-dir) + $(tidy-venv) diff --git a/doc/rose-api.html b/doc/rose-api.html index e8b7690911..1cfb81df20 100644 --- a/doc/rose-api.html +++ b/doc/rose-api.html @@ -785,7 +785,8 @@

Rose Macros

e.g. adding/removing options.
  • Upgraders - these are special transformer macros - for upgrading and downgrading configurations.
  • + for upgrading and downgrading configurations. (covered in the + next section)
  • Reporters - output information about a configuration.
  • @@ -839,24 +840,30 @@

    Examples

    "rose-rug-advanced-tutorials-macro.html">Advanced Tutorial.

    +

    API Documentation

    +

    The rose.macro.MacroBase class (subclassed by all + rose macros) is documented here. +

    API Reference

    -

    Macros should subclass from - rose.macro.MacroBase. There are two types - of macro - checkers (validators) which do not alter a - configuration but provide error messages, and changers - (transformers) which return an altered configuration. A - macro class can contain one or both of these - behaviours.

    - -

    Your macro should provide a method called - validate (for checking) and/or a - method called transform (for - changing). These methods should accept two - rose.config.ConfigNode instances as - arguments - one is the configuration, and one is the - metadata configuration that provides information about - the configuration items.

    +

    Validator, transformer and reporter macros are + python classes which subclass from + rose.macro.MacroBase (api docs). + +

    These macros implement their behaviours by providing a + validate, transform or + report method. A macro can contain any + combination of these methods so, for example, a macro + might be both a validator and a transformer.

    + +

    These methods should accept two + rose.config.ConfigNode (api docs) + instances as arguments - one is the configuration, and + one is the metadata configuration that provides + information about the configuration items.

    A validator macro should look like:

    @@ -963,6 +970,8 @@ 

    API Reference

         def report(self, config, meta_config=None):
    +        """ Write some information about the configuration to a report file."""
    +        # Note: report methods do not have a return value.
             with open('report/file', 'r') as report_file:
                 report_file.write(str(config.get(["namelist:snowflakes"])))
     
    diff --git a/doc/rose-config-api.html b/doc/rose-config-api.html new file mode 100644 index 0000000000..06b88d10a0 --- /dev/null +++ b/doc/rose-config-api.html @@ -0,0 +1,28 @@ + + + + Rose API Documentation - Config + + + + + + + +
    +
    +

    Rose Config API

    +
    + +
    +

    View the API docs here. +

    +
    + + + diff --git a/doc/rose.html b/doc/rose.html index 022f91a84b..82f35b317d 100644 --- a/doc/rose.html +++ b/doc/rose.html @@ -281,6 +281,8 @@

    Content

  • Variables
  • + +
  • Rose Config API
  • diff --git a/doc/scripts/sphinx-install b/doc/scripts/sphinx-install new file mode 100755 index 0000000000..e0896d46fd --- /dev/null +++ b/doc/scripts/sphinx-install @@ -0,0 +1,38 @@ +#!/bin/bash +# Determines whether it is necessary to install sphinx-build, or whether to +# activate a virtualenv located in $ENV_PATH. + +ENV_PATH="$1" +SPHINX_VERSION="$2" +USE_ENV='false' + +# Activate virtualenv if present +if [[ ${SPHINX_DEV_MODE} == 'true' && -d "${ENV_PATH}" ]]; then + . "${ENV_PATH}"/bin/activate + USE_ENV='true' +fi + +# Compare installed sphinx-build version (if present). +python -c " +import sys +min_version = tuple((int(num) for num in '${SPHINX_VERSION}'.split('.'))) +cur_version_string = '$(sphinx-build --version | cut -d ' ' -f 3)' +try: + cur_version = tuple((int(num) for num in cur_version_string.split('.'))) +except ValueError: + sys.exit(1) # sphinx-build not installed. +if not cur_version or min_version > cur_version: + sys.exit(1) # sphinx-build version too old. +sys.exit(0) # sphinx-build sufficient. +" + +# Echo any required action: 'sphinx-install', 'sphinx-activate' or ''. +if [[ $? == 1 ]]; then + echo 'sphinx-install' + if [[ ${USE_ENV} == 'true' ]]; then + # Existing virtualenv has too early a version of sphinx-build. + rm -rf "${ENV_PATH}" + fi +elif [[ ${USE_ENV} == 'true' ]]; then + echo 'sphinx-activate' +fi diff --git a/doc/sphinx/Makefile b/doc/sphinx/Makefile new file mode 100755 index 0000000000..fd1e634b16 --- /dev/null +++ b/doc/sphinx/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/rose-api-doc.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/rose-api-doc.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/rose-api-doc" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/rose-api-doc" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/doc/sphinx/_static/- b/doc/sphinx/_static/- new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py new file mode 100644 index 0000000000..9a4b858038 --- /dev/null +++ b/doc/sphinx/conf.py @@ -0,0 +1,268 @@ +# -*- coding: utf-8 -*- +# +# rose-api-doc documentation build configuration file, created by +# sphinx-quickstart on Thu Jan 26 12:36:22 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +sys.path.append(os.path.abspath('exts')) +#sys.path.append(os.path.abspath('exts/napoleon')) +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + 'sphinx.ext.autosummary' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'rose-api-documentation' +copyright = u'2017, Met Office' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = 'HEAD' +# The full version, including alpha/beta/rc tags. +release = 'HEAD' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + 'page_width': '1080px' # Permit 80 character width code listings. +} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'rose-api-docdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'rose-api-doc.tex', u'rose-api-doc Documentation', + u'Metomi', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'rose-api-doc', u'rose-api-doc Documentation', + [u'Metomi'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'rose-api-doc', u'rose-api-doc Documentation', + u'Metomi', 'rose-api-doc', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/doc/sphinx/config.rst b/doc/sphinx/config.rst new file mode 100644 index 0000000000..9c008b2b13 --- /dev/null +++ b/doc/sphinx/config.rst @@ -0,0 +1,5 @@ +Rose Config API Reference +========================= + +.. automodule:: rose.config + :members: diff --git a/doc/sphinx/index.rst b/doc/sphinx/index.rst new file mode 100644 index 0000000000..de493760f7 --- /dev/null +++ b/doc/sphinx/index.rst @@ -0,0 +1,34 @@ +.. rose-api-doc documentation master file, created by + sphinx-quickstart on Thu Jan 26 12:36:22 2017. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Rose API Documentation +====================== + +This sub-site provides auto-generated API documentation for selected rose +modules. + +.. toctree:: + :caption: Table of Contents + :name: mastertoc + :maxdepth: 2 + + config + macro + + +.. toctree:: + :hidden: + :name: devtoc + + sphinx + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/sphinx/macro.rst b/doc/sphinx/macro.rst new file mode 100644 index 0000000000..1d86c678a6 --- /dev/null +++ b/doc/sphinx/macro.rst @@ -0,0 +1,19 @@ +Rose Macro API Reference +========================= + +.. _Rose Reference Guide: ../../../rose-api.html#macro + +Macro Introduction +------------------ + +Rose macros enable the checking, changing and reporting of configurations. +This document is meant to assist with the development of custom macros. + +For further information on rose macros see the `Rose Reference Guide`_. + + +Macro API +--------- + +.. automodule:: rose.macro + :members: MacroBase, MacroReport diff --git a/doc/sphinx/sphinx.rst b/doc/sphinx/sphinx.rst new file mode 100644 index 0000000000..f8ff9ba3f1 --- /dev/null +++ b/doc/sphinx/sphinx.rst @@ -0,0 +1,151 @@ +Sphinx Readme +============= + +This file is for rose developers writing documentation. It contains some useful +links and a quick description of the Sphinx documentation system. + +Building Docs +------------- + +.. code-block:: bash + + make -C doc # will build: clean doctest html tidy + +If ``sphinx-build`` is not installed (or is antiquated) ``make`` will install +sphinx in a virtualenv which it will then tidy. + +For development purposes export +``SPHINX_DEV_MODE=true`` to prevent this virtualenv being rebuilt/destroyed +each time ``make`` is invoked. + + +reStructuredText +---------------- + +Sphinx uses the `reStructuredText markup language +`_. + +It is sensitive to indentation and sometimes requires three-space indentation +(e.g. lines following ``.. something::`` should be flush with the letter +``s``). + + +Writing Docstrings +------------------ + +Sphinx has been configured to use the `Napoleon +`_ extension which +allows `autodoc `_ +to work with docstrings in the `Google format +`_ (`example module +`_). +Avoid convoluting docstrings with reStructuredText, if any un-expected +behaviour occurs when attempting to use reStructuredText in docstrings it will +likely be due to Napoleon. + +Some quick examples: + + +.. code-block:: python + + def Some_Class(object): + """Some summary. + + Note __init__ methods are not autodocumented, specify constructor + parameters in the class docstring. + + Args: + param1 (type): Description. + param2 (type): Really really really really really really + long description. + kwarg (type - optional): Description. + + """ # Blank line. + + def __init(self, param1, param2, kwarg=None): + pass + + def some_generator(self, param1, param2): + """Some summary. + + Args: + param1 (str/int): Argument can be of multiple types. + param2 (obj): Argument can have many types. + + Yields: + type: Some description, note that unlike the argument lines, + continuation lines in yields/returns sections are not indented. + + """ + + @classmethod + def some_function_with_multiple_return_values(cls): + """Some summary. + + Example: + >>> # Some doctest code. + >>> Some_Class().some_function_with_multiple_return_values() + ('param1', 'param2') + + Returns: + tuple - (param1, param2) + - param1 (str) - If a function returns a tuple you can if + desired list the components of the tuple like this. + - param2 (str) - Something else. + + """ + return ('param1', 'param2') + + +Writing Doctests +---------------- + +Examples [in docstrings] written in `doctest format +`_ will appear nicely +formatted in the API docs, as an added bonus they are testable (``make -C doc +doctest``, incorporated in the rose test battery). + +Use ``>>>`` for statements and ``...`` for continuation lines. Any return +values will have to be provided and should sit on the next newline. + +.. code-block:: python + + >>> import rose.config + >>> rose.config.ConfigNode() + {'state': '', 'comments': [], 'value': {}} + +If return values are not known in advance use ellipses: + +.. code-block:: python + + >>> import time + >>> print 'here', time.time(), 'there' + here ... there + +If return values are lengthy use ``NORMALIZE_WHITESPACE`` (see source code for +this page): + +.. code-block:: python + + >>> print [1,2,3] # doctest: +NORMALIZE_WHITESPACE + [1, + 2, + 3] + +Note that you can ONLY break a line on a comma i.e. this wont work (note the +``+SKIP`` directive [in the source code for this page] prevents this doctest +from being run): + +.. code-block:: python + + >>> print {'a': {'b': {}}} # doctest: +NORMALIZE_WHITESPACE, +SKIP + {'a': + {'b': {} + }} + +Doctests are performed in the doc/sphinx directory and any files created will +have to be `tidied up +`_. + +See `doctest `_ for more +details. diff --git a/lib/python/rose/config.py b/lib/python/rose/config.py index cb5e5f41a8..b3b75b4bd6 100644 --- a/lib/python/rose/config.py +++ b/lib/python/rose/config.py @@ -19,39 +19,74 @@ # ----------------------------------------------------------------------------- """Simple INI-style configuration data model, loader and dumper. -Synopsis: +.. testsetup:: * + + import os + from rose.config import * + +.. testcleanup:: rose.config - import rose.config try: - config = rose.config.load(file) - except rose.config.ConfigSyntaxError: - # ... do something to handle exception - value = config.get([section, option]) - # ... + os.remove('config.conf') + except OSError: + pass + +Synopsis: + >>> # Create a config file. + >>> with open('config.conf', 'w+') as config_file: + ... config_file.write(''' + ... [foo] + ... bar=Bar + ... !baz=Baz + ... ''') + + >>> # Load in a config file. + >>> try: + ... config_node = load('config.conf') + ... except ConfigSyntaxError: + ... # Handle exception. + ... pass + + >>> # Retrieve config settings. + >>> config_node.get(['foo', 'bar']) + {'state': '', 'comments': [], 'value': 'Bar'} + + >>> # Set new config settings. + >>> _ = config_node.set(['foo', 'new'], 'New') + + >>> # Overwrite existing config settings. + >>> _ = config_node.set(['foo', 'baz'], state=ConfigNode.STATE_NORMAL, + ... value='NewBaz') + + >>> # Write out config to a file. + >>> dump(config_node, sys.stdout) + [foo] + bar=Bar + baz=NewBaz + new=New - config.set([section, option], value) - # ... - rose.config.dump(config, file) Classes: - ConfigNode - represents an individual node (setting or section). - ConfigNodeDiff - represent differences between ConfigNode instances. - ConfigDumper - dumps a configuration to stdout or a file. - ConfigLoader - loads a configuration into a Config instance. + .. autosummary:: + rose.config.ConfigNode + rose.config.ConfigNodeDiff + rose.config.ConfigDumper + rose.config.ConfigLoader Functions: - dump - shorthand for ConfigDumper().dump - load - shorthand for ConfigLoader().load + .. autosummary:: + rose.config.load + rose.config.dump Limitations: - * The loader does not handle trailing comments. + - The loader does not handle trailing comments. What about the standard library ConfigParser? Well, it is problematic: - * The comment character and style is hard-coded. - * The assignment character is hard-coded. - * A duplicated section header causes an exception to be raised. - * Option keys are transformed to lower case by default. - * It is far too complicated and confusing. + - The comment character and style is hard-coded. + - The assignment character is hard-coded. + - A duplicated section header causes an exception to be raised. + - Option keys are transformed to lower case by default. + - It is far too complicated and confusing. """ @@ -79,14 +114,67 @@ class ConfigNode(object): - """Represent a node in a configuration file.""" + """Represent a node in a configuration file. + + Nodes are stored hierarchically, for instance the following config + [foo] + bar = Bar + + When loaded by ConfigNode.load(file) would result in three levels of + ConfigNodes, the first representing "root" (i.e. the top level of the + config), one representing the config section "foo" and one representing the + setting "bar". + + Examples: + >>> # Create a new ConfigNode. + >>> config_node = ConfigNode() + + >>> # Add sub-nodes. + >>> _ = config_node.set(keys=['foo', 'bar'], value='Bar') + >>> _ = config_node.set(keys=['foo', 'baz'], value='Baz') + >>> config_node # doctest: +NORMALIZE_WHITESPACE + {'state': '', 'comments': [], + 'value': {'foo': {'state': '', 'comments': [], + 'value': {'baz': {'state': '', 'comments': [], + 'value': 'Baz'}, + 'bar': {'state': '', 'comments': [], + 'value': 'Bar'}}}}} + + >>> # Set the state of a node. + >>> _ = config_node.set(keys=['foo', 'bar'], + ... state=ConfigNode.STATE_USER_IGNORED) + + >>> # Get the value of the node at a position. + >>> config_node.get_value(keys=['foo', 'baz']) + 'Baz' + + >>> # Walk over the hierarchical structure of a node. + >>> [keys for keys, sub_node in config_node.walk()] + [['foo'], ['foo', 'bar'], ['foo', 'baz']] + + >>> # Walk over the config skipping ignored sections. + >>> [keys for keys, sub_node in config_node.walk(no_ignore=True)] + [['foo'], ['foo', 'baz']] + + >>> # Add two ConfigNode instances to create a new "merged" node. + >>> another_config_node = ConfigNode() + >>> _ = another_config_node.set(keys=['new'], value='New') + >>> new_config_node = config_node + another_config_node + >>> [keys for keys, sub_node in new_config_node.walk()] + [['foo'], ['foo', 'baz'], ['foo', 'bar'], ['', 'new']] + + """ __slots__ = ["STATE_NORMAL", "STATE_USER_IGNORED", "STATE_SYST_IGNORED", "value", "state", "comments"] STATE_NORMAL = "" + """The default state of a ConfigNode.""" STATE_USER_IGNORED = "!" + """ConfigNode state if it has been specifically ignored in the config.""" STATE_SYST_IGNORED = "!!" + """ConfigNode state if a metadata opperation has logically ignored the + config.""" def __init__(self, value=None, state=STATE_NORMAL, comments=None): if value is None: @@ -143,19 +231,48 @@ def __ne__(self, other): return not self.__eq__(other) def is_ignored(self): - """Return true if current node is in the "ignored" state.""" + """Return True if current node is in the "ignored" state.""" return self.state != self.STATE_NORMAL def walk(self, keys=None, no_ignore=False): """Return all keylist - sub-node pairs below keys. - keys is a list defining a hierarchy of node.value - 'keys'. If an entry in keys is the null string, - it is skipped. - - If a sub-node is at the top level, and does not - contain any node children, a null string will be - prepended to the returned keylist. + Args: + keys (list): A list defining a hierarchy of node.value 'keys'. + If an entry in keys is the null string, it is skipped. + no_ignore (bool): If True any ignored nodes will be skipped. + + Yields: + tuple - (keys, sub_node) + - keys (list) - A list defining a hierarchy of node.value + 'keys'. If a sub-node is at the top level, and does not + contain any node children, a null string will be + prepended to the returned keylist. + - sub_node (ConfigNode) - The config node at the position of + keys. + + Examples: + >>> config_node = ConfigNode() + >>> _ = config_node.set(['foo', 'bar'], 'Bar') + >>> _ = config_node.set(['foo', 'baz'], 'Baz', + ... state=ConfigNode.STATE_USER_IGNORED) + + >>> # Walk over the full hierarchy. + >>> [keys for keys, sub_node in config_node.walk()] + [['foo'], ['foo', 'bar'], ['foo', 'baz']] + + >>> # Walk over one branch of the hierarchy + >>> [keys for keys, sub_node in config_node.walk(keys=['foo'])] + [['foo', 'bar'], ['foo', 'baz']] + + >>> # Skip over ignored nodes. + >>> [keys for keys, sub_node in config_node.walk(no_ignore=True)] + [['foo'], ['foo', 'bar']] + + >>> # Invalid/non-existent keys. + >>> [keys for keys, sub_node in config_node.walk( + ... keys=['elephant'])] + [] """ if keys is None: @@ -183,13 +300,37 @@ def walk(self, keys=None, no_ignore=False): def get(self, keys=None, no_ignore=False): """Return a node at the position of keys, if any. - keys is a list defining a hierarchy of node.value - 'keys'. If an entry in keys is the null string, - it is skipped. - - no_ignore switches on filtering nodes by their - ignored status. If True, ignored nodes will not be - returned. If False, ignored nodes will be returned. + Args: + keys (list, optional): A list defining a hierarchy of + node.value 'keys'. If an entry in keys is the null + string, it is skipped. + no_ignore (bool, optional): If True any ignored nodes will + not be returned. + + Returns: + ConfigNode: The config node located at the position of keys or + None. + + Examples: + >>> # Create ConfigNode. + >>> config_node = ConfigNode() + >>> _ = config_node.set(['foo', 'bar'], 'Bar') + >>> _ = config_node.set(['foo', 'baz'], 'Baz', + ... state=ConfigNode.STATE_USER_IGNORED) + + >>> # A ConfigNode containing sub-nodes. + >>> config_node.get(keys=['foo']) # doctest: +NORMALIZE_WHITESPACE + {'state': '', 'comments': [], + 'value': {'baz': {'state': '!', 'comments': [], 'value': 'Baz'}, + 'bar': {'state': '', 'comments': [], 'value': 'Bar'}}} + + >>> # A bottom level sub-node. + >>> config_node.get(keys=['foo', 'bar']) + {'state': '', 'comments': [], 'value': 'Bar'} + + >>> # Skip ignored nodes. + >>> print config_node.get(keys=['foo', 'baz'], no_ignore=True) + None """ if not keys: @@ -207,9 +348,29 @@ def get(self, keys=None, no_ignore=False): return node def get_filter(self, no_ignore): - """Return None if no_ignore and node is in ignored state. + """Return this ConfigNode unless no_ignore and node is ignored. + + Args: + no_ignore (bool): If True only return this node if it is not + ignored. + + Returns: + ConfigNode: This ConfigNode unless no_ignore and node is ignored. + + Examples: + >>> config_node = ConfigNode(value=42) + >>> config_node.get_filter(False) + {'state': '', 'comments': [], 'value': 42} + >>> config_node.get_filter(True) + {'state': '', 'comments': [], 'value': 42} + + >>> config_node = ConfigNode(value=42, + ... state=ConfigNode.STATE_USER_IGNORED) + >>> config_node.get_filter(False) + {'state': '!', 'comments': [], 'value': 42} + >>> print config_node.get_filter(True) + None - Return self otherwise. """ if no_ignore and self.state: @@ -221,25 +382,85 @@ def get_value(self, keys=None, default=None): If the node does not exist or is ignored, return None. + Args: + keys (list, optional): A list defining a hierarchy of node.value + 'keys'. If an entry in keys is the null string, it is skipped. + default (obj, optional): Return default if the value is not set. + + Returns: + obj: The value of this ConfigNode at the position of keys or + default if not set. + + Examples: + >>> # Create ConfigNode. + >>> config_node = ConfigNode(value=42) + >>> _ = config_node.set(['foo'], 'foo') + >>> _ = config_node.set(['foo', 'bar'], 'Bar') + + >>> # Get value without specifying keys returns the value of the + >>> # root ConfigNode (which in this case is a dict of its + >>> # sub-nodes). + >>> config_node.get_value() # doctest: +NORMALIZE_WHITESPACE + {'foo': {'state': '', 'comments': [], + 'value': {'bar': {'state': '', 'comments': [], + 'value': 'Bar'}}}} + + >>> # Intermediate level ConfigNode. + >>> config_node.get_value(keys=['foo']) + {'bar': {'state': '', 'comments': [], 'value': 'Bar'}} + + >>> # Bottom level ConfigNode. + >>> config_node.get_value(keys=['foo', 'bar']) + 'Bar' + + >>> # If there is no node located at the position of keys or if + >>> # that node is unset then the default value is returned. + >>> config_node.get_value(keys=['foo', 'bar', 'baz'], + ... default=True) + True """ return getattr(self.get(keys, no_ignore=True), "value", default) def set(self, keys=None, value=None, state=None, comments=None): + # TODO: Are keys=[''] really skipped? + # TODO: Should comments be a list? """Set node properties at the position of keys, if any. - keys is a list defining a hierarchy of node.value - 'keys'. If an entry in keys is the null string, - it is skipped. - - value defines the node.value property at this position. - - state defines the node.state property at this position. - - comments defines the node.comments property at this position. - - If state is None, the node.state property is unchanged. - - If comments is None, the node.comments property is unchanged. + Arguments: + keys (list): A list defining a hierarchy of node.value 'keys'. + If an entry in keys is the null string, it is skipped. + value (obj): The node.value property to set at this position. + state (str): The node.state property to set at this position. + If None, the node.state property is unchanged. + comments (str): The node.comments property to set at this position. + If None, the node.comments property is unchanged. + + Returns: + ConfigNode: This config node. + + Examples: + >>> # Create ConfigNode. + >>> config_node = ConfigNode() + >>> config_node + {'state': '', 'comments': [], 'value': {}} + + >>> # Add a sub-node at the position 'foo' with the comment 'Info'. + >>> config_node.set(keys=['foo'], comments='Info') + ... # doctest: +NORMALIZE_WHITESPACE + {'state': '', 'comments': [], + 'value': {'foo': {'state': '', 'comments': 'Info', 'value': {}}}} + + >>> # Set the value for the sub-node at the position + >>> # 'foo' to 'Foo'. + >>> config_node.set(keys=['foo'], value='Foo') + ... # doctest: +NORMALIZE_WHITESPACE + {'state': '', 'comments': [], 'value': {'foo': {'state': '', + 'comments': 'Info', 'value': 'Foo'}}} + + >>> # Set the value of the ConfigNode to True, this overwrites all + >>> # sub-nodes! + >>> config_node.set(keys=[''], value=True) + {'state': '', 'comments': [], 'value': True} """ if keys is None: @@ -268,11 +489,34 @@ def set(self, keys=None, value=None, state=None, comments=None): return self def unset(self, keys=None): - """Remove a node at this (keys) position, if any. + """Remove a node at the position of keys, if any. + + Args: + keys (list): A list defining a hierarchy of node.value 'keys'. + If an entry in keys is the null string, it is skipped. + + Returns: + ConfigNode: The ConfigNode instance that was removed, else None. + + Examples: + >>> # Create ConfigNode. + >>> config_node = ConfigNode() + >>> _ = config_node.set(keys=['foo'], value='Foo') - keys is a list defining a hierarchy of node.value - 'keys'. If an entry in keys is the null string, - it is skipped. + >>> # Unset without providing any keys does nothing. + >>> print config_node.unset() + None + + >>> # Unset with invalid keys does nothing. + >>> print config_node.unset(keys=['bar']) + None + + >>> # Unset with valid keys removes the node from the node. + >>> config_node.unset(keys=['foo']) + {'state': '', 'comments': [], 'value': 'Foo'} + + >>> config_node + {'state': '', 'comments': [], 'value': {}} """ if keys is None: @@ -287,7 +531,28 @@ def unset(self, keys=None): return None def add(self, config_diff): - """Apply a config node diff object to self.""" + """Apply a ConfigNodeDiff object to self. + + Args: + config_diff (ConfigNodeDiff): A diff to apply to this ConfigNode. + + Examples: + >>> # Create ConfigNode + >>> config_node = ConfigNode() + >>> _ = config_node.set(keys=['foo', 'bar'], value='Bar') + >>> [keys for keys, sub_node in config_node.walk()] + [['foo'], ['foo', 'bar']] + + >>> # Create ConfigNodeDiff + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_added_setting(keys=['foo', 'baz'], + ... data='Baz') + + >>> # Apply ConfigNodeDiff to ConfigNode + >>> config_node.add(config_node_diff) + >>> [keys for keys, sub_node in config_node.walk()] + [['foo'], ['foo', 'bar'], ['foo', 'baz']] + """ for added_key, added_data in config_diff.get_added(): value, state, comments = added_data self.set(keys=added_key, value=value, state=state, @@ -312,9 +577,35 @@ def add(self, config_diff): self.unset(keys=removed_key) def __add__(self, config_diff): - """Return a new node by applying a config node diff object to self. - Alternatively a config node can be provided, the diff will then be - applied to self to return a new node.""" + """Return a new node by applying a ConfigNodeDiff or ConfigNode to self. + + Create a new node by applying either a ConfigNodeDiff or ConfigNode + instance to this ConfigNode. + + Arguments: + config_diff - Either a ConfigNodeDiff or a ConfigNode. + + Returns: + node - The new ConfigNode. + + Examples: + >>> # Add one ConfigNode to another ConfigNode + >>> config_node_1 = ConfigNode() + >>> config_node_1.set(keys=['foo'], value='Foo') + >>> config_node_2 = ConfigNode() + >>> config_node_2.set(keys=['bar'], value='Bar') + >>> new_config_node = config_node_1 + config_node_2 + >>> [keys for keys, sub_node in new_config_node.walk()] + [['', 'bar'], ['', 'foo']] + + >>> # Add a ConfigNodeDiff to a ConfigNode + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_added_setting(keys=['baz'], data='Baz') + >>> new_config_node = config_node_1 + config_node_diff + >>> [keys for keys, sub_node in new_config_node.walk()] + [['', 'baz'], ['', 'foo']] + + """ if type(config_diff) is ConfigNode: config_node = config_diff config_diff = ConfigNodeDiff() @@ -325,7 +616,30 @@ def __add__(self, config_diff): return new_node def __sub__(self, other_config_node): - """Produce a diff from another node.""" + """Produce a ConfigNodeDiff from another ConfigNode. + + Arguments: + other_config_node - The ConfigNode to be applied to this ConfigNode + to produce the ConfigNodeDiff. + + Returns: + config_diff - A ConfigNodeDiff instance. + + Examples: + >>> # Create a ConfigNodeDiff from two ConfigNodes + >>> config_node_1 = ConfigNode() + >>> config_node_1.set(keys=['foo'], value='Foo') + >>> config_node_2 = ConfigNode() + >>> config_node_2.set(keys=['bar'], value='Bar') + >>> config_node_diff = config_node_1 - config_node_2 + + >>> config_node_diff.get_added() + [(('', 'foo'), ('Foo', '', []))] + + >>> config_node_diff.get_removed() + [(('', 'bar'), ('Bar', '', []))] + + """ diff = ConfigNodeDiff() diff.set_from_configs(other_config_node, self) return diff @@ -349,7 +663,44 @@ def __setstate__(self, state): class ConfigNodeDiff(object): - """Represent differences between two ConfigNode instances.""" + """Represent differences between two ConfigNode instances. + + Examples: + >>> # Create a new ConfigNodeDiff. + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_added_setting(keys=['bar'], + ... data=('Bar', None, None,)) + + >>> # Create a new ConfigNode. + >>> config_node = ConfigNode() + >>> _ = config_node.set(keys=['baz'], value='Baz') + + >>> # Apply the diff to the node. + >>> config_node.add(config_node_diff) + >>> [(keys, sub_node.get_value()) for keys, sub_node in + ... config_node.walk()] + [(['', 'baz'], 'Baz'), (['', 'bar'], 'Bar')] + + >>> # Create a ConfigNodeDiff by comparing two ConfigNodes. + >>> another_config_node = ConfigNode() + >>> _ = another_config_node.set(keys=['bar'], value='NewBar') + >>> _ = another_config_node.set(keys=['new'], value='New') + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_from_configs(config_node, + ... another_config_node) + >>> config_node_diff.get_added() + [(('', 'new'), ('New', '', []))] + >>> config_node_diff.get_removed() + [(('', 'baz'), ('Baz', '', []))] + >>> config_node_diff.get_modified() + [(('', 'bar'), (('Bar', '', []), ('NewBar', '', [])))] + + >>> # Inverse a ConfigNodeDiff. + >>> reversed_diff = config_node_diff.get_reversed() + >>> reversed_diff.get_added() + [(('', 'baz'), ('Baz', '', []))] + + """ KEY_ADDED = "added" KEY_MODIFIED = "modified" @@ -360,7 +711,30 @@ def __init__(self): self.KEY_MODIFIED: {}} def set_from_configs(self, config_node_1, config_node_2): - """Create diff data from two ConfigNode instances.""" + """Create diff data from two ConfigNode instances. + + Args: + config_node_1 (ConfigNode): The node for which to base the diff + off of. + config_node_2 (ConfigNode): The "new" node the changes of which + this diff will "apply". + + Example: + >>> # Create two ConfigNode instances to compare. + >>> config_node_1 = ConfigNode() + >>> _ = config_node_1.set(keys=['foo']) + >>> config_node_2 = ConfigNode() + >>> _ = config_node_2.set(keys=['bar']) + + >>> # Create a ConfigNodeDiff instance. + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_from_configs(config_node_1, config_node_2) + >>> config_node_diff.get_added() + [(('bar',), (None, '', []))] + >>> config_node_diff.get_removed() + [(('foo',), (None, '', []))] + + """ settings_1 = {} settings_2 = {} for config_node, settings in [(config_node_1, settings_1), @@ -385,6 +759,20 @@ def get_as_opt_config(self): Add all the added settings, add all the modified settings, add all the removed settings as user-ignored. + Returns: + ConfigNode: A new ConfigNode instance. + + Example: + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_added_setting(['foo'], + ... ('Foo', None, None,)) + >>> config_node_diff.set_removed_setting(['bar'], + ... ('Bar', None, None,)) + >>> config_node = config_node_diff.get_as_opt_config() + >>> list(config_node.walk()) # doctest: +NORMALIZE_WHITESPACE + [(['', 'bar'], {'state': '!', 'comments': [], 'value': 'Bar'}), + (['', 'foo'], {'state': '', 'comments': [], 'value': 'Foo'})] + """ node = ConfigNode() for keys, old_and_new_info in self.get_modified(): @@ -402,15 +790,92 @@ def get_as_opt_config(self): return node def set_added_setting(self, keys, data): - """Set a setting to be "added".""" + # TODO: type(comments) == str ? + """Set a config setting to be "added" in this ConfigNodeDiff. + + Args: + keys (list/tuple): The position of the setting to add. + data (obj, str, str): A tuple (value, state, comments) for the + setting to add. + + Examples: + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_added_setting(['foo'], + ... ('Foo', None, None,)) + >>> config_node_diff.set_added_setting( + ... ['bar'], + ... ('Bar', ConfigNode.STATE_USER_IGNORED, 'Some Info',)) + + >>> config_node = ConfigNode() + >>> config_node.add(config_node_diff) + + >>> list(config_node.walk()) # doctest: +NORMALIZE_WHITESPACE + [(['', 'bar'], {'state': '!', 'comments': 'Some Info', + 'value': 'Bar'}), + (['', 'foo'], {'state': '', 'comments': [], 'value': 'Foo'})] + + """ + keys = tuple(keys) self._data[self.KEY_ADDED][keys] = data def set_modified_setting(self, keys, old_data, data): - """Set a setting to be "modified".""" + # TODO: Really? + """Set a config setting to be "modified" in this ConfigNodeDiff. + + If a property in both the old_data and data (new data) are both set to + None then no change will be made to any pre-existing value. + + Args: + keys (list/tuple): The position of the setting to add. + old_data (obj, str, str): A tuple (value, state, comments) for + the "current" properties of the setting to modify. + data (obj, str, str): A tuple (value, state, comments) for "new" + properties to change this setting to. + + Examples: + >>> # Create a ConfigNodeDiff. + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_modified_setting( + ... ['foo'], ('Foo', None, None), ('New Foo', None, None)) + + >>> # Create a ConfigNode. + >>> config_node = ConfigNode() + >>> _ = config_node.set(keys=['foo'], value='Foo', + ... comments='Some Info') + + >>> # Apply the ConfigNodeDiff to the ConfigNode + >>> config_node.add(config_node_diff) + >>> config_node.get(keys=['foo']) + {'state': '', 'comments': 'Some Info', 'value': 'New Foo'} + """ + keys = tuple(keys) self._data[self.KEY_MODIFIED][keys] = (old_data, data) def set_removed_setting(self, keys, data): - """Set a setting to be "removed".""" + """Set a config setting to be "removed" in this ConfigNodeDiff. + + Arguments: + keys (list): The position of the setting to add. + data (obj, str, str): A tuple (value, state, comments) of the + properties for the setting to remove. + + Example: + >>> # Create a ConfigNodeDiff. + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_removed_setting(['foo'], ('X', 'Y', 'Z')) + + >>> # Create a ConfigNode. + >>> config_node = ConfigNode() + >>> _ = config_node.set(keys=['foo'], value='Foo', + ... comments='Some Info') + + >>> # Apply the ConfigNodeDiff to the ConfigNode + >>> config_node.add(config_node_diff) + >>> print config_node.get(keys=['foo']) + None + + """ + keys = tuple(keys) self._data[self.KEY_REMOVED][keys] = data def get_added(self): @@ -419,6 +884,19 @@ def get_added(self): The data is a tuple of value, state, comments, where value is set to None for sections. + Returns: + list: A list of the form [(keys, data), ...] + - keys - The position of an added setting. + - data - Tuple of the form (value, state, comments) of the + properties of the added setting. + + Examples: + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_added_setting(['foo'], + ... ('Foo', None, None)) + >>> config_node_diff.get_added() + [(('foo',), ('Foo', None, None))] + """ return sorted(self._data[self.KEY_ADDED].items()) @@ -428,6 +906,21 @@ def get_modified(self): The data is a list of two tuples (before and after) of value, state, comments, where value is set to None for sections. + Returns: + list: A list of the form [(keys, data), ...]: + - keys - The position of an added setting. + - data - Tuple of the form (value, state, comments) for the + properties of the setting before the modification. + - old_data - The same tuple as data but representing the + properties of the setting after the modification. + + Examples: + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_modified_setting( + ... ['foo'], ('Foo', None, None), ('New Foo', None, None)) + >>> config_node_diff.get_modified() + [(('foo',), (('Foo', None, None), ('New Foo', None, None)))] + """ return sorted(self._data[self.KEY_MODIFIED].items()) @@ -437,18 +930,62 @@ def get_removed(self): The data is a tuple of value, state, comments, where value is set to None for sections. + Returns: + list - A list of the form [(keys, data), ...]: + - keys - The position of an added setting. + - data - Tuple of the form (value, state, comments) of the + properties of the removed setting. + + Examples: + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_removed_setting(keys=['foo'], + ... data=('foo', None, None)) + >>> config_node_diff.get_removed() + [(('foo',), ('foo', None, None))] + """ return sorted(self._data[self.KEY_REMOVED].items()) def get_all_keys(self): - """Return all changed keys.""" + """Return all keys affected by this ConfigNodeDiff. + + Returns: + list: A list containing any keys affected by this diff as tuples, + e.g. ('foo', 'bar'). + + Examples: + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_added_setting(['foo'],('foo', None, None)) + >>> config_node_diff.set_removed_setting(['bar'], + ... ('bar', None, None)) + >>> config_node_diff.get_all_keys() + [('bar',), ('foo',)] + """ return sorted( set(self._data[self.KEY_ADDED]) | set(self._data[self.KEY_MODIFIED]) | set(self._data[self.KEY_REMOVED])) def get_reversed(self): - """Return an inverse (add->remove, etc) copy of this diff.""" + """Return an inverse (add->remove, etc) copy of this ConfigNodeDiff. + + Returns: + ConfigNodeDiff: A new ConfigNodeDiff instance. + + Examples: + >>> # Create ConfigNodeDiff instance. + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_added_setting(['foo'],('foo', None, None)) + >>> config_node_diff.set_removed_setting(['bar'], + ... ('bar', None, None)) + + >>> # Generate reversed diff. + >>> reversed_diff = config_node_diff.get_reversed() + >>> reversed_diff.get_added() + [(('bar',), ('bar', None, None))] + >>> reversed_diff.get_removed() + [(('foo',), ('foo', None, None))] + """ rev_diff = ConfigNodeDiff() for keys, data in self.get_removed(): rev_diff.set_added_setting(keys, data) @@ -459,13 +996,36 @@ def get_reversed(self): return rev_diff def delete_removed(self): - """Deletes all 'removed' keys from this diff.""" + """Deletes all 'removed' keys from this ConfigNodeDiff.. + + Examples: + >>> config_node_diff = ConfigNodeDiff() + >>> config_node_diff.set_removed_setting(['foo'], + ... ('foo', None, None)) + >>> config_node_diff.delete_removed() + >>> config_node_diff.get_removed() + [] + """ self._data[self.KEY_REMOVED] = {} class ConfigDumper(object): - """Dumper of a ConfigNode object in Rose INI format.""" + """Dumper of a ConfigNode object in Rose INI format. + + Examples: + >>> config_node = ConfigNode() + >>> _ = config_node.set(keys=['foo', 'bar'], value='Bar') + >>> _ = config_node.set(keys=['foo', 'baz'], value='Baz', + ... comments=['Currently ignored!'], + ... state=ConfigNode.STATE_USER_IGNORED) + >>> dumper = ConfigDumper() + >>> dumper(config_node, sys.stdout) + [foo] + bar=Bar + #Currently ignored! + !baz=Baz + """ def __init__(self, char_assign=CHAR_ASSIGN): """Initialise the configuration dumper utility. @@ -480,20 +1040,20 @@ def dump(self, root, target=sys.stdout, sort_sections=None, sort_option_items=None, env_escape_ok=False, concat_mode=False): """Format a ConfigNode object and write result to target. - Arguments: - root -- the root node, a ConfigNode object. - target -- an open file handle or a string containing a - file path. If not specified, the result is written to - sys.stdout. - sort_sections -- an optional argument that should be a function - for sorting a list of section keys. - sort_option_items -- an optional argument that should be a - function for sorting a list of option (key, value) tuples. - in string values. - env_escape_ok -- an optional argument to indicate that $NAME - and ${NAME} syntax in values should be escaped. - concat_mode -- switch on concatenation mode. If True, add [] - before root level options. + Args: + root (ConfigNode): The root config node. + target (str/file): An open file handle or a string containing a + file path. If not specified, the result is written to + sys.stdout. + sort_sections (fcn - optional): An optional argument that should be + a function for sorting a list of section keys. + sort_option_items (fcn - optional): An optional argument that + should be a function for sorting a list of option (key, value) + tuples in string values. + env_escape_ok (bool - optional): An optional argument to indicate + that $NAME and ${NAME} syntax in values should be escaped. + concat_mode (bool - optional): Switch on concatenation mode. If + True, add [] before root level options. """ if sort_sections is None: @@ -586,7 +1146,23 @@ def _comment_format(cls, comment): class ConfigLoader(object): - """Loader of an INI format configuration into a Config object.""" + """Loader of an INI format configuration into a ConfigNode object. + + Example: + >>> with open('config.conf', 'w+') as config_file: + ... config_file.write(''' + ... [foo] + ... !bar=Bar + ... baz=Baz + ... ''') + >>> loader = ConfigLoader() + >>> try: + ... config_node = loader.load('config.conf') + ... except ConfigSyntaxError: + ... raise # Handle exception. + >>> config_node.get(keys=['foo', 'bar']) + {'state': '!', 'comments': [], 'value': 'Bar'} + """ RE_SECTION = re.compile( r"^(?P\s*\[(?P!?!?))(?P
    .*)\]\s*$") @@ -625,32 +1201,73 @@ def load_with_opts(self, source, node=None, more_keys=None, """Read a source configuration file with optional configurations. Arguments: - source -- A string for a file path. - node --- A ConfigNode object if specified, otherwise created. - more_keys -- A list of additional optional configuration names. If - source is "rose-${TYPE}.conf", the file of each name - should be "opt/rose-${TYPE}-${NAME}.conf". - used_keys -- If defined, it should be a list for this method to append - to. The key of each successfully loaded optional - configuration will be appended to the list (unless the key - is already in the list). Missing optional configurations - that are specified in more_keys will not raise an error. - If not defined, any missing optional configuration will - trigger an OSError. - mark_opt_configs (default False) if True, add comments above any - settings which have been loaded from an optional - config. - return_config_map -- (default False) if True, construct and return a - dict (config_map) containing config names vs - their uncombined nodes. Optional configurations - use their opt keys as keys, and the main - configuration uses 'None'. - - Return node if return_config_map is False (default). - Return node, config_map if return_config_map is True. + source (str): A file path. + node (ConfigNode - optional): A ConfigNode object if specified, + otherwise one is created. + more_keys (list - optional): A list of additional optional + configuration names. If source is "rose-${TYPE}.conf", the + file of each name should be "opt/rose-${TYPE}-${NAME}.conf". + used_keys (list - optional): If defined, it should be a list for + this method to append to. The key of each successfully loaded + optional configuration will be appended to the list (unless the + key is already in the list). Missing optional configurations + that are specified in more_keys will not raise an error. + If not defined, any missing optional configuration will + trigger an OSError. + mark_opt_configs (bool - optional): if True, add comments above any + settings which have been loaded from an optional config. + return_config_map (bool - optional): If True, construct and return + a dict (config_map) containing config names vs their uncombined + nodes. Optional configurations use their opt keys as keys, and + the main configuration uses 'None'. + + Returns: + tuple: node or (node, config_map): + - node - The loaded configuration as a ConfigNode. + - config_map - A dictionary containing opt_conf_key: ConfigNode + pairs. Only returned if return_config_map is True. + + Examples: + .. testcleanup:: rose.config.ConfigLoader.load_with_opts + + try: + os.remove('config.conf') + os.remove('opt/config-foo.conf') + os.rmdir('opt') + except OSError: + pass + + >>> # Write config file. + >>> with open('config.conf', 'w+') as config_file: + ... config_file.write(''' + ... [foo] + ... bar=Bar + ... ''') + >>> # Write optional config file (foo). + >>> os.mkdir('opt') + >>> with open('opt/config-foo.conf', 'w+') as opt_config_file: + ... opt_config_file.write(''' + ... [foo] + ... bar=Baz + ... ''') + + >>> loader = ConfigLoader() + >>> config_node, config_map = loader.load_with_opts( + ... 'config.conf', more_keys=['foo'], return_config_map=True) + >>> config_node.get_value(keys=['foo', 'bar']) + 'Baz' + + >>> original_config_node = config_map[None] + >>> original_config_node.get_value(keys=['foo', 'bar']) + 'Bar' + + >>> optional_config_node = config_map['foo'] + >>> optional_config_node.get_value(keys=['foo', 'bar']) + 'Baz' """ - node = self.load(source, node) + if not node: + node = self.load(source, node) if return_config_map: config_map = {None: copy.deepcopy(node)} opt_conf_keys_node = node.unset(["opts"]) @@ -700,10 +1317,30 @@ def load(self, source, node=None, default_comments=None): """Read a source configuration file. Arguments: - source -- an open file handle or a string for a file path. - node --- a ConfigNode object if specified, otherwise created. - - Return node. + source (str): An open file handle or a string for a file path. + node (ConfigNode): A ConfigNode object if specified, otherwise + created. + + Returns: + ConfigNode: A new ConfigNode object. + + Examples: + >>> # Create example config file. + >>> with open('config.conf', 'w+') as config_file: + ... config_file.write(''' + ... [foo] + ... # Some comment + ... !bar=Bar + ... ''') + + >>> # Load config file. + >>> loader = ConfigLoader() + >>> try: + ... config_node = loader.load('config.conf') + ... except ConfigSyntaxError: + ... raise # Handle exception. + >>> config_node.get(keys=['foo', 'bar']) + {'state': '!', 'comments': [' Some comment'], 'value': 'Bar'} """ if node is None: @@ -857,14 +1494,26 @@ class ConfigSyntaxError(Exception): """Exception raised for syntax error loading a configuration file. - It has the following attributes: - exc.code -- Error code. Can be one of: + Attributes: + exc.code: Error code. Can be one of: ConfigSyntaxError.BAD_CHAR (bad characters in a name) ConfigSyntaxError.BAD_SYNTAX (general syntax error) - exc.file_name -- The name of the file that triggers the error. - exc.line_num -- The line number (from 1) in the file with the error. - exc.col_num -- The column number (from 0) in the line with the error. - exc.line -- The content of the line that contains the error. + exc.file_name: The name of the file that triggers the error. + exc.line_num: The line number (from 1) in the file with the error. + exc.col_num: The column number (from 0) in the line with the error. + exc.line: The content of the line that contains the error. + + Examples: + >>> with open('config.conf', 'w+') as config_file: + ... config_file.write('[foo][foo]') + >>> loader = ConfigLoader() + >>> try: + ... loader.load('config.conf') + ... except ConfigSyntaxError as exc: + ... print 'Error (%s) in file "%s" at %s:%s' % ( + ... exc.code, exc.file_name, exc.line_num, exc.col_num) + Error (BAD_CHAR) in file "..." at 1:5 + """ BAD_CHAR = "BAD_CHAR" diff --git a/lib/python/rose/macro.py b/lib/python/rose/macro.py index 4bf0755162..cb79415b0b 100644 --- a/lib/python/rose/macro.py +++ b/lib/python/rose/macro.py @@ -18,6 +18,21 @@ # along with Rose. If not, see . # ----------------------------------------------------------------------------- """ +.. testsetup:: * + + import os + from rose.macro import * + + def test_cleanup(stuff_to_remove): + for item in stuff_to_remove: + try: + os.remove(item) + except OSError: + try: + os.rmdir(item) + except OSError: + pass + Module to list or run available custom macros for a configuration. It also stores macro base classes and macro library functions. @@ -56,6 +71,7 @@ ERROR_MACRO_CASE_MISMATCH = ("Error: case mismatch; \n {0} does not match {1}," " please only use lowercase.") ERROR_MACRO_NOT_FOUND = "Error: could not find macro {0}\n" +ERROR_NO_MACRO_HELP = "No help docstring provided, macro \"{0}\"." ERROR_NO_MACROS = "Please specify a macro name.\n" ERROR_RETURN_TYPE = "{0}: {1}: invalid returned type: {2}, expect {3}" ERROR_RETURN_VALUE = "{0}: incorrect return value" @@ -157,7 +173,33 @@ def __str__(self): class MacroBase(object): - """Base class for macros for validating or transforming configurations.""" + """Base class for macros for validating or transforming configurations. + + Synopsis: + >>> import rose.macro + ... + >>> class SomeValidator(rose.macro.MacroBase): + ... + ... '''Important: Add a docstring for your macro like this. + ... + ... A macro class should implement one of the following methods: + ... + ... ''' + ... + ... def validate(self, config, meta_config=None): + ... # Some check on config appends to self.reports using + ... # self.add_report. + ... return self.reports + ... + ... def transform(self, config, meta_config=None): + ... # Some operation on config which calls self.add_report + ... # for each change. + ... return config, self.reports + ... + ... def report(self, config, meta_config=None): + ... # Perform some analysis of the config but return nothing. + ... pass + """ def __init__(self): self.reports = [] # MacroReport instances for errors or changes @@ -186,6 +228,42 @@ def _load_meta_config(self, config, meta=None, directory=None, return load_meta_config(config, directory, config_type=config_type) def get_metadata_for_config_id(self, setting_id, meta_config): + """Return a dict of metadata properties and values for a setting id. + + Args: + setting_id (str): The name of the setting to extract metadata for. + meta_config (rose.config.ConfigNode): Config node containing the + metadata to extract from. + + Return: + dict: A dictionary containing metadata options. + + Example: + >>> # Create a rose app. + >>> with open('rose-app.conf', 'w+') as app_config: + ... app_config.write(''' + ... [foo] + ... bar=2 + ... ''') + >>> os.mkdir('meta') + >>> with open('meta/rose-meta.conf', 'w+') as meta_config: + ... meta_config.write(''' + ... [foo=bar] + ... values = 1,2,3 + ... ''') + ... + >>> # Load config. + >>> app_conf, config_map, meta_config = load_conf_from_file( + ... '.', 'rose-app.conf') + ... + >>> # Extract metadata for foo=bar. + >>> get_metadata_for_config_id('foo=bar', meta_config) + {'values': '1,2,3', 'id': 'foo=bar'} + + .. testcleanup:: rose.macro.MacroBase.get_metadata_for_config_id + + test_cleanup(['rose-app.conf', 'meta/rose-meta.conf', 'meta']) + """ return get_metadata_for_config_id(setting_id, meta_config) def get_resource_path(self, filename=''): @@ -196,7 +274,14 @@ def get_resource_path(self, filename=''): If the calling macro is lib/python/macro/levels.py, and the filename is 'rules.json', the returned path will be - etc/macro/levels/rules.json . + etc/macro/levels/rules.json. + + Args: + filename (str): The filename of the resource to request the path + to. + + Return: + str: The path to the requested resource. """ last_frame = inspect.getouterframes(inspect.currentframe())[1] @@ -213,14 +298,44 @@ def get_resource_path(self, filename=''): return resource_path def pretty_format_config(self, config): - """Pretty-format the configuration values.""" + """Standardise the keys and values of a config node. + + Args: + config (rose.config.ConfigNode): The config node to convert. + + """ pretty_format_config(config) def standard_format_config(self, config): - """Standardise configuration syntax.""" + """Standardise any degenerate representations e.g. namelist repeats. + + Args: + config (rose.config.ConfigNode): The config node to convert. + + """ standard_format_config(config) def add_report(self, *args, **kwargs): + """Add a rose.macro.MacroReport. + + See :class:`rose.macro.MacroReport` for details of arguments. + + Examples: + >>> # An example validator macro which adds a report to the setting + >>> # env=MY_FAVOURITE_STREAM_EDITOR. + >>> class My_Macro(MacroBase): + ... def validate(self, config, meta_config=None): + ... editor_value = config.get( + ... ['env', 'MY_FAVOURITE_STREAM_EDITOR']).value + ... if editor_value != 'sed': + ... self.add_report( + ... 'env', # Section + ... 'MY_FAVOURITE_STREAM_EDITOR', # Option + ... editor_value, # Value + ... 'Should be "sed"!') # Message + ... return self.reports + + """ self.reports.append(MacroReport(*args, **kwargs)) @@ -362,7 +477,23 @@ def transform(self, config, meta_config=None): class MacroReport(object): - """Class to hold information about a macro issue.""" + """Class to hold information about a macro issue. + + Arguments: + section (str): The name of the section to attach this report to. + option (str): The name of the option (within the section) to + attach this report to. + value (obj): The value of the configuration associated with this + report. + info (str): Text information describing the nature of the report. + is_warning (bool): If True then this report will be logged as a + warning. + + Example: + >>> report = MacroReport('env', 'WORLD', 'Earth', + ... 'World changed to Earth', True) + + """ def __init__(self, section=None, option=None, value=None, info=None, is_warning=False): @@ -800,7 +931,12 @@ def transform_config(config, meta_config, transformer_macro, modules, def pretty_format_config(config, ignore_error=False): - """Improve configuration prettiness.""" + """Standardise the keys and values of a config node. + + Args: + config (rose.config.ConfigNode): The Config node to convert. + + """ for s_key, s_node in config.value.items(): scheme = s_key if ":" in scheme: @@ -812,7 +948,7 @@ def pretty_format_config(config, ignore_error=False): except AttributeError: continue for keylist, node in list(s_node.walk()): - # FIXME: Surely, only the scheme know how to splits its array? + # FIXME: Surely, only the scheme knows how to split its array? values = rose.variable.array_split(node.value, ",") node.value = pretty_format_value(values) new_keylist = pretty_format_keys(keylist) @@ -826,7 +962,12 @@ def pretty_format_config(config, ignore_error=False): def standard_format_config(config): - """Standardise any degenerate representations e.g. namelist repeats.""" + """Standardise any degenerate representations e.g. namelist repeats. + + Args: + config (rose.config.ConfigNode): The config node to convert. + + """ for keylist, node in config.walk(): if len(keylist) == 2: scheme, option = keylist @@ -842,7 +983,43 @@ def standard_format_config(config): def get_metadata_for_config_id(setting_id, meta_config): - """Return a dict of metadata properties and values for a setting id.""" + """Return a dict of metadata properties and values for a setting id. + + Args: + setting_id (str): The name of the setting to extract metadata for. + meta_config (rose.config.ConfigNode): Config node containing the + metadata to extract from. + + Return: + dict: A dictionary containing metadata options. + + Example: + >>> # Create a rose app. + >>> with open('rose-app.conf', 'w+') as app_config: + ... app_config.write(''' + ... [foo] + ... bar=2 + ... ''') + >>> os.mkdir('meta') + >>> with open('meta/rose-meta.conf', 'w+') as meta_config: + ... meta_config.write(''' + ... [foo=bar] + ... values = 1,2,3 + ... ''') + ... + >>> # Load config. + >>> app_conf, config_map, meta_config = load_conf_from_file( + ... '.', 'rose-app.conf') + ... + >>> # Extract metadata for foo=bar. + >>> get_metadata_for_config_id('foo=bar', meta_config) + {'values': '1,2,3', 'id': 'foo=bar'} + + .. testcleanup:: rose.macro.get_metadata_for_config_id + + test_cleanup(['rose-app.conf', 'meta/rose-meta.conf', 'meta']) + + """ metadata = {} if rose.CONFIG_DELIMITER in setting_id: section, option = setting_id.split(rose.CONFIG_DELIMITER, 1) @@ -937,10 +1114,16 @@ def run_macros(config_map, meta_config, config_name, macro_names, for module_name, class_name, method, help in macro_tuples: macro_name = ".".join([module_name, class_name]) macro_id = MACRO_OUTPUT_ID.format(method.upper()[0], macro_name) - reporter(macro_id + "\n", prefix="") - for help_line in help.split("\n"): - reporter(MACRO_OUTPUT_HELP.format(help_line), - level=reporter.V, prefix="") + if help: + reporter(macro_id + "\n", prefix="") + for help_line in help.split("\n"): + reporter(MACRO_OUTPUT_HELP.format(help_line), + level=reporter.V, prefix="") + else: + # No "help" docstring provided in macro. + reporter(ERROR_NO_MACRO_HELP.format(macro_name), + level=reporter.FAIL, prefix=reporter.PREFIX_FAIL) + return False return True # Categorise macros given as arguments. diff --git a/t/docs/00-urls.t b/t/docs/00-basic.t similarity index 51% rename from t/docs/00-urls.t rename to t/docs/00-basic.t index dbdd4a25aa..fe09b3a075 100755 --- a/t/docs/00-urls.t +++ b/t/docs/00-basic.t @@ -17,13 +17,32 @@ # You should have received a copy of the GNU General Public License # along with Rose. If not, see . #------------------------------------------------------------------------------- -# Test rose documentation for broken links. +# Test Sphinx documentation building and validate urls in "doc/*.html". #------------------------------------------------------------------------------- . $(dirname $0)/test_header #------------------------------------------------------------------------------- -tests 2 +tests 8 +#------------------------------------------------------------------------------- +# Build Sphinx documentation and run doctests. +TEST_KEY="${TEST_KEY_BASE}-doctest" +# export SPHINX_DEV_MODE=true # For development, don't rebuild the virtualenv. +run_pass "${TEST_KEY}" make -C "${ROSE_HOME}/doc" +file_grep "${TEST_KEY}-build" "build succeeded." "${TEST_KEY}.out" +file_grep "${TEST_KEY}-tests-setup" "0 failures in setup code" \ + "${TEST_KEY}.out" +file_grep "${TEST_KEY}-tests-run" "0 failures in tests" \ + "${TEST_KEY}.out" +file_grep "${TEST_KEY}-tests-clean" "0 failures in cleanup code" \ + "${TEST_KEY}.out" +# Make sure that the virtual environment is removed (if created). +ls -l "${ROSE_HOME}" > "${TEST_DIR}/ls" +file_grep_fail "${TEST_KEY}-clean" "venv" "${TEST_DIR}/ls" +#------------------------------------------------------------------------------- +# Test rose documentation for broken links. +TEST_KEY="${TEST_KEY_BASE}-urls" +run_pass "${TEST_KEY}" python $TEST_SOURCE_DIR/lib/python/urlvalidator.py \ + "${ROSE_HOME}/doc/" +file_cmp "${TEST_KEY}.err" "${TEST_KEY}.err" <'/dev/null' #------------------------------------------------------------------------------- -run_pass ${TEST_KEY_BASE} python $TEST_SOURCE_DIR/lib/python/urlvalidator.py "${ROSE_HOME}/doc/" -file_cmp "${TEST_KEY_BASE}.err" "${TEST_KEY_BASE}.err" <'/dev/null' exit diff --git a/t/docs/01-validate-validator.t b/t/docs/01-validate-url-validator.t similarity index 100% rename from t/docs/01-validate-validator.t rename to t/docs/01-validate-url-validator.t From 754a5fd108a097eee3f3ee3e8ca4d4fc4bcefa4d Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Wed, 29 Mar 2017 09:59:53 +0100 Subject: [PATCH 2/3] Fixed load_with_opts. --- lib/python/rose/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/python/rose/config.py b/lib/python/rose/config.py index b3b75b4bd6..b055a8e78c 100644 --- a/lib/python/rose/config.py +++ b/lib/python/rose/config.py @@ -1266,8 +1266,7 @@ def load_with_opts(self, source, node=None, more_keys=None, 'Baz' """ - if not node: - node = self.load(source, node) + node = self.load(source, node) if return_config_map: config_map = {None: copy.deepcopy(node)} opt_conf_keys_node = node.unset(["opts"]) From 1a52ec7a3d1883143f0f432c03cf317dc278b965 Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Wed, 5 Apr 2017 14:01:55 +0100 Subject: [PATCH 3/3] Address feedback. --- doc/rose-api.html | 9 ++++++--- doc/rose-config-api.html | 2 +- doc/sphinx/sphinx.rst | 2 +- lib/python/rose/config.py | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/rose-api.html b/doc/rose-api.html index 1cfb81df20..5c8a89432e 100644 --- a/doc/rose-api.html +++ b/doc/rose-api.html @@ -786,7 +786,7 @@

    Rose Macros

  • Upgraders - these are special transformer macros for upgrading and downgrading configurations. (covered in the - next section)
  • + Upgrade Macro API)
  • Reporters - output information about a configuration.
  • @@ -970,8 +970,11 @@

    API Reference

         def report(self, config, meta_config=None):
    -        """ Write some information about the configuration to a report file."""
    -        # Note: report methods do not have a return value.
    +        """ Write some information about the configuration to a report file.
    +
    +        Note: report methods do not have a return value.
    +
    +        """
             with open('report/file', 'r') as report_file:
                 report_file.write(str(config.get(["namelist:snowflakes"])))
     
    diff --git a/doc/rose-config-api.html b/doc/rose-config-api.html index 06b88d10a0..c43f2f8590 100644 --- a/doc/rose-config-api.html +++ b/doc/rose-config-api.html @@ -20,7 +20,7 @@

    Rose Config API

    -

    View the API docs here. +

    View the API docs here.

    diff --git a/doc/sphinx/sphinx.rst b/doc/sphinx/sphinx.rst index f8ff9ba3f1..21065b053e 100644 --- a/doc/sphinx/sphinx.rst +++ b/doc/sphinx/sphinx.rst @@ -62,7 +62,7 @@ Some quick examples: """ # Blank line. - def __init(self, param1, param2, kwarg=None): + def __init__(self, param1, param2, kwarg=None): pass def some_generator(self, param1, param2): diff --git a/lib/python/rose/config.py b/lib/python/rose/config.py index b055a8e78c..e68aba4f3b 100644 --- a/lib/python/rose/config.py +++ b/lib/python/rose/config.py @@ -996,7 +996,7 @@ def get_reversed(self): return rev_diff def delete_removed(self): - """Deletes all 'removed' keys from this ConfigNodeDiff.. + """Deletes all 'removed' keys from this ConfigNodeDiff. Examples: >>> config_node_diff = ConfigNodeDiff()