pax_global_header00006660000000000000000000000064147603172410014517gustar00rootroot0000000000000052 comment=9f6b703f4fc7cb96a5addcb9f602567b7abd8035 doxylink-1.13.0/000077500000000000000000000000001476031724100134425ustar00rootroot00000000000000doxylink-1.13.0/.github/000077500000000000000000000000001476031724100150025ustar00rootroot00000000000000doxylink-1.13.0/.github/dependabot.yml000066400000000000000000000003221476031724100176270ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2022 Matt Williams # SPDX-License-Identifier: CC0-1.0 version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" doxylink-1.13.0/.github/workflows/000077500000000000000000000000001476031724100170375ustar00rootroot00000000000000doxylink-1.13.0/.github/workflows/check.yml000066400000000000000000000020661476031724100206430ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2022 Matt Williams # SPDX-License-Identifier: MIT name: Check on: push: branches: - master pull_request: branches: - master workflow_call: {} permissions: contents: read jobs: code-checks: name: Tests and lints runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - name: Install doxygen run: | sudo apt install -y doxygen - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: pip install poetry - name: Setup package run: poetry install - name: Run mypy run: poetry run mypy --install-types --non-interactive sphinxcontrib/doxylink - name: Run pytest run: poetry run pytest - name: Test that package builds run: poetry build doxylink-1.13.0/.github/workflows/release.yml000066400000000000000000000065321476031724100212100ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2022 Matt Williams # SPDX-License-Identifier: MIT name: Release on: workflow_dispatch: inputs: version: description: The new version, can be "patch", "minor", "major" (or a valid semver string) required: true type: string permissions: {} jobs: run-tests: uses: ./.github/workflows/check.yml permissions: contents: read make-release: name: "Release new ${{ inputs.version }} version" needs: run-tests runs-on: ubuntu-latest permissions: contents: write steps: # Set things up - uses: actions/checkout@v4 with: ref: ${{ github.ref }} # Use ref here since we must commit on a branch ssh-key: ${{secrets.DEPLOY_KEY}} - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.*" - name: Install Poetry run: python -m pip install poetry # Update and save the version - name: update version run: | poetry version ${{ inputs.version }} git add pyproject.toml - name: Save the version id: get_version run: echo ::set-output name=version::"$(poetry version --short)" - name: Update version in code run: | sed --in-place "s/__version__ = \".*\"/__version__ = \"${{ steps.get_version.outputs.version }}\"/" sphinxcontrib/doxylink/__init__.py git add sphinxcontrib/doxylink/__init__.py # Update and save the changelog - name: Install chachacha run: python -m pip install chachacha - name: Update changelog version run: | head -n 5 CHANGELOG.md > header.tmp chachacha release "${{ steps.get_version.outputs.version }}" sed -i '/and this project adheres to/a \\n## [Unreleased]' CHANGELOG.md cat header.tmp CHANGELOG.md > CHANGELOG.fixed.md mv CHANGELOG.fixed.md CHANGELOG.md rm header.tmp git add CHANGELOG.md - name: Extract changelog section run: | cargo install markdown-extract changelog=$(markdown-extract --no-print-matched-heading "${{ steps.get_version.outputs.version }}" CHANGELOG.md) echo "changelog<> $GITHUB_ENV echo "$changelog" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV - name: Commit and tag run: | git config --global user.name "GitHub Action" git config --global user.email "action@github.com" git commit -m "Release ${{ steps.get_version.outputs.version }}" git tag "${{ steps.get_version.outputs.version }}" git push --atomic --tags origin HEAD - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ steps.get_version.outputs.version }} release_name: ${{ steps.get_version.outputs.version }} body: ${{ env.changelog }} draft: false # TODO set prerelease based on version passed in prerelease: false # Publish release to PyPI - name: Set PyPI credentials run: poetry config pypi-token.pypi ${PYPI_TOKEN} env: PYPI_TOKEN: ${{ secrets.pypi_token }} - name: Publish run: poetry publish --build doxylink-1.13.0/.gitignore000066400000000000000000000001361476031724100154320ustar00rootroot00000000000000.tox/ parsing_profile *.pyc sphinxcontrib_doxylink.egg-info/ dist/ build/ examples/my_lib.tag doxylink-1.13.0/CHANGELOG.md000066400000000000000000000104301476031724100152510ustar00rootroot00000000000000 # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [1.13.0] - 2025-02-28 ### Added - Add configuration variable to suppress specific parse errors [PR #65] ## [1.12.4] - 2025-02-06 ### Changed - Clarify and complete documentation about 'Configuration values' [PR #58] ### Fixed - Support multiple documentation items with the same name [PR #63] - Parse exception and ref qualifier [PR #60] - Handle arbitrary number of `*` and `&` [PR #61] - Handle parameter packs `T...` [PR #62] ## [1.12.3] - 2023-10-24 ### Fixed - Write the extension version into the cache which will now avoid having to re-parse the Doxygen tag file [PR #50] ## [1.12.2] - 2022-08-02 ### Fixed - Ignore friend declarations as members to avoid crash [PR #45] ## [1.12.1] - 2022-07-28 ### Changed - Improve performance of class SymbolMap [PR #42] ## [1.12.0] - 2022-05-06 ### Changed - Update packaging to use Poetry [PR #39] ### Fixed - Handle the `operator()` method correctly ## [1.11.2] - 2022-04-28 ### Fixed - Add support for sphinx parallel read/write [PR #37] ## [1.11.1] - 2021-11-15 ### Fixed - Only link to Doxygen's PDF output when Sphinx uses the latex format [PR #36] ## [1.11] - 2021-09-22 ### Added - Add feature to download remote and copy local pdf files [PR #35] ## [1.10] - 2021-09-10 ### Fixed - Fix links to files in Doxygen's PDF output [PR #34] ## [1.9] - 2021-09-02 ### Added - Add support for linking to Doxygen's PDF output [PR #32] ## [1.8] - 2021-01-28 ### Added - Add support for pages in addition to files [PR #25] - Add volatile as qualifier [PR #26] ## [1.7] - 2021-01-11 ### Added - Add support for argument packs in C++11 [PR #20] - Add support for linking to remote tag files [issue #12] - Add support for multiple tag files with the same name [PR #27] ## [1.6.1] - 2019-04-27 ### Fixed - Fix for deprecated `app.info()` [PR #23] ## [1.6] - 2018-07-22 ### Added - Add support for linking to Doxygen groups [issue #11]. - Add possibility to link to #DEFINE macros. ### Fixed - Do a better job of parsing compound fundamental types. - Rewrite internals to a more structured style. - Fix error in namespace resolution. ## [1.5] - 2017-12-09 ### Fixed - Fix #6: convert dict_values to list before indexing [Stein Heselmans] - fix parsing for C++11 functions with specifiers () final, () override or () = default [Elco Jacobs] ## [1.4] - 2017-12-04 ### Removed - Remove Python 2 compatibility - ### Fix - Add bug fix from Stein Heselmans to force the qualifier to be a single string ## [1.3] - 2012-09-13 ### Fixed - Add fix from Matthias Tuma from Shark3 to allow friend declarations inside classes. ## [1.2] - 2011-11-03 ### Added - Add Python 3 support ## [1.1] - 2011-02-19 ### Added - Add support for linking directly to struct definitions. - Allow to link to functions etc. which are in a header/source file but not a member of a class. ## [1.0] - 2010-12-14 ### Added - New Dependency: PyParsing (http://pyparsing.wikispaces.com/) - Completely new tag file parsing system. Allows for function overloading. - The parsed results are cached to speed things up. - Full usage documentation. Build with `sphinx-build -W -b html doc html`. ### Fixed - Fix problem with mixed slashes when building on Windows. ## [0.4] - 2010-08-15 ### Added - Allow URLs as base paths for the HTML links. ### Fixed - Don't append parentheses if the user has provided them already in their query. ## [0.3] - 2010-08-10 ### Added - Only parse the tag file once per run. This should increase the speed. - Automatically add parentheses to functions if the `add_function_parentheses` config variable is set. ## [0.2] - 2010-07-31 ### Added - When a target cannot be found, make the node an `inline` node so there's no link created. ### Fixed - No longer require a trailing slash on the `doxylink` config variable HTML link path. - Allow doxylinks to work correctly when created from a documentation subdirectory. ## [0.1] - 2010-07-22 ### Added - Initial release [//]: # (C3-2-DKAC) doxylink-1.13.0/CONTRIBUTING.rst000066400000000000000000000007631476031724100161110ustar00rootroot00000000000000Contributing to doxylink ======================== Making releases --------------- Releases are managed by GitHub Actions. There is an action called `Release `_ which can be manually triggered. On that page, you can select "Run workflow" and set the type of release, either "patch", "minor" or "major". We conform to SemVer so if there have only been bug fixes, use "patch", and if there have been new features use "minor". doxylink-1.13.0/LICENSE000066400000000000000000000024461476031724100144550ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2017, Matt Williams All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. doxylink-1.13.0/README.rst000066400000000000000000000007341476031724100151350ustar00rootroot00000000000000###################### sphinxcontrib-doxylink ###################### A Sphinx_ extension to link to external Doxygen API documentation. Usage ----- Please refer to the documentation_ for information on using this extension. Installation ------------ This extension can be installed from the Python Package Index:: pip install sphinxcontrib-doxylink .. _`Sphinx`: http://www.sphinx-doc.org .. _`documentation`: http://sphinxcontrib-doxylink.readthedocs.io/en/stable/ doxylink-1.13.0/doc/000077500000000000000000000000001476031724100142075ustar00rootroot00000000000000doxylink-1.13.0/doc/api.rst000066400000000000000000000002071476031724100155110ustar00rootroot00000000000000Function reference ================== .. automodule:: sphinxcontrib.doxylink.doxylink .. automodule:: sphinxcontrib.doxylink.parsing doxylink-1.13.0/doc/conf.py000066400000000000000000000164401476031724100155130ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # sphinxcontrib-doxylink documentation build configuration file, created by # sphinx-quickstart on Thu Aug 12 16:41:53 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os from sphinxcontrib.doxylink import __version__ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('../')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.todo', 'sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'sphinxcontrib-doxylink' copyright = u'2022, Matt Williams' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. #pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'sphinxcontrib-doxylinkdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'sphinxcontrib-doxylink.tex', u'sphinxcontrib-doxylink Documentation', u'Matt Williams', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'sphinxcontrib-doxylink', u'sphinxcontrib-doxylink Documentation', [u'Matt Williams'], 1) ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} autodoc_default_flags = ['members'] def setup(app): app.add_object_type('confval', 'confval', 'pair: %s; configuration value') doxylink-1.13.0/doc/index.rst000066400000000000000000000203311476031724100160470ustar00rootroot00000000000000Welcome to sphinxcontrib-doxylink's documentation ================================================= Doxylink is a Sphinx extension to link to external Doxygen API documentation. It allows you to specify C++ symbols and it will convert them into links to the HTML page of their Doxygen documentation. .. toctree:: :hidden: api Usage ----- You use Doxylink like: .. code-block:: rst :polyvox:`PolyVox::Volume` You use :qtogre:`QtOgre::Log` to log events for the user. :polyvox:`PolyVox::Array::operator[]` Where :rst:role:`polyvox` and :rst:role:`qtogre` roles are defined by the :confval:`doxylink` configuration value. Like any interpreted text role in Sphinx, if you want to display different text to what you searched for, you can include some angle brackets ``<...>``. In this case, the text inside the angle brackets will be used to match up with Doxygen and the part in front will be displayed to the user: .. code-block:: rst :polyvox:`Array `. :polyvox:`tidyUpMemory ` will reduce memory usage. .. note:: In C++, it is common that classes and functions will be templated and so will have angle brackets themselves. For example, the C++ class: .. code-block:: c++ PolyVox::Array<0,ElementType> would be naively linked to with Doxylink with: .. code-block:: rst :polyvox:`PolyVox::Array<0,ElementType>` but that would result in Sphinx parsing it as you wanting to search for ``0,ElementType`` and display ``PolyVox::Array`` as the text to the user. To avoid this misparsing you must escape the opening ``<`` by prepending it with a ``\``: .. code-block:: rst :polyvox:`PolyVox::Array\<0,ElementType>` If you want to use templated symbols inside the angle brackets like: .. code-block:: rst :polyvox:`Array >` then that will work without having to escape anything. Namespaces, classes etc. ^^^^^^^^^^^^^^^^^^^^^^^^ For non-functions (i.e. namespaces, classes, enums, variables) you simply pass in the name of the symbol. If you pass in a partial symbol, e.g. ```Volume``` when you have a symbol in C++ called ``PolyVox::Utils::Volume`` then it would be able to match it as long as there is no ambiguity (e.g. with another symbol called ``PolyVox::Old::Volume``). If there is ambiguity then simply enter the fully qualified name like: .. code-block:: rst :polyvox:`PolyVox::Utils::Volume` or :polyvox:`PolyVox::Utils::Volume ` Functions ^^^^^^^^^ For functions there is more to be considered due to C++'s ability to overload a function with multiple signatures. If you want to link to a function and either that function is not overloaded or you don't care which version of it you link to, you can simply give the name of the function with no parentheses: .. code-block:: rst :polyvox:`PolyVox::Volume::getVoxelAt` Depending on whether you have set the :confval:`add_function_parentheses` configuration value, Doxylink will automatically add on parentheses to that it will be printed as ``PolyVox::Volume::getVoxelAt()``. If you want to link to a specific version of the function, you must provide the correct signature. For a requested signature to match on in the tag file, it must exactly match a number of features: - The types must be correct, including all qualifiers, e.g. ``unsigned const int`` - You must include any pointer or reference labeling, e.g. ``char*``, ``const QString &`` or ``int **`` - You must include whether the function is const, e.g. ``getx() const`` The argument list is not whitespace sensitive (any more than C++ is anyway) and the names of the arguments and their default values are ignored so the following are all considered equivalent: .. code-block:: rst :myapi:`foo( const QString & text, bool recalc, bool redraw = true )` :myapi:`foo(const QString &foo, bool recalc, bool redraw = true )` :myapi:`foo( const QString& text, bool recalc, bool redraw )` :myapi:`foo(const QString&,bool,bool)` When making a match, Doxylink splits up the requested string into the function symbol and the argument list. If it finds a match for the function symbol part but not for the argument list then it will return a link to any one of the function versions. Files ^^^^^ You can also link directly to a header or source file by giving the name of the file: .. code-block:: rst :myapi:`main.cpp` :myapi:`MainWindow.h` Setup ----- When generating your Doxygen documentation, you need to instruct it to create a 'tag' file. This is an XML file which contains the mapping between symbols and HTML files. To make Doxygen create this file ensure that you have a line like: .. code-block:: ini GENERATE_TAGFILE = PolyVox.tag in your ``Doxyfile``. Configuration values -------------------- .. confval:: doxylink The environment is set up with a dictionary that maps the interpreted text role, which must be lower-case, to a tuple with at most three elements, of which the third is optional: - The path to the Doxygen tag file, which can be: - absolute, - relative to the location where `sphinx-build` is executed, - a URL so that the file will be downloaded first. - The path to the root of HTML documentation, which can be: - absolute - relative to `Sphinx' output directory`_. - The filename of a Doxygen pdf file, to be used when Sphinx uses the LaTeX builder. Otherwise, the second element of the tuple will be used to link to. .. code-block:: python doxylink = { 'polyvox' : ('/home/matt/PolyVox.tag', '/home/matt/PolyVox/html/', 'polyvox_doxygen.pdf'), 'qtogre' : ('/home/matt/QtOgre.tag', '/home/matt/QtOgre/html/', 'qtogre_doxygen.pdf'), } .. note:: The links in your pdf document to your Doxygen pdf file(s) may not work (properly) in a browser or a basic PDF-reader. They should work in Adobe Reader for example. .. _`Sphinx' output directory`: https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.outdir .. confval:: add_function_parentheses A boolean that decides whether parentheses are appended to function and method role text. Default is ``True``. .. confval:: doxylink_pdf_files Doxylink can be configured to download remote Doxygen pdf files or copy them from a local location. You should use the output file name as the third element of the value of the ``doxylink`` dictionary **and** as key in the ``doxylink_pdf_files`` dictionary, which should contain the URL to the remote location or local location as value. If the pdf file already exists locally in Sphinx' output directory, it will not be downloaded or overwritten. .. code-block:: python doxylink_pdf_files = { 'polyvox_doxygen.pdf': url_to_remote_doxygen_pdf, 'qtogre_doxygen.pdf': '/home/matt/qtogre/doxygen.pdf', } .. confval:: doxylink_parse_error_ignore_regexes A list of regular expressions that can be used to ignore specific errors reported from the parser. Default is ``[]``. This is useful if you have a lot of errors that you know are not important. For example, you may want to ignore errors related to a specific namespace. The regular expression is matched against the error message using Python's `re.search `_ function. Bug reports ----------- If you find any errors, bugs, crashes etc. then please raise an issue `on GitHub `_. If there is a crash please include the backtrace and log returned by Sphinx. If you have a bug, particularly with Doxylink not being able to parse a function, please send the tag file so tat I can reproduce and fix it. :requires: Python 3.4 .. todo:: Parallelise the calls to normalise() in parse_tag_file() using multiprocessing. Set up a pool of processes and pass in a queue of strings. Non-function calls will be done in the same way as present. For function calls, build up the information into a list of tuples, convert it into an appropriate Queue format and run it. Maybe even a multiprocessing.Pool.map could do the job. :copyright: Copyright 2022 by Matt Williams :license: BSD, see LICENSE for details. doxylink-1.13.0/examples/000077500000000000000000000000001476031724100152605ustar00rootroot00000000000000doxylink-1.13.0/examples/Doxyfile000066400000000000000000000002261476031724100167660ustar00rootroot00000000000000FILE_PATTERNS = *.h EXTRACT_ALL = YES GENERATE_TAGFILE = my_lib.tag GENERATE_LATEX = NO GENERATE_HTML = NO doxylink-1.13.0/examples/conf.py000066400000000000000000000004741476031724100165640ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import sys sys.path.insert(0, os.path.abspath('..')) extensions = ['sphinxcontrib.doxylink'] doxylink = { 'my_lib': (os.path.abspath('./my_lib.tag'), 'https://examples.com/'), } doxylink_parse_error_ignore_regexes = [r"DEFINE.*"] master_doc = 'index' doxylink-1.13.0/examples/index.rst000066400000000000000000000003111476031724100171140ustar00rootroot00000000000000:my_lib:`my_func` :my_lib:`my_namespace::MyClass` :my_lib:`my_namespace::MyClass::MyClass` :my_lib:`my_namespace::MyClass::my_method` :my_lib:`MyClass::my_method` :my_lib:`Color` :my_lib:`Color_c` doxylink-1.13.0/examples/my_lib.h000066400000000000000000000015241476031724100167060ustar00rootroot00000000000000#include #include /** * Example documented function */ int my_func(); int my_func(int foo); int my_func(float); int my_func(std::string a, int b); int my_func(int b, std::string a); /// \defgroup ClassesGroup A group of the classes /// @{ namespace my_namespace { class MyClass: public QObject { Q_OBJECT Q_PROPERTY(double my_method READ my_method); public: MyClass(); double my_method(); }; } /// This class has the same name but is a different class class MyClass { public: MyClass(); }; /// @} /// A simple macro #define MY_MACRO(x) foo(x) // A simple enum enum Color { red, green, blue }; // An enum class enum class Color_c { red, green, blue }; // A function that triggers a warning from the parser void DEFINE_bool(show, false, "Enable visualization"); doxylink-1.13.0/examples/my_lib_2.h000066400000000000000000000000551476031724100171250ustar00rootroot00000000000000namespace my_namespace { int my_func(); }doxylink-1.13.0/pyproject.toml000066400000000000000000000016371476031724100163650ustar00rootroot00000000000000# SPDX-FileCopyrightText: © 2022 Matt Williams # SPDX-License-Identifier: BSD [tool.poetry] name = "sphinxcontrib-doxylink" packages = [{include = "sphinxcontrib"}] version = "1.13.0" description = "Sphinx extension for linking to Doxygen documentation." readme = "README.rst" documentation = "https://sphinxcontrib-doxylink.readthedocs.io" repository = "https://github.com/sphinx-contrib/doxylink" authors = ["Matt Williams "] license = "BSD" keywords = ["sphinx", "doxygen", "documentation", "c++"] [tool.poetry.dependencies] python = "^3.7" Sphinx = ">=1.6" pyparsing = "^3.0.8" python-dateutil = "^2.8.2" [tool.poetry.dev-dependencies] pytest = "^7.1.2" testfixtures = "^8.0.0" mypy = "^1.4" [tool.pytest.ini_options] testpaths = ["sphinxcontrib", "tests"] addopts = "--doctest-modules" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" doxylink-1.13.0/sphinxcontrib/000077500000000000000000000000001476031724100163345ustar00rootroot00000000000000doxylink-1.13.0/sphinxcontrib/doxylink/000077500000000000000000000000001476031724100201755ustar00rootroot00000000000000doxylink-1.13.0/sphinxcontrib/doxylink/__init__.py000066400000000000000000000007671476031724100223200ustar00rootroot00000000000000__version__ = "1.13.0" def setup(app): from .doxylink import setup_doxylink_roles app.add_config_value('doxylink', {}, 'env') app.add_config_value('doxylink_pdf_files', {}, 'env') app.add_config_value('doxylink_parse_error_ignore_regexes', default=[], types=[str], rebuild='env') app.connect('builder-inited', setup_doxylink_roles) return { "version": __version__, "parallel_read_safe": True, "parallel_write_safe": True, } doxylink-1.13.0/sphinxcontrib/doxylink/doxylink.py000066400000000000000000000531011476031724100224100ustar00rootroot00000000000000import bisect import os import re import requests import shutil import time import xml.etree.ElementTree as ET import urllib.parse from collections import namedtuple from typing import List, Optional, Union from dateutil.parser import parse as parsedate from docutils import nodes, utils from sphinx.util.nodes import split_explicit_title from sphinx.util.console import bold, standout # type: ignore # These are not explicitly exported as functions from sphinx import __version__ as sphinx_version if sphinx_version >= '1.6.0': from sphinx.util.logging import getLogger from . import __version__ from .parsing import normalise, ParseException class Entry(namedtuple('_Entry', ['name', 'kind', 'file', 'arglist'])): '''Represents a documentation entry produced by Doxygen.''' def matches(self, name: str, kind: Optional[str], arglist: Optional[str]) -> bool: ''' Checks whether this entry has the specified name, kind, and argument list. Args: name (str): symbol name kind (Optional[str]): restrict to symbols of this kind arglist (Optional[str]): normalized argument list for overload resolution ''' # Are we of the correct kind? if kind and self.kind != kind: return False # Ensure the name matches if not self.name.endswith(name): return False # "do_foo" doesn't match "foo" prefix = self.name[:-len(name)] if prefix and (prefix[-1].isidentifier() or prefix[-1].isnumeric()): return False if not arglist: # If no argument list is provided, anything matches return True return self.arglist == arglist def __lt__(self, other: Union["Entry", str]) -> bool: # type:ignore ''' Compares entries for sorting by reverse name. This allows `SymbolMap` to match "foo::bar" when searching for "bar". ''' if isinstance(other, Entry): return self.name[::-1] < other.name[::-1] return self.name[::-1] < other @property def is_class(self) -> bool: '''Returns true if this is a class entry (``kind`` is ``"class"``)''' return self.kind == 'class' @property def is_template(self) -> bool: '''Returns true if this is a template entry''' return '<' in self.name def report_info(env, msg, docname=None, lineno=None): '''Convenience function for logging an informational Args: msg (str): Message of the warning docname (str): Name of the document on which the error occured lineno (str): Line number in the document on which the error occured ''' if sphinx_version >= '1.6.0': logger = getLogger(__name__) if lineno is not None: logger.info(msg, location=(docname, lineno)) else: logger.info(msg, location=docname) else: env.info(docname, msg, lineno=lineno) def report_warning(env, msg, docname=None, lineno=None): '''Convenience function for logging a warning Args: msg (str): Message of the warning docname (str): Name of the document on which the error occured lineno (str): Line number in the document on which the error occured ''' if sphinx_version >= '1.6.0': logger = getLogger(__name__) if lineno is not None: logger.warning(msg, location=(docname, lineno)) else: logger.warning(msg, location=docname) else: env.warn(docname, msg, lineno=lineno) def is_url(str_to_validate: str) -> bool: ''' Helper function to check if string contains URL Args: str_to_validate (str): String to validate as URL Returns: bool: True if given string is a URL, False otherwise ''' regex = re.compile( r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... r'localhost|' #localhost... r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) return bool(re.match(regex, str_to_validate)) class SymbolMap: """A SymbolMap maps symbols to Entries.""" def __init__(self, xml_doc: ET.ElementTree, parse_error_ignore_regexes: Optional[List[str]] = None) -> None: entries = parse_tag_file(xml_doc, parse_error_ignore_regexes) # Sort the entry list for use with bisect self._entries = sorted(entries) def _find_entries(self, name: str, kind: Optional[str], arglist: Optional[str]) -> List[Entry]: ''' Finds all potentially matching entries in the symbol list. Args: name (str): the name symbol to search for kind (Optional[str]): the kind of symbols to search for arglist (str): normalised function argument list Returns: list[Entry]: all entries whose name ends with 'name' ''' matches = [] # Thanks to the sorting, all we need to do is iterate from the first to # the last matching entry. start = bisect.bisect_left(self._entries, name[::-1]) # type:ignore for candidate in self._entries[start:]: if not candidate.name.endswith(name): # Reached the end of entries that end in 'name' break if candidate.matches(name, kind, arglist): # Found one matches.append(candidate) return matches def _disambiguate(self, name: str, candidates: List[Entry]) -> Entry: ''' Returns the best-fitting candidate for the given symbol name. All candidates are expected to be valid. Args: name (str): symbol name candidates (list[Entry]): list of candidates to choose from Returns: Entry: the best candidate ''' if not candidates: raise LookupError(f'No documentation entry matching "{name}"') # An exact match would appear at the beginning of the list. if len(candidates) == 1 or candidates[0].name == name: return candidates[0] # If there is more than one candidate then there is an ambiguity # Often this is due to the symbol matching the name of the constructor as well as the class name itself # We will prefer the class classes = [c for c in candidates if c.is_class] # If there is only one by here we return it. if len(classes) == 1: return classes[0] # Now, to disambiguate between ``PolyVox::Array< 1, ElementType >::operator[]`` and ``PolyVox::Array::operator[]`` matching ``operator[]``, # we will ignore templated (as in C++ templates) tag names by removing names containing ``<`` no_templates = [c for c in candidates if not c.is_template] if len(no_templates) == 1: return no_templates[0] # If not found by now, return the shortest match, assuming that's the most specific if no_templates: # TODO return a warning here? return min(no_templates, key=lambda entry: len(entry.name)) # TODO Offer fuzzy suggestion raise LookupError('Could not find a match') def __getitem__(self, item: str) -> Entry: symbol, normalised_arglist = normalise(item) # Restrict to functions when given an argument list kind = 'function' if normalised_arglist else None candidates = self._find_entries(symbol, kind, normalised_arglist) return self._disambiguate(symbol, candidates) def parse_tag_file(doc: ET.ElementTree, parse_error_ignore_regexes: Optional[List[str]]) -> List[Entry]: """ Takes in an XML tree from a Doxygen tag file and returns a list that looks something like: .. code-block:: python [Entry('PolyVox', ...), Entry('PolyVox::Array', ...), Entry('PolyVox::Array1DDouble'), Entry('PolyVox::Array1DFloat'), Entry('PolyVox::Array1DInt16'), Entry('QScriptContext::throwError'), Entry('QScriptContext::toString'), ] :Parameters: doc : xml.etree.ElementTree The XML DOM object :return: a list of entries mapping fully qualified symbols to files """ entries: List[Entry] = [] for compound in doc.findall('./compound'): compound_kind = compound.get('kind') if compound_kind not in {'namespace', 'class', 'struct', 'file', 'define', 'group', 'page'}: continue compound_name = compound.findtext('name') compound_filename = compound.findtext('filename') if compound_name is None: raise KeyError(f"Compound does not have a name") if compound_filename is None: raise KeyError(f"Compound {compound_name} does not have a filename") # TODO The following is a hack bug fix I think # Doxygen doesn't seem to include the file extension to entries # If it's a 'file' type, check if it _does_ have an extension, if not append '.html' if compound_kind in ('file', 'page') and not os.path.splitext(compound_filename)[1]: compound_filename = compound_filename + '.html' # If it's a compound we can simply add it entries.append(Entry(compound_name, kind=compound_kind, file=compound_filename, arglist=None)) for member in compound.findall('member'): # If the member doesn't have an element, use the parent compounds instead # This is the way it is in the qt.tag and is perhaps an artefact of old Doxygen anchorfile = member.findtext('anchorfile') or compound_filename member_name = member.findtext('name') if member_name is None: raise KeyError(f"Member of {compound_name} does not have a name") member_symbol = compound_name + '::' + member_name member_kind = member.get('kind') arglist = member.findtext('./arglist') # If it has an then we assume it's a function. Empty returns '', not None. Things like typedefs and enums can have empty arglists member_file = join(anchorfile, '#', member.findtext('anchor')) if arglist and member_kind not in {'variable', 'typedef', 'enumeration', 'enumvalue'}: try: # Parse arguments to do overload resolution later normalised_arglist = normalise(member_symbol + arglist)[1] entries.append( Entry(name=member_symbol, kind=member_kind, file=member_file, arglist=normalised_arglist)) except ParseException as e: message = f'Skipping {member_kind} {member_symbol}{arglist}. Error reported from parser was: {e}' should_report = True if parse_error_ignore_regexes: for pattern in parse_error_ignore_regexes: try: if re.search(pattern, message): should_report = False break except re.error: # Invalid regex pattern - ignore it continue if should_report: report_warning(None, message) # Use None as env since we don't have access to it here continue else: # Put the simple things directly into the list entries.append(Entry(name=member_symbol, kind=member_kind, file=member_file, arglist=None)) return entries def join(*args): return ''.join(args) def create_role(app, tag_filename, rootdir, cache_name, pdf=""): parse_error_ignore_regexes = getattr(app.config, 'doxylink_parse_error_ignore_regexes', []) if parse_error_ignore_regexes: report_info(app.env, f'Using parse error ignore patterns: {", ".join(parse_error_ignore_regexes)}') # Tidy up the root directory path if not rootdir.endswith(('/', '\\')): rootdir = join(rootdir, os.sep) try: if is_url(tag_filename): hresponse = requests.head(tag_filename, allow_redirects=True) if hresponse.status_code != 200: raise FileNotFoundError try: modification_time = parsedate(hresponse.headers['last-modified']).timestamp() except KeyError: # no last-modified header from server modification_time = time.time() def _parse(): response = requests.get(tag_filename, allow_redirects=True) if response.status_code != 200: raise FileNotFoundError return ET.fromstring(response.text) else: modification_time = os.path.getmtime(tag_filename) def _parse(): return ET.parse(tag_filename) report_info(app.env, bold('Checking tag file cache for %s: ' % cache_name)) if not hasattr(app.env, 'doxylink_cache'): # no cache present at all, initialise it report_info(app.env, 'No cache at all, rebuilding...') mapping = SymbolMap(_parse(), parse_error_ignore_regexes) app.env.doxylink_cache = {cache_name: {'mapping': mapping, 'mtime': modification_time, 'version': __version__}} elif not app.env.doxylink_cache.get(cache_name): # Main cache is there but the specific sub-cache for this tag file is not report_info(app.env, 'Sub cache is missing, rebuilding...') mapping = SymbolMap(_parse(), parse_error_ignore_regexes) app.env.doxylink_cache[cache_name] = {'mapping': mapping, 'mtime': modification_time, 'version': __version__} elif app.env.doxylink_cache[cache_name]['mtime'] < modification_time: # tag file has been modified since sub-cache creation report_info(app.env, 'Sub-cache is out of date, rebuilding...') mapping = SymbolMap(_parse(), parse_error_ignore_regexes) app.env.doxylink_cache[cache_name] = {'mapping': mapping, 'mtime': modification_time} elif not app.env.doxylink_cache[cache_name].get('version') or app.env.doxylink_cache[cache_name].get('version') != __version__: # sub-cache doesn't have a version or the version doesn't match report_info(app.env, 'Sub-cache schema version doesn\'t match, rebuilding...') mapping = SymbolMap(_parse(), parse_error_ignore_regexes) app.env.doxylink_cache[cache_name] = {'mapping': mapping, 'mtime': modification_time, 'version': __version__} else: # The cache is up to date report_info(app.env, 'Sub-cache is up-to-date') except FileNotFoundError: tag_file_found = False report_warning(app.env, standout('Could not find tag file %s. Make sure your `doxylink` config variable is set correctly.' % tag_filename)) else: tag_file_found = True def find_doxygen_link(name, rawtext, text, lineno, inliner, options={}, content=[]): # from :name:`title ` has_explicit_title, title, part = split_explicit_title(text) part = utils.unescape(part) warning_messages = [] if not tag_file_found: warning_messages.append('Could not find match for `%s` because tag file not found' % part) return [nodes.inline(title, title)], [] try: url = app.env.doxylink_cache[cache_name]['mapping'][part] except LookupError as error: inliner.reporter.warning(f'Could not find match for `{part}` in `{tag_filename}` tag file. Error reported was {error}', line=lineno) return [nodes.inline(title, title)], [] except ParseException as error: inliner.reporter.warning('Error while parsing `%s`. Is not a well-formed C++ function call or symbol.' 'If this is not the case, it is a doxylink bug so please report it.' 'Error reported was: %s' % (part, error), line=lineno) return [nodes.inline(title, title)], [] if pdf and app.builder.format == 'latex': full_url = join(pdf, '#', url.file) full_url = full_url.replace('.html#', '_') # for links to variables and functions full_url = full_url.replace('.html', '') # for links to files # If it's an absolute path then the link will work regardless of the document directory # Also check if it is a URL (i.e. it has a 'scheme' like 'http' or 'file') elif os.path.isabs(rootdir) or urllib.parse.urlparse(rootdir).scheme: full_url = join(rootdir, url.file) # But otherwise we need to add the relative path of the current document to the root source directory to the link else: relative_path_to_docsrc = os.path.relpath(app.env.srcdir, os.path.dirname(inliner.document.attributes['source'])) full_url = join(relative_path_to_docsrc, '/', rootdir, url.file) # We always use the '/' here rather than os.sep since this is a web link avoids problems like documentation/.\../library/doc/ (mixed slashes) if url.kind == 'function' and app.config.add_function_parentheses and normalise(title)[1] == '' and not has_explicit_title: title = join(title, '()') pnode = nodes.reference(title, title, internal=False, refuri=full_url) return [pnode], [] return find_doxygen_link def extract_configuration(values): if len(values) == 3: tag_filename, rootdir, pdf_filename = values elif len(values) == 2: tag_filename = values[0] if values[1].endswith('.pdf'): pdf_filename = values[1] rootdir = "" else: rootdir = values[1] pdf_filename = "" else: raise ValueError("Config variable `doxylink` is incorrectly configured. Expected a tuple with 2 to 3 " "elements; got %s" % values) return tag_filename, rootdir, pdf_filename def fetch_file(app, source, output_path): """Fetches file and puts it in the desired location if it does not exist yet. Local files will be copied and remote files will be downloaded. Directories in the ``output_path`` get created if needed. Args: app: Sphinx' application instance source (str): Path to local file or URL to remote file output_path (str): Path with filename to copy/download the source to, relative to Sphinx' output directory """ if not os.path.isabs(output_path): output_path = os.path.join(app.outdir, output_path) if os.path.exists(output_path): return os.makedirs(os.path.dirname(output_path), exist_ok=True) if is_url(source): response = requests.get(source, allow_redirects=True) if response.status_code != 200: report_warning(app.env, standout("Could not find file %r. Make sure your `doxylink_pdf_files` config variable is " "set correctly." % source)) return with open(output_path, 'wb') as file: file.write(response.content) else: if not os.path.isabs(source): source = os.path.join(app.outdir, source) if os.path.exists(source): shutil.copy(source, output_path) else: report_warning(app.env, standout("Expected a URL or a path that exists as value for `doxylink_pdf_files` " "config variable; got %r" % source)) def process_configuration(app, tag_filename, rootdir, pdf_filename): """Processes the configured values for ``doxylink`` and ``doxylink_pdf_files`` and warns about potential issues. The type of builder decides which values shall be used. Args: app: Sphinx' application instance tag_filename (str): Path to the Doxygen tag file rootdir (str): Path to the root directory of Doxygen HTML documentation pdf_filename (str): Path to the pdf file; may be empty when LaTeX builder is not used """ if app.builder.format == 'latex': if not pdf_filename: if is_url(rootdir): report_warning(app.env, "Linking from PDF to remote Doxygen html is not supported yet; got %r." "Consider linking to a Doxygen pdf file instead as " "third element of the tuple in the `doxylink` config variable." % rootdir) else: report_warning(app.env, "Linking from PDF to local Doxygen html is not possible; got %r." "Consider linking to a Doxygen pdf file instead as third element of the tuple in the " "`doxylink` config variable." % rootdir) elif pdf_filename in app.config.doxylink_pdf_files: source = app.config.doxylink_pdf_files[pdf_filename] fetch_file(app, source, pdf_filename) elif pdf_filename and not rootdir: report_warning(app.env, "Linking from HTML to Doxygen pdf (%r) is not supported. Consider setting " "the root directory of Doxygen's HTML output as value instead." % pdf_filename) def setup_doxylink_roles(app): for name, values in app.config.doxylink.items(): tag_filename, rootdir, pdf_filename = extract_configuration(values) process_configuration(app, tag_filename, rootdir, pdf_filename) app.add_role(name, create_role(app, tag_filename, rootdir, name, pdf=pdf_filename)) doxylink-1.13.0/sphinxcontrib/doxylink/parsing.py000066400000000000000000000170651476031724100222230ustar00rootroot00000000000000from typing import Tuple from pyparsing import Word, Literal, nums, alphanums, OneOrMore, Opt, \ SkipTo, ParseException, Group, Combine, delimitedList, quotedString, \ nestedExpr, ParseResults, oneOf, ungroup, Keyword, ZeroOrMore # define punctuation - reuse of expressions helps packratting work better LPAR, RPAR, LBRACK, RBRACK, LCBRACK, RCBRACK, COMMA, EQ = map(Literal, "()[]{},=") # Qualifier to go in front of type in the argument list (unsigned const int foo) qualifier_grouped = OneOrMore(Keyword('const') ^ Keyword('volatile') ^ Keyword('typename') ^ Keyword('struct') ^ Keyword('enum')) qualifier = ungroup(qualifier_grouped.addParseAction(' '.join)) def turn_parseresults_to_list(s, loc, toks): return ParseResults(normalise_templates(toks[0].asList())) def normalise_templates(toks): s_list = ['<'] s_list_append = s_list.append # lookup append func once, instead of many times for tok in toks: if isinstance(tok, str): # See if it's a string s_list_append(' ' + tok) else: # If it's not a string s_list_append(normalise_templates(tok)) s_list_append(' >') return ''.join(s_list) # Skip pairs of brackets. angle_bracket_pair = nestedExpr(opener='<', closer='>').setParseAction(turn_parseresults_to_list) # TODO Fix for nesting brackets parentheses_pair = LPAR + SkipTo(RPAR) + RPAR square_bracket_pair = LBRACK + SkipTo(RBRACK) + RBRACK curly_bracket_pair = LCBRACK + SkipTo(RCBRACK) + RCBRACK # TODO I guess this should be a delimited list (by '::') of name and angle brackets nonfundamental_input_type = Combine(Word(alphanums + ':_') + Opt(angle_bracket_pair + Opt(Word(alphanums + ':_')))) fundamental_input_type = OneOrMore(Keyword('bool') ^ Keyword('short') ^ Keyword('int') ^ Keyword('long') ^ Keyword('signed') ^ Keyword('unsigned') ^ Keyword('char') ^ Keyword('float') ^ Keyword('double')) input_type = fundamental_input_type ^ nonfundamental_input_type # A number. e.g. -1, 3.6 or 5 number = Word('-.' + nums) # The name of the argument. We will ignore this but it must be matched anyway. input_name = OneOrMore(Word(alphanums + '_') | angle_bracket_pair | parentheses_pair | square_bracket_pair) # Grab the '&', '*' or '**' type bit in (const QString & foo, int ** bar) pointer = Literal('*') + Opt(Literal('const') | Literal('volatile')) reference = Literal('&') pointer_or_reference = pointer | reference # The '=QString()' or '=false' bit in (int foo = 4, bool bar = false) default_value = Literal('=') + OneOrMore(number | quotedString | input_type | parentheses_pair | angle_bracket_pair | square_bracket_pair | curly_bracket_pair | Word('|&^')) # A combination building up the interesting bit -- the argument type, e.g. 'const QString &', 'int' or 'char*' argument_type = Opt(qualifier, default='')("qualifier1") + \ input_type("input_type").setParseAction(' '.join) + \ Opt(qualifier, default='')("qualifier2") + \ Group(ZeroOrMore(pointer_or_reference))("pointer_or_references") + \ Opt('...')("parameter_pack") # Argument + variable name + default argument = Group(argument_type('argument_type') + Opt(input_name) + Opt(default_value)) # List of arguments in parentheses with an optional 'const' on the end arglist = LPAR + delimitedList(argument)('arg_list') + Opt(COMMA + '...')('var_args') + RPAR def normalise(symbol: str) -> Tuple[str, str]: """ Takes a c++ symbol or function and splits it into symbol and a normalised argument list. :Parameters: symbol : string A C++ symbol or function definition like ``PolyVox::Volume``, ``Volume::printAll() const`` :return: a tuple consisting of two strings: ``(qualified function name or symbol, normalised argument list)`` """ try: if "operator()" in symbol: # The ``operator()`` method is special enough to warant an override # We can't just split on ``(`` so find the start of the arg list manually bracket_location = symbol.index("operator()(") + len("operator()") else: bracket_location = symbol.index('(') # Split the input string into everything before the opening bracket and everything else function_name = symbol[:bracket_location] arglist_input_string = symbol[bracket_location:] except ValueError: # If there's no brackets, then there's no function signature. This means the passed in symbol is just a type name return symbol, '' # This is a very common signature so we'll make a special case for it. It requires no parsing anyway if arglist_input_string.startswith('()'): arglist_input_string_no_spaces = arglist_input_string substrings_to_remove = (' override', ' final', ' ', '=0', '=default') for part in substrings_to_remove: arglist_input_string_no_spaces = arglist_input_string_no_spaces.replace(part, '') exception_qualifier = max(arglist_input_string_no_spaces.find('noexcept'), arglist_input_string_no_spaces.find('throw')) if exception_qualifier != -1: # Remove everything starting with the exception keyword arglist_input_string_no_spaces = arglist_input_string_no_spaces[:exception_qualifier] return function_name, arglist_input_string_no_spaces.replace('const', ' const') # By now we're left with something like "(blah, blah)", "(blah, blah) const" or "(blah, blah) const =0" try: closing_bracket_location = arglist_input_string.rindex(')') arglist_suffix = arglist_input_string[closing_bracket_location + 1:] arglist_input_string = arglist_input_string[:closing_bracket_location + 1] except ValueError: # This shouldn't happen. print('Could not find closing bracket in %s' % arglist_input_string) raise try: result = arglist.parseString(arglist_input_string) except ParseException: raise else: # Will be a list or normalised string arguments # e.g. ['OBMol&', 'vector< int >&', 'OBBitVec&', 'OBBitVec&', 'int', 'int'] normalised_arg_list = [] # Cycle through all the matched arguments for arg in result.arg_list: # Here is where we build up our normalised form of the argument argument_string_list = [''] if arg.qualifier1: argument_string_list.append(''.join((arg.qualifier1, ' '))) if arg.qualifier2: argument_string_list.append(''.join((arg.qualifier2, ' '))) argument_string_list.append(arg.input_type) # Functions can have a funny combination of *, & and const between the type and the name so build up a list of those here: argument_string_list.extend(''.join(arg.pointer_or_references)) # Add template parameter pack argument_string_list.append(arg.parameter_pack) # Finally we join our argument string and add it to our list normalised_arg_list.append(''.join(argument_string_list)) # If the function contains a variable number of arguments (int foo, ...) then add them on. if result.var_args: normalised_arg_list.append('...') # Combine all the arguments and put parentheses around it normalised_arg_list_string = ''.join(['(', ', '.join(normalised_arg_list), ')']) # Add a const onto the end if 'const' in arglist_suffix: normalised_arg_list_string += ' const' return function_name, normalised_arg_list_string # TODO Maybe this should raise an exception? return None doxylink-1.13.0/tests/000077500000000000000000000000001476031724100146045ustar00rootroot00000000000000doxylink-1.13.0/tests/__init__.py000066400000000000000000000000001476031724100167030ustar00rootroot00000000000000doxylink-1.13.0/tests/test_doxylink.py000066400000000000000000000223421476031724100200610ustar00rootroot00000000000000import datetime import glob import os import os.path import subprocess import xml.etree.ElementTree as ET from unittest.mock import MagicMock import pytest from testfixtures import LogCapture from sphinxcontrib.doxylink import doxylink @pytest.fixture def examples_tag_file(): basedir = os.path.join(os.path.dirname(__file__), '../examples') extensions = None tagfile = None with open(os.path.join(basedir, 'Doxyfile')) as doxyfile: for line in doxyfile: if line.startswith('FILE_PATTERNS'): extensions = line.split('=')[1].strip().split(',') elif line.startswith('GENERATE_TAGFILE'): tagfile_name = line.split('=')[1].strip() tagfile = os.path.join(basedir, tagfile_name) if None in [extensions, tagfile]: raise RuntimeError('Could not find FILE_PATTERNS or GENERATE_TAGFILE in Doxyfile') matches = [] for extension in extensions: m = glob.glob(os.path.join(basedir, extension)) matches.extend(m) if not os.path.isfile(tagfile): recreate = True else: latest_file_changed = max(datetime.datetime.fromtimestamp(os.stat(f).st_mtime) for f in matches) tagfile_changed = datetime.datetime.fromtimestamp(os.stat(tagfile).st_mtime) if latest_file_changed > tagfile_changed: recreate = True else: recreate = False if recreate: subprocess.call('doxygen', cwd=basedir) return tagfile @pytest.mark.parametrize('symbol, file', [ ('my_func', 'my__lib_8h.html'), ('my_func()', 'my__lib_8h.html'), ('my_namespace::my_func', 'namespacemy__namespace.html'), ('my_lib.h', 'my__lib_8h.html'), ('my_lib.h::my_func', 'my__lib_8h.html'), ('my_namespace', 'namespacemy__namespace.html'), ('my_namespace::MyClass', 'classmy__namespace_1_1MyClass.html'), ('my_lib.h::MY_MACRO', 'my__lib_8h.html'), ('my_namespace::MyClass::my_method', 'classmy__namespace_1_1MyClass.html'), ('ClassesGroup', 'group__ClassesGroup.html'), ]) def test_file_html(examples_tag_file, symbol, file): tag_file = ET.parse(examples_tag_file) mapping = doxylink.SymbolMap(tag_file) assert mapping[symbol].file.startswith(file) @pytest.mark.parametrize('symbol1, symbol2', [ ('my_func', 'my_lib.h::my_func'), ]) def test_file_equivalent(examples_tag_file, symbol1, symbol2): tag_file = ET.parse(examples_tag_file) mapping = doxylink.SymbolMap(tag_file) assert mapping[symbol1].file == mapping[symbol2].file @pytest.mark.parametrize('symbol1, symbol2', [ ('my_func', 'my_namespace::my_func'), ('my_func()', 'my_func(int)'), ('my_func(float)', 'my_func(int)'), ]) def test_file_different(examples_tag_file, symbol1, symbol2): tag_file = ET.parse(examples_tag_file) mapping = doxylink.SymbolMap(tag_file) assert mapping[symbol1].file != mapping[symbol2].file def test_parse_tag_file(examples_tag_file): tag_file = ET.parse(examples_tag_file) mapping = doxylink.parse_tag_file(tag_file, None) def has_entry(name): """ Checks if we have at least one entry with the specified name """ return any(filter(lambda entry: entry.name == name, mapping)) assert has_entry('my_lib.h') assert has_entry('my_lib.h::my_func') assert has_entry('my_namespace') assert has_entry('my_namespace::MyClass') assert has_entry('my_lib.h::MY_MACRO') assert has_entry('my_namespace::MyClass::my_method') assert has_entry('ClassesGroup') @pytest.mark.parametrize('symbol, expected_matches', [ ('my_namespace', {'my_namespace'}), ('my_namespace::MyClass', {'my_namespace::MyClass'}), ('MyClass', {'my_namespace::MyClass', 'my_namespace::MyClass::MyClass', 'MyClass', 'MyClass::MyClass'}), ('my_lib.h::MY_MACRO', {'my_lib.h::MY_MACRO'}), ('MY_MACRO', {'my_lib.h::MY_MACRO'}), ('my_namespace::MyClass::my_method', {'my_namespace::MyClass::my_method'}), ('MyClass::my_method', {'my_namespace::MyClass::my_method'}), ('ClassesGroup', {'ClassesGroup'}), ('lassesGroup', set()), ('yClass::my_method', set()), ]) def test_find_url_piecewise(examples_tag_file, symbol, expected_matches): tag_file = ET.parse(examples_tag_file) mapping = doxylink.SymbolMap(tag_file) matches = mapping._find_entries(symbol, None, None) matched_names = {entry.name for entry in matches} assert expected_matches == matched_names assert set(matches).issubset(set(mapping._entries)) @pytest.mark.parametrize('str_to_validate, expected', [ ('http://example.com', True), ('https://example.com/sub', True), ('http://1.1.1.1', True), ('http://1.1.1.1/sub', True), ('http://localhost', True), ('ftp://example.com', True), ('example', False), ('http_dir', False), ('http://1.2.3', False), ]) def test_is_url(str_to_validate, expected): result = doxylink.is_url(str_to_validate) assert result == expected @pytest.mark.parametrize('values, out_rootdir, out_pdf', [ (['doxygen/project.tag', 'https://example.com'], 'https://example.com', ''), (['doxygen/project.tag', 'https://example.com', ''], 'https://example.com', ''), (['doxygen/project.tag', 'doxygen.pdf'], '', 'doxygen.pdf'), (['doxygen/project.tag', 'https://example.com', 'doxygen.pdf'], 'https://example.com', 'doxygen.pdf'), ]) def test_extract_configuration_pass(values, out_rootdir, out_pdf): tag_filename, rootdir, pdf_filename = doxylink.extract_configuration(values) assert rootdir == out_rootdir assert pdf_filename == out_pdf @pytest.mark.parametrize('values', [ (['doxygen/project.tag']), (['doxygen/project.tag', 'https://example.com', 'doxygen.pdf', 'fail']), ]) def test_extract_configuration_fail(values): with pytest.raises(ValueError): doxylink.extract_configuration(values) @pytest.mark.parametrize('tag_filename, rootdir, pdf_filename, builder', [ ('doxygen/project.tag', 'https://example.com', '', 'html'), ('doxygen/project.tag', '', 'doxygen.pdf', 'latex'), ('doxygen/project.tag', 'html/doxygen', 'doxygen.pdf', 'latex'), ]) def test_process_configuration_pass(tag_filename, rootdir, pdf_filename, builder): app = MagicMock() app.builder.format = builder with LogCapture() as l: doxylink.process_configuration(app, tag_filename, rootdir, pdf_filename) l.check() @pytest.mark.parametrize('rootdir, pdf_filename, builder, msg', [ ('', 'doxygen.pdf', 'html', "Linking from HTML to Doxygen pdf ('doxygen.pdf') is not supported. " "Consider setting the root directory of Doxygen's HTML output as value instead."), ('https://example.com', '', 'latex', "Linking from PDF to remote Doxygen html is not supported yet; got 'https://example.com'." "Consider linking to a Doxygen pdf file instead as third element of the tuple in the `doxylink` config variable."), ('html/doxygen', '', 'latex', "Linking from PDF to local Doxygen html is not possible; got 'html/doxygen'." "Consider linking to a Doxygen pdf file instead as third element of the tuple in the `doxylink` config variable."), ]) def test_process_configuration_warn(rootdir, pdf_filename, builder, msg): app = MagicMock() app.builder.format = builder with LogCapture() as l: doxylink.process_configuration(app, 'doxygen/project.tag', rootdir, pdf_filename) l.check(('sphinx.sphinxcontrib.doxylink.doxylink', 'WARNING', msg)) def test_parse_error_ignore_regexes(): # Create a modified tag file content with problematic entries problematic_xml = """ test.h test_8h foo (transform_pb2.Rotation2f a) 1234 bad (*int i) 5678 baz (int i, float f) 9012 unexpected (*int i) 5678 """ # Write temporary tag file test_tag_file = 'test_temp.tag' with open(test_tag_file, 'w') as f: f.write(problematic_xml) try: tag_file = ET.parse(test_tag_file) patterns = [r'kipping function test\.h::foo', r'kipping.*bad'] with LogCapture() as log: mapping = doxylink.parse_tag_file(tag_file, patterns) # Verify that the mapping still contains valid entries assert any(entry.name.endswith('baz') for entry in mapping) # Verify that messages matching our patterns were not logged assert not any('test.h::foo' in record.msg for record in log.records) assert not any('bar' in record.msg for record in log.records) # Verify other error messages were logged assert any('Skipping' in record.msg for record in log.records) finally: if os.path.exists(test_tag_file): os.unlink(test_tag_file) doxylink-1.13.0/tests/test_doxylink_edge_cases.py000066400000000000000000000032311476031724100222170ustar00rootroot00000000000000import xml.etree.ElementTree as ET from sphinxcontrib.doxylink import doxylink TEMPLATE_CLASS_WITH_SELF_FRIEND = """ base_string.h /workspaces/test base__string_8h.html container::BaseString container container::BaseString classcontainer_1_1_base_string.html class T std::size_t N BaseString classcontainer_1_1_base_string.html e452ce3dc0c4848d8fb5f441311185dc1 () friend class BaseString classcontainer_1_1_base_string.html 4f2bed5eca1588cf30324f67c13ca7990 container namespacecontainer.html container::BaseString """ def test_doxylink_wont_crash_on_self_friend_template_classes(): tag_file = ET.ElementTree(ET.fromstring(TEMPLATE_CLASS_WITH_SELF_FRIEND)) try: doxylink.SymbolMap(tag_file) except RuntimeError as exc: assert False, f"template class with self friend definition raises a Runtime Error: {exc}" doxylink-1.13.0/tests/test_parser.py000066400000000000000000000227711476031724100175220ustar00rootroot00000000000000import pytest from sphinxcontrib.doxylink import parsing import pstats # List of tuples of: (input, correct output) # Input is a string, output is a tuple. arglists = [ ('( QUrl source )', ('', '(QUrl)')), ('( QUrl * source )', ('', '(QUrl*)')), ('( QUrl ** source )', ('', '(QUrl**)')), ('( QUrl ***&&** source )', ('', '(QUrl***&&**)')), ('( const QUrl ** source )', ('', '(const QUrl**)')), ('( const QUrl source )', ('', '(const QUrl)')), ('( QUrl & source )', ('', '(QUrl&)')), ('( const QUrl & source )', ('', '(const QUrl&)')), ('( const QUrl * source )', ('', '(const QUrl*)')), ('( const QUrl * const source )', ('', '(const QUrl*const)')), ('( const QByteArray & data, const QUrl & documentUri = QUrl() )', ('', '(const QByteArray&, const QUrl&)')), ('(void)', ('', '(void)')), ('(uint32_t uNoOfBlocksToProcess=(std::numeric_limits< uint32_t >::max)())', ('', '(uint32_t)')), ('( QWidget * parent = 0, const char * name = 0, Qt::WindowFlags f = 0 )', ('', '(QWidget*, const char*, Qt::WindowFlags)')), ('()', ('', '()')), ('() noexcept', ('', '()')), ('() const noexcept', ('', '() const')), ('() noexcept(true)', ('', '()')), ('() throw()', ('', '()')), ('() const throw()', ('', '() const')), ('() throw(false)', ('', '()')), ('() const &', ('', '() const&')), ('() &', ('', '()&')), ('() &&', ('', '()&&')), ('( int index = 0 )', ('', '(int)')), ('( int index = {0} )', ('', '(int)')), ('( bool ascending = true )', ('', '(bool)')), ('( const QIcon & icon, const QString & label, int width = -1 )', ('', '(const QIcon&, const QString&, int)')), ('( QWidget * parent = 0, const char * name = 0, Qt::WindowFlags f = Qt::WType_TopLevel )', ('', '(QWidget*, const char*, Qt::WindowFlags)')), ('( QMutex * mutex, unsigned long time = ULONG_MAX )', ('', '(QMutex*, unsigned long)')), ('(const VolumeSampler< VoxelType > &volIter)', ('', '(const VolumeSampler< VoxelType >&)')), ('(VolumeSampler< VoxelType > &volIter)', ('', '(VolumeSampler< VoxelType >&)')), ('(const VolumeSampler< VoxelType > &volIter)', ('', '(const VolumeSampler< VoxelType >&)')), ('(const uint32_t(&pDimensions)[noOfDims])', ('', '(const uint32_t)')), ('(Array< noOfDims, ElementType > &rhs)', ('', '(Array< noOfDims, ElementType >&)')), ('( const QString & path, const QString & nameFilter, SortFlags sort = SortFlags( Name | IgnoreCase ))', ('', '(const QString&, const QString&, SortFlags)')), ('( GLuint texture_id )', ('', '(GLuint)')), ('( Q3ValueList::size_type i )', ('', '(Q3ValueList< T >::size_type)')), ('(STLAllocator< T, P > const *, STLAllocator< T2, P > const &)', ('', '(const STLAllocator< T, P >*, const STLAllocator< T2, P >&)')), ('(STLAllocator< T, P > const &, STLAllocator< T2, P > const &)', ('', '(const STLAllocator< T, P >&, const STLAllocator< T2, P >&)')), ('(const String &errorMessage, String logName="")', ('', '(const String&, String)')), ('(PixelFormat = PF_BYTE)', ('', '(PixelFormat)')), ('(const SharedPtr< ControllerValue< T > > &src)', ('', '(const SharedPtr< ControllerValue < T > >&)')), ('(typename T::iterator start, typename T::iterator last)', ('', '(typename T::iterator, typename T::iterator)')), ('(const Matrix4 *const *blendMatrices)', ('', '(const Matrix4*const*)')), ('(const Matrix4 *const *blendMatrices) const =0', ('', '(const Matrix4*const*) const')), ] varargs = [ ('(Args&& ... args)', ('', '(Args&&...)')), ('(int nb=0,...)', ('', '(int, ...)')), ('printf( const char* format, ... )', ('printf', '(const char*, ...)')), ('fprintf( std::FILE* stream, const char* format, ... )', ('fprintf', '(std::FILE*, const char*, ...)')), ('sprintf( char* buffer, const char* format, ... )', ('sprintf', '(char*, const char*, ...)')), ('snprintf( char* buffer, std::size_t buf_size, const char* format, ... )', ('snprintf', '(char*, std::size_t, const char*, ...)')), ] multiple_qualifiers = [ ('(const unsigned short int &time)', ('', '(const unsigned short int&)')), ('(const unsigned long long int &value)', ('', '(const unsigned long long int&)')), ('(const int &nx, const long long *pixels=NULL)', ('', '(const int&, const long long*)')), ('(const int &naxis, const int *naxes, const long long *pixels=NULL)', ('', '(const int&, const int*, const long long*)')), ('( QReadWriteLock * readWriteLock, unsigned long time = ULONG_MAX )', ('', '(QReadWriteLock*, unsigned long)')), ] fundamental_types = [ ('(bool)', ('', '(bool)')), ('(signed char)', ('', '(signed char)')), ('(unsigned char)', ('', '(unsigned char)')), ('(short)', ('', '(short)')), ('(short int)', ('', '(short int)')), ('(signed short)', ('', '(signed short)')), ('(signed short int)', ('', '(signed short int)')), ('(unsigned short)', ('', '(unsigned short)')), ('(unsigned short int)', ('', '(unsigned short int)')), ('(int)', ('', '(int)')), ('(signed)', ('', '(signed)')), ('(signed int)', ('', '(signed int)')), ('(unsigned)', ('', '(unsigned)')), ('(unsigned int)', ('', '(unsigned int)')), ('(long)', ('', '(long)')), ('(long int)', ('', '(long int)')), ('(signed long)', ('', '(signed long)')), ('(signed long int)', ('', '(signed long int)')), ('(unsigned long)', ('', '(unsigned long)')), ('(unsigned long int)', ('', '(unsigned long int)')), ('(long long)', ('', '(long long)')), ('(long long int)', ('', '(long long int)')), ('(signed long long)', ('', '(signed long long)')), ('(signed long long int)', ('', '(signed long long int)')), ('(unsigned long long)', ('', '(unsigned long long)')), ('(unsigned long long int)', ('', '(unsigned long long int)')), ('(float)', ('', '(float)')), ('(double)', ('', '(double)')), ('(long double)', ('', '(long double)')), ] numbers_for_defaults = [ ('( const QPixmap & pixmap, const QString & text, int index = -1 )', ('', '(const QPixmap&, const QString&, int)')), ('( const char ** strings, int numStrings = -1, int index = -1 )', ('', '(const char**, int, int)')), ('( const QStringList & list, int index = -1 )', ('', '(const QStringList&, int)')), ] flags_in_defaults = [ ('( const QString & text, int column, ComparisonFlags compare = ExactMatch | Qt::CaseSensitive )', ('', '(const QString&, int, ComparisonFlags)')), ] functions = [ ('PolyVox::Volume::getDepth', ('PolyVox::Volume::getDepth', '')), ('PolyVox::Volume::getDepth()', ('PolyVox::Volume::getDepth', '()')), ('Volume::getVoxelAt(uint16_t uXPos, uint16_t uYPos, uint16_t uZPos, VoxelType tDefault=VoxelType()) const', ('Volume::getVoxelAt', '(uint16_t, uint16_t, uint16_t, VoxelType) const')), ('PolyVox::Array::operator[]', ('PolyVox::Array::operator[]', '')), ('operator[]', ('operator[]', '')), ('MyClass::operator()', ('MyClass::operator()', '')), ('operator()', ('operator()', '')), ] multiple_namespaces = [ ('PolyVox::Test::TestFunction(int foo)', ('PolyVox::Test::TestFunction', '(int)')), ] keywords_almost_in_typenames = [ ('evolve(quantumdata::StateVector &, const structure::QuantumSystem &, const evolution::Pars &)', ('evolve', '(quantumdata::StateVector&, const structure::QuantumSystem&, const evolution::Pars&)')), ] @pytest.mark.parametrize('test_input, expected', functions) def test_split_function(test_input, expected): assert parsing.normalise(test_input) == expected @pytest.mark.parametrize('test_input, expected', arglists) def test_normalise_arglist(test_input, expected): assert parsing.normalise(test_input) == expected @pytest.mark.parametrize('test_input, expected', varargs) def test_varargs(test_input, expected): assert parsing.normalise(test_input) == expected @pytest.mark.parametrize('test_input, expected', multiple_qualifiers) def test_multiple_qualifiers(test_input, expected): assert parsing.normalise(test_input) == expected @pytest.mark.parametrize('test_input, expected', fundamental_types) def test_fundamental_types(test_input, expected): assert parsing.normalise(test_input) == expected @pytest.mark.parametrize('test_input, expected', numbers_for_defaults) def test_numbers_for_defaults(test_input, expected): assert parsing.normalise(test_input) == expected @pytest.mark.parametrize('test_input, expected', flags_in_defaults) def test_flags_in_defaults(test_input, expected): assert parsing.normalise(test_input) == expected @pytest.mark.parametrize('test_input, expected', multiple_namespaces) def test_multiple_namespaces(test_input, expected): assert parsing.normalise(test_input) == expected @pytest.mark.parametrize('test_input, expected', keywords_almost_in_typenames) def test_keywords_almost_in_typenames(test_input, expected): assert parsing.normalise(test_input) == expected def test_false_signatures(): # This is an invalid function definition. Caused by a bug in Doxygen. See openbabel/src/ops.cpp : theOpCenter("center") from pyparsing import ParseException with pytest.raises(ParseException): parsing.normalise('("center")') if __name__ == "__main__": try: import cProfile as profile except ImportError: import profile all_tests = arglists + varargs + multiple_qualifiers + functions + numbers_for_defaults + flags_in_defaults all_tests += all_tests + all_tests + all_tests + all_tests profile.runctx("for arglist in all_tests: parsing.normalise(arglist[0])", globals(), locals(), filename='parsing_profile') p = pstats.Stats('parsing_profile') p.strip_dirs().sort_stats('time', 'cumtime').print_stats(40) doxylink-1.13.0/tox.ini000066400000000000000000000012601476031724100147540ustar00rootroot00000000000000[tox] envlist = benchmark, test, examples, doc isolated_build = True [testenv:benchmark] allowlist_externals = poetry commands= poetry install python tests/test_parser.py [testenv:examples] changedir = examples allowlist_externals = doxygen poetry commands= poetry install doxygen Doxyfile sphinx-build -W -b html . {envtmpdir}/examples/_build [testenv:test] allowlist_externals = poetry commands= poetry install pytest [testenv:doc] allowlist_externals = poetry commands= poetry install sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees doc {envtmpdir}/linkcheck sphinx-build -W -b html -d {envtmpdir}/doctrees doc {envtmpdir}/html