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
+
+
+
+
+
+
+
+
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)
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"])))