"""
``sphinx_ros.directives`` module
================================
"""
import re
import sphinx
from docutils import nodes
from docutils.parsers.rst import directives, Directive
from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.util.docfields import Field, TypedField
ros_sig_re = re.compile(
r'''^((?:[^\.]*\.)*?) # package name
(?:(msg|srv|action)\.)? # object type
(\w+) \s*?$ # thing name
''', re.VERBOSE)
def name_to_key(name):
return unicode(name[0].upper())
def split_pkg_object(signature, obj_type):
try:
pkg, object_ = signature.split('.' + obj_type + '.')
except ValueError:
try:
pkg, object_ = signature.split('/')
except ValueError:
pkg = ''
object_ = signature
return pkg, object_
# This override allows our inline type specifiers to behave like :class: link
# when it comes to handling "." and "~" prefixes.
class RosXRefMixin(object):
def make_xref(self, rolename, domain, target, innernode=nodes.emphasis,
contnode=None, env=None):
if sphinx.version_info[:2] >= (1, 5):
result = super(RosXRefMixin, self).make_xref(rolename, domain,
target, innernode,
contnode, env)
else:
result = super(RosXRefMixin, self).make_xref(rolename, domain,
target, innernode,
contnode)
result['refspecific'] = True
if target.startswith(('.', '~')):
prefix, result['reftarget'] = target[0], target[1:]
if prefix == '.':
text = target[1:]
elif prefix == '~':
# text = target.split('.')[-1]
text = re.split(r'[\./]', target)[-1]
for node in result.traverse(nodes.Text):
node.parent[node.parent.index(node)] = nodes.Text(text)
break
return result
[docs]class RosObject(ObjectDescription):
"""
Description of a general ROS object.
"""
option_spec = {
'noindex': directives.flag,
'package': directives.unchanged,
}
[docs] def get_signature_prefix(self, sig):
"""
May return a prefix to put before the object name in the signature.
"""
return ''
[docs] def get_object_type_prefix(self):
"""
May return an optional name prefix that defines object type, e.g.
'msg' or 'srv'.
"""
return ''
[docs] def handle_signature(self, sig, signode):
"""
Transform a ROS signature into rST nodes.
Return (fully qualified name of the thing, package name if any).
If inside a package, the current package name is handled intelligently:
* it is stripped from the displayed name if present
* it is added to the full name (return value) if not present
"""
m = ros_sig_re.match(sig)
if m is None:
raise ValueError
name_prefix, obj_type, name = m.groups()
name_prefix = name_prefix.rstrip('.')
pkgname = self.options.get(
'package', self.env.ref_context.get('ros:package'))
if not obj_type:
obj_type = self.get_object_type_prefix()
if name_prefix and name_prefix.startswith(pkgname):
fullname = '.'.join([name_prefix, obj_type, name])
name_prefix = name_prefix[len(pkgname):].lstrip('.')
elif name_prefix:
fullname = '.'.join([pkgname, name_prefix, obj_type, name])
else:
fullname = '.'.join([pkgname, obj_type, name])
signode['package'] = pkgname
signode['fullname'] = fullname
sig_prefix = self.get_signature_prefix(sig)
if sig_prefix:
signode += addnodes.desc_annotation(sig_prefix, sig_prefix)
if name_prefix:
signode += addnodes.desc_addname(name_prefix, name_prefix)
elif self.env.config.ros_add_package_names:
if pkgname:
nodetext = pkgname + '.'
if obj_type:
nodetext += obj_type + '.'
signode += addnodes.desc_addname(nodetext, nodetext)
signode += addnodes.desc_name(name, name)
return fullname, name_prefix, obj_type
[docs] def get_index_text(self, pkgname, name):
"""
Return the text for the index entry of the object.
"""
raise NotImplementedError('must be implemented in subclasses')
[docs] def add_object_to_domain_data(self, fullname, obj_type):
"""
Add the object to the object lists of the ROS domain data.
"""
raise NotImplementedError('must be implemented in subclasses')
def add_target_and_index(self, name, sig, signode):
pkgname = self.options.get('package',
self.env.ref_context.get('ros:package'))
fullname = name[0]
obj_type = name[2]
# Note target
if fullname not in self.state.document.ids:
signode['names'].append(fullname)
signode['ids'].append(fullname)
signode['first'] = (not self.names)
self.state.document.note_explicit_target(signode)
self.add_object_to_domain_data(fullname, obj_type)
indextext = self.get_index_text(pkgname, name)
if sphinx.version_info[:2] >= (1, 4):
entry = ('single', indextext, fullname, '',
name_to_key(name[0]))
else:
entry = ('single', indextext, fullname, '')
if indextext:
self.indexnode['entries'].append(entry)
class RosField(RosXRefMixin, Field):
pass
class RosTypedField(RosXRefMixin, TypedField):
pass
[docs]class RosType(RosObject):
"""
Super class for messages, services, and actions.
"""
def handle_signature(self, sig, signode):
pkg, name = split_pkg_object(sig, self.get_object_type_prefix())
env_pkg = self.options.get('package',
self.env.ref_context.get('ros:package'))
if not pkg == env_pkg:
# TODO: issue warning that message is in wrong package
pass
pkg_name = pkg and pkg or env_pkg
name_prefix = '.'.join([pkg_name, self.get_object_type_prefix(),
''])
fullname = name_prefix + name
signode['package'] = pkg_name
signode['fullname'] = fullname
sig_prefix = self.get_signature_prefix(sig)
signode += addnodes.desc_annotation(sig_prefix, sig_prefix)
if self.env.config.ros_add_package_names:
signode += addnodes.desc_addname(pkg_name + '/', pkg_name + '/')
signode += addnodes.desc_name(name, name)
return fullname, name_prefix, self.objtype, name
def get_object_type_prefix(self):
if self.objtype == 'message':
return 'msg'
elif self.objtype == 'service':
return 'srv'
elif self.objtype == 'action':
return 'action'
def get_signature_prefix(self, sig):
return self.objtype + ' '
def get_index_text(self, pkgname, name):
fullname = name[0]
name_prefix = name[1]
obj_type = name[2]
newname = name[3]
if pkgname:
text = '{} ({} in package {})'.format(newname, self.objtype,
pkgname)
else:
text = '{} ({})'.format(fullname, self.objtype)
return text
def add_target_and_index(self, name, sig, signode):
pkgname = self.options.get('package',
self.env.ref_context.get('ros:package'))
fullname = name[0]
obj_type = name[2]
short_name = name[3]
# Note target
if fullname not in self.state.document.ids:
signode['names'].append(fullname)
signode['ids'].append(fullname)
signode['first'] = (not self.names)
self.state.document.note_explicit_target(signode)
self.add_object_to_domain_data(fullname, obj_type)
indextext = self.get_index_text(pkgname, name)
if sphinx.version_info[:2] >= (1, 4):
entry = [('single', indextext, short_name, '',
name_to_key(short_name[0]))]
else:
entry = [('single', indextext, short_name, '')]
if indextext:
self.indexnode['entries'] += entry
# self.indexnode['entries'].append(entry)
def add_object_to_domain_data(self, fullname, obj_type):
objects = self.env.domaindata['ros']['objects']
if fullname in objects:
self.state_machine.reporter.warning(
'duplicate object description of %s, ' % fullname +
'other instance in ' +
self.env.doc2path(objects[fullname][0]) +
', use :noindex: for one of them',
line=self.lineno)
objects[fullname] = (self.env.docname, self.objtype)
[docs]class RosCurrentPackageDirective(Directive):
"""
This directive is just to tell Sphinx that we're documenting stuff in this
package, but links to this package will not lead here.
"""
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = {}
def run(self):
env = self.state.document.settings.env
pkgname = self.arguments[0].strip()
if pkgname == 'None':
env.ref_context.pop('ros:package', None)
else:
env.ref_context['ros:package'] = pkgname
return []
[docs]class RosPackageDirective(Directive):
"""
Directive to mark description of a new package.
"""
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = {
'noindex': directives.flag,
'deprecated': directives.flag
}
def run(self):
env = self.state.document.settings.env
pkgname = self.arguments[0].strip()
noindex = 'noindex' in self.options
env.ref_context['ros:package'] = pkgname
ret = []
if not noindex:
ros_domain = env.get_domain('ros')
anchor = ros_domain.add_package(pkgname,
'deprecated' in self.options)
targetnode = nodes.target('', '', ids=[anchor])
self.state.document.note_explicit_target(targetnode)
ret.append(targetnode)
indextext = '{} (package)'.format(pkgname)
if sphinx.version_info[:2] >= (1, 4):
entry = ('single', indextext, anchor, '',
name_to_key(pkgname))
else:
entry = ('single', indextext, anchor, '')
inode = addnodes.index(entries=[entry])
ret.append(inode)
return ret
[docs]class RosActionDirective(RosType):
"""
Description of a ROS action type.
"""
doc_field_types = [
RosTypedField('goal_parameter',
label='Goal parameters',
names=('goal_param',),
typerolename='obj',
typenames=('goal_paramtype',),
can_collapse=True),
RosTypedField('result_parameter',
label='Result parameters',
names=('result_param',),
typerolename='obj',
typenames=('result_paramtype',),
can_collapse=True),
RosTypedField('feedback_parameter',
label='Feedback parameters',
names=('feedback_param',),
typerolename='obj',
typenames=('feedback_paramtype',),
can_collapse=True)
]
[docs]class RosServiceDirective(RosType):
"""
Description of a ROS service type.
"""
doc_field_types = [
RosTypedField('req_parameter',
label='Request parameters',
names=('req_param',),
typerolename='obj',
typenames=('req_paramtype',),
can_collapse=True),
RosTypedField('resp_parameter',
label='Response parameters',
names=('resp_param',),
typerolename='obj',
typenames=('resp_paramtype',),
can_collapse=True)
]
[docs]class RosMessageDirective(RosType):
"""
Description of a ROS message type.
"""
option_spec = {
'noindex': directives.flag,
'package': directives.unchanged,
'deprecated': directives.flag,
}
# option_spec['deprecated'] = directives.flag
doc_field_types = [
RosTypedField('parameter',
label='Parameters',
names=('msg_param',),
typerolename='obj',
typenames=('msg_paramtype',),
can_collapse=True),
]
def add_object_to_domain_data(self, fullname, obj_type):
ros_domain = self.env.get_domain('ros')
ros_domain.add_message(fullname, 'deprecated' in self.options)