Source code for rose_domain

# Copyright (C) British Crown (Met Office) & Contributors.
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Rose is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Rose. If not, see <http://www.gnu.org/licenses/>.
# -----------------------------------------------------------------------------
"""The Rose domain is for documenting Rose configurations and built-in
applications.

Rose Objects:
    The Rose domain supports the following object types:

    * ``rose:app`` - Rose Applications.
    * ``rose:file`` - Root Rose configurations.
    * ``rose:conf`` - Rose configurations. Nest them to create configuration
      sections.

    See the corresponding directives for more information on each object type.

Reference Syntax:
    Once documented, objects can be referenced using the following syntax:

    .. code-block:: none

        :rose:CONFIG-FILE[parent-section]child-config
        :rose:CONFIG-FILE|top-level-config

    Where ``CONFIG-FILE`` is:

    * The ``APP-NAME`` for applications (e.g. ``fcm_make``).
    * The ``FILE-NAME`` for configuration files (e.g. ``rose.conf``).

Referencing From RST Files:
    To reference a Rose object add the object ``TYPE`` into the domain
    (e.g. ``conf`` or ``app``).

    .. code-block:: rst

       :rose:TYPE:`CONFIG-FILE[parent-section]child-config`

    e.g:

    .. code-block:: rst

       * :rose:app:`fcm_make`
       * :rose:conf:`fcm_make.args`

Autodocumentation:
    Documentation can be auto-built from RST formatted comments in Rose
    configuration files using the ``autoconfig`` directive.

    Note that due to the implementation of :py:mod:`metomi.rose.config` the
    autodocumenter will represent empty sections as top level configuration
    nodes.

Example:
    .. code-block:: rst

       .. rose:file:: rose-suite.conf

          The config file used for configuring suite level settings.

          .. rose:conf:: jinja2:suite.rc

             A section for specifying Jinja2 settings for use in the
             ``flow.cylc`` file.

             Note that one ommits the square brackets for config sections. If
             :rose:conf: contains other :rose:conf:'s then it is implicitly a
             section and the brackets are added automatically. If you wish to
             document a section which contains no settings write it using
             square brackets.

             .. rose:conf:: ROSE_VERSION

                provide the intended Rose version to the suite.

                .. deprecated:: 6.1.0

                   No longer required, this context is now provided internally.

             .. rose:conf:: CYLC_VERSION

                provide the intended Rose version to the suite.

                .. deprecated:: 6.1.0

                   See :rose:conf:`ROSE_VERSION`.

                   ..            ^ relative reference

Referencing Objects Via Intersphinx:
    Reference Rose objects as you would any other e.g:

    .. code-block:: rst

       :rose:file:`intersphinx-mapping:rose-app.conf`

    A quick way to get the object reference is to extract if from the URL.
    For example in the following URL:

    .. code-block:: none

        doc-root/api/configuration/application.html#rose:file:rose-app.conf

    The reference is ``rose:file:rose-app.conf``.

"""

import re

from docutils.nodes import block_quote
from docutils.parsers.rst import Directive
from docutils.statemachine import StringList

from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.docfields import Field, TypedField
from sphinx.util.nodes import make_refnode

from metomi.rose import config

LOGGER = logging.getLogger(__name__)

ROSE_DOMAIN_REGEX = re.compile(  # Regex for a fully qualified Rose domain.
    r'rose:(\w+):'  # Rose domain prefix + object type.
    r'([^\|\[ \n]+)'  # Configuration file.
    r'(\[.*\])?'  # Configuration section.
    r'(?:\|?(.*))?'  # Configuration setting.
)
SECTION_REGEX = re.compile(  # Regex for splitting sections and settings.
    r'(\[.*\])(.*)'
)
OPT_ARG_REGEX = re.compile(  # Regex for splitting domains and arguments
    r'(^(?:[^\[\|=]+)?'  # Configuration file.
    r'(?:\[[^\]]+\])?'  # Configuration section.
    r'(?:[^=]+)?)'  # Configuration setting.
    r'(?:=(.*))?$'  # Argument.
)
CROSS_DOMAIN_REGEX = re.compile(  # Catch leading target from inter-sphinx ref.
    r'^([\w\-]+)'  # inter-sphinx mapping
    r':'  # colon divider
    r'([\w\-].*)$'  # rose object reference
)


def tokenise_namespace(namespace):
    """Convert a namespace string into a list of tokens.

    Tokens are (domain, name) tuples.

    Examples:
        # Partial namespaces.
        >>> tokenise_namespace('rose:conf:foo')
        [('rose:conf', 'foo')]
        >>> tokenise_namespace('rose:conf:[foo]')
        [('rose:conf', '[foo]')]
        >>> tokenise_namespace('rose:conf:[foo]bar')
        [('rose:conf', '[foo]'), ('rose:conf', 'bar')]

        # Full namespaces.
        >>> tokenise_namespace('rose:app:foo')
        [('rose:app', 'foo')]
        >>> tokenise_namespace('rose:file:foo.conf')
        [('rose:file', 'foo.conf')]
        >>> tokenise_namespace('rose:conf:foo|bar')
        [('rose:app', 'foo'), ('rose:conf', 'bar')]
        >>> tokenise_namespace('rose:conf:foo[bar]')
        [('rose:app', 'foo'), ('rose:conf', '[bar]')]
        >>> tokenise_namespace('rose:conf:foo[bar]baz')
        [('rose:app', 'foo'), ('rose:conf', '[bar]'), ('rose:conf', 'baz')]
        >>> tokenise_namespace('rose:conf:foo.conf[bar:pub]baz'
        ... ) # doctest: +NORMALIZE_WHITESPACE
        [('rose:file', 'foo.conf'), ('rose:conf', '[bar:pub]'),
        ('rose:conf', 'baz')]
    """
    match = ROSE_DOMAIN_REGEX.match(namespace)

    domain = ':'.join(namespace.split(':')[2:])

    if not match:
        # Namespace is only partial, return it as a single configuration token.
        section, setting = SECTION_REGEX.match(domain).groups()
        ret = [('rose:conf', section)]
        if setting:
            ret.append(('rose:conf', setting))
        return ret

    typ, conf_file, conf_section, setting = match.groups()

    if typ == 'conf' and not (conf_section or setting):
        # Namespace is only partial, return it as a single configuration token.
        return [(('rose:conf'), domain)]

    if typ in ['file', 'app']:
        ret = [('rose:%s' % typ, conf_file)]
    elif '.' in conf_file:
        # Must be a Rose config file.
        ret = [('rose:file', conf_file)]
    else:
        # Must be a Rose application.
        ret = [('rose:app', conf_file)]

    if conf_section:
        ret.append(('rose:conf', conf_section))
    if setting:
        ret.append(('rose:conf', setting))

    return ret


def namespace_from_tokens(tokens):
    """Convert a list of tokens into a string namespace.

    Tokens are (domain, name) tuples.

    Examples:
       >>> namespace_from_tokens([('rose:app', 'foo')])
       'rose:app:foo'
       >>> namespace_from_tokens([('rose:file', 'foo')])
       'rose:file:foo'
       >>> namespace_from_tokens([('rose:app', 'foo'), ('rose:conf', 'bar')])
       'rose:conf:foo|bar'
       >>> namespace_from_tokens([('rose:app', 'foo'), ('rose:conf', '[bar]')])
       'rose:conf:foo[bar]'
       >>> namespace_from_tokens([('rose:app', 'foo'), ('rose:conf', '[bar]'),
       ...                        ('rose:conf', 'baz')])
       'rose:conf:foo[bar]baz'
       >>> namespace_from_tokens([('rose:file', 'foo.conf'),
       ...                        ('rose:conf', '[bar]'),
       ...                        ('rose:conf', 'baz')])
       'rose:conf:foo.conf[bar]baz'
    """
    ret = tokens[-1][0] + ':'
    previous_domain = None
    for domain, name in tokens:
        # Root level configuration files.
        if domain in ['rose:app', 'rose:file']:
            if previous_domain:
                # App must be a root domain.
                LOGGER.warning('Invalid tokens "%s"' % tokens)
                return False
            else:
                ret += name

        # Rose configuration.
        elif domain == 'rose:conf':
            if previous_domain in ['rose:app', 'rose:file']:
                if name.startswith('['):
                    # section requires no separator
                    ret += name
                else:
                    # Setting requires `|` separator.
                    ret += '|%s' % name
            elif previous_domain == 'rose:conf':
                # Setting requires no separator if following a section.
                ret += name
            else:
                LOGGER.warning('Invalid tokens "%s"' % tokens)
                return False
        else:
            LOGGER.warning('Invalid tokens "%s"' % tokens)
            return False
        previous_domain = domain
    return ret


TOKEN_ORDER = [
    ('rose:file', 'rose:file'),
    ('rose:app', 'rose:app'),
    ('rose:conf-section', 'rose:conf'),
    ('rose:conf', 'rose:conf'),
]


def tokens_from_ref_context(ref_context):
    """Extract a list of tokens from a ref_context dictionary.

    Examples:
        >>> tokens_from_ref_context({'rose:conf-section': '[bar]',
        ...                          'rose:conf': 'baz',
        ...                          'rose:file': 'foo'})
        [('rose:file', 'foo'), ('rose:conf', '[bar]'), ('rose:conf', 'baz')]
        >>> tokens_from_ref_context({'rose:conf-section': '[bar]',
        ...                          'rose:conf': 'baz',
        ...                          'rose:app': 'foo'})
        [('rose:app', 'foo'), ('rose:conf', '[bar]'), ('rose:conf', 'baz')]
    """
    # Generate context namespace from ref_context.
    ret = [(v, ref_context[k]) for k, v in TOKEN_ORDER if k in ref_context]
    # Remove any duplicate items (happens for conf-sections where you
    # may get {'rose:conf-section': '[foo]', 'rose:conf': '[foo]'}.
    last = None
    for item in list(ret):
        if last is not None and item == last:
            ret.remove(item)
        last = item
    return ret


[docs]class RoseDirective(ObjectDescription): """Base class for implementing Rose objects. Subclasses must provide: - ``NAME`` Subclasses can provide: - ``LABEL`` - ``ARGUMENT_SEPARATOR`` & ``ARGUMENT_REGEX`` - ``ALT_FORM_SEPARATOR`` - ``ALT_FORM_TEMPLATE`` - ``doc_field_types`` - List of ``Field`` objects for object parameters. - ``run()`` - For adding special rev_context variables via ``add_rev_context``. - ``handle_signature()`` - For changing the display of objects. - ``custom_name_template`` - String template accepting one string format argument for custom formatting the node name (e.g. ``'[%s]'`` for conf sections). ref_context Variables: Sphinx domains use ``ref_context`` variables to determine the relationship between objects. E.g. for a Rose app the ``rose:app`` variable is set to the name of the app. This variable will be made available to all children which is how they determine that they belong to the app. * Variables set in ``run()`` are available to this object along with all of its children. * Variables set in ``before_content()`` are only available to children. * All variables set via ``add_ref_context()`` will be automatically removed in ``after_content()`` to prevent leaking scope. """ allow_nesting = False """Override in settings which are permitted to be nested (e.g. 'conf').""" NAME = None """The Rose domain this directive implements, see RoseDomain.directives""" LABEL = '' """Label to prepend to objects.""" ARGUMENT_SEPARATOR = None """String for separating the configuration name and argument.""" ARGUMENT_REGEX = None """Regex for splitting the directive name and argument.""" ALT_FORM_SEPARATOR = '&' """String for splitting alternate names.""" ALT_FORM_TEMPLATE = '(alternate: %s)' """String template for writing out alternate names. Takes one string format argument.""" ROSE_CONTEXT = 'rose:%s' """Prefix for all Rose ref_context variables.""" def __init__(self, *args, **kwargs): self.ref_context_to_remove = [] # Cannot do this in run(). self.registered_children = [] ObjectDescription.__init__(self, *args, **kwargs) def run(self): # Automatically generate the "rose:NAME" ref_context variable. self.add_ref_context(self.NAME) index_node, cont_node = ObjectDescription.run(self) # Add a marker on the output node so we can determine the context # namespace later (see RoseDomain.resolve_xref). context_var = self.ROSE_CONTEXT % self.NAME cont_node.ref_context = { context_var: self.process_name(self.arguments[0].strip())[0] } # Add children if initialised via python - see RoseAutoDirective. block = block_quote() # Create indented section. for child_node in self.registered_children: block.append(child_node) # Add child node to indented section. cont_node.append(block) # Add indented section to this node. return [index_node, cont_node] def register_subnode(self, subnode): """Register a sub-configuration when creating Rose objects with Python. Special method for the RoseDirective to facilitate building Rose domain objects using the Python API rather than RST. See :py:meth:`RoseAutoDirective.run` for usage example. """ self.registered_children.append(subnode) def add_ref_context(self, key): """Add a new ``ref_context`` variable. * The variable will be set to the name of the object. * The variable will be automatically removed in ``after_content()`` to prevent leaking scope. Args: key (str): The name for the new variable without the ``rose:`` prefix. """ ref_context = self.state.document.settings.env.ref_context context_var = self.ROSE_CONTEXT % key ref_context[context_var] = self.process_name( self.arguments[0].strip() )[0] self.ref_context_to_remove.append(key) def remove_ref_context(self, key): """Remove a ``ref_context`` variable. Args: key (str): The name of the variable to remove without the ``rose:`` prefix. """ ref_context = self.state.document.settings.env.ref_context context_var = self.ROSE_CONTEXT % key if ref_context.get(context_var) == ( self.process_name(self.arguments[0].strip())[0] ): del ref_context[context_var] def get_ref_context(self, key): """Return the value of a ``ref_context`` variable. Args: key (str): The name of the variable without the ``rose:`` prefix. Return: The value of the context variable, if set via ``add_ref_context()`` this will be the name of the object which set it. """ ref_context = self.state.document.settings.env.ref_context return ref_context.get(self.ROSE_CONTEXT % key) def after_content(self): """This method is called after child objects have been processed. There is also the ``before_content()`` method which is called during ``run()`` before child objects have been processed but after the current object has been processed. """ for ind, child_node in enumerate(self.registered_children): self.registered_children[ind] = child_node.run()[1] for context_var in self.ref_context_to_remove: self.remove_ref_context(context_var) ObjectDescription.after_content(self) def process_name(self, name): """Perform standard pre-processing of a node name. * Process argument strings (e.g. ``bar`` in ``foo=bar``). * Process alternate forms (e.g. ``bar`` in ``foo & bar``). * Process custom name templates. Return: tuple - (name, argument, alt_forms) - name (str) - The processed name with everything else stripped. - argument (str) - Any specified argument string else ''. - alt_forms (list) - List of strings containing alternate names for the node. """ alt_forms = [] if self.ALT_FORM_SEPARATOR: ret = name.split(self.ALT_FORM_SEPARATOR) name = ret[0].strip() alt_forms = [x.strip() for x in ret[1:]] # Separate argument strings (e.g. foo=FOO). argument = '' if self.ARGUMENT_REGEX: try: name, argument = self.ARGUMENT_REGEX.search(name).groups() except ValueError: pass # Apply custom name template if specified. if hasattr(self, 'custom_name_template'): name = self.custom_name_template % name return name, argument, alt_forms def handle_signature(self, sig, signode, display_text=None): """This method determines the appearance of the object. Overloaded Args: display_test: Used for overriding the object name. """ # Override sig with display_text if provided. if display_text is None: display_text = sig display_text, argument, alt_forms = self.process_name(display_text) # Add a label before the name of the object. signode += addnodes.desc_annotation(*('%s ' % self.LABEL,) * 2) # Add the object name. signode += addnodes.desc_name(sig, display_text) # Add arguments. if argument: argument = '%s %s' % (self.ARGUMENT_SEPARATOR, argument) signode += addnodes.desc_annotation(argument, argument) # Add alternate object names. if alt_forms: signode += addnodes.desc_annotation( *(self.ALT_FORM_TEMPLATE % (', '.join(alt_forms)),) * 2 ) signode['fullname'] = sig return (sig, self.NAME, sig) def needs_arglist(self): return False def add_target_and_index(self, name_cls, _, signode): """This method handles namespacing.""" name = self.process_name(name_cls[0])[0] # Get the current context in tokenised form. context_tokens = [] ref_context = self.state.document.settings.env.ref_context for key in ['app', 'file', 'conf-section', 'conf']: token = self.ROSE_CONTEXT % key if token in ref_context: value = ref_context[token] if key == 'conf-section': token = self.ROSE_CONTEXT % 'conf' new_token = (token, value) if new_token not in context_tokens: context_tokens.append(new_token) # Add a token representing the current node. new_token = (self.ROSE_CONTEXT % self.NAME, name) if new_token not in context_tokens: context_tokens.append(new_token) # Generate a namespace from the tokens. namespace = namespace_from_tokens(context_tokens) if namespace is False: LOGGER.error( 'Invalid namespace for Rose object "%s"' % namespace, location=signode, ) # Register this namespace. signode['ids'].append(namespace) self.env.domaindata['rose']['objects'][namespace] = ( self.env.docname, '', ) def get_index_text(self, modname, name): return ''
[docs]class RoseAppDirective(RoseDirective): """Directive for documenting Rose apps. Example: Click :guilabel:`source` to view source code. .. code-block:: rst .. rose:app:: foo An app called ``foo``. """ NAME = 'app' LABEL = 'Rose App'
[docs]class RoseFileDirective(RoseDirective): """Directive for documenting Rose files. Example: Click :guilabel:`source` to view source code. .. code-block:: rst .. rose:file:: foo An configuration file called ``foo``. """ NAME = 'file' LABEL = 'Rose Configuration'
[docs]class RoseConfigDirective(RoseDirective): """Directive for documenting config sections. Optional Attributes: * ``envvar`` - Associate an environment variable with this configuration option. * ``compulsory`` - Set to ``True`` for compulsory settings, omit this field for optional settings. Additional ref_context: * ``rose:conf-section`` - Set for parent configurations, is available to any child nodes. Example: Click :guilabel:`source` to view source code. .. code-block:: rst .. rose:conf:: foo :default: foo :opt argtype foo: Description of option ``foo``. :opt bar: Description of bar A setting called ``foo``. .. rose:conf:: bar A section called ``bar``. .. rose:conf:: baz :compulsory: True :env: AN_ASSOCIATED_ENVIRONMENT_VARIABLE A config called ``[bar]baz``. """ NAME = 'conf' LABEL = 'Config' SECTION_REF_CONTEXT = 'conf-section' ARGUMENT_REGEX = OPT_ARG_REGEX ARGUMENT_SEPARATOR = '=' # Add custom fields. doc_field_types = [ # NOTE: The field label must be sort to avoid causing a line break. Field( 'envvar', label='Env Var', has_arg=False, names=('env',), bodyrolename='obj', ), Field( 'compulsory', label='Compulsory', has_arg=True, names=('compulsory',), ), Field('default', label='Default', has_arg=False, names=('default',)), TypedField( 'option', label='Options', names=('opt',), typerolename='obj', typenames=('paramtype', 'type'), can_collapse=True, ), ] def run(self): """Overridden to add the :rose:conf-section: ``ref_context`` variable for nested sections.""" if self.registered_children or any( '.. rose:conf::' in line for line in self.content ): # This configuration contains other configurations i.e. it is a # configuration section. Apply a custom_name_template so that it is # written inside square brackets. self.custom_name_template = '[%s]' # Sections cannot be written with argument examples. self.ARGUMENT_SEPARATOR = None self.ARGUMENT_REGEX = None # Add a ref_context variable to mark this node as a config section. self.add_ref_context(self.SECTION_REF_CONTEXT) return RoseDirective.run(self)
class RoseXRefRole(XRefRole): """Handle references to Rose objects. This should be minimal.""" def process_link(self, env, refnode, has_explicit_title, title, target): # copy ref_context to the refnode so that we can access it in # resolve_xref. Note that walking through the node tree to extract # ref_context items appears only to work in the HTML buider. refnode['ref_context'] = dict(env.ref_context) return title, target class RoseDomain(Domain): """Sphinx extension to add the ability to document Rose objects. Example: Click :guilabel:`source` to view source code. .. code-block:: rst .. rose:app: foo An app called ``foo``. .. rose:conf: bar A setting called ``bar`` for the app ``foo``. .. rose:conf: baz A config section called ``baz`` for the app ``foo``. .. rose:conf: pub A config setting called ``[baz]pub`` for the app ``foo``. """ name = 'rose' """Prefix for Rose domain (used by sphinx).""" label = 'Rose' """Display label for the Rose domain (used by sphinx).""" object_types = { 'app': ObjType('app', 'app', 'obj'), 'file': ObjType('file', 'file', 'obj'), 'conf': ObjType('conf', 'conf', 'obj'), } """List of object types, this should mirror ``directives``.""" directives = { 'app': RoseAppDirective, 'file': RoseFileDirective, 'conf': RoseConfigDirective, } """List of domains associated with prefixes (e.g. ``app`` becomes the ``rose:app`` domain.""" roles = { 'app': RoseXRefRole(), 'file': RoseXRefRole(), 'conf': RoseXRefRole(), } """Sphinx text roles associated with the domain. Text roles are required for referencing objects. There should be one role for each item in ``object_types``""" initial_data = {'objects': {}} # path: (docname, synopsis) """This sets ``self.data`` on initialisation.""" def clear_doc(self, docname): """Removes documented items from the Rose domain. Not sure why, but apparently necessary. """ for fullname, (pkg_docname, _l) in list(self.data['objects'].items()): if pkg_docname == docname: del self.data['objects'][fullname] def get_objects(self): """Iterates through documented items in the Rose domain. This method is used to generate the Rose component of the inventory file which in turn is used to cross-reference objects between sphinx projects (via inter-sphinx). See ``sphinx.util.inventory.InventoryFile.dump``. Note: The Rose domain does things a little bit differently in this area to the Python domain as all Rose objects are stored in a single flat dictionary whereas Python objects are stored in a nested one and are namespaced accordingly. Yields: tuple - (name, dispname, type, docname, anchor, priority) - name - The Rose object reference minus the ``:rose:type:`` stuff. - dispname - For searching / linking. - type - The Rose object type i.e. ``file``, ``app``, ``conf``. - docname - The path to the file where the object is documented (without extension). - anchor - URL fragment for locating object on the page. - priority - Hardwired to 1. """ for obj_name, (doc_name, _) in list(self.data['objects'].items()): # obj_name -> The full object reference to the rose object # e.g. `:rose:file:rose-app.conf`. # doc_name -> The path to the document containing the rose object. try: # Split the obj_name into it's constituent components: # 1. domain - i.e. `rose`. # 2. type - i.e. `file`, `app`, `conf`. # 3. ref_name - The remainder of the reference. _, type_, ref_name = obj_name.split(':', 2) except ValueError: LOGGER.warning( 'Could not parse object reference for "%s"' % ref_name ) continue yield ref_name, ref_name, type_, doc_name, obj_name, 1 @staticmethod def validate_external_xref(env, typ, target, node, intersphinx_mapping): """Check that the cross-reference is valid else log a warning. Args: intersphinx_mapping (str): A key from the Sphinx configuration of the same name. Returns: bool: True if valid. """ try: # Get the inter-sphinx mapping. cross_target = env.config.intersphinx_mapping[intersphinx_mapping][ 0 ] except KeyError: LOGGER.warning( 'Could not find inter-sphinx mapping for "%s"' % intersphinx_mapping ) return False except AttributeError: LOGGER.warning( 'inter-sphinx required for cross-project ' 'references.' ) return False try: # Test that there is a rose object in that mapping. env.intersphinx_cache[cross_target][2]['rose:%s' % typ][target] except (IndexError, KeyError, ValueError): LOGGER.warning( 'No Ref for "rose:%s:`%s:%s`"' % (typ, intersphinx_mapping, target), location=node, ) return False return True def resolve_xref( self, env, fromdocname, builder, typ, target, node, contnode ): """Associate a reference with a documented object. The important parameters are: typ (str): The object type - see ``RoseDomain.object_types``. target (str): The rest of the reference as written in the rst. This implementation simplifies things by storing objects under their namespace which is the same syntax used to reference the objects i.e: .. rose:app: Foo Creates the entry ``self.data['objects']['rose:app:foo']``. Which can be referenced in rst by: .. code-block:: rst :rose:app:`foo` """ # If target has a trailing argument ignore it. target = OPT_ARG_REGEX.search(target).groups()[0] # Handle inter-sphinx cross-references match = CROSS_DOMAIN_REGEX.match(target) if match: # This is a cross-reference to a Rose object in another Sphinx # project (via inter-sphinx). intersphinx_mapping, target = match.groups() self.validate_external_xref( env, typ, target, node, intersphinx_mapping ) # The reference itself is handled somewhere else. return # Determine the namespace of the object being referenced. if typ in ['app', 'file']: # Object is a root rose config - path is absolute. namespace = 'rose:%s:%s' % (typ, target) elif typ == 'obj': # Object is an 'obj' e.g. an environment variable, 'obj's are not # referenceable (yet). return None elif typ == 'conf': relative_to_conf = False if target.startswith('['): # Target is relative to the context conf_file. relative_to_conf = True # Get the referenced namespace in tokenised form. reference_namespace = tokenise_namespace( 'rose:%s:%s' % (typ, target) ) if reference_namespace[0][0] in ['rose:app', 'rose:file']: # Target is a root rose config - path is absolute. namespace = 'rose:%s:%s' % (typ, target) else: # Target is not a root config - path is relative. context_namespace = tokens_from_ref_context( node['ref_context'] ) if not context_namespace: LOGGER.warning( 'Relative reference requires local context ' + '"%s".' % (target), location=node, ) return if relative_to_conf: # Target is relative to the context conf_file. namespace_tokens = ( context_namespace[:1] + reference_namespace ) else: # Target is relative to the current namespace. namespace_tokens = ( context_namespace[:-1] + reference_namespace ) # Convert the tokenised namespace into a string namespace. namespace = namespace_from_tokens(namespace_tokens) # Lookup the object from the namespace. try: data = self.data['objects'][namespace] except KeyError: # No reference exists for "object_name". LOGGER.warning('No Ref for "%s"' % namespace, location=node) return None # Create a link pointing at the object. return make_refnode( builder, fromdocname, data[0], namespace, contnode, namespace )
[docs]class RoseAutoDirective(Directive): """Directive for autodocumenting Rose configuration files. Uses RST formatted comments in Rose configuration files using :py:mod:`metomi.rose.config`. Note the directive only documents config objects not the file itself. Example: .. code-block:: rst .. rose:file: foo.conf .. autoconfig:: path/to/foo.conf """ option_spec = {} required_arguments = 1 domain = 'rose' def run(self): filename = self.arguments[0] # Load rose configuration. try: conf = config.load(filename) except config.ConfigSyntaxError: LOGGER.error( 'Syntax error in Rose configuration file "%s".' % filename ) raise nodes = [] nodes.append( addnodes.highlightlang( lang='rose', force=False, linenothreshold=20 ) ) # Append file level comments if present. if conf.comments: contentnode = addnodes.desc_content() contentnode.document = self.state.document self.state.nested_parse( StringList(conf.comments), self.content_offset, contentnode ) nodes.append(contentnode) # Append configurations. section = None node = block_quote() for key, conf_node in sorted(conf.walk()): if isinstance(conf_node.value, str): # Configuration setting - "name=arg". name = '%s=%s' % (key[-1], conf_node.value or '') else: # Configuration section - "name" name = key[-1] # Prepare directive object. directive = RoseConfigDirective( 'rose:conf', [name], {}, StringList(conf_node.comments), self.lineno, self.content_offset, self.block_text, self.state, self.state_machine, ) if isinstance(conf_node.value, dict): # Configuration section. if section: node.append(section.run()[1]) section = directive elif key[0]: # Sub-configuration. section.register_subnode(directive) else: # Top-level configuration node.append(directive.run()[1]) if section: node.append(section.run()[1]) nodes.append(node) nodes.append( addnodes.highlightlang( lang='bash', force=False, linenothreshold=20 ) ) return nodes
def setup(app): app.add_domain(RoseDomain) app.add_directive('autoconfig', RoseAutoDirective)