From fc3c6002b64c9037fb855676cd1e7d8ee764b3a2 Mon Sep 17 00:00:00 2001 From: Jani Nikula Date: Mon, 23 Oct 2023 23:15:49 +0300 Subject: [PATCH 1/3] docstring: fix line number references Fix some issues in line number references. Take into account stripping blank lines from the comments and adding header lines before the comments. Take into account that ViewList uses 0-based line numbers. With this, error reporting should have more accurate line numbers. --- src/hawkmoth/__init__.py | 8 ++++---- src/hawkmoth/__main__.py | 3 ++- src/hawkmoth/docstring.py | 19 +++++++++++++------ test/test_parser.py | 2 +- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/hawkmoth/__init__.py b/src/hawkmoth/__init__.py index 7f979b22..84892b19 100644 --- a/src/hawkmoth/__init__.py +++ b/src/hawkmoth/__init__.py @@ -109,11 +109,11 @@ def __get_docstrings_for_root(self, viewlist, root): filter_names=self._get_names()): num_matches += 1 for docstr in docstrings.walk(filter_names=self._get_members()): - lineoffset = docstr.get_line() - 1 - lines = docstr.get_docstring(process_docstring=process_docstring) + lines, line_number = docstr.get_docstring(process_docstring=process_docstring) for line in lines: - viewlist.append(line, root.get_filename(), lineoffset) - lineoffset += 1 + # viewlist line numbers are 0-based + viewlist.append(line, root.get_filename(), line_number - 1) + line_number += 1 return num_matches diff --git a/src/hawkmoth/__main__.py b/src/hawkmoth/__main__.py index 281ff652..edb83abd 100644 --- a/src/hawkmoth/__main__.py +++ b/src/hawkmoth/__main__.py @@ -55,7 +55,8 @@ def main(): for comment in comments.walk(): if args.verbose: print(f'# {comment.get_meta()}') - print('\n'.join(comment.get_docstring(process_docstring=process_docstring))) + lines, _ = comment.get_docstring(process_docstring=process_docstring) + print('\n'.join(lines)) for error in errors: print(f'{error.level.name}: {error.get_message()}', file=sys.stderr) diff --git a/src/hawkmoth/docstring.py b/src/hawkmoth/docstring.py index f70ae01f..6397db08 100644 --- a/src/hawkmoth/docstring.py +++ b/src/hawkmoth/docstring.py @@ -113,7 +113,12 @@ def _get_comment_lines(self): @staticmethod def _remove_comment_markers(lines): - """Remove comment markers and line prefixes from comment lines.""" + """Remove comment markers and line prefixes from comment lines. + + Return the number of lines removed from the beginning. + """ + line_offset = 0 + lines[0] = re.sub(r'^/\*\*[ \t]*', '', lines[0]) lines[-1] = re.sub(r'[ \t]*\*/$', '', lines[-1]) @@ -121,11 +126,14 @@ def _remove_comment_markers(lines): lines[1:-1] = [line[prefix_len:] for line in lines[1:-1]] while lines and (not lines[0] or lines[0].isspace()): + line_offset += 1 del lines[0] while lines and (not lines[-1] or lines[-1].isspace()): del lines[-1] + return line_offset + @staticmethod def _nest(lines, nest): """ @@ -143,10 +151,7 @@ def get_docstring(self, process_docstring=None): header_lines = self._get_header_lines() comment_lines = self._get_comment_lines() - # FIXME: This changes the number of lines in output. This impacts the - # error reporting via meta['line']. Adjust meta to take this into - # account. - Docstring._remove_comment_markers(comment_lines) + line_offset = Docstring._remove_comment_markers(comment_lines) if process_docstring is not None: process_docstring(comment_lines) @@ -161,6 +166,8 @@ def get_docstring(self, process_docstring=None): if header_lines[-1] != '': header_lines.append('') + line_offset -= len(header_lines) + lines = header_lines + comment_lines Docstring._nest(lines, self._nest) @@ -169,7 +176,7 @@ def get_docstring(self, process_docstring=None): if lines[-1] != '': lines.append('') - return lines + return lines, self.get_line() + line_offset def get_meta(self): return self._meta diff --git a/test/test_parser.py b/test/test_parser.py index 892f32c2..b8207d62 100755 --- a/test/test_parser.py +++ b/test/test_parser.py @@ -111,7 +111,7 @@ def get_output(self): for docstrings in root.walk(recurse=False, filter_types=_filter_types(directive), filter_names=_filter_names(directive)): for docstr in docstrings.walk(filter_names=_filter_members(directive)): - lines = docstr.get_docstring(process_docstring=process_docstring) + lines, _ = docstr.get_docstring(process_docstring=process_docstring) docs_str += '\n'.join(lines) + '\n' return docs_str, errors_str From 3db1d812f74a3e108662ee1a81a66003f30d383b Mon Sep 17 00:00:00 2001 From: Jani Nikula Date: Sat, 7 Oct 2023 00:09:23 +0300 Subject: [PATCH 2/3] extension: add support for source code links Sphinx has sphinx.ext.viewcode and sphinx.ext.linkcode extensions to fully incorporate source code into the documentation and to link to externally hosted source code, respectively. They are not trivial to integrate with Hawkmoth, however. Add a dedicated version of linkcode that can be configured with a simple template URI. --- doc/extension.rst | 21 ++++++++++++++++++ src/hawkmoth/__init__.py | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/doc/extension.rst b/doc/extension.rst index e6ffe9cd..9aca9b1c 100644 --- a/doc/extension.rst +++ b/doc/extension.rst @@ -85,6 +85,27 @@ See also additional configuration options in the :ref:`built-in extensions You can also pass in the compiler to use, for example ``get_include_args('gcc')``. +.. py:data:: hawkmoth_source_uri + :type: str + + A template URI to source code. If set, add links to externally hosted source + code for each documented symbol, similar to the :external+sphinx:doc:`Sphinx + linkcode extension `. Defaults to ``None``. + + The template URI will be formatted using + :external+python:py:meth:`str.format`, with the following replacement fields: + + ``{source}`` + Path to source file relative to :py:data:`hawkmoth_root`. + ``{line}`` + Line number in source file. + + Example: + + .. code-block:: python + + hawkmoth_source_uri = 'https://example.org/src/{source}#L{line}' + .. py:data:: cautodoc_root :type: str diff --git a/src/hawkmoth/__init__.py b/src/hawkmoth/__init__.py index 84892b19..bac4292c 100644 --- a/src/hawkmoth/__init__.py +++ b/src/hawkmoth/__init__.py @@ -13,6 +13,7 @@ from docutils import nodes from docutils.parsers.rst import directives from docutils.statemachine import ViewList +from sphinx import addnodes from sphinx.util.nodes import nested_parse_with_titles from sphinx.util.docutils import switch_source_input, SphinxDirective from sphinx.util import logging @@ -303,6 +304,47 @@ class CppAutoClassDirective(_AutoCompoundDirective): _domain = 'cpp' _docstring_types = [docstring.ClassDocstring] +def _uri_format(env, signode): + """Generate a source URI for signode""" + uri_template = env.config.hawkmoth_source_uri + + if signode.source is None or signode.line is None: + return None + + source = os.path.relpath(signode.source, start=env.config.hawkmoth_root) + + # Note: magic +1 to take into account we've added signode ourselves and its + # not present in source + uri = uri_template.format(source=source, line=signode.line + 1) + + return uri + +def _doctree_read(app, doctree): + env = app.builder.env + + # Bail out early if not configured + uri_template = env.config.hawkmoth_source_uri + if uri_template is None: + return + + for objnode in list(doctree.findall(addnodes.desc)): + if objnode.get('domain') not in ['c', 'cpp']: + continue + + for signode in objnode: + if not isinstance(signode, addnodes.desc_signature): + continue + + uri = _uri_format(env, signode) + if not uri: + continue + + # Similar to sphinx.ext.linkcode + inline = nodes.inline('', '[source]', classes=['viewcode-link']) + onlynode = addnodes.only(expr='html') + onlynode += nodes.reference('', '', inline, internal=False, refuri=uri) + signode += onlynode + def _deprecate(conf, old, new, default=None): if conf[old]: logger = logging.getLogger(__name__) @@ -357,5 +399,9 @@ def setup(app): # Setup transformations for compatibility. app.setup_extension('hawkmoth.ext.transformations') + # Source code link + app.add_config_value('hawkmoth_source_uri', None, 'env', [str]) + app.connect('doctree-read', _doctree_read) + return dict(version=__version__, parallel_read_safe=True, parallel_write_safe=True) From d0e28b5362b15133463b7a845d982c9837de3a76 Mon Sep 17 00:00:00 2001 From: Jani Nikula Date: Sat, 7 Oct 2023 21:07:46 +0300 Subject: [PATCH 3/3] doc: enable source links in examples This is a bit more convoluted than usual, because we want documentation built for tagged releases to point at the source code for the release, and master otherwise. We rely on version having a '.dev0' suffix (or '.', really) once we've deviated from the latest release. The first formatting pass also requires us to wrap {source} and {line} in extra {}'s. --- doc/conf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/conf.py b/doc/conf.py index c3e7135f..dc69f4b0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -61,6 +61,11 @@ hawkmoth_root = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../test/examples') +source_uri = 'https://github.com/jnikula/hawkmoth/tree/{version}/test/examples/{{source}}#L{{line}}' +source_version = f'v{version}' if len(version.split('.')) == 3 else 'master' + +hawkmoth_source_uri = source_uri.format(version=source_version) + # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output