You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
python-breathe/0001-Handle-parsing-errors....

1775 lines
68 KiB

From bbf329963dea00a82fef4a7aa511fbfe216336c4 Mon Sep 17 00:00:00 2001
From: Jakob Lykke Andersen <Jakob@caput.dk>
Date: Sun, 2 May 2021 11:26:07 +0200
Subject: [PATCH] Handle parsing errors
In a few places the C++ domain in Sphinx is used to parse fragments, but potential errors are not handled. This tries to fix that.
One case is the parsing of the function parameters in doxygenfunction, so .. doxygenfunction:: f( would crash Breathe.
The second case is parsing the function parameters in the XML, I don't know how to trigger that directly, but it should be handled now.
The third case is in the hax to insert the name in template parameters (#681). For parameters typename ...Args it will trigger a parsing error (observed in the pybind11 project).
Make WarningHandler private
Directives, make create_warning a method
Move BaseDirective from directive.base to directive
Make more base directive classes private
Move directive setup to separate file
Make function directive helpers private
Move function directive to separate file
Move class-like directives to separate file
Move content block directives to separate file
Move item directives to directive module
Rename directive module to directives
Handle parsing errors from the C++ domain
Fix typo
Co-authored-by: Bruce Merry <1963944+bmerry@users.noreply.github.com>
---
Makefile | 2 +-
breathe/__init__.py | 12 +-
breathe/directive/__init__.py | 0
breathe/directives.py | 619 ------------------
.../base.py => directives/__init__.py} | 35 +-
breathe/directives/class_like.py | 74 +++
breathe/directives/content_block.py | 117 ++++
breathe/{directive => directives}/file.py | 28 +-
breathe/directives/function.py | 249 +++++++
breathe/{directive => directives}/index.py | 22 +-
breathe/directives/item.py | 102 +++
breathe/directives/setup.py | 133 ++++
breathe/renderer/sphinxrenderer.py | 32 +-
tests/test_renderer.py | 2 +-
14 files changed, 738 insertions(+), 689 deletions(-)
delete mode 100644 breathe/directive/__init__.py
delete mode 100644 breathe/directives.py
rename breathe/{directive/base.py => directives/__init__.py} (85%)
create mode 100644 breathe/directives/class_like.py
create mode 100644 breathe/directives/content_block.py
rename breathe/{directive => directives}/file.py (83%)
create mode 100644 breathe/directives/function.py
rename breathe/{directive => directives}/index.py (89%)
create mode 100644 breathe/directives/item.py
create mode 100644 breathe/directives/setup.py
diff --git a/Makefile b/Makefile
index e34a282..129a441 100644
--- a/Makefile
+++ b/Makefile
@@ -37,7 +37,7 @@ dev-test:
.PHONY: flake8
flake8:
flake8 breathe/*.py \
- breathe/directive/*.py \
+ breathe/directives/*.py \
breathe/finder/*.py \
breathe/renderer/sphinxrenderer.py \
breathe/renderer/filter.py \
diff --git a/breathe/__init__.py b/breathe/__init__.py
index 311e958..1821c89 100644
--- a/breathe/__init__.py
+++ b/breathe/__init__.py
@@ -1,6 +1,6 @@
-from . import directives
-from . import file_state_cache
-from .renderer import sphinxrenderer
+from breathe.directives.setup import setup as directive_setup
+from breathe.file_state_cache import setup as file_state_cache_setup
+from breathe.renderer.sphinxrenderer import setup as renderer_setup
from sphinx.application import Sphinx
@@ -8,9 +8,9 @@ __version__ = '4.30.0'
def setup(app: Sphinx):
- directives.setup(app)
- file_state_cache.setup(app)
- sphinxrenderer.setup(app)
+ directive_setup(app)
+ file_state_cache_setup(app)
+ renderer_setup(app)
return {
'version': __version__,
diff --git a/breathe/directive/__init__.py b/breathe/directive/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/breathe/directives.py b/breathe/directives.py
deleted file mode 100644
index a57e075..0000000
--- a/breathe/directives.py
+++ /dev/null
@@ -1,619 +0,0 @@
-from breathe.directive.base import create_warning
-from breathe.directive.file import DoxygenFileDirective, AutoDoxygenFileDirective
-from breathe.directive.index import DoxygenIndexDirective, AutoDoxygenIndexDirective
-from breathe.exception import BreatheError
-from breathe.finder.factory import FinderFactory
-from breathe.directive.base import BaseDirective
-from breathe.file_state_cache import MTimeError
-from breathe.parser import DoxygenParserFactory, ParserError, FileIOError
-from breathe.process import AutoDoxygenProcessHandle
-from breathe.project import ProjectInfoFactory, ProjectError
-from breathe.renderer import format_parser_error, RenderContext
-from breathe.renderer.sphinxrenderer import WithContext
-from breathe.renderer.filter import Filter
-from breathe.renderer.mask import (
- MaskFactory, NullMaskFactory, NoParameterNamesMask
-)
-from breathe.renderer.sphinxrenderer import SphinxRenderer
-from breathe.renderer.target import create_target_handler
-
-from sphinx.application import Sphinx
-from sphinx.domains import cpp
-
-from docutils import nodes
-from docutils.nodes import Node
-from docutils.parsers.rst.directives import unchanged_required, unchanged, flag
-
-import os
-import re
-import subprocess
-
-from typing import Any, List, Optional, Type # noqa
-
-
-class NoMatchingFunctionError(BreatheError):
- pass
-
-
-class UnableToResolveFunctionError(BreatheError):
- def __init__(self, signatures: List[str]) -> None:
- self.signatures = signatures
-
-
-class DoxygenFunctionDirective(BaseDirective):
- required_arguments = 1
- option_spec = {
- "path": unchanged_required,
- "project": unchanged_required,
- "outline": flag,
- "no-link": flag,
- }
- has_content = False
- final_argument_whitespace = True
-
- def run(self) -> List[Node]:
- # Separate possible arguments (delimited by a "(") from the namespace::name
- match = re.match(r"([^(]*)(.*)", self.arguments[0])
- assert match is not None # TODO: this is probably not appropriate, for now it fixes typing
- namespaced_function, args = match.group(1), match.group(2)
-
- # Split the namespace and the function name
- try:
- (namespace, function_name) = namespaced_function.rsplit("::", 1)
- except ValueError:
- (namespace, function_name) = "", namespaced_function
- namespace = namespace.strip()
- function_name = function_name.strip()
-
- try:
- project_info = self.project_info_factory.create_project_info(self.options)
- except ProjectError as e:
- warning = create_warning(None, self.state, self.lineno)
- return warning.warn('doxygenfunction: %s' % e)
-
- try:
- finder = self.finder_factory.create_finder(project_info)
- except MTimeError as e:
- warning = create_warning(None, self.state, self.lineno)
- return warning.warn('doxygenfunction: %s' % e)
-
- # Extract arguments from the function name.
- args = self._parse_args(args)
-
- finder_filter = self.filter_factory.create_function_and_all_friend_finder_filter(
- namespace, function_name)
-
- # TODO: find a more specific type for the Doxygen nodes
- matchesAll = [] # type: List[Any]
- finder.filter_(finder_filter, matchesAll)
- matches = []
- for m in matchesAll:
- # only take functions and friend functions
- # ignore friend classes
- node = m[0]
- if node.kind == 'friend' and not node.argsstring:
- continue
- matches.append(m)
-
- # Create it ahead of time as it is cheap and it is ugly to declare it for both exception
- # clauses below
- warning = create_warning(
- project_info,
- self.state,
- self.lineno,
- namespace='%s::' % namespace if namespace else '',
- function=function_name,
- args=str(args)
- )
-
- try:
- node_stack = self._resolve_function(matches, args, project_info)
- except NoMatchingFunctionError:
- return warning.warn('doxygenfunction: Cannot find function "{namespace}{function}" '
- '{tail}')
- except UnableToResolveFunctionError as error:
- message = 'doxygenfunction: Unable to resolve function ' \
- '"{namespace}{function}" with arguments {args} {tail}.\n' \
- 'Potential matches:\n'
-
- text = ''
- for i, entry in enumerate(sorted(error.signatures)):
- text += '- %s\n' % entry
- block = nodes.literal_block('', '', nodes.Text(text))
- formatted_message = warning.format(message)
- warning_nodes = [
- nodes.paragraph("", "", nodes.Text(formatted_message)),
- block
- ]
- result = warning.warn(message, rendered_nodes=warning_nodes,
- unformatted_suffix=text)
- return result
-
- target_handler = create_target_handler(self.options, project_info, self.state.document)
- filter_ = self.filter_factory.create_outline_filter(self.options)
-
- return self.render(node_stack, project_info, filter_, target_handler, NullMaskFactory(),
- self.directive_args)
-
- def _parse_args(self, function_description: str) -> Optional[cpp.ASTParametersQualifiers]:
- if function_description == '':
- return None
-
- parser = cpp.DefinitionParser(function_description,
- location=self.get_source_info(),
- config=self.config)
- paramQual = parser._parse_parameters_and_qualifiers(paramMode='function')
- # now erase the parameter names
- for p in paramQual.args:
- if p.arg is None:
- assert p.ellipsis
- continue
- declarator = p.arg.type.decl
- while hasattr(declarator, 'next'):
- declarator = declarator.next # type: ignore
- assert hasattr(declarator, 'declId')
- declarator.declId = None # type: ignore
- p.arg.init = None # type: ignore
- return paramQual
-
- def _create_function_signature(self, node_stack, project_info, filter_, target_handler,
- mask_factory, directive_args) -> str:
- "Standard render process used by subclasses"
-
- try:
- object_renderer = SphinxRenderer(
- self.parser_factory.app,
- project_info,
- node_stack,
- self.state,
- self.state.document,
- target_handler,
- self.parser_factory.create_compound_parser(project_info),
- filter_,
- )
- except ParserError as e:
- return format_parser_error("doxygenclass", e.error, e.filename, self.state,
- self.lineno, True)
- except FileIOError as e:
- return format_parser_error("doxygenclass", e.error, e.filename, self.state,
- self.lineno, False)
-
- context = RenderContext(node_stack, mask_factory, directive_args)
- node = node_stack[0]
- with WithContext(object_renderer, context):
- # this part should be kept in sync with visit_function in sphinxrenderer
- name = node.get_name()
- # assume we are only doing this for C++ declarations
- declaration = ' '.join([
- object_renderer.create_template_prefix(node),
- ''.join(n.astext() for n in object_renderer.render(node.get_type())),
- name,
- node.get_argsstring()
- ])
- parser = cpp.DefinitionParser(declaration,
- location=self.get_source_info(),
- config=self.config)
- ast = parser.parse_declaration('function', 'function')
- return str(ast)
-
- def _resolve_function(self, matches, args: Optional[cpp.ASTParametersQualifiers], project_info):
- if not matches:
- raise NoMatchingFunctionError()
-
- res = []
- candSignatures = []
- for entry in matches:
- text_options = {'no-link': u'', 'outline': u''}
-
- # Render the matches to docutils nodes
- target_handler = create_target_handler({'no-link': u''},
- project_info, self.state.document)
- filter_ = self.filter_factory.create_outline_filter(text_options)
- mask_factory = MaskFactory({'param': NoParameterNamesMask})
-
- # Override the directive args for this render
- directive_args = self.directive_args[:]
- directive_args[2] = text_options
-
- signature = self._create_function_signature(entry, project_info, filter_,
- target_handler,
- mask_factory, directive_args)
- candSignatures.append(signature)
-
- if args is not None:
- match = re.match(r"([^(]*)(.*)", signature)
- assert match
- _match_args = match.group(2)
-
- # Parse the text to find the arguments
- match_args = self._parse_args(_match_args)
-
- # Match them against the arg spec
- if args != match_args:
- continue
-
- res.append((entry, signature))
-
- if len(res) == 1:
- return res[0][0]
- else:
- raise UnableToResolveFunctionError(candSignatures)
-
-
-class _DoxygenClassLikeDirective(BaseDirective):
- required_arguments = 1
- optional_arguments = 0
- final_argument_whitespace = True
- option_spec = {
- "path": unchanged_required,
- "project": unchanged_required,
- "members": unchanged,
- "membergroups": unchanged_required,
- "members-only": flag,
- "protected-members": flag,
- "private-members": flag,
- "undoc-members": flag,
- "show": unchanged_required,
- "outline": flag,
- "no-link": flag,
- }
- has_content = False
-
- def run(self) -> List[Node]:
- name = self.arguments[0]
-
- try:
- project_info = self.project_info_factory.create_project_info(self.options)
- except ProjectError as e:
- warning = create_warning(None, self.state, self.lineno, kind=self.kind)
- return warning.warn('doxygen{kind}: %s' % e)
-
- try:
- finder = self.finder_factory.create_finder(project_info)
- except MTimeError as e:
- warning = create_warning(None, self.state, self.lineno, kind=self.kind)
- return warning.warn('doxygen{kind}: %s' % e)
-
- finder_filter = self.filter_factory.create_compound_finder_filter(name, self.kind)
-
- # TODO: find a more specific type for the Doxygen nodes
- matches = [] # type: List[Any]
- finder.filter_(finder_filter, matches)
-
- if len(matches) == 0:
- warning = create_warning(project_info, self.state, self.lineno, name=name,
- kind=self.kind)
- return warning.warn('doxygen{kind}: Cannot find class "{name}" {tail}')
-
- target_handler = create_target_handler(self.options, project_info, self.state.document)
- filter_ = self.filter_factory.create_class_filter(name, self.options)
-
- mask_factory = NullMaskFactory()
- return self.render(matches[0], project_info, filter_, target_handler, mask_factory,
- self.directive_args)
-
-
-class DoxygenClassDirective(_DoxygenClassLikeDirective):
- kind = "class"
-
-
-class DoxygenStructDirective(_DoxygenClassLikeDirective):
- kind = "struct"
-
-
-class DoxygenInterfaceDirective(_DoxygenClassLikeDirective):
- kind = "interface"
-
-
-class _DoxygenContentBlockDirective(BaseDirective):
- """Base class for namespace and group directives which have very similar behaviours"""
-
- required_arguments = 1
- optional_arguments = 1
- option_spec = {
- "path": unchanged_required,
- "project": unchanged_required,
- "content-only": flag,
- "outline": flag,
- "members": flag,
- "protected-members": flag,
- "private-members": flag,
- "undoc-members": flag,
- "no-link": flag
- }
- has_content = False
-
- def run(self) -> List[Node]:
- name = self.arguments[0]
-
- try:
- project_info = self.project_info_factory.create_project_info(self.options)
- except ProjectError as e:
- warning = create_warning(None, self.state, self.lineno, kind=self.kind)
- return warning.warn('doxygen{kind}: %s' % e)
-
- try:
- finder = self.finder_factory.create_finder(project_info)
- except MTimeError as e:
- warning = create_warning(None, self.state, self.lineno, kind=self.kind)
- return warning.warn('doxygen{kind}: %s' % e)
-
- finder_filter = self.filter_factory.create_finder_filter(self.kind, name)
-
- # TODO: find a more specific type for the Doxygen nodes
- matches = [] # type: List[Any]
- finder.filter_(finder_filter, matches)
-
- # It shouldn't be possible to have too many matches as namespaces & groups in their nature
- # are merged together if there are multiple declarations, so we only check for no matches
- if not matches:
- warning = create_warning(project_info, self.state, self.lineno, name=name,
- kind=self.kind)
- return warning.warn('doxygen{kind}: Cannot find {kind} "{name}" {tail}')
-
- if 'content-only' in self.options and self.kind != "page":
- # Unpack the single entry in the matches list
- (node_stack,) = matches
-
- filter_ = self.filter_factory.create_content_filter(self.kind, self.options)
- # Having found the compound node for the namespace or group in the index we want to grab
- # the contents of it which match the filter
- contents_finder = self.finder_factory.create_finder_from_root(node_stack[0],
- project_info)
- # TODO: find a more specific type for the Doxygen nodes
- contents = [] # type: List[Any]
- contents_finder.filter_(filter_, contents)
-
- # Replaces matches with our new starting points
- matches = contents
-
- target_handler = create_target_handler(self.options, project_info, self.state.document)
- filter_ = self.filter_factory.create_render_filter(self.kind, self.options)
-
- node_list = []
- for node_stack in matches:
- object_renderer = SphinxRenderer(
- self.parser_factory.app,
- project_info,
- node_stack,
- self.state,
- self.state.document,
- target_handler,
- self.parser_factory.create_compound_parser(project_info),
- filter_
- )
-
- mask_factory = NullMaskFactory()
- context = RenderContext(node_stack, mask_factory, self.directive_args)
- node_list.extend(object_renderer.render(context.node_stack[0], context))
-
- return node_list
-
-
-class DoxygenNamespaceDirective(_DoxygenContentBlockDirective):
- kind = "namespace"
-
-
-class DoxygenGroupDirective(_DoxygenContentBlockDirective):
- kind = "group"
- option_spec = {
- **_DoxygenContentBlockDirective.option_spec,
- "inner": flag,
- }
-
-
-class DoxygenPageDirective(_DoxygenContentBlockDirective):
- kind = "page"
- option_spec = {
- "path": unchanged_required,
- "project": unchanged_required,
- "content-only": flag,
- }
-
-
-# TODO: is this comment still relevant?
-# This class was the same as the DoxygenBaseDirective above, except that it
-# wraps the output in a definition_list before passing it back. This should be
-# abstracted in a far nicer way to avoid repeating so much code
-#
-# Now we've removed the definition_list wrap so we really need to refactor this!
-class _DoxygenBaseItemDirective(BaseDirective):
- required_arguments = 1
- optional_arguments = 1
- option_spec = {
- "path": unchanged_required,
- "project": unchanged_required,
- "outline": flag,
- "no-link": flag,
- }
- has_content = False
-
- def create_finder_filter(self, namespace: str, name: str) -> Filter:
- """Creates a filter to find the node corresponding to this item."""
-
- return self.filter_factory.create_member_finder_filter(
- namespace, name, self.kind)
-
- def run(self) -> List[Node]:
- try:
- namespace, name = self.arguments[0].rsplit("::", 1)
- except ValueError:
- namespace, name = "", self.arguments[0]
-
- try:
- project_info = self.project_info_factory.create_project_info(self.options)
- except ProjectError as e:
- warning = create_warning(None, self.state, self.lineno, kind=self.kind)
- return warning.warn('doxygen{kind}: %s' % e)
-
- try:
- finder = self.finder_factory.create_finder(project_info)
- except MTimeError as e:
- warning = create_warning(None, self.state, self.lineno, kind=self.kind)
- return warning.warn('doxygen{kind}: %s' % e)
-
- finder_filter = self.create_finder_filter(namespace, name)
-
- # TODO: find a more specific type for the Doxygen nodes
- matches = [] # type: List[Any]
- finder.filter_(finder_filter, matches)
-
- if len(matches) == 0:
- display_name = "%s::%s" % (namespace, name) if namespace else name
- warning = create_warning(project_info, self.state, self.lineno, kind=self.kind,
- display_name=display_name)
- return warning.warn('doxygen{kind}: Cannot find {kind} "{display_name}" {tail}')
-
- target_handler = create_target_handler(self.options, project_info, self.state.document)
- filter_ = self.filter_factory.create_outline_filter(self.options)
-
- node_stack = matches[0]
- mask_factory = NullMaskFactory()
- return self.render(node_stack, project_info, filter_, target_handler, mask_factory,
- self.directive_args)
-
-
-class DoxygenVariableDirective(_DoxygenBaseItemDirective):
- kind = "variable"
-
-
-class DoxygenDefineDirective(_DoxygenBaseItemDirective):
- kind = "define"
-
-
-class DoxygenEnumDirective(_DoxygenBaseItemDirective):
- kind = "enum"
-
-
-class DoxygenEnumValueDirective(_DoxygenBaseItemDirective):
- kind = "enumvalue"
-
- def create_finder_filter(self, namespace: str, name: str) -> Filter:
- return self.filter_factory.create_enumvalue_finder_filter(name)
-
-
-class DoxygenTypedefDirective(_DoxygenBaseItemDirective):
- kind = "typedef"
-
-
-class DoxygenUnionDirective(_DoxygenBaseItemDirective):
- kind = "union"
-
- def create_finder_filter(self, namespace: str, name: str) -> Filter:
- # Unions are stored in the xml file with their fully namespaced name
- # We're using C++ namespaces here, it might be best to make this file
- # type dependent
- #
- xml_name = "%s::%s" % (namespace, name) if namespace else name
- return self.filter_factory.create_compound_finder_filter(xml_name, 'union')
-
-
-# Setup Administration
-# --------------------
-
-class DirectiveContainer:
- def __init__(self, app: Sphinx, directive: Type[BaseDirective],
- finder_factory: FinderFactory, project_info_factory: ProjectInfoFactory,
- parser_factory: DoxygenParserFactory):
- self.app = app
- self.directive = directive
- self.finder_factory = finder_factory
- self.project_info_factory = project_info_factory
- self.parser_factory = parser_factory
-
- # Required for sphinx to inspect
- self.required_arguments = directive.required_arguments
- self.optional_arguments = directive.optional_arguments
- self.option_spec = directive.option_spec
- self.has_content = directive.has_content
- self.final_argument_whitespace = directive.final_argument_whitespace
-
- def __call__(self, *args):
- call_args = [
- self.finder_factory,
- self.project_info_factory,
- self.parser_factory
- ]
- call_args.extend(args)
- return self.directive(*call_args)
-
-
-def setup(app: Sphinx) -> None:
- directives = {
- "doxygenindex": DoxygenIndexDirective,
- "autodoxygenindex": AutoDoxygenIndexDirective,
- "doxygenfunction": DoxygenFunctionDirective,
- "doxygenstruct": DoxygenStructDirective,
- "doxygenclass": DoxygenClassDirective,
- "doxygeninterface": DoxygenInterfaceDirective,
- "doxygenvariable": DoxygenVariableDirective,
- "doxygendefine": DoxygenDefineDirective,
- "doxygenenum": DoxygenEnumDirective,
- "doxygenenumvalue": DoxygenEnumValueDirective,
- "doxygentypedef": DoxygenTypedefDirective,
- "doxygenunion": DoxygenUnionDirective,
- "doxygennamespace": DoxygenNamespaceDirective,
- "doxygengroup": DoxygenGroupDirective,
- "doxygenfile": DoxygenFileDirective,
- "autodoxygenfile": AutoDoxygenFileDirective,
- "doxygenpage": DoxygenPageDirective,
- }
-
- # note: the parser factory contains a cache of the parsed XML
- # note: the project_info_factory also contains some caching stuff
- # TODO: is that actually safe for when reading in parallel?
- project_info_factory = ProjectInfoFactory(app)
- parser_factory = DoxygenParserFactory(app)
- finder_factory = FinderFactory(app, parser_factory)
- for name, directive in directives.items():
- # ordinarily app.add_directive takes a class it self, but we need to inject extra arguments
- # so we give a DirectiveContainer object which has an overloaded __call__ operator.
- app.add_directive(name, DirectiveContainer( # type: ignore
- app,
- directive,
- finder_factory,
- project_info_factory,
- parser_factory
- ))
-
- app.add_config_value("breathe_projects", {}, True) # Dict[str, str]
- app.add_config_value("breathe_default_project", "", True) # str
- # Provide reasonable defaults for domain_by_extension mapping. Can be overridden by users.
- app.add_config_value("breathe_domain_by_extension",
- {'py': 'py', 'cs': 'cs'}, True) # Dict[str, str]
- app.add_config_value("breathe_domain_by_file_pattern", {}, True) # Dict[str, str]
- app.add_config_value("breathe_projects_source", {}, True)
- app.add_config_value("breathe_build_directory", '', True)
- app.add_config_value("breathe_default_members", (), True)
- app.add_config_value("breathe_show_define_initializer", False, 'env')
- app.add_config_value("breathe_show_enumvalue_initializer", False, 'env')
- app.add_config_value("breathe_implementation_filename_extensions", ['.c', '.cc', '.cpp'], True)
- app.add_config_value("breathe_doxygen_config_options", {}, True)
- app.add_config_value("breathe_use_project_refids", False, "env")
- app.add_config_value("breathe_order_parameters_first", False, 'env')
- app.add_config_value("breathe_separate_member_pages", False, 'env')
-
- breathe_css = "breathe.css"
- if (os.path.exists(os.path.join(app.confdir, "_static", breathe_css))): # type: ignore
- app.add_css_file(breathe_css)
-
- def write_file(directory, filename, content):
- # Check the directory exists
- if not os.path.exists(directory):
- os.makedirs(directory)
-
- # Write the file with the provided contents
- with open(os.path.join(directory, filename), "w") as f:
- f.write(content)
-
- doxygen_handle = AutoDoxygenProcessHandle(
- subprocess.check_call,
- write_file,
- project_info_factory)
-
- def doxygen_hook(app):
- doxygen_handle.generate_xml(
- app.config.breathe_projects_source,
- app.config.breathe_doxygen_config_options
- )
- app.connect("builder-inited", doxygen_hook)
diff --git a/breathe/directive/base.py b/breathe/directives/__init__.py
similarity index 85%
rename from breathe/directive/base.py
rename to breathe/directives/__init__.py
index f808306..db27fe2 100644
--- a/breathe/directive/base.py
+++ b/breathe/directives/__init__.py
@@ -16,7 +16,7 @@ from docutils.nodes import Node
from typing import Any, Dict, List, Optional, Sequence
-class WarningHandler:
+class _WarningHandler:
def __init__(self, state, context: Dict[str, Any]) -> None:
self.state = state
self.context = context
@@ -35,23 +35,6 @@ class WarningHandler:
return text.format(**self.context)
-def create_warning(project_info: Optional[ProjectInfo], state, lineno: int,
- **kwargs) -> WarningHandler:
- tail = ''
- if project_info:
- tail = 'in doxygen xml output for project "{project}" from directory: {path}'.format(
- project=project_info.name(),
- path=project_info.project_path()
- )
-
- context = dict(
- lineno=lineno,
- tail=tail,
- **kwargs
- )
- return WarningHandler(state, context)
-
-
class BaseDirective(SphinxDirective):
def __init__(self, finder_factory: FinderFactory,
project_info_factory: ProjectInfoFactory,
@@ -69,6 +52,22 @@ class BaseDirective(SphinxDirective):
def kind(self) -> str:
raise NotImplementedError
+ def create_warning(self, project_info: Optional[ProjectInfo], **kwargs) -> _WarningHandler:
+ if project_info:
+ tail = 'in doxygen xml output for project "{project}" from directory: {path}'.format(
+ project=project_info.name(),
+ path=project_info.project_path()
+ )
+ else:
+ tail = ''
+
+ context = dict(
+ lineno=self.lineno,
+ tail=tail,
+ **kwargs
+ )
+ return _WarningHandler(self.state, context)
+
def render(self, node_stack, project_info: ProjectInfo, filter_: Filter,
target_handler: TargetHandler, mask_factory: MaskFactoryBase,
directive_args) -> List[Node]:
diff --git a/breathe/directives/class_like.py b/breathe/directives/class_like.py
new file mode 100644
index 0000000..325251d
--- /dev/null
+++ b/breathe/directives/class_like.py
@@ -0,0 +1,74 @@
+from breathe.directives import BaseDirective
+from breathe.file_state_cache import MTimeError
+from breathe.project import ProjectError
+from breathe.renderer.mask import NullMaskFactory
+from breathe.renderer.target import create_target_handler
+
+from docutils.nodes import Node
+from docutils.parsers.rst.directives import unchanged_required, unchanged, flag
+
+from typing import Any, List, Optional, Type # noqa
+
+
+class _DoxygenClassLikeDirective(BaseDirective):
+ required_arguments = 1
+ optional_arguments = 0
+ final_argument_whitespace = True
+ option_spec = {
+ "path": unchanged_required,
+ "project": unchanged_required,
+ "members": unchanged,
+ "membergroups": unchanged_required,
+ "members-only": flag,
+ "protected-members": flag,
+ "private-members": flag,
+ "undoc-members": flag,
+ "show": unchanged_required,
+ "outline": flag,
+ "no-link": flag,
+ }
+ has_content = False
+
+ def run(self) -> List[Node]:
+ name = self.arguments[0]
+
+ try:
+ project_info = self.project_info_factory.create_project_info(self.options)
+ except ProjectError as e:
+ warning = self.create_warning(None, kind=self.kind)
+ return warning.warn('doxygen{kind}: %s' % e)
+
+ try:
+ finder = self.finder_factory.create_finder(project_info)
+ except MTimeError as e:
+ warning = self.create_warning(None, kind=self.kind)
+ return warning.warn('doxygen{kind}: %s' % e)
+
+ finder_filter = self.filter_factory.create_compound_finder_filter(name, self.kind)
+
+ # TODO: find a more specific type for the Doxygen nodes
+ matches = [] # type: List[Any]
+ finder.filter_(finder_filter, matches)
+
+ if len(matches) == 0:
+ warning = self.create_warning(project_info, name=name, kind=self.kind)
+ return warning.warn('doxygen{kind}: Cannot find class "{name}" {tail}')
+
+ target_handler = create_target_handler(self.options, project_info, self.state.document)
+ filter_ = self.filter_factory.create_class_filter(name, self.options)
+
+ mask_factory = NullMaskFactory()
+ return self.render(matches[0], project_info, filter_, target_handler, mask_factory,
+ self.directive_args)
+
+
+class DoxygenClassDirective(_DoxygenClassLikeDirective):
+ kind = "class"
+
+
+class DoxygenStructDirective(_DoxygenClassLikeDirective):
+ kind = "struct"
+
+
+class DoxygenInterfaceDirective(_DoxygenClassLikeDirective):
+ kind = "interface"
diff --git a/breathe/directives/content_block.py b/breathe/directives/content_block.py
new file mode 100644
index 0000000..f50d686
--- /dev/null
+++ b/breathe/directives/content_block.py
@@ -0,0 +1,117 @@
+from breathe.directives import BaseDirective
+from breathe.file_state_cache import MTimeError
+from breathe.project import ProjectError
+from breathe.renderer import RenderContext
+from breathe.renderer.mask import NullMaskFactory
+from breathe.renderer.sphinxrenderer import SphinxRenderer
+from breathe.renderer.target import create_target_handler
+
+from docutils.nodes import Node
+from docutils.parsers.rst.directives import unchanged_required, flag
+
+from typing import Any, List, Optional, Type # noqa
+
+
+class _DoxygenContentBlockDirective(BaseDirective):
+ """Base class for namespace and group directives which have very similar behaviours"""
+
+ required_arguments = 1
+ optional_arguments = 1
+ option_spec = {
+ "path": unchanged_required,
+ "project": unchanged_required,
+ "content-only": flag,
+ "outline": flag,
+ "members": flag,
+ "protected-members": flag,
+ "private-members": flag,
+ "undoc-members": flag,
+ "no-link": flag
+ }
+ has_content = False
+
+ def run(self) -> List[Node]:
+ name = self.arguments[0]
+
+ try:
+ project_info = self.project_info_factory.create_project_info(self.options)
+ except ProjectError as e:
+ warning = self.create_warning(None, kind=self.kind)
+ return warning.warn('doxygen{kind}: %s' % e)
+
+ try:
+ finder = self.finder_factory.create_finder(project_info)
+ except MTimeError as e:
+ warning = self.create_warning(None, kind=self.kind)
+ return warning.warn('doxygen{kind}: %s' % e)
+
+ finder_filter = self.filter_factory.create_finder_filter(self.kind, name)
+
+ # TODO: find a more specific type for the Doxygen nodes
+ matches = [] # type: List[Any]
+ finder.filter_(finder_filter, matches)
+
+ # It shouldn't be possible to have too many matches as namespaces & groups in their nature
+ # are merged together if there are multiple declarations, so we only check for no matches
+ if not matches:
+ warning = self.create_warning(project_info, name=name, kind=self.kind)
+ return warning.warn('doxygen{kind}: Cannot find {kind} "{name}" {tail}')
+
+ if 'content-only' in self.options and self.kind != "page":
+ # Unpack the single entry in the matches list
+ (node_stack,) = matches
+
+ filter_ = self.filter_factory.create_content_filter(self.kind, self.options)
+ # Having found the compound node for the namespace or group in the index we want to grab
+ # the contents of it which match the filter
+ contents_finder = self.finder_factory.create_finder_from_root(node_stack[0],
+ project_info)
+ # TODO: find a more specific type for the Doxygen nodes
+ contents = [] # type: List[Any]
+ contents_finder.filter_(filter_, contents)
+
+ # Replaces matches with our new starting points
+ matches = contents
+
+ target_handler = create_target_handler(self.options, project_info, self.state.document)
+ filter_ = self.filter_factory.create_render_filter(self.kind, self.options)
+
+ node_list = []
+ for node_stack in matches:
+ object_renderer = SphinxRenderer(
+ self.parser_factory.app,
+ project_info,
+ node_stack,
+ self.state,
+ self.state.document,
+ target_handler,
+ self.parser_factory.create_compound_parser(project_info),
+ filter_
+ )
+
+ mask_factory = NullMaskFactory()
+ context = RenderContext(node_stack, mask_factory, self.directive_args)
+ node_list.extend(object_renderer.render(context.node_stack[0], context))
+
+ return node_list
+
+
+class DoxygenNamespaceDirective(_DoxygenContentBlockDirective):
+ kind = "namespace"
+
+
+class DoxygenGroupDirective(_DoxygenContentBlockDirective):
+ kind = "group"
+ option_spec = {
+ **_DoxygenContentBlockDirective.option_spec,
+ "inner": flag,
+ }
+
+
+class DoxygenPageDirective(_DoxygenContentBlockDirective):
+ kind = "page"
+ option_spec = {
+ "path": unchanged_required,
+ "project": unchanged_required,
+ "content-only": flag,
+ }
diff --git a/breathe/directive/file.py b/breathe/directives/file.py
similarity index 83%
rename from breathe/directive/file.py
rename to breathe/directives/file.py
index 923c728..ccdf4aa 100644
--- a/breathe/directive/file.py
+++ b/breathe/directives/file.py
@@ -1,7 +1,6 @@
from ..renderer.mask import NullMaskFactory
-from ..directive.base import BaseDirective
+from ..directives import BaseDirective
from ..project import ProjectError
-from .base import create_warning
from breathe.renderer import RenderContext
from breathe.renderer.sphinxrenderer import SphinxRenderer
@@ -10,7 +9,7 @@ from breathe.renderer.target import create_target_handler
from docutils.parsers.rst.directives import unchanged_required, flag
-class BaseFileDirective(BaseDirective):
+class _BaseFileDirective(BaseDirective):
"""Base class handle the main work when given the appropriate file and project info to work
from.
"""
@@ -27,13 +26,10 @@ class BaseFileDirective(BaseDirective):
finder.filter_(finder_filter, matches)
if len(matches) > 1:
- warning = create_warning(None, self.state, self.lineno, file=file_,
- directivename=self.directive_name)
+ warning = self.create_warning(None, file=file_, directivename=self.directive_name)
return warning.warn('{directivename}: Found multiple matches for file "{file} {tail}')
-
elif not matches:
- warning = create_warning(None, self.state, self.lineno, file=file_,
- directivename=self.directive_name)
+ warning = self.create_warning(None, file=file_, directivename=self.directive_name)
return warning.warn('{directivename}: Cannot find file "{file} {tail}')
target_handler = create_target_handler(self.options, project_info, self.state.document)
@@ -59,8 +55,7 @@ class BaseFileDirective(BaseDirective):
return node_list
-class DoxygenFileDirective(BaseFileDirective):
-
+class DoxygenFileDirective(_BaseFileDirective):
directive_name = 'doxygenfile'
required_arguments = 0
@@ -71,25 +66,23 @@ class DoxygenFileDirective(BaseFileDirective):
"outline": flag,
"no-link": flag,
"sections": unchanged_required,
- }
+ }
has_content = False
def run(self):
"""Get the file from the argument and the project info from the factory."""
file_ = self.arguments[0]
-
try:
project_info = self.project_info_factory.create_project_info(self.options)
except ProjectError as e:
- warning = create_warning(None, self.state, self.lineno)
+ warning = self.create_warning(None)
return warning.warn('doxygenfile: %s' % e)
return self.handle_contents(file_, project_info)
-class AutoDoxygenFileDirective(BaseFileDirective):
-
+class AutoDoxygenFileDirective(_BaseFileDirective):
directive_name = 'autodoxygenfile'
required_arguments = 1
@@ -98,7 +91,7 @@ class AutoDoxygenFileDirective(BaseFileDirective):
"outline": flag,
"no-link": flag,
"sections": unchanged_required,
- }
+ }
has_content = False
def run(self):
@@ -107,11 +100,10 @@ class AutoDoxygenFileDirective(BaseFileDirective):
"""
file_ = self.arguments[0]
-
try:
project_info = self.project_info_factory.retrieve_project_info_for_auto(self.options)
except ProjectError as e:
- warning = create_warning(None, self.state, self.lineno)
+ warning = self.create_warning(None)
return warning.warn('autodoxygenfile: %s' % e)
return self.handle_contents(file_, project_info)
diff --git a/breathe/directives/function.py b/breathe/directives/function.py
new file mode 100644
index 0000000..ea89f53
--- /dev/null
+++ b/breathe/directives/function.py
@@ -0,0 +1,249 @@
+from breathe.directives import BaseDirective
+from breathe.exception import BreatheError
+from breathe.file_state_cache import MTimeError
+from breathe.parser import ParserError, FileIOError
+from breathe.project import ProjectError
+from breathe.renderer import format_parser_error, RenderContext
+from breathe.renderer.sphinxrenderer import WithContext
+from breathe.renderer.mask import (
+ MaskFactory, NullMaskFactory, NoParameterNamesMask
+)
+from breathe.renderer.sphinxrenderer import SphinxRenderer
+from breathe.renderer.target import create_target_handler
+
+from docutils.nodes import Node
+from docutils.parsers.rst.directives import unchanged_required, flag
+
+from sphinx.domains import cpp
+
+from docutils import nodes
+
+import re
+
+from typing import Any, List, Optional, Type # noqa
+
+
+class _NoMatchingFunctionError(BreatheError):
+ pass
+
+
+class _UnableToResolveFunctionError(BreatheError):
+ def __init__(self, signatures: List[str]) -> None:
+ self.signatures = signatures
+
+
+class DoxygenFunctionDirective(BaseDirective):
+ required_arguments = 1
+ option_spec = {
+ "path": unchanged_required,
+ "project": unchanged_required,
+ "outline": flag,
+ "no-link": flag,
+ }
+ has_content = False
+ final_argument_whitespace = True
+
+ def run(self) -> List[Node]:
+ # Separate possible arguments (delimited by a "(") from the namespace::name
+ match = re.match(r"([^(]*)(.*)", self.arguments[0])
+ assert match is not None # TODO: this is probably not appropriate, for now it fixes typing
+ namespaced_function, args = match.group(1), match.group(2)
+
+ # Split the namespace and the function name
+ try:
+ (namespace, function_name) = namespaced_function.rsplit("::", 1)
+ except ValueError:
+ (namespace, function_name) = "", namespaced_function
+ namespace = namespace.strip()
+ function_name = function_name.strip()
+
+ try:
+ project_info = self.project_info_factory.create_project_info(self.options)
+ except ProjectError as e:
+ warning = self.create_warning(None)
+ return warning.warn('doxygenfunction: %s' % e)
+
+ try:
+ finder = self.finder_factory.create_finder(project_info)
+ except MTimeError as e:
+ warning = self.create_warning(None)
+ return warning.warn('doxygenfunction: %s' % e)
+
+ # Extract arguments from the function name.
+ try:
+ args = self._parse_args(args)
+ except cpp.DefinitionError as e:
+ return self.create_warning(
+ project_info,
+ namespace='%s::' % namespace if namespace else '',
+ function=function_name,
+ args=str(args),
+ cpperror=str(e)
+ ).warn('doxygenfunction: Unable to resolve function '
+ '"{namespace}{function}" with arguments "{args}".\n'
+ 'Could not parse arguments. Parsing eror is\n{cpperror}')
+
+ finder_filter = self.filter_factory.create_function_and_all_friend_finder_filter(
+ namespace, function_name)
+
+ # TODO: find a more specific type for the Doxygen nodes
+ matchesAll = [] # type: List[Any]
+ finder.filter_(finder_filter, matchesAll)
+ matches = []
+ for m in matchesAll:
+ # only take functions and friend functions
+ # ignore friend classes
+ node = m[0]
+ if node.kind == 'friend' and not node.argsstring:
+ continue
+ matches.append(m)
+
+ # Create it ahead of time as it is cheap and it is ugly to declare it for both exception
+ # clauses below
+ warning = self.create_warning(
+ project_info,
+ namespace='%s::' % namespace if namespace else '',
+ function=function_name,
+ args=str(args)
+ )
+
+ try:
+ node_stack = self._resolve_function(matches, args, project_info)
+ except _NoMatchingFunctionError:
+ return warning.warn('doxygenfunction: Cannot find function "{namespace}{function}" '
+ '{tail}')
+ except _UnableToResolveFunctionError as error:
+ message = 'doxygenfunction: Unable to resolve function ' \
+ '"{namespace}{function}" with arguments {args} {tail}.\n' \
+ 'Potential matches:\n'
+
+ text = ''
+ for i, entry in enumerate(sorted(error.signatures)):
+ text += '- %s\n' % entry
+ block = nodes.literal_block('', '', nodes.Text(text))
+ formatted_message = warning.format(message)
+ warning_nodes = [
+ nodes.paragraph("", "", nodes.Text(formatted_message)),
+ block
+ ]
+ result = warning.warn(message, rendered_nodes=warning_nodes,
+ unformatted_suffix=text)
+ return result
+ except cpp.DefinitionError as error:
+ warning.context['cpperror'] = str(error)
+ return warning.warn(
+ 'doxygenfunction: Unable to resolve function '
+ '"{namespace}{function}" with arguments "{args}".\n'
+ 'Candidate function could not be parsed. Parsing error is\n{cpperror}')
+
+ target_handler = create_target_handler(self.options, project_info, self.state.document)
+ filter_ = self.filter_factory.create_outline_filter(self.options)
+
+ return self.render(node_stack, project_info, filter_, target_handler, NullMaskFactory(),
+ self.directive_args)
+
+ def _parse_args(self, function_description: str) -> Optional[cpp.ASTParametersQualifiers]:
+ # Note: the caller must catch cpp.DefinitionError
+ if function_description == '':
+ return None
+
+ parser = cpp.DefinitionParser(function_description,
+ location=self.get_source_info(),
+ config=self.config)
+ paramQual = parser._parse_parameters_and_qualifiers(paramMode='function')
+ # now erase the parameter names
+ for p in paramQual.args:
+ if p.arg is None:
+ assert p.ellipsis
+ continue
+ declarator = p.arg.type.decl
+ while hasattr(declarator, 'next'):
+ declarator = declarator.next # type: ignore
+ assert hasattr(declarator, 'declId')
+ declarator.declId = None # type: ignore
+ p.arg.init = None # type: ignore
+ return paramQual
+
+ def _create_function_signature(self, node_stack, project_info, filter_, target_handler,
+ mask_factory, directive_args) -> str:
+ "Standard render process used by subclasses"
+
+ try:
+ object_renderer = SphinxRenderer(
+ self.parser_factory.app,
+ project_info,
+ node_stack,
+ self.state,
+ self.state.document,
+ target_handler,
+ self.parser_factory.create_compound_parser(project_info),
+ filter_,
+ )
+ except ParserError as e:
+ return format_parser_error("doxygenclass", e.error, e.filename, self.state,
+ self.lineno, True)
+ except FileIOError as e:
+ return format_parser_error("doxygenclass", e.error, e.filename, self.state,
+ self.lineno, False)
+
+ context = RenderContext(node_stack, mask_factory, directive_args)
+ node = node_stack[0]
+ with WithContext(object_renderer, context):
+ # this part should be kept in sync with visit_function in sphinxrenderer
+ name = node.get_name()
+ # assume we are only doing this for C++ declarations
+ declaration = ' '.join([
+ object_renderer.create_template_prefix(node),
+ ''.join(n.astext() for n in object_renderer.render(node.get_type())),
+ name,
+ node.get_argsstring()
+ ])
+ parser = cpp.DefinitionParser(declaration,
+ location=self.get_source_info(),
+ config=self.config)
+ ast = parser.parse_declaration('function', 'function')
+ return str(ast)
+
+ def _resolve_function(self, matches, args: Optional[cpp.ASTParametersQualifiers], project_info):
+ if not matches:
+ raise _NoMatchingFunctionError()
+
+ res = []
+ candSignatures = []
+ for entry in matches:
+ text_options = {'no-link': u'', 'outline': u''}
+
+ # Render the matches to docutils nodes
+ target_handler = create_target_handler({'no-link': u''},
+ project_info, self.state.document)
+ filter_ = self.filter_factory.create_outline_filter(text_options)
+ mask_factory = MaskFactory({'param': NoParameterNamesMask})
+
+ # Override the directive args for this render
+ directive_args = self.directive_args[:]
+ directive_args[2] = text_options
+
+ signature = self._create_function_signature(entry, project_info, filter_,
+ target_handler,
+ mask_factory, directive_args)
+ candSignatures.append(signature)
+
+ if args is not None:
+ match = re.match(r"([^(]*)(.*)", signature)
+ assert match
+ _match_args = match.group(2)
+
+ # Parse the text to find the arguments
+ # This one should succeed as it came from _create_function_signature
+ match_args = self._parse_args(_match_args)
+
+ # Match them against the arg spec
+ if args != match_args:
+ continue
+
+ res.append((entry, signature))
+
+ if len(res) == 1:
+ return res[0][0]
+ else:
+ raise _UnableToResolveFunctionError(candSignatures)
diff --git a/breathe/directive/index.py b/breathe/directives/index.py
similarity index 89%
rename from breathe/directive/index.py
rename to breathe/directives/index.py
index 80e7384..d460f74 100644
--- a/breathe/directive/index.py
+++ b/breathe/directives/index.py
@@ -1,8 +1,7 @@
from ..renderer.mask import NullMaskFactory
-from ..directive.base import BaseDirective
+from ..directives import BaseDirective
from ..project import ProjectError
from ..parser import ParserError, FileIOError
-from .base import create_warning
from breathe.renderer import format_parser_error, RenderContext
from breathe.renderer.sphinxrenderer import SphinxRenderer
@@ -11,12 +10,11 @@ from breathe.renderer.target import create_target_handler
from docutils.parsers.rst.directives import unchanged_required, flag
-class RootDataObject(object):
-
+class RootDataObject:
node_type = "root"
-class BaseIndexDirective(BaseDirective):
+class _BaseIndexDirective(BaseDirective):
"""Base class handle the main work when given the appropriate project info to work from.
"""
@@ -25,7 +23,6 @@ class BaseIndexDirective(BaseDirective):
# pass way too much stuff to a helper object to be reasonable.
def handle_contents(self, project_info):
-
try:
finder = self.finder_factory.create_finder(project_info)
except ParserError as e:
@@ -65,7 +62,7 @@ class BaseIndexDirective(BaseDirective):
return node_list
-class DoxygenIndexDirective(BaseIndexDirective):
+class DoxygenIndexDirective(_BaseIndexDirective):
required_arguments = 0
optional_arguments = 2
option_spec = {
@@ -73,7 +70,7 @@ class DoxygenIndexDirective(BaseIndexDirective):
"project": unchanged_required,
"outline": flag,
"no-link": flag,
- }
+ }
has_content = False
def run(self):
@@ -82,21 +79,20 @@ class DoxygenIndexDirective(BaseIndexDirective):
try:
project_info = self.project_info_factory.create_project_info(self.options)
except ProjectError as e:
- warning = create_warning(None, self.state, self.lineno)
+ warning = self.create_warning(None)
return warning.warn('doxygenindex: %s' % e)
return self.handle_contents(project_info)
-class AutoDoxygenIndexDirective(BaseIndexDirective):
-
+class AutoDoxygenIndexDirective(_BaseIndexDirective):
required_arguments = 0
final_argument_whitespace = True
option_spec = {
"project": unchanged_required,
"outline": flag,
"no-link": flag,
- }
+ }
has_content = False
def run(self):
@@ -107,7 +103,7 @@ class AutoDoxygenIndexDirective(BaseIndexDirective):
try:
project_info = self.project_info_factory.retrieve_project_info_for_auto(self.options)
except ProjectError as e:
- warning = create_warning(None, self.state, self.lineno)
+ warning = self.create_warning(None)
return warning.warn('autodoxygenindex: %s' % e)
return self.handle_contents(project_info)
diff --git a/breathe/directives/item.py b/breathe/directives/item.py
new file mode 100644
index 0000000..26f901c
--- /dev/null
+++ b/breathe/directives/item.py
@@ -0,0 +1,102 @@
+from breathe.directives import BaseDirective
+from breathe.file_state_cache import MTimeError
+from breathe.project import ProjectError
+from breathe.renderer.filter import Filter
+from breathe.renderer.mask import NullMaskFactory
+from breathe.renderer.target import create_target_handler
+
+from docutils.nodes import Node
+from docutils.parsers.rst.directives import unchanged_required, flag
+
+from typing import Any, List, Optional, Type # noqa
+
+
+class _DoxygenBaseItemDirective(BaseDirective):
+ required_arguments = 1
+ optional_arguments = 1
+ option_spec = {
+ "path": unchanged_required,
+ "project": unchanged_required,
+ "outline": flag,
+ "no-link": flag,
+ }
+ has_content = False
+
+ def create_finder_filter(self, namespace: str, name: str) -> Filter:
+ """Creates a filter to find the node corresponding to this item."""
+
+ return self.filter_factory.create_member_finder_filter(
+ namespace, name, self.kind)
+
+ def run(self) -> List[Node]:
+ try:
+ namespace, name = self.arguments[0].rsplit("::", 1)
+ except ValueError:
+ namespace, name = "", self.arguments[0]
+
+ try:
+ project_info = self.project_info_factory.create_project_info(self.options)
+ except ProjectError as e:
+ warning = self.create_warning(None, kind=self.kind)
+ return warning.warn('doxygen{kind}: %s' % e)
+
+ try:
+ finder = self.finder_factory.create_finder(project_info)
+ except MTimeError as e:
+ warning = self.create_warning(None, kind=self.kind)
+ return warning.warn('doxygen{kind}: %s' % e)
+
+ finder_filter = self.create_finder_filter(namespace, name)
+
+ # TODO: find a more specific type for the Doxygen nodes
+ matches = [] # type: List[Any]
+ finder.filter_(finder_filter, matches)
+
+ if len(matches) == 0:
+ display_name = "%s::%s" % (namespace, name) if namespace else name
+ warning = self.create_warning(project_info, kind=self.kind,
+ display_name=display_name)
+ return warning.warn('doxygen{kind}: Cannot find {kind} "{display_name}" {tail}')
+
+ target_handler = create_target_handler(self.options, project_info, self.state.document)
+ filter_ = self.filter_factory.create_outline_filter(self.options)
+
+ node_stack = matches[0]
+ mask_factory = NullMaskFactory()
+ return self.render(node_stack, project_info, filter_, target_handler, mask_factory,
+ self.directive_args)
+
+
+class DoxygenVariableDirective(_DoxygenBaseItemDirective):
+ kind = "variable"
+
+
+class DoxygenDefineDirective(_DoxygenBaseItemDirective):
+ kind = "define"
+
+
+class DoxygenEnumDirective(_DoxygenBaseItemDirective):
+ kind = "enum"
+
+
+class DoxygenEnumValueDirective(_DoxygenBaseItemDirective):
+ kind = "enumvalue"
+
+ def create_finder_filter(self, namespace: str, name: str) -> Filter:
+ return self.filter_factory.create_enumvalue_finder_filter(name)
+
+
+class DoxygenTypedefDirective(_DoxygenBaseItemDirective):
+ kind = "typedef"
+
+
+class DoxygenUnionDirective(_DoxygenBaseItemDirective):
+ kind = "union"
+
+ def create_finder_filter(self, namespace: str, name: str) -> Filter:
+ # Unions are stored in the xml file with their fully namespaced name
+ # We're using C++ namespaces here, it might be best to make this file
+ # type dependent
+ #
+ xml_name = "%s::%s" % (namespace, name) if namespace else name
+ return self.filter_factory.create_compound_finder_filter(xml_name, 'union')
diff --git a/breathe/directives/setup.py b/breathe/directives/setup.py
new file mode 100644
index 0000000..fff6136
--- /dev/null
+++ b/breathe/directives/setup.py
@@ -0,0 +1,133 @@
+from breathe.directives import BaseDirective
+from breathe.directives.class_like import (
+ DoxygenStructDirective, DoxygenClassDirective, DoxygenInterfaceDirective,
+)
+from breathe.directives.content_block import (
+ DoxygenNamespaceDirective, DoxygenGroupDirective, DoxygenPageDirective,
+)
+from breathe.directives.file import DoxygenFileDirective, AutoDoxygenFileDirective
+from breathe.directives.function import DoxygenFunctionDirective
+from breathe.directives.index import DoxygenIndexDirective, AutoDoxygenIndexDirective
+from breathe.directives.item import (
+ DoxygenVariableDirective, DoxygenDefineDirective, DoxygenUnionDirective,
+ DoxygenEnumDirective, DoxygenEnumValueDirective, DoxygenTypedefDirective,
+)
+from breathe.finder.factory import FinderFactory
+from breathe.parser import DoxygenParserFactory
+from breathe.project import ProjectInfoFactory
+from breathe.process import AutoDoxygenProcessHandle
+
+from sphinx.application import Sphinx
+
+import os
+import subprocess
+
+from typing import Any, List, Optional, Type # noqa
+
+
+class DirectiveContainer:
+ def __init__(self, app: Sphinx, directive: Type[BaseDirective],
+ finder_factory: FinderFactory, project_info_factory: ProjectInfoFactory,
+ parser_factory: DoxygenParserFactory):
+ self.app = app
+ self.directive = directive
+ self.finder_factory = finder_factory
+ self.project_info_factory = project_info_factory
+ self.parser_factory = parser_factory
+
+ # Required for sphinx to inspect
+ self.required_arguments = directive.required_arguments
+ self.optional_arguments = directive.optional_arguments
+ self.option_spec = directive.option_spec
+ self.has_content = directive.has_content
+ self.final_argument_whitespace = directive.final_argument_whitespace
+
+ def __call__(self, *args):
+ call_args = [
+ self.finder_factory,
+ self.project_info_factory,
+ self.parser_factory
+ ]
+ call_args.extend(args)
+ return self.directive(*call_args)
+
+
+def setup(app: Sphinx) -> None:
+ directives = {
+ "doxygenindex": DoxygenIndexDirective,
+ "autodoxygenindex": AutoDoxygenIndexDirective,
+ "doxygenfunction": DoxygenFunctionDirective,
+ "doxygenstruct": DoxygenStructDirective,
+ "doxygenclass": DoxygenClassDirective,
+ "doxygeninterface": DoxygenInterfaceDirective,
+ "doxygenvariable": DoxygenVariableDirective,
+ "doxygendefine": DoxygenDefineDirective,
+ "doxygenenum": DoxygenEnumDirective,
+ "doxygenenumvalue": DoxygenEnumValueDirective,
+ "doxygentypedef": DoxygenTypedefDirective,
+ "doxygenunion": DoxygenUnionDirective,
+ "doxygennamespace": DoxygenNamespaceDirective,
+ "doxygengroup": DoxygenGroupDirective,
+ "doxygenfile": DoxygenFileDirective,
+ "autodoxygenfile": AutoDoxygenFileDirective,
+ "doxygenpage": DoxygenPageDirective,
+ }
+
+ # note: the parser factory contains a cache of the parsed XML
+ # note: the project_info_factory also contains some caching stuff
+ # TODO: is that actually safe for when reading in parallel?
+ project_info_factory = ProjectInfoFactory(app)
+ parser_factory = DoxygenParserFactory(app)
+ finder_factory = FinderFactory(app, parser_factory)
+ for name, directive in directives.items():
+ # ordinarily app.add_directive takes a class it self, but we need to inject extra arguments
+ # so we give a DirectiveContainer object which has an overloaded __call__ operator.
+ app.add_directive(name, DirectiveContainer( # type: ignore
+ app,
+ directive,
+ finder_factory,
+ project_info_factory,
+ parser_factory
+ ))
+
+ app.add_config_value("breathe_projects", {}, True) # Dict[str, str]
+ app.add_config_value("breathe_default_project", "", True) # str
+ # Provide reasonable defaults for domain_by_extension mapping. Can be overridden by users.
+ app.add_config_value("breathe_domain_by_extension",
+ {'py': 'py', 'cs': 'cs'}, True) # Dict[str, str]
+ app.add_config_value("breathe_domain_by_file_pattern", {}, True) # Dict[str, str]
+ app.add_config_value("breathe_projects_source", {}, True)
+ app.add_config_value("breathe_build_directory", '', True)
+ app.add_config_value("breathe_default_members", (), True)
+ app.add_config_value("breathe_show_define_initializer", False, 'env')
+ app.add_config_value("breathe_show_enumvalue_initializer", False, 'env')
+ app.add_config_value("breathe_implementation_filename_extensions", ['.c', '.cc', '.cpp'], True)
+ app.add_config_value("breathe_doxygen_config_options", {}, True)
+ app.add_config_value("breathe_use_project_refids", False, "env")
+ app.add_config_value("breathe_order_parameters_first", False, 'env')
+ app.add_config_value("breathe_separate_member_pages", False, 'env')
+
+ breathe_css = "breathe.css"
+ if (os.path.exists(os.path.join(app.confdir, "_static", breathe_css))): # type: ignore
+ app.add_css_file(breathe_css)
+
+ def write_file(directory, filename, content):
+ # Check the directory exists
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+
+ # Write the file with the provided contents
+ with open(os.path.join(directory, filename), "w") as f:
+ f.write(content)
+
+ doxygen_handle = AutoDoxygenProcessHandle(
+ subprocess.check_call,
+ write_file,
+ project_info_factory)
+
+ def doxygen_hook(app):
+ doxygen_handle.generate_xml(
+ app.config.breathe_projects_source,
+ app.config.breathe_doxygen_config_options
+ )
+ app.connect("builder-inited", doxygen_hook)
diff --git a/breathe/renderer/sphinxrenderer.py b/breathe/renderer/sphinxrenderer.py
index 8c16d45..07ae46c 100644
--- a/breathe/renderer/sphinxrenderer.py
+++ b/breathe/renderer/sphinxrenderer.py
@@ -2019,8 +2019,8 @@ class SphinxRenderer:
signode += nodes.Text(node.name)
return [desc]
- def visit_param(self, node: compound.paramTypeSub, *,
- insertDeclNameByParsing: bool = False) -> List[Node]:
+ def visit_templateparam(self, node: compound.paramTypeSub, *,
+ insertDeclNameByParsing: bool = False) -> List[Node]:
nodelist = []
# Parameter type
@@ -2048,15 +2048,22 @@ class SphinxRenderer:
''.join(n.astext() for n in nodelist),
location=self.state.state_machine.get_source_and_line(),
config=self.app.config)
- ast = parser._parse_type(named='single', outer='templateParam')
- assert ast.name is None
- nn = cpp.ASTNestedName(
- names=[cpp.ASTNestedNameElement(cpp.ASTIdentifier(node.declname), None)],
- templates=[False], rooted=False)
- ast.name = nn
- # the actual nodes don't matter, as it is astext()-ed later
- nodelist = [nodes.Text(str(ast))]
- appendDeclName = False
+ try:
+ # we really should use _parse_template_parameter()
+ # but setting a name there is non-trivial, so we use type
+ ast = parser._parse_type(named='single', outer='templateParam')
+ assert ast.name is None
+ nn = cpp.ASTNestedName(
+ names=[cpp.ASTNestedNameElement(
+ cpp.ASTIdentifier(node.declname), None)],
+ templates=[False], rooted=False)
+ ast.name = nn
+ # the actual nodes don't matter, as it is astext()-ed later
+ nodelist = [nodes.Text(str(ast))]
+ appendDeclName = False
+ except cpp.DefinitionError:
+ # happens with "typename ...Args", so for now, just append
+ pass
if appendDeclName:
if nodelist:
@@ -2086,7 +2093,7 @@ class SphinxRenderer:
for i, item in enumerate(node.param):
if i:
nodelist.append(nodes.Text(", "))
- nodelist.extend(self.visit_param(item, insertDeclNameByParsing=True))
+ nodelist.extend(self.visit_templateparam(item, insertDeclNameByParsing=True))
self.output_defname = True
return nodelist
@@ -2225,7 +2232,6 @@ class SphinxRenderer:
"compoundref": visit_compoundref,
"mixedcontainer": visit_mixedcontainer,
"description": visit_description,
- "param": visit_param,
"templateparamlist": visit_templateparamlist,
"docparamlist": visit_docparamlist,
"docxrefsect": visit_docxrefsect,
diff --git a/tests/test_renderer.py b/tests/test_renderer.py
index c2c8423..cf6ab7f 100644
--- a/tests/test_renderer.py
+++ b/tests/test_renderer.py
@@ -422,7 +422,7 @@ def test_render_innergroup(app):
options=['inner']))
def get_directive(app):
- from breathe.directives import DoxygenFunctionDirective
+ from breathe.directives.function import DoxygenFunctionDirective
from breathe.project import ProjectInfoFactory
from breathe.parser import DoxygenParserFactory
from breathe.finder.factory import FinderFactory
--
2.31.1