././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2647624 sphinx_codeautolink-0.17.4/0000755000076600000240000000000014762512105014713 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/LICENSE0000644000076600000240000000206314740025011015710 0ustar00felixstaffMIT License Copyright (c) 2021-2025 Felix Hildén Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/MANIFEST.in0000644000076600000240000000024014762511057016452 0ustar00felixstaffgraft docs prune docs/build graft requirements graft src graft tests include contributing.rst readme_pypi.rst tox.ini global-exclude *.py[cod] __pycache__ *.so ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2644296 sphinx_codeautolink-0.17.4/PKG-INFO0000644000076600000240000001042714762512105016014 0ustar00felixstaffMetadata-Version: 2.2 Name: sphinx-codeautolink Version: 0.17.4 Summary: Automatic links from code examples to reference documentation. Author-email: Felix Hildén Maintainer-email: Felix Hildén License: MIT License Copyright (c) 2021-2025 Felix Hildén Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Project-URL: homepage, https://pypi.org/project/sphinx-codeautolink Project-URL: download, https://pypi.org/project/sphinx-codeautolink Project-URL: source, https://github.com/felix-hilden/sphinx-codeautolink Project-URL: issues, https://github.com/felix-hilden/sphinx-codeautolink/issues Project-URL: documentation, https://sphinx-codeautolink.rtfd.org Keywords: sphinx,extension,code,link Classifier: Development Status :: 4 - Beta Classifier: Framework :: Sphinx Classifier: Framework :: Sphinx :: Extension Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Classifier: Topic :: Software Development :: Documentation Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: sphinx>=3.2.0 Requires-Dist: beautifulsoup4>=4.8.1 Provides-Extra: ipython Requires-Dist: ipython!=8.7.0; extra == "ipython" sphinx-codeautolink =================== |pyversions| |downloads| |license| |readthedocs| sphinx-codeautolink makes code examples clickable by inserting links from individual code elements to the corresponding reference documentation. We aim for a minimal setup assuming your examples are already valid Python. For a live demo, see our online documentation on `Read The Docs `_. Installation ------------ sphinx-codeautolink can be installed from the following sources: .. code:: sh $ pip install sphinx-codeautolink $ conda install -c conda-forge sphinx-codeautolink Note that the library is in early development, so version pinning is advised. To enable sphinx-codeautolink, modify the extension list in ``conf.py``. Note that the extension name uses an underscore rather than a hyphen. .. code:: python extensions = [ ..., "sphinx_codeautolink", ] That's it! Now your code examples are linked. For ways of concatenating examples, setting default import statements, or customising link style among other things, see the `online documentation `_. .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sphinx-codeautolink :alt: Python versions .. |downloads| image:: https://img.shields.io/pypi/dm/sphinx-codeautolink :alt: Monthly downloads .. |license| image:: https://img.shields.io/badge/License-MIT-blue.svg :target: https://choosealicense.com/licenses/mit :alt: License: MIT .. |readthedocs| image:: https://rtfd.org/projects/sphinx-codeautolink/badge/?version=stable :target: https://sphinx-codeautolink.rtfd.org/en/stable/ :alt: Documentation ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/contributing.rst0000644000076600000240000000775314762511057020175 0ustar00felixstaffContributing ============ |issues_open| |issue_resolution| Thank you for considering contributing to sphinx-codeautolink! If you've found a bug or would like to propose a feature, please submit an `issue `_. If you'd like to get more involved, `here's how `_. There are many valuable contributions in addition to contributing code! If you're so inclined, triaging issues, improving documentation, helping other users and reviewing existing code and PRs is equally appreciated! The rest of this guide focuses on development and code contributions. Installation ------------ Start by cloning the most recent version, either from the main repository or a fork you created, and installing the source as an editable package. Using a virtual environment of your choice for the installation is recommended. .. code:: sh $ git clone https://github.com/felix-hilden/sphinx-codeautolink.git $ cd sphinx-codeautolink $ pip install -e . $ pip install -r requirements/dev The last command installs all the necessary tools for development as well as all optional dependencies. If you forked, consider adding the upstream repository as a remote to easily update your main branch with the latest upstream changes. For tips and tricks on contributing, see `how to submit a contribution `_, specifically `opening a pull request `_. If you want to contribute to someone else's fork and find yourself forgetting how to do it every time, here's the runbook: .. code:: sh git remote add [name] git@github.com:[name]/sphinx-codeautolink.git git fetch [name] git switch -c branch [name]/branch git remote rm [name] Testing ------- The installation can be verified, and any changes tested by running tox. .. code:: sh $ tox Developing ---------- A number of tools are used to automate development tasks. They are available through tox labels. .. code:: sh $ coverage run && coverage report # execute test suite $ tox -m docs # build documentation to docs/build/html/index.html $ tox -m lint # check code style $ tox -m format # autoformat code $ tox -m build # packaging dry run Releasing --------- Before releasing, make sure the version number is incremented and the release notes reference the new release. .. note:: With sphinx-codeautolink specifically, if Sphinx's environment data structure was modified, increment the environment version number before releasing a new version. Running tests once more is also good practice. Tox is used to build the appropriate distributions and publish them on PyPI. .. code:: sh $ tox -m publish If you'd like to test the upload and the resulting package, upload manually to `TestPyPI `_ instead. .. code:: sh $ python -m build $ twine upload --repository testpypi dist/* $ pip install --index-url https://test.pypi.org/simple/ sphinx-codeautolink Translations ------------ You are also welcome to contribute translations! Use the following Babel commands to add new translations and locales. .. code:: sh $ cd src/sphinx_codeautolink $ pybabel init --input-file=locale/sphinx-codeautolink.pot --domain=sphinx-codeautolink --output-dir=locale --locale=fi_FI $ pybabel update --input-file=locale/sphinx-codeautolink.pot --domain=sphinx-codeautolink --output-dir=locale $ pybabel compile --directory=locale --domain=sphinx-codeautolink .. |issue_resolution| image:: http://isitmaintained.com/badge/resolution/felix-hilden/sphinx-codeautolink.svg :target: https://isitmaintained.com/project/felix-hilden/sphinx-codeautolink :alt: issue resolution time .. |issues_open| image:: http://isitmaintained.com/badge/open/felix-hilden/sphinx-codeautolink.svg :target: https://isitmaintained.com/project/felix-hilden/sphinx-codeautolink :alt: open issues ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2352343 sphinx_codeautolink-0.17.4/docs/0000755000076600000240000000000014762512105015643 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/docs/requirements.txt0000644000076600000240000000020114740025011021107 0ustar00felixstaff# Manually pin Sphinx + docs requirements sphinx==7.2.6 sphinx-rtd-theme==2.0.0 docutils==0.19 matplotlib==3.8.3 ipython==8.22.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2367728 sphinx_codeautolink-0.17.4/docs/src/0000755000076600000240000000000014762512105016432 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/docs/src/404.rst0000644000076600000240000000040614740025011017462 0ustar00felixstaff:orphan: sphinx-codeautolink =================== Oops! The page you are looking for was not found. Maybe you'll find what you're looking for by searching the documentation or returning to the `home page `_. .. _rtd: https://sphinx-codeautolink.rtfd.org ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/docs/src/about.rst0000644000076600000240000001546014762511057020311 0ustar00felixstaff.. _about: About ===== sphinx-codeautolink is built with a few major components: code analysis, import and type hint resolving, and HTML injection. Code analysis is performed with the builtin ``ast`` parsing tool to generate a set of reference chains to imported modules. That information is fed to the name resolver, which attempts to match a series of attributes and calls to the concrete type in question by following type hints and other information accessible via imports of the library. If a match is found, a link to the correct reference documentation entry is injected after the ordinary Sphinx build is finished. .. _caveats: Caveats ------- - **Only works with HTML or DIRHTML documentation**, disabled otherwise. If the extension is off, it silently removes directives that would produce output. - **Only processes blocks, not inline code**. Sphinx has great tools for linking definitions inline, and longer code should be in a block anyway. - **Doesn't run example code**. Therefore all possible resolvable types are not found, and the runtime correctness of code cannot be validated. Nonsensical operations that would result in errors at runtime are possible. However, syntax errors are caught while parsing! - **Parsing and type hint resolving is incomplete**. While all Python syntax is supported, some ambiguous cases might produce unintuitive results or even incorrect results when compared to runtime behavior. We try to err on the side of caution, but here are some of the compromises and limitations: - Only simple assignments of names, attributes and calls to a single name are tracked and used to resolve later values. - Only simple return type hints that consist of a single, possibly optional type are tracked through call and attribute access chains. - Type hints of intersphinx-linked definitions are not necessarily available. Resolving names using type hints is only possible if the package is installed, but simple usage can be tracked via documentation entries alone. - Deleting or assigning to a global variable from an inner scope is not recognised in outer scopes. This is because the value depends on when the function is called, which is not tracked. Additionally, variable values are assumed to be static after leaving an inner scope, i.e. a function referencing a global variable. This is not the case in Python: values may change after the definition and impact the function. Encountering this should be unlikely, because it only occurs in practice when a variable shadows or overwrites an imported module or its part. These cases are subject to change when the library matures. For more details on the expected failures, see our `test suite on GitHub `_. Please report any unexpected failures! Sphinx semantics ---------------- Warnings ******** For an easier time with debugging, we recommend enabling all warnings, treating them as errors with ``-W`` and only ignoring specific warning types with :confval:`suppress_warnings`. This is also easier if :confval:`show_warning_types` is set. Clean build *********** For correct partial builds, code reference information is saved to a file which is updated when parsing new or outdated files. It shouldn't become outdated, but a clean build can be achieved with `sphinx-build -E `_ or by deleting the build directory. Sphinx cache ************ A function specified in :confval:`codeautolink_custom_blocks` prevents Sphinx from caching documentation results. Consider using an importable instead. For more information, see the discussion in :issue:`76`. You can also suppress the warning. Parallel build and custom parsers ********************************* Locally defined custom block parsers in :confval:`codeautolink_custom_blocks` cannot be passed to Pickle, which prevents parallel Sphinx builds. Please consider using an importable function instead. Priority ******** The extension parses code blocks in the ``doctree-read`` Sphinx event. The priority is set to 490 to catch nodes removed by ``sphinx.ext.doctest`` (priority 500). In other cases the priority of the extension is default. Copying code blocks ------------------- If you feel like code links make copying code a bit more difficult, `sphinx-copybutton `_ is a fantastic extension to use. It adds a button to copy an entire code block to the clipboard. So give it a go, perhaps even if you don't think links make copying harder! Matching failures ----------------- Matching can fail on two levels, for a whole code example or a specific line. Firstly, failing to match an entire code example is almost always considered a bug, which you can report on `GitHub `_. If third-party code blocks are in use, matching may fail because of inconsistent or unrecognised CSS classes. The class related to the block lexer name is automatically added to the list of CSS classes that are searched when matching code examples as ``highlight-{lexer}``. If the class has another value, :confval:`codeautolink_search_css_classes` can be used to extend the search. To find out which classes should be added, build your documentation, locate the code example and use the class of the outermost ``div`` tag. For example: .. code:: python codeautolink_search_css_classes = ["highlight-default"] Secondly, matching can fail on a specific line or range of lines. This is often a bug, but the known expected failure cases are presented here (none currently). Debugging missing links ----------------------- There are multiple potential reasons for missing links. Here are some common causes and ways to debug and resolve the issue. First, please enable all warning messages found in :ref:`configuration` to see information about known link misses. Missing Sphinx inventory entry ****************************** Links cannot be resolved, because the documentation entry for a particular object cannot be found in the Sphinx inventory. Likely causes: - The autodoc (or equivalent) entry is missing entirely. To resolve, add the corresponding entry to your documentation. - The object has been relocated and is documented elsewhere, i.e. the ``__module__`` attribute and Sphinx location are out of sync. To resolve, provide the correct location in :confval:`codeautolink_inventory_map`. Failed link resolving ********************* Determining the canonical location of an object failed. Likely causes: - Missing type hints in function returns or class attributes. To resolve, add appropriate type hints. See :ref:`caveats` for limitations. - Highly dynamic or runtime-dependent code which is not possible to parse only via imports. To resolve, consider simplifying or filing an issue. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/docs/src/conf.py0000644000076600000240000000361414762511057017742 0ustar00felixstaffimport os import sys from pathlib import Path from sphinx_codeautolink import __version__, clean_pycon # Insert package root to path _src_dir = Path(os.path.realpath(__file__)).parent _package_root = _src_dir.parent.parent / "src" sys.path.insert(0, str(_package_root)) sys.path.insert(0, str(_src_dir)) project = "sphinx-codeautolink" author = "Felix Hildén" copyright = "2021-2025, Felix Hildén" version = __version__ release = version extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.extlinks", "sphinx_rtd_theme", "sphinx_codeautolink", "matplotlib.sphinxext.plot_directive", "sphinx.ext.doctest", "IPython.sphinxext.ipython_directive", "IPython.sphinxext.ipython_console_highlighting", ] # Builtin options html_theme = "sphinx_rtd_theme" python_use_unqualified_type_names = True show_warning_types = True # Extension options codeautolink_autodoc_inject = True codeautolink_custom_blocks = {"python3": None, "pycon3": clean_pycon} suppress_warnings = ["config.cache"] autodoc_default_options = {"members": True, "undoc-members": True} autodoc_typehints = "description" intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "numpy": ("https://numpy.org/doc/stable/", None), "matplotlib": ("https://matplotlib.org/stable/", None), } extlinks = { "issue": ("https://github.com/felix-hilden/sphinx-codeautolink/issues/%s", "#%s") } # Copy plot directive options from Seaborn # Include the example source for plots in API docs plot_include_source = True plot_formats = [("png", 90)] plot_html_show_formats = False plot_html_show_source_link = False html_static_path = ["static"] html_css_files = ["custom.css"] def setup(app) -> None: app.add_object_type( "confval", "confval", objname="configuration value", indextemplate="pair: %s; configuration value", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/docs/src/example_library.rst0000644000076600000240000000043714303102654022342 0ustar00felixstaff.. _example-library: Example library --------------- This document contains the reference documentation of a dummy library used in sphinx-codeautolink's documentation. Besides providing valid hyperlink targets, it also demonstrates the default autodoc integration. .. automodule:: lib ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/docs/src/examples.rst0000644000076600000240000002223514762511057021013 0ustar00felixstaff.. _examples: Examples ======== Short examples about how to achieve certain tasks with sphinx-codeautolink. Basic use --------- Once sphinx-codeautolink has been enabled, code in all Python code blocks will be analysed and linked to known reference documentation entries. .. code:: python import lib knight = lib.Knight() while knight.limbs >= 0: print(knight.taunt()) knight.scratch() Different import styles are supported, along with all Python syntax. Star imports might be particularly handy in code examples. A number of other types of blocks are also supported along with standard Sphinx configuration, like: - Literal blocks, the ``.. highlight::`` directive and ``highlight_language`` configuration - `Doctest blocks `_ - Console blocks using :code:`.. code:: pycon` - Including code via :rst:dir:`literalinclude`, but requires using one of the `Python Lexers `_ .. code:: pycon >>> from lib import * >>> def visit_town(time_spent: int, budget: int) -> Shrubbery: ... return Shrubbery(time_spent > 20, budget > 300) >>> visit_town(35, 200) Shrubbery(looks_nice=True, too_expensive=False) A list of all code examples where a particular definition is used is handy particularly in the reference documentation itself: .. autolink-examples:: lib.Knight Such a table is generated with :rst:dir:`autolink-examples`: .. code-block:: rst .. autolink-examples:: lib.Knight Invisible imports ----------------- When writing lots of small snippets of code, having the same import at the beginning of every example becomes quite repetitive. The import can be hidden instead. .. autolink-preface:: import lib .. code:: python lib.Knight().taunt() The previous block is produced with :rst:dir:`autolink-preface`: .. code-block:: rst .. autolink-preface:: import lib .. code:: python lib.Knight().taunt() A multiline preface can be written in the content portion of the directive: .. code-block:: rst .. autolink-preface:: import lib from lib import Knight A global preface can be set in :confval:`codeautolink_global_preface` to avoid writing the same imports repeatedly. Concatenating examples ---------------------- Examples interlaced with explanations can make for more comprehensible docs. .. autolink-concat:: section .. code:: python import lib knight = lib.Knight() After explaining some details, the following block may continue where the previous left off. .. code:: python while knight.limbs >= 0: print(knight.taunt()) knight.scratch() This was achieved with :rst:dir:`autolink-concat`: .. code-block:: rst .. autolink-concat:: section .. code:: python import lib knight = lib.Knight() .. code:: python while knight.limbs >= 0: print(knight.taunt()) knight.scratch() Now all Python code blocks within the same section will be concatenated. See :rst:dir:`autolink-concat` for more information and options. Skipping blocks --------------- If needed, Python blocks can be skipped, resulting in no links for that block and preventing it from being included in further sources with concatenation. .. autolink-skip:: .. code:: python import lib lib.Knight() Which is done via :rst:dir:`autolink-skip`: .. code-block:: rst .. autolink-skip:: .. code:: python import lib lib.Knight() Skipping is supported for single blocks, sections and entire files. See :rst:dir:`autolink-skip` for more information and options. Autodoc integration ------------------- A backreference table of the code examples that use a definition is handy for example in reference documentation. sphinx-codeautolink provides an autodoc integration for that purpose, which injects the appropriate table to each autodoc entry. .. autofunction:: lib.Knight.scratch :noindex: To enable the integration, set :confval:`codeautolink_autodoc_inject`. If you'd like to place the directive manually, implement a small `Sphinx extension `_ with a listener for the ``autodoc-process-docstring`` `event `_. An object type "class" seems to work for other types as well. .. code:: python def process_docstring(app, what, name, obj, options, lines): lines.append("") lines.append(".. autolink-examples:: " + name) lines.append(" :type: class") lines.append(" :collapse:") def setup(app): app.connect("autodoc-process-docstring", process_docstring) Intersphinx integration ----------------------- When writing code examples that use builtins or other libraries, `intersphinx `_ can be used to enable links to documentation on other Sphinx-generated sites. Intersphinx is integrated seamlessly, linking objects as long as the correct ``intersphinx_mapping`` is defined. .. code:: python if __debug__: print(...) else: raise RuntimeError(f"Could not debug!") .. code:: python import numpy as np from matplotlib import pyplot as plt x = np.linspace(0, 2 * np.pi, 100) plt.plot(x, np.sin(x)) plt.show() Reference tables across intersphinx work too: .. autolink-examples:: numpy.linspace :type: func It seems that the reference type information is more important for Sphinx when dealing with external modules, likely because the references cannot be resolved dynamically. Please specify a ``type`` in :rst:dir:`autolink-examples`: .. code-block:: rst .. autolink-examples:: numpy.linspace :type: func Doctest code blocks ------------------- Using the ``sphinx.ext.doctest`` extension for code examples only requires activating the extension:: extensions = [ ..., "sphinx.ext.doctest", ] ``doctest`` and ``testcode`` blocks now work as expected. Code in :rst:dir:`testsetup` is considered to be the first block :rst:dir:`autolink-preface` before each matching test group. However, `skipif` is not supported. .. doctest:: >>> import lib >>> lib.Knight() IPython blocks and notebooks ---------------------------- Code blocks using ``ipython`` or ``ipython3`` lexers are supported by default. The function :func:`~sphinx_codeautolink.clean_ipython` is used to handle IPython-specific syntax like `magic functions`_ and console prefixes. .. _magic functions: https://ipython.readthedocs.io/en/stable/ interactive/tutorial.html#magic-functions .. code:: ipython3 %reset import lib lib.Knight().taunt() IPython's ``.. ipython::`` `directive `_ is also supported: .. ipython:: In [1]: import lib In [2]: lib.Knight().taunt() Out[2]: -taunt here- They are also useful for integrating Jupyter notebooks and similar source code, which is possible with separate Sphinx extensions like nbsphinx_ or MyST-NB_. Enabling :confval:`codeautolink_concat_default` with notebooks is recommended. IPython processing is enabled if the ``ipython`` library is installed. It is also included in the ``ipython`` extra of sphinx-codeautolink. .. code:: sh pip install sphinx-codeautolink[ipython] .. _nbsphinx: https://nbsphinx.readthedocs.io/ .. _MyST-NB: https://myst-nb.readthedocs.io/ Third-party code blocks ----------------------- Third-party code blocks that use the basic Pygments lexers for Python are supported out of the box. The example below uses matplotlib's :mod:`~matplotlib.sphinxext.plot_directive` to automatically run the code and include a plot in the documentation: .. plot:: import numpy as np from matplotlib import pyplot as plt x = np.linspace(0, 2 * np.pi, 100) plt.plot(x, np.cos(x)) Code blocks with special highlighting or syntax are supported with custom transformer functions in :confval:`codeautolink_custom_blocks`. For example, a transformer could be implemented as follows: .. code:: python def transform(source): """Ignore lines starting with `!--`.""" lines = [] for line in source.split("\n"): if line.strip().startswith("!--"): line = "" lines.append(line) return source, "\n".join(lines) codeautolink_custom_blocks = {"python": transform} Sometimes links with third-party code blocks are broken. See :ref:`about` for a potential solution. .. _examples-css: Custom link styles ------------------ If you want a specific style to be applied to code block links, you may add your own CSS file to the Sphinx build. .. code:: python # conf.py html_static_path = ['static'] html_css_files = ['custom.css'] To include only in specific documents, see the ``html-page-context`` event and the :meth:`Sphinx.add_css_file` function. For example, you can add similar underlines as this documentation and change the hover colour: .. code:: css /* static/custom.css */ .sphinx-codeautolink-a{ border-bottom-color: #ccc; border-bottom-style: solid; border-bottom-width: 1px; } .sphinx-codeautolink-a:hover{ color: rgb(255, 139, 139); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/docs/src/index.rst0000644000076600000240000000625314762511057020306 0ustar00felixstaffsphinx-codeautolink =================== |license| sphinx-codeautolink makes code examples clickable by inserting links from individual code elements to the corresponding reference documentation. We aim for a minimal setup assuming your examples are already valid Python. Click any undelined name in the example below for a demonstration: .. code:: python import lib knight = lib.Knight() while knight.limbs >= 0: print(knight.taunt()) knight.scratch() A directive to create a table of references from code examples to a single definition is also provided, which can also be integrated with autodoc entries. For example, :code:`.. autolink-examples:: lib.Knight` produces: .. autolink-examples:: lib.Knight Integration with intersphinx is seamless: .. code:: python import numpy as np from matplotlib import pyplot as plt x = np.linspace(0, 2 * np.pi, 100) plt.plot(x, np.sin(x)) plt.show() Quick start ----------- |pypi| |conda-forge| sphinx-codeautolink can be installed from the following sources: .. code:: sh $ pip install sphinx-codeautolink $ conda install -c conda-forge sphinx-codeautolink To enable sphinx-codeautolink, modify the extension list in ``conf.py``. Note that the extension name uses an underscore rather than a hyphen. .. code:: python extensions = [ ..., "sphinx_codeautolink", ] That's it! Now your code examples are linked. For ways of concatenating examples, setting default import statements, or customising link style among other things, have a look at the :ref:`reference` documentation. sphinx-codeautolink elsewhere: - Package on `PyPI `_ - Development on `GitHub `_ Caveats ------- For a more thorough explanation, see :ref:`about`. - Only works with HTML or DIRHTML documentation - Only processes blocks, not inline code - Doesn't run example code - Parsing and type hint resolving is incomplete Thanks ------ The inspiration for sphinx-codeautolink came from seeing similar awesome docs generated by `Sphinx-Gallery `_! Their source was also immensely helpful to read when I was stumbling through Sphinx and docutils. If you have a folder full of example Python scripts you'd like to include in your documentation, you'll not be disappointed in their offer. .. toctree:: :hidden: :caption: Package release_notes reference about .. toctree:: :hidden: :caption: Guide examples example_library .. toctree:: :hidden: :caption: Links ↪ PyPI ↪ GitHub .. |pypi| image:: https://img.shields.io/pypi/v/sphinx-codeautolink.svg :target: https://pypi.org/project/sphinx-codeautolink :alt: PyPI package .. |conda-forge| image:: https://anaconda.org/conda-forge/sphinx-codeautolink/badges/version.svg :target: https://anaconda.org/conda-forge/sphinx-codeautolink :alt: Conda-Forge package .. |license| image:: https://img.shields.io/badge/License-MIT-blue.svg :target: https://choosealicense.com/licenses/mit :alt: License: MIT ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736498541.0 sphinx_codeautolink-0.17.4/docs/src/lib.py0000644000076600000240000000130514740156555017562 0ustar00felixstaffclass Knight: limbs: int = 4 taunts: list[str] = [ "None shall pass!", "'Tis but a scratch!", "It's just a flesh wound... Chicken!", "Right, I'll do you for that!", "Oh, I see, running away?", ] def scratch(self) -> None: """Scratch the knight.""" self.limbs -= 1 def taunt(self) -> str: """Knight taunts the adversary.""" return self.taunts[::-1][self.limbs] class Shrubbery: """A shrubbery bought in town.""" looks_nice: bool too_expensive: bool def __init__(self, looks_nice: bool, too_expensive: bool) -> None: self.looks_nice = looks_nice self.too_expensive = too_expensive ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/docs/src/reference.rst0000644000076600000240000001740714762511057021140 0ustar00felixstaff.. _reference: Reference ========= The public API of sphinx-codeautolink consists mostly of the configuration and directives made available to Sphinx. The extension is enabled with the name ``sphinx_codeautolink``. During the build phase, a cache containing code example information is saved to the Sphinx doctree directory to track references during partial builds. .. _configuration: Configuration ------------- .. confval:: codeautolink_autodoc_inject Type: ``bool``. Inject an :rst:dir:`autolink-examples` table to the end of all autodoc definitions. Defaults to :code:`False`. .. confval:: codeautolink_global_preface Type: ``str``. Include an :rst:dir:`autolink-preface` before all blocks. When other prefaces or concatenated sources are used in a block, the global preface is included first and only once. .. confval:: codeautolink_concat_default Type: ``bool``. Default behavior for code block concatenation (see :rst:dir:`autolink-concat`). Value corresponds to the "on" and "off" settings in the directive. Defaults to :code:`False`. .. confval:: codeautolink_custom_blocks Type: ``Dict[str, None | str | Callable[[str], Tuple[str, str]]]``. Register custom parsers for lexers of unknown types of code blocks. They are registered as a dict mapping a block lexer name to a function possibly cleaning up the block content to valid Python syntax. If none is specified, no transformations are applied. A string is interpreted as an importable transformer function. The transformer must return two strings: the code appearing in documentation (often just the original source) and the cleaned Python source code. The transformer must preserve line numbers for correct matching. The transformer may raise a syntax error, which is caught automatically and a corresponding Sphinx warning using subtype "parsing_error" is issued. .. confval:: codeautolink_search_css_classes Type: ``List[str]``. Extra CSS classes used to search for code examples when matching the final HTML. May contain multiple values separated by spaces as they would be passed to :code:`bs4.BeautifulSoup.find_all`. .. confval:: codeautolink_inventory_map Type: ``Dict[str, str]``. Remap the final location of any inventory entry. Useful when objects are imported and documented somewhere else than their original location as advertised by ``__module__``. .. confval:: codeautolink_warn_on_missing_inventory Type: ``bool``. Warn when an object cannot be found in the inventory (autodoc or intersphinx). Defaults to :code:`False`. .. confval:: codeautolink_warn_on_failed_resolve Type: ``bool``. Warn when failing to resolve the canonical location of an object that a code element references. Defaults to :code:`False`. .. confval:: codeautolink_warn_on_no_backreference Type: ``bool``. Warn when no backreference could be found from reference documentation using the :rst:dir:`autolink-examples` table. This highlights objects for which no tutorial, example or how-to exists. Defaults to :code:`False`. .. confval:: codeautolink_warn_on_default_parse_fail Type: ``bool``. Warn when a code block using the ``default`` lexer cannot be parsed as Python. By default these cases are ignored by the syntax highlighter, so we match the behavior. Defaults to :code:`False`. Directives ---------- .. rst:directive:: .. autolink-examples:: object Insert a table containing links to sections that reference ``object`` in their code examples. The table is removed if it would be empty. .. rubric:: Options .. rst:directive:option:: type :type: object's reference type, single value The object's reference type as used in other RST roles, e.g. ``:func:`function```. ``type`` is "class" by default, which seems to work for other types as well. .. rst:directive:option:: collapse :type: no value Make the table collapsible (using a "details" HTML tag). .. rst:directive:: .. autolink-concat:: [mode] Toggle block concatenation. Concatenated code blocks are treated as a continuous source, so that imports and statements in previous blocks affect later blocks. Concatenation is begun at the directive, not applied retroactively. The directive also resets concatenation state. Until this directive is encountered, :confval:`codeautolink_concat_default` is used as the default behavior. ``mode``, if specified, must be one of: - "on" - concatenate all blocks in the current file (default value) - "off" - stop concatenation - "section" - concatenate until the next title, then reset to the previous value ("on" or "off") also resetting concatenation state .. rst:directive:: .. autolink-preface:: [code] Include a hidden preface before a code block. A multiline preface can be written in the content portion of the directive. Prefaces are preserved in block concatenation, and are added to the source in the following order: :confval:`codeautolink_global_preface` > file preface > :rst:dir:`autolink-concat` sources (with their block prefaces) > block prefaces > block source. .. rubric:: Options .. rst:directive:option:: level :type: preface level - "next" - add preface only to the next block (default). Multiple prefaces are combined, and the next block consumes this directive even if it's not processed (e.g. non-Python blocks) to avoid placement confusion. - "file" - set a preface for all blocks in the current file, placed after but before block-level prefaces. .. rst:directive:: .. autolink-skip:: [level] Skip sphinx-codeautolink functionality. ``level``, if specified, must be one of: - "next" - next encountered block (default) - "section" - blocks until the next title - "file" - all blocks in the current file - "off" - turn skipping off If "next" was specified, the following block consumes this directive even if it is not processed (e.g. non-Python blocks) to avoid placement confusion. Skipped blocks are ignored in block concatenation as well, and concatenation is resumed without breaks after skipping is over. "Next" doesn't need to be specified right before the block that would consume it. For e.g. literal blocks the skip directive would be inserted before the preceding paragraph. CSS class --------- The CSS class used in all code block links is ``sphinx-codeautolink-a``. By default, the links only have a light blue hover colour. The style has been made more obvious in the documentation for demonstration. See :ref:`examples-css` for more information. Cleanup functions ----------------- The functions below are usable for cleaning ``pycon`` and ``ipython`` code blocks. They are intended to be used with :confval:`codeautolink_custom_blocks`. .. autofunction:: sphinx_codeautolink.clean_pycon .. autofunction:: sphinx_codeautolink.clean_ipython Warning types ------------- Sphinx logging machinery is used to issue warnings during documentation builds. All warning subtypes below are in the ``codeautolink.*`` namespace and can be ignored with configuring ``suppress_warnings``. - ``invalid_argument``: issued when a directive is used incorrectly - ``clean_block``: issued when cleaning a block fails with a ``SyntaxError`` - ``parse_block``: issued when parsing a block fails with a ``SyntaxError`` - ``import_star``: issued when a library cannot be imported to determine the names that an ``import *`` would introduce - ``match_block``: issued when a block cannot be matched - ``match_name``: issued when a code snippet cannot be matched The following warnings are only issued depending on configuration: - ``missing_inventory``: issued when an object cannot be found in the inventory - ``failed_resolve``: issued when an object's canonical location in a module cannot be determined ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329201.0 sphinx_codeautolink-0.17.4/docs/src/release_notes.rst0000644000076600000240000002153014762511461022021 0ustar00felixstaff.. _release-notes: Release notes ============= These release notes are based on `Keep a Changelog `_. sphinx-codeautolink adheres to `Semantic Versioning `_. 0.17.4 (2025-03-07) ------------------- - Fix backreference links using relative URIs (:issue:`190`) 0.17.3 (2025-03-06) ------------------- - Fix Sphinx InventoryItem deprecation warning (:issue:`173`) - Add support for the DIRHTML Sphinx builder (:issue:`188`) 0.17.2 (2025-03-02) ------------------- - Support :rst:dir:`testsetup` from ``sphinx.ext.doctest`` as another type of preface (:issue:`186`) 0.17.1 (2025-02-24) ------------------- - Add translations via i18n (:issue:`164`) - Fix attribute and call after walrus leading parser error (:issue:`174`) - Fix parsing error in doctest blocks with empty lines (:issue:`176`) - Improve error message on uncaught parsing errors (:issue:`177`) - Add ``level`` argument to :rst:dir:`autolink-preface` to support file-level prefaces (:issue:`180`) 0.17.0 (2025-02-18) ------------------- Added ***** - Add more Pygments lexer aliases in code blocks (:issue:`160`) - Introduce :confval:`codeautolink_warn_on_no_backreference` to highlight where reference documentation does not appear to have a corresponding tutorial or how-to (:issue:`161`) - Support the ``default`` lexer, literal blocks, ``.. highlight::`` directive and ``highlight_language`` configuration (:issue:`166`) - Add :confval:`codeautolink_warn_on_default_parse_fail` to warn about failing to link code blocks without a language parameter (:issue:`166`) - Detect and implicitly use ``pycon`` lexer for blocks that look like console code (:issue:`168`) Fixed ***** - Fix undocumented class attribute leading to a crash (:issue:`165`) - Fix skipping blocks with identical content to linked ones (:issue:`172`) 0.16.2 (2025-01-16) ------------------- - Fix regression in not handling malformed return types (:issue:`159`) 0.16.1 (2025-01-15) ------------------- - Fix regression in not handling invalid return type hints (:issue:`158`) 0.16.0 (2025-01-11) ------------------- - Declare support for Python 3.12 and 3.13 (:issue:`150`) - Remove support for Python 3.7-3.9 (:issue:`150`, :issue:`157`) - Fix changed whitespace handling in Pygments 2.19 (:issue:`152`) - Improve support for future and string annotations (:issue:`155`) 0.15.2 (2024-06-03) ------------------- - Fix matching of ``import a, b`` (:issue:`142`) 0.15.1 (2024-04-17) ------------------- - Fix linking blocks with line numbers (:issue:`137`) - Use safer version of ``mro`` to support ``type`` (:issue:`120`) 0.15.0 (2023-02-05) ------------------- - Fix handling of syntax errors in parsed blocks (:issue:`135`) - Differentiate warning types of block cleaning and parsing (:issue:`136`) 0.14.1 (2023-01-30) ------------------- - Fix added debug info on failed resolving crashing the build (:issue:`134`) 0.14.0 (2023-01-28) ------------------- - Add configuration for mapping inventory locations (:confval:`codeautolink_inventory_map`) (:issue:`131`) - Improve warning messages to include debugging hints (:issue:`131`) - Fix AnnAssigns with no links not overwriting values (:issue:`133`) 0.13.2 (2023-01-26) ------------------- - Fix parsing IPython blocks that had a leading comment (:issue:`130`) 0.13.1 (2023-01-16) ------------------- - Fix IPython block parsing where output is not prefixed with `Out` (:issue:`129`) 0.13.0 (2023-01-08) ------------------- - Declare support for Python 3.11 (:issue:`122`) - Remove support for Python 3.6 (:issue:`123`) - Disallow faulty IPython version 8.7.0 (:issue:`124`) - Correctly require Beautiful Soup version 4.8.1 (:issue:`128`) 0.12.1 (2022-11-05) ------------------- - Created an Anaconda (Conda-Forge) binary (:issue:`111`) - Fix IPython parsing on multiline output and empty input (:issue:`119`) 0.12.0 (2022-09-14) ------------------- - Link assignment targets, bare names and annotated function arguments (:issue:`109`) - Initial support for match statement (:issue:`110`) - Fix links when assigning walrus statement result (:issue:`112`) - Fix links in multi-assignments when one target is unlinkable (:issue:`113`) 0.11.0 (2022-06-08) ------------------- - Support Python 3.10 (:issue:`33`) - Include the expected location of a type in :confval:`codeautolink_warn_on_failed_resolve` for debugging (:issue:`106`) - Define extension environment version for Sphinx (:issue:`107`) - Merge environments only when the extension is active (:issue:`107`) - Link arguments and annotated assignment with type hints (:issue:`108`) 0.10.0 (2022-01-25) ------------------- - Don't try to link empty name between two subsequent calls (:issue:`96`) - Introduce :confval:`codeautolink_warn_on_missing_inventory` and :confval:`codeautolink_warn_on_failed_resolve` to issue additional warnings when linking or resolving an object fails (:issue:`97`) - Support callable classes (:issue:`98`) 0.9.0 (2022-01-13) ------------------ - Use Sphinx logging instead of raising exceptions (:issue:`86`) - Link builtins if visible to intersphinx (:issue:`87`) - Use Sphinx logging instead of the builtin ``warnings`` to warn (:issue:`89`, :issue:`94`) - Support IPython's ``.. ipython::`` directive (:issue:`91`) 0.8.0 (2021-12-16) ------------------ - Correctly test for optional types in annotations (:issue:`72`) - Don't check for ``notranslate`` CSS class, allowing for additional classes (:issue:`75`) - Allow to specify block parsers as importable references (:issue:`76`) - Allow parallel builds (:issue:`77`) - Automatic support for ``ipython3`` code blocks (:issue:`79`) - Correctly produce links for ``py`` code blocks (:issue:`81`) 0.7.0 (2021-11-28) ------------------ - Declare CSS class as public API (:issue:`3`) - Add ability to link to subclass documentation (:issue:`68`) - Append a newline to error messages with source code (:issue:`70`) - Fix unpacking starred assignment (:issue:`71`) - Improve errors with information about the current document (:issue:`71`) 0.6.0 (2021-11-21) ------------------ - Remove text decoration from produced links (:issue:`3`) - Turn autodoc integration off by default (:issue:`58`) - Avoid index error when handling syntax errors (:issue:`60`) - Construct fully-qualified names more strictly to avoid hiding other issues (:issue:`61`) - Resolve string annotations in the module scope (:issue:`62`) - Correctly ensure that return annotations are valid types (:issue:`63`) - Resolve imported functions to their original location if a documentation entry is not found in the used location (:issue:`64`) - Fix multi-target assignment and unpacked assignment (:issue:`66`) - Correctly accept ``None`` as a custom block transformer (:issue:`67`) - Document support for ``sphinx.ext.doctest`` blocks (:issue:`67`) 0.5.1 (2021-11-20) ------------------ - Fix intersphinx links in documents inside folders (:issue:`56`) 0.5.0 (2021-11-07) ------------------ This release changes an internal API. Please delete the cache file before building documentation. - Link import statements (:issue:`42`) - Gracefully handle functions that don't have an annotations dict (:issue:`47`) - Enable configurations without autodoc (:issue:`48`) - Support custom code block syntax (:issue:`49`) - Fix crash on annotation-only assignment (:issue:`50`) - Fix issue with filenames that have dots (:issue:`52`) - Correctly remove extension when building non-HTML documentation (:issue:`53`) - Support searching extra CSS classes for code example matching (:issue:`54`) - Add configuration for global default concatenation state (:issue:`55`) 0.4.0 (2021-10-08) ------------------ - Support fluent interfaces (:issue:`37`) - Fix links for names that shadow builtins (:issue:`38`) - Support doctest blocks (:issue:`39`) 0.3.0 (2021-10-05) ------------------ - Treat optional types as their underlying type (:issue:`21`) - Improve ``autolink-examples`` argument structure and provide an option making a collapsible table (:issue:`25`) - Rename directives for consistency (:issue:`27`) - Correctly link decorators (:issue:`28`) - Move cache to Sphinx doctree directory (:issue:`29`) - Support Python console blocks (:issue:`30`) - Add configuration for default import statements (:issue:`31`) - Support star imports (:issue:`32`) - Accept multiline prefaces (:issue:`35`) - Fix autodoc injection on one-line docstrings (:issue:`36`) 0.2.1 (2021-10-01) ------------------ - Fix type resolving for class instances (:issue:`24`) 0.2.0 (2021-10-01) ------------------ - Improve code analysis and follow simple type hints (:issue:`5`) - Improve directive arguments and behavior (:issue:`16`) - Correctly consume :code:`autolink-skip:: next` (:issue:`17`) - Find type hints via imports, fix links in partial builds (:issue:`18`) 0.1.1 (2021-09-22) ------------------ - Correctly filter out names from concatenated sources (:issue:`14`) - Fix links in documents inside folder (:issue:`15`) 0.1.0 (2021-09-22) ------------------ Initial release ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2369566 sphinx_codeautolink-0.17.4/docs/src/static/0000755000076600000240000000000014762512105017721 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/docs/src/static/custom.css0000644000076600000240000000016714762511057021756 0ustar00felixstaff.sphinx-codeautolink-a{ border-bottom-color: #ccc; border-bottom-style: solid; border-bottom-width: 1px; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/pyproject.toml0000644000076600000240000000572114762511057017641 0ustar00felixstaff[build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "sphinx-codeautolink" description = "Automatic links from code examples to reference documentation." readme = "readme_pypi.rst" license = {file = "LICENSE"} dynamic = ["version"] requires-python = ">=3.10" dependencies = [ "sphinx>=3.2.0", "beautifulsoup4>=4.8.1", ] # Keep extras in sync with requirements manually optional-dependencies = {ipython = ["ipython!=8.7.0"]} keywords = ["sphinx", "extension", "code", "link"] authors = [{name = "Felix Hildén", email = "felix.hilden@gmail.com"}] maintainers = [{name = "Felix Hildén", email = "felix.hilden@gmail.com"}] classifiers = [ "Development Status :: 4 - Beta", "Framework :: Sphinx", "Framework :: Sphinx :: Extension", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Documentation", "Topic :: Documentation :: Sphinx", "Topic :: Software Development :: Documentation", ] [project.urls] homepage = "https://pypi.org/project/sphinx-codeautolink" download = "https://pypi.org/project/sphinx-codeautolink" source = "https://github.com/felix-hilden/sphinx-codeautolink" issues = "https://github.com/felix-hilden/sphinx-codeautolink/issues" documentation = "https://sphinx-codeautolink.rtfd.org" [tool.setuptools.dynamic] version = {attr = "sphinx_codeautolink.__version__"} [tool.pytest.ini_options] python_files = "*.py" testpaths = ["tests"] filterwarnings = [ "ignore:.*IPython:UserWarning", ] [tool.coverage.run] source = ["src"] branch = true command_line = "-m pytest" [tool.coverage.report] precision = 1 show_missing = true skip_covered = true [tool.ruff.lint] select = ["ALL"] ignore = [ "ANN", # annotations - too oppressive "TC", # type checking - too much hassle ] extend-ignore = [ "ARG001", # unused args needed in interfaces "ARG002", # unused args needed in interfaces "D107", # docstring missing is fine "D203", # recommended by ruff format "D212", # docstring format clash "D413", # empty docstring ending line "COM812", # recommended by ruff format "ISC001", # recommended by ruff format "PLR0913", # many arguments is fine "PLR0915", # many statements is fine ] extend-unsafe-fixes = ["F401"] isort.split-on-trailing-comma = false [tool.ruff.lint.extend-per-file-ignores] "src/*/__init__.py" = ["F401"] "src/sphinx_codeautolink/parse.py" = ["N802"] "docs/*" = ["ALL"] "tests/*" = [ "D", # docstring "ANN", # annotations "S101", # assertions - necessary in tests "T201", # print - helpful in tests "PLR2004", # magic number ] # TODO: support future annotated hints properly "tests/extension/src/test_project/__init__.py" = ["FA100"] [tool.ruff.format] skip-magic-trailing-comma = true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/readme_pypi.rst0000644000076600000240000000323614762511057017754 0ustar00felixstaffsphinx-codeautolink =================== |pyversions| |downloads| |license| |readthedocs| sphinx-codeautolink makes code examples clickable by inserting links from individual code elements to the corresponding reference documentation. We aim for a minimal setup assuming your examples are already valid Python. For a live demo, see our online documentation on `Read The Docs `_. Installation ------------ sphinx-codeautolink can be installed from the following sources: .. code:: sh $ pip install sphinx-codeautolink $ conda install -c conda-forge sphinx-codeautolink Note that the library is in early development, so version pinning is advised. To enable sphinx-codeautolink, modify the extension list in ``conf.py``. Note that the extension name uses an underscore rather than a hyphen. .. code:: python extensions = [ ..., "sphinx_codeautolink", ] That's it! Now your code examples are linked. For ways of concatenating examples, setting default import statements, or customising link style among other things, see the `online documentation `_. .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sphinx-codeautolink :alt: Python versions .. |downloads| image:: https://img.shields.io/pypi/dm/sphinx-codeautolink :alt: Monthly downloads .. |license| image:: https://img.shields.io/badge/License-MIT-blue.svg :target: https://choosealicense.com/licenses/mit :alt: License: MIT .. |readthedocs| image:: https://rtfd.org/projects/sphinx-codeautolink/badge/?version=stable :target: https://sphinx-codeautolink.rtfd.org/en/stable/ :alt: Documentation ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2377486 sphinx_codeautolink-0.17.4/requirements/0000755000076600000240000000000014762512105017436 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/requirements/build0000644000076600000240000000001414740025011020442 0ustar00felixstaffbuild twine ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736498541.0 sphinx_codeautolink-0.17.4/requirements/dev0000644000076600000240000000010414740156555020143 0ustar00felixstaff-r extras -r docs -r tests -r build tox>=4 doc8>=0.9 ruff pygments ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/requirements/docs0000644000076600000240000000004714303102654020305 0ustar00felixstaff-r extras sphinx-rtd-theme matplotlib ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/requirements/extras0000644000076600000240000000001714740025011020654 0ustar00felixstaffipython!=8.7.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/requirements/tests0000644000076600000240000000004714303102654020517 0ustar00felixstaff-r extras pytest>=6 coverage[toml]>=5 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2648256 sphinx_codeautolink-0.17.4/setup.cfg0000644000076600000240000000004614762512105016534 0ustar00felixstaff[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2310934 sphinx_codeautolink-0.17.4/src/0000755000076600000240000000000014762512105015502 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2383986 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/0000755000076600000240000000000014762512105021554 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329183.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/__init__.py0000644000076600000240000000565114762511437023703 0ustar00felixstaff"""Sphinx extension for linking code examples to reference documentation.""" from pathlib import Path from sphinx.application import Sphinx from .extension import SphinxCodeAutoLink, backref, directive from .extension.block import clean_ipython, clean_pycon from .extension.translation import MESSAGE_CATALOG_NAME __version__ = "0.17.4" def setup(app: Sphinx): """Set up extension, directives and events.""" state = SphinxCodeAutoLink() app.setup_extension("sphinx.ext.autodoc") app.add_css_file("sphinx-codeautolink.css") app.add_config_value( "codeautolink_autodoc_inject", default=False, rebuild="html", types=[bool] ) app.add_config_value( "codeautolink_global_preface", default="", rebuild="html", types=[str] ) app.add_config_value( "codeautolink_custom_blocks", default={}, rebuild="html", types=[dict] ) app.add_config_value( "codeautolink_concat_default", default=False, rebuild="html", types=[bool] ) app.add_config_value( "codeautolink_search_css_classes", default=[], rebuild="html", types=[list] ) app.add_config_value( "codeautolink_inventory_map", default={}, rebuild="html", types=[dict] ) app.add_config_value( "codeautolink_warn_on_missing_inventory", default=False, rebuild="html", types=[bool], ) app.add_config_value( "codeautolink_warn_on_failed_resolve", default=False, rebuild="html", types=[bool], ) app.add_config_value( "codeautolink_warn_on_no_backreference", default=False, rebuild="html", types=[bool], ) app.add_config_value( "codeautolink_warn_on_default_parse_fail", default=False, rebuild="html", types=[bool], ) app.add_directive("autolink-concat", directive.Concat) app.add_directive("autolink-examples", directive.Examples) app.add_directive("autolink-preface", directive.Preface) app.add_directive("autolink-skip", directive.Skip) app.connect("builder-inited", state.build_inited) app.connect("autodoc-process-docstring", state.autodoc_process_docstring) app.connect("doctree-read", state.parse_blocks, priority=490) app.connect("env-merge-info", state.merge_environments) app.connect("env-purge-doc", state.purge_doc_from_environment) app.connect("env-updated", state.create_references) app.connect("doctree-resolved", state.generate_backref_tables) app.connect("build-finished", state.apply_links) app.add_node( backref.DetailsNode, html=(backref.visit_details, backref.depart_details) ) app.add_node( backref.SummaryNode, html=(backref.visit_summary, backref.depart_summary) ) locale_dir = Path(__file__).resolve().parent / "locale" app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir) return {"version": __version__, "env_version": 1, "parallel_read_safe": True} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2405574 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/extension/0000755000076600000240000000000014762512105023570 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329086.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/extension/__init__.py0000644000076600000240000002773014762511276025722 0ustar00felixstaff"""Sphinx extension implementation.""" from __future__ import annotations from dataclasses import dataclass from functools import wraps from pathlib import Path from traceback import print_exc from sphinx import version_info as sphinx_version from sphinx.ext.intersphinx import InventoryAdapter from sphinx.util import import_object from sphinx_codeautolink.parse import Name from sphinx_codeautolink.warn import logger, warn_type from .backref import CodeExample, CodeRefsVisitor from .block import CodeBlockAnalyser, SourceTransform, link_html from .cache import DataCache from .directive import RemoveExtensionVisitor from .resolve import CouldNotResolve, resolve_location @dataclass class DocumentedObject: """Autodoc-documented code object.""" what: str obj: object return_type: str = None def print_exceptions(*, append_source: bool = False): """ Print the traceback of uncaught and unexpected exceptions. This is done because the Sphinx process masks the traceback and only displays the main error message making debugging difficult. If append_source is set, information about the currently processed document is pulled from the second argument named "doctree" and added to the message. """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: print_exc() if append_source: doctree = args[2] if len(args) > 1 else kwargs["doctree"] source = doctree["source"] msg = f"in document `{source}`" if e.args: e.args = (e.args[0] + f" ({msg})",) + e.args[1:] else: e.args = (f"Unexpected error {msg}",) raise return wrapper return decorator class SphinxCodeAutoLink: """Provide functionality and manage state between events.""" def __init__(self) -> None: # Configuration self.do_nothing = False self.global_preface: list[str] = [] self.custom_blocks = None self.concat_default = None self.search_css_classes = None self.inventory_map: dict[str, str] = {} self.warn_missing_inventory = None self.warn_failed_resolve = None self.warn_no_backreference = None self.warn_default_parse_fail = None self.highlight_lang = None # Populated once self.outdated_docs: set[str] = set() self.inventory = {} self.code_refs: dict[str, list[CodeExample]] = {} # Changing state self.cache: DataCache | None = None @print_exceptions() def build_inited(self, app) -> None: """Handle initial setup.""" if app.builder.name not in ("html", "dirhtml"): self.do_nothing = True return self.cache = DataCache(app.doctreedir, app.srcdir) self.cache.read() app.env.sphinx_codeautolink_transforms = self.cache.transforms self.outdated_docs = {str(Path(d)) for d in app.builder.get_outdated_docs()} self.custom_blocks = app.config.codeautolink_custom_blocks for k, v in self.custom_blocks.items(): if isinstance(v, str): self.custom_blocks[k] = import_object(v) self.concat_default = app.config.codeautolink_concat_default self.search_css_classes = app.config.codeautolink_search_css_classes self.inventory_map = app.config.codeautolink_inventory_map self.warn_missing_inventory = app.config.codeautolink_warn_on_missing_inventory self.warn_failed_resolve = app.config.codeautolink_warn_on_failed_resolve self.warn_no_backreference = app.config.codeautolink_warn_on_no_backreference self.warn_default_parse_fail = ( app.config.codeautolink_warn_on_default_parse_fail ) self.highlight_lang = app.config.highlight_language # Append static resources path so references in setup() are valid app.config.html_static_path.append( str(Path(__file__).parent.with_name("static").absolute()) ) preface = app.config.codeautolink_global_preface if preface: self.global_preface = preface.split("\n") @print_exceptions() def autodoc_process_docstring(self, app, what, name, obj, options, lines) -> None: """Handle autodoc-process-docstring event.""" if self.do_nothing: return if app.config.codeautolink_autodoc_inject: lines.append("") lines.append(".. autolink-examples:: " + name) lines.append(" :collapse:") @print_exceptions(append_source=True) def parse_blocks(self, app, doctree) -> None: """Parse code blocks for later link substitution.""" if self.do_nothing: return visitor = CodeBlockAnalyser( doctree, source_dir=app.srcdir, global_preface=self.global_preface, custom_blocks=self.custom_blocks, concat_default=self.concat_default, default_highlight_lang=self.highlight_lang, warn_default_parse_fail=self.warn_default_parse_fail, ) doctree.walkabout(visitor) self.cache.transforms[visitor.current_document] = visitor.source_transforms def merge_environments(self, app, env, docnames, other) -> None: """Merge transform information.""" if self.do_nothing: return env.sphinx_codeautolink_transforms.update(other.sphinx_codeautolink_transforms) def purge_doc_from_environment(self, app, env, docname) -> None: """Remove transforms from cache.""" if self.cache: self.cache.transforms.pop(docname, None) @staticmethod def make_inventory(app): """Create object inventory from local info and intersphinx.""" inv_parts = { k: str( Path(app.outdir) / (app.builder.get_target_uri(v.docname) + f"#{v.node_id}") ) for k, v in app.env.domains["py"].objects.items() } inventory = { "py:class": {k: (None, None, v, None) for k, v in inv_parts.items()} } inter_inv = InventoryAdapter(app.env).main_inventory transposed = transpose_inventory(inter_inv, relative_to=app.outdir) transposed.update( transpose_inventory(inventory, relative_to=app.outdir, use_tuple=True) ) return transposed @print_exceptions() def create_references(self, app, env) -> None: """Clean source transforms and create code references.""" if self.do_nothing: return skipped = set() self.inventory = self.make_inventory(app) for doc, transforms in self.cache.transforms.items(): self.filter_and_resolve(transforms, skipped, doc) for transform in transforms: for name in transform.names: self.code_refs.setdefault(name.resolved_location, []).append( transform.example ) if skipped and self.warn_missing_inventory: tops = sorted({s.split(".")[0] for s in skipped}) msg = ( f"Cannot locate modules: {str(tops)[1:-1]}" "\n because of missing intersphinx or documentation entries" ) logger.warning(msg, type=warn_type, subtype="missing_inventory") def filter_and_resolve( self, transforms: list[SourceTransform], skipped: set[str], doc: str ) -> None: """Try to link name chains to objects.""" for transform in transforms: filtered = [] for name in transform.names: if not name.code_str: continue # empty transform target (2 calls in a row) try: key = resolve_location(name, self.inventory) except CouldNotResolve as e: if self.warn_failed_resolve: path = ".".join(name.import_components).replace(".()", "()") msg = ( f"Could not resolve {self._resolve_msg(name)}" f" using path `{path}`.\n{e!s}" ) logger.warning( msg, type=warn_type, subtype="failed_resolve", location=(doc, transform.doc_lineno), ) continue key = self.inventory_map.get(key, key) if key not in self.inventory: if self.warn_missing_inventory: msg = ( f"Inventory missing `{key}`" f" when resolving {self._resolve_msg(name)}." "\nPossibly missing documentation entry entirely," " or the object has been relocated from the source file." ) logger.warning( msg, type=warn_type, subtype="missing_inventory", location=(doc, transform.doc_lineno), ) skipped.add(key) continue name.resolved_location = key filtered.append(name) transform.names = filtered @staticmethod def _resolve_msg(name: Name) -> str: if name.lineno == name.end_lineno: line = f"line {name.lineno}" else: line = f"lines {name.lineno}-{name.end_lineno}" return f"`{name.code_str}` on {line}" @print_exceptions(append_source=True) def generate_backref_tables(self, app, doctree, docname): """Generate backreference tables.""" if self.do_nothing: rm_vis = RemoveExtensionVisitor(doctree) return doctree.walkabout(rm_vis) visitor = CodeRefsVisitor( doctree, code_refs=self.code_refs, docname=docname, builder=app.builder, warn_no_backreference=self.warn_no_backreference, ) doctree.walk(visitor) return None @print_exceptions() def apply_links(self, app, exception) -> None: """Apply links to HTML output and write refs file.""" if self.do_nothing or exception is not None: return for doc, transforms in self.cache.transforms.items(): if not transforms or str(Path(doc)) not in self.outdated_docs: continue link_html( doc, app.outdir, transforms, self.inventory, self.custom_blocks, self.search_css_classes, builder_name=app.builder.name, ) self.cache.write() def transpose_inventory( inv: dict, relative_to: str, *, use_tuple: bool = False ) -> dict[str, str]: """ Transpose Sphinx inventory from {type: {name: (..., location)}} to {name: location}. Also filters the inventory to Python domain only. Parameters ---------- inv Sphinx inventory relative_to if a local file is found, transform it to be relative to this dir use_tuple force using Sphinx inventory tuple interface, TODO: move to class interface if it becomes public (#173) """ transposed = {} for type_, items in inv.items(): if not type_.startswith("py:"): continue for item, info in items.items(): location = ( info.uri if not use_tuple and sphinx_version >= (8, 2) else info[2] ) if not location.startswith("http"): location = str(Path(location).relative_to(relative_to)) transposed[item] = location return transposed ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329086.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/extension/backref.py0000644000076600000240000000777614762511276025570 0ustar00felixstaff"""Backreference tables implementation.""" from dataclasses import dataclass from docutils import nodes from sphinx.builders import Builder from sphinx_codeautolink.warn import logger, warn_type from .directive import DeferredExamples from .translation import tr @dataclass class CodeExample: """Code example in the documentation.""" document: str ref_id: str headings: list[str] class DetailsNode(nodes.Element): """Collapsible details node for HTML.""" def copy(self): """Copy element.""" return self.__class__() def visit_details(self, node: DetailsNode) -> None: """Insert a details tag.""" self.body.append("
") def depart_details(self, node: DetailsNode) -> None: """Close a details tag.""" self.body.append("
") class SummaryNode(nodes.TextElement): """Summary node inside a DetailsNode for HTML.""" def copy(self): """Copy element.""" return self.__class__() def visit_summary(self, node: SummaryNode) -> None: """Insert a summary tag.""" self.body.append("") def depart_summary(self, node: SummaryNode) -> None: """Close a summary tag.""" self.body.append("") class CodeRefsVisitor(nodes.SparseNodeVisitor): """Replace :class:`DeferredCodeReferences` with table of concrete references.""" def __init__( self, *args, code_refs: dict[str, list[CodeExample]], docname: str, builder: Builder, warn_no_backreference: bool = False, **kwargs, ) -> None: super().__init__(*args, **kwargs) self.code_refs = code_refs self.docname = docname self.builder = builder self.warn_no_backreference = warn_no_backreference def unknown_departure(self, node) -> None: """Ignore unknown nodes.""" def unknown_visit(self, node) -> None: """Insert table in :class:`DeferredExamples`.""" if not isinstance(node, DeferredExamples): return items = [] for ref in self.code_refs.get(node.ref, []): link = self.builder.get_relative_uri(self.docname, ref.document) if ref.ref_id is not None: link += f"#{ref.ref_id}" items.append((link, " / ".join(ref.headings))) items = sorted(set(items)) if not items: if self.warn_no_backreference: msg = f"No backreference for: '{node.ref}'" logger.warning( msg, type=warn_type, subtype="no_backreference", location=node ) # Remove surrounding paragraph too node.parent.parent.remove(node.parent) return orig_ref = node.children[0] node.parent.remove(node) # Table definition table = nodes.table() tgroup = nodes.tgroup(cols=1) table += tgroup tgroup += nodes.colspec(colwidth=1) if not node.collapse: thead = nodes.thead() tgroup += thead row = nodes.row() thead += row entry = nodes.entry() row += entry title = nodes.paragraph() title += nodes.Text(tr("References to") + " ") title += orig_ref entry += title tbody = nodes.tbody() tgroup += tbody for link, text in items: row = nodes.row() tbody += row entry = nodes.entry() par = nodes.paragraph() par += nodes.reference(internal=True, refuri=link, text=text) entry += par row += entry parent_par = nodes.paragraph() if node.collapse: details = DetailsNode() summary = SummaryNode() summary += nodes.Text(tr("Expand for references to") + " ") summary += orig_ref details += summary details += table parent_par += details else: parent_par += table node.parent.replace_self(parent_par) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/extension/block.py0000644000076600000240000004412314762511057025245 0ustar00felixstaff"""Code block processing.""" from __future__ import annotations import re from collections.abc import Callable from copy import copy from dataclasses import dataclass from pathlib import Path from bs4 import BeautifulSoup from docutils import nodes from sphinx_codeautolink.parse import LinkContext, Name, parse_names from sphinx_codeautolink.warn import logger, warn_type from .backref import CodeExample from .directive import ConcatMarker, PrefaceMarker, SkipMarker # list from https://pygments.org/docs/lexers/#pygments.lexers.python.PythonLexer BUILTIN_BLOCKS = { "default": None, "python": None, "python3": None, "py": None, "py3": None, "pyi": None, "sage": None, "bazel": None, "starlark": None, } @dataclass class SourceTransform: """Transforms on source code.""" source: str names: list[Name] example: CodeExample doc_lineno: int def clean_pycon(source: str) -> tuple[str, str]: """Clean up Python console syntax to pure Python.""" in_statement = False source = re.sub(r"^\s*", "", source, flags=re.MULTILINE) clean_lines = [] for line in source.split("\n"): if line.startswith(">>> "): in_statement = True clean_lines.append(line[4:]) elif in_statement and line.startswith("..."): clean_lines.append(line[4:]) else: in_statement = False clean_lines.append("") return source, "\n".join(clean_lines) BUILTIN_BLOCKS["pycon"] = clean_pycon def _exclude_ipython_output(source: str) -> str: in_regex = r"In \[[0-9]+\]: " # If the first line doesn't begin with a console prompt, # assume the entire block to be purely IPython *code*. # An arbitrary number of comments and empty lines are exempt. if not re.match(rf"^(\s*(#[^\n]*)?\n)*{in_regex}", source): return source clean_lines = [] for line in source.split("\n"): # Space after "In" is required by transformer but removed in RST preprocessing. # All comment are passed through even if they are strictly not input to allow # leading comment lines to not be stripped by the IPython transformer. if ( re.match(rf"^{in_regex}", line) or re.match(r"^\s*\.*\.\.\.: ", line) or re.match(r"^\s*#", line) ): in_statement = True else: in_statement = False clean_lines.append(line * in_statement) return "\n".join(clean_lines) def clean_ipython(source: str) -> tuple[str, str]: """Clean up IPython magics and console syntax to pure Python.""" from IPython.core.inputtransformer2 import TransformerManager clean = _exclude_ipython_output(source) return source, TransformerManager().transform_cell(clean) try: import IPython except ImportError: pass else: del IPython BUILTIN_BLOCKS["ipython"] = clean_ipython BUILTIN_BLOCKS["ipython3"] = clean_ipython class CodeBlockAnalyser(nodes.SparseNodeVisitor): """Transform blocks of Python with links to reference documentation.""" def __init__( self, *args, source_dir: str, global_preface: list[str], custom_blocks: dict[str, Callable[[str], str]], concat_default: bool, default_highlight_lang: str | None, warn_default_parse_fail: bool, **kwargs, ) -> None: super().__init__(*args, **kwargs) self.source_transforms: list[SourceTransform] = [] relative_path = Path(self.document["source"]).relative_to(source_dir) self.current_document = str(relative_path.with_suffix("")) self.global_preface = global_preface self.file_preface = [] self.prefaces = [] self.doctest_preface: dict[str, list[str]] = {} self.transformers = BUILTIN_BLOCKS.copy() self.transformers.update(custom_blocks) self.valid_blocks = self.transformers.keys() self.title_stack = [] self.current_refid = None self.concat_global = concat_default self.concat_section = False self.concat_sources = [] self.skip = None self.highlight_lang = default_highlight_lang self.warn_default_parse_fail = warn_default_parse_fail def unknown_visit(self, node) -> None: """Handle and delete custom directives, ignore others.""" if isinstance(node, ConcatMarker): if node.mode not in ("off", "section", "on"): msg = f"Invalid concatenation argument: `{node.mode}`" logger.error( msg, type=warn_type, subtype="invalid_argument", location=node ) self.concat_sources = [] if node.mode == "section": self.concat_section = True else: self.concat_section = False self.concat_global = node.mode == "on" node.parent.remove(node) elif isinstance(node, PrefaceMarker): lines = node.content.split("\n") or [] if node.level == "next": self.prefaces.extend(lines) elif node.level == "file": self.file_preface = lines else: msg = f"Invalid preface argument: `{node.level}`" logger.error( msg, type=warn_type, subtype="invalid_argument", location=node ) node.parent.remove(node) elif isinstance(node, SkipMarker): if node.level not in ("next", "section", "file", "off"): msg = f"Invalid skipping argument: `{node.level}`" logger.error( msg, type=warn_type, subtype="invalid_argument", location=node ) self.skip = node.level if node.level != "off" else None node.parent.remove(node) def unknown_departure(self, node) -> None: """Ignore unknown nodes.""" def visit_highlightlang(self, node) -> None: """Set expected highlight language.""" self.highlight_lang = node["lang"] def visit_title(self, node) -> None: """Track section names and break concatenation and skipping.""" self.title_stack.append(node.astext()) if self.concat_section: self.concat_section = False self.concat_sources = [] if self.skip == "section": self.skip = None def visit_section(self, node) -> None: """Record first section ID.""" self.current_refid = node["ids"][0] def depart_section(self, node) -> None: """Pop latest title.""" self.title_stack.pop() def visit_doctest_block(self, node: nodes.doctest_block): """Visit a Python doctest block.""" return self.parse_source(node, "pycon") def visit_comment(self, node: nodes.comment): """Visit an ext.doctest setup block.""" if node.get("testnodetype") == "testsetup": groups = node["groups"] lines = [ln for c in node.children for ln in c.astext().split("\n")] for g in groups: if g == "*": self.doctest_preface = {"*": lines} else: self.doctest_preface[g] = lines def visit_literal_block(self, node: nodes.literal_block): """Visit a generic literal block.""" return self.parse_source(node, node.get("language", self.highlight_lang)) def parse_source( # noqa: C901,PLR0912 self, node: nodes.literal_block | nodes.doctest_block, language: str | None ) -> None: """Analyse Python code blocks.""" prefaces = self.prefaces self.prefaces = [] if node.get("testnodetype") == "doctest": groups = node["groups"] doctest_prefaces = [ ln for g in groups for ln in self.doctest_preface.get(g, self.doctest_preface.get("*", [])) ] prefaces = doctest_prefaces + prefaces skip = self.skip if skip == "next": self.skip = None if ( len(node.children) != 1 or not isinstance(node.children[0], nodes.Text) or language not in self.valid_blocks ): return source = node.children[0].astext() example = CodeExample( self.current_document, self.current_refid, list(self.title_stack) ) transform = SourceTransform(source, [], example, node.line) self.source_transforms.append(transform) if skip: return # Sphinx uses a similar trick to use pycon implicitly (#168) pycon_candidates = ("py", "python", "py3", "python3", "default", "pycon3") if source.startswith(">>>") and language in pycon_candidates: language = "pycon" transformer = self.transformers[language] if transformer: try: source, clean_source = transformer(source) except SyntaxError as e: msg = self._parsing_error_msg(e, language, source) logger.warning( msg, type=warn_type, subtype="clean_block", location=node ) return else: clean_source = source transform.source = source full_source = "\n".join( self.global_preface + self.file_preface + self.concat_sources + prefaces + [clean_source] ) try: names = parse_names(full_source, node) except SyntaxError as e: if language == "default" and not self.warn_default_parse_fail: return show_source = self._format_source_for_error( self.global_preface, self.file_preface, self.concat_sources, prefaces, transform.source, ) msg = self._parsing_error_msg(e, language, show_source) logger.warning(msg, type=warn_type, subtype="parse_block", location=node) return except Exception as e: show_source = self._format_source_for_error( self.global_preface, self.file_preface, self.concat_sources, prefaces, transform.source, ) msg = self._parsing_error_msg(e, language, show_source) raise type(e)(msg) from e if prefaces or self.concat_sources or self.global_preface or self.file_preface: concat_lens = [s.count("\n") + 1 for s in self.concat_sources] hidden_len = len(prefaces + self.global_preface + self.file_preface) + sum( concat_lens ) for name in names: name.lineno -= hidden_len name.end_lineno -= hidden_len if self.concat_section or self.concat_global: self.concat_sources.extend([*prefaces, clean_source]) # Remove transforms from concatenated sources transform.names.extend([n for n in names if n.lineno > 0]) @staticmethod def _format_source_for_error( global_preface: list[str], file_preface: list[str], concat_sources: list[str], prefaces: list[str], source: str, ) -> str: lines = ( global_preface + file_preface + concat_sources + prefaces + source.split("\n") ) guides = [""] * len(lines) ix = 0 if global_preface: guides[ix] = "global preface:" ix += len(global_preface) if file_preface: guides[ix] = "file preface:" ix += len(concat_sources) if concat_sources: guides[ix] = "concatenations:" ix += len(concat_sources) if prefaces: guides[ix] = "local preface:" ix += len(prefaces) guides[ix] = "block source:" pad = max(len(i) + 1 for i in guides) guides = [g.ljust(pad) for g in guides] return "\n".join([g + s for g, s in zip(guides, lines, strict=True)]) def _parsing_error_msg(self, error: Exception, language: str, source: str) -> str: return "\n".join( [ str(error) + f" in document {self.current_document!r}", f"Parsed source in `{language}` block:", source, "", ] ) def link_html( document: str, out_dir: str, transforms: list[SourceTransform], inventory: dict, custom_blocks: dict, search_css_classes: list, builder_name: str = "html", ) -> None: """Inject links to code blocks on disk.""" if builder_name == "dirhtml" and Path(document).name != "index": html_file = Path(out_dir) / document / "index.html" else: html_file = Path(out_dir) / (document + ".html") text = html_file.read_text("utf-8") soup = BeautifulSoup(text, "html.parser") block_types = BUILTIN_BLOCKS.keys() | custom_blocks.keys() classes = [f"highlight-{t}" for t in block_types] + ["doctest"] classes += search_css_classes blocks = [] for c in classes: blocks.extend(list(soup.find_all("div", attrs={"class": c}))) unique_blocks = {b.sourceline: b for b in blocks}.values() blocks = sorted(unique_blocks, key=lambda b: b.sourceline) inners = [block.select("div > pre")[0] for block in blocks] up_lvls = len(html_file.relative_to(out_dir).parents) - 1 local_prefix = "../" * up_lvls link_pattern = ( '{text}' ) for trans in transforms: for ix in range(len(inners)): candidate = copy(inners[ix]) # remove line numbers for matching for lineno in candidate.find_all("span", attrs={"class": "linenos"}): lineno.extract() if trans.source.rstrip() == "".join(candidate.strings).rstrip(): inner = inners.pop(ix) break else: msg = f"Could not match a code example to HTML, source:\n{trans.source}" logger.warning( msg, type=warn_type, subtype="match_block", location=document ) continue lines = str(inner).split("\n") for name in trans.names: begin_line = name.lineno - 1 end_line = name.end_lineno - 1 selection = "\n".join(lines[begin_line : end_line + 1]) # Reverse because a.b = a.b should replace from the right matches = list(re.finditer(construct_name_pattern(name), selection))[::-1] if not matches: msg = ( f"Could not match transformation of `{name.code_str}` " f"on source lines {name.lineno}-{name.end_lineno}, " f"source:\n{trans.source}" ) logger.warning( msg, type=warn_type, subtype="match_name", location=document ) continue start, end = matches[0].span() start += len(matches[0].group(1)) location = inventory[name.resolved_location] if not any(location.startswith(s) for s in ("http://", "https://")): location = local_prefix + location link = link_pattern.format( link=location, title=name.resolved_location, text=selection[start:end] ) transformed = selection[:start] + link + selection[end:] lines[begin_line : end_line + 1] = transformed.split("\n") inner.replace_with(BeautifulSoup("\n".join(lines), "html.parser")) html_file.write_text(str(soup), "utf-8") # --------------------------------------------------------------- # Patterns for different types of name access in highlighted HTML # --------------------------------------------------------------- period = r'\s*.\s*' name_pattern = '{name}' # Pygments has special classes for different types of nouns # which are also highlighted in import statements first_name_pattern = '@?{name}' import_target_pattern = '{name}' import_from_pattern = '{name}' # The builtin re doesn't support variable-width lookbehind, # so instead we use a match groups in all pre patterns to remove the non-content. no_dot_pre = r'(?\.)()' # Potentially instead assert an initial closing parenthesis followed by a dot. call_dot_pre = r'(\)\s*\.\s*)' no_dot_post = r'(?!(\.)|())' # Pygments 2.19 changed import whitespace highlighting so we need to support both # with "w" class and raw whitespace for now (see #152) whitespace = r'(\s*)|(\s*)' import_pre = ( rf'((import{whitespace}(\(\s*)?)' rf'|(,{whitespace}))' ) import_post = r'(?=($)|(\s+)|())(?!)' from_pre = rf'(from{whitespace})' from_post = rf'(?={whitespace}import)' def construct_name_pattern(name: Name) -> str: """Construct a regex pattern for searching a name in HTML.""" if name.context == LinkContext.none: parts = name.code_str.split(".") pattern = period.join( [first_name_pattern.format(name=parts[0])] + [name_pattern.format(name=p) for p in parts[1:]] ) return no_dot_pre + pattern + no_dot_post if name.context == LinkContext.after_call: parts = name.code_str.split(".") pattern = period.join( [first_name_pattern.format(name=parts[0])] + [name_pattern.format(name=p) for p in parts[1:]] ) return call_dot_pre + pattern + no_dot_post if name.context == LinkContext.import_from: pattern = import_from_pattern.format(name=name.code_str) return from_pre + pattern + from_post if name.context == LinkContext.import_target: pattern = import_target_pattern.format(name=name.code_str) return import_pre + pattern + import_post return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736498541.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/extension/cache.py0000644000076600000240000000265714740156555025230 0ustar00felixstaff"""Extension data cache.""" import json from dataclasses import asdict from pathlib import Path from .block import CodeExample, Name, SourceTransform class DataCache: """Data cache.""" cache_filename = "codeautolink-cache.json" def __init__(self, cache_dir: str, src_dir: str) -> None: self.cache_dir: Path = Path(cache_dir) self.src_dir: Path = Path(src_dir) self.transforms: dict[str, list[SourceTransform]] = {} def read(self) -> None: """Read from cache.""" cache = self.cache_dir / self.cache_filename if not cache.exists(): return content = json.loads(cache.read_text("utf-8")) for file, transforms in content.items(): full_path = self.src_dir / (file + ".rst") if not full_path.exists(): continue for transform in transforms: transform["example"] = CodeExample(**transform["example"]) transform["names"] = [Name(**n) for n in transform["names"]] self.transforms[file] = [SourceTransform(**t) for t in transforms] def write(self) -> None: """Write to cache.""" cache = self.cache_dir / self.cache_filename transforms_dict = {} for file, transforms in self.transforms.items(): transforms_dict[file] = [asdict(t) for t in transforms] cache.write_text(json.dumps(transforms_dict, indent=2), "utf-8") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/extension/directive.py0000644000076600000240000000756314762511057026140 0ustar00felixstaff"""Directive implementations.""" from __future__ import annotations from typing import ClassVar from docutils import nodes from docutils.parsers.rst import Directive, directives from sphinx import addnodes class DeferredExamples(nodes.Element): """Deferred node for substitution later when references are known.""" def __init__(self, ref: str, collapse: bool) -> None: # noqa: FBT001 super().__init__() self.ref = ref self.collapse = collapse def copy(self): """Copy element.""" return self.__class__(self.ref, self.collapse) class Examples(Directive): """Gather and display references in code examples.""" has_content = False required_arguments = 1 optional_arguments = 0 option_spec: ClassVar = {"collapse": directives.flag, "type": directives.unchanged} def run(self): """Run directive to insert a :class:`DeferredExamples`.""" name = self.arguments[0] collapse = self.options.get("collapse", False) is None par = nodes.paragraph() deferred = DeferredExamples(name, collapse) par += deferred ref = addnodes.pending_xref( refdomain="py", refexplicit=False, refwarn=False, reftype=self.options.get("type", "class"), reftarget=name, ) ref += nodes.literal(classes=["xref", "py", "py-class"], text=name) deferred += ref return [par] class ConcatMarker(nodes.Element): """Marker for :class:`Concat`.""" def __init__(self, mode: str | None = None) -> None: super().__init__() self.mode = mode def copy(self): """Copy element.""" return self.__class__(self.mode) class Concat(Directive): """Toggle and cut block concatenation in a document.""" has_content = False required_arguments = 0 optional_arguments = 1 def run(self): """Insert :class:`ConcatMarker`.""" arg = self.arguments[0] if self.arguments else "on" return [ConcatMarker(arg)] class PrefaceMarker(nodes.Element): """Marker for :class:`Preface`.""" def __init__(self, content: str, level: str) -> None: super().__init__() self.content = content self.level = level def copy(self): """Copy element.""" return self.__class__(self.content) class Preface(Directive): """Include a preface in the next code block.""" has_content = True required_arguments = 0 optional_arguments = 1 final_argument_whitespace = True option_spec: ClassVar = {"level": directives.unchanged} def run(self): """Insert :class:`PrefaceMarker`.""" lines = list(self.arguments) + list(self.content) level = self.options.get("level", "next") return [PrefaceMarker("\n".join(lines), level)] class SkipMarker(nodes.Element): """Marker for :class:`Skip`.""" def __init__(self, level: str) -> None: super().__init__() self.level = level def copy(self): """Copy element.""" return self.__class__(self.level) class Skip(Directive): """Skip auto-linking next code block.""" has_content = False required_arguments = 0 optional_arguments = 1 def run(self): """Insert :class:`SkipMarker`.""" arg = self.arguments[0] if self.arguments else "next" return [SkipMarker(arg)] class RemoveExtensionVisitor(nodes.SparseNodeVisitor): """Silently remove all codeautolink directives.""" def unknown_departure(self, node) -> None: """Ignore unknown nodes.""" def unknown_visit(self, node) -> None: """Remove nodes.""" if isinstance(node, DeferredExamples): # Remove surrounding paragraph too node.parent.parent.remove(node.parent) if isinstance(node, ConcatMarker | PrefaceMarker | SkipMarker): node.parent.remove(node) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739623647.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/extension/resolve.py0000644000076600000240000001343014754106337025631 0ustar00felixstaff"""Resolve import locations and type hints.""" from __future__ import annotations from collections.abc import Callable from contextlib import suppress from dataclasses import dataclass from functools import cache from importlib import import_module from inspect import isclass, isroutine from types import UnionType from typing import Any, Union, get_type_hints from sphinx_codeautolink.parse import Name, NameBreak def resolve_location(chain: Name, inventory) -> str: """Find the final type that a name refers to.""" comps = [] cursor = None for comp in chain.import_components: if comp != NameBreak.call: comps.append(comp) continue if cursor is None: comps, cursor = make_cursor(comps) cursor = locate_type(cursor, tuple(comps), inventory) call_value(cursor) comps = [] if cursor is None: try: comps, cursor = make_cursor(comps) except CouldNotResolve: # Last ditch effort to locate based on the string only return ".".join(comps) cursor = locate_type(cursor, tuple(comps), inventory) return cursor.location if cursor is not None else None class CouldNotResolve(Exception): # noqa: N818 """Could not resolve type to inventory.""" @dataclass class Cursor: """Cursor to follow imports, attributes and calls to the final type.""" location: str value: Any instance: bool def make_cursor(components: list[str]) -> tuple[list[str], Cursor]: """Divide components into module and rest, create cursor for following the rest.""" value, index = closest_module(tuple(components)) location = ".".join(components[:index]) return components[index:], Cursor(location, value, instance=False) def locate_type(cursor: Cursor, components: tuple[str, ...], inventory) -> Cursor: """Find type hint and resolve to new location.""" previous = cursor for i, component in enumerate(components): cursor = Cursor( cursor.location + "." + component, getattr(cursor.value, component, None), cursor.instance, ) if cursor.value is None: msg = f"{cursor.location} does not exist." raise CouldNotResolve(msg) if isclass(cursor.value): cursor.instance = False if isclass(cursor.value) or ( isroutine(cursor.value) and cursor.location not in inventory ): # Normalise location of type or imported function # If odd construct encountered: don't try to be clever but continue with suppress(AttributeError, TypeError): cursor.location = fully_qualified_name(cursor.value) # Check bases if member not found in current class if isclass(previous.value) and cursor.location not in inventory: for val in previous.value.__mro__[1:]: name = fully_qualified_name(val) if name + "." + component in inventory: previous.location = name return locate_type(previous, components[i:], inventory) previous = cursor return cursor def call_value(cursor: Cursor) -> None: """Call class, instance or function.""" if isclass(cursor.value) and not cursor.instance: # class definition: "instantiate" class cursor.instance = True return if callable(cursor.value) and not isroutine(cursor.value): # callable class instance cursor.value = cursor.value.__call__ elif not isroutine(cursor.value): raise CouldNotResolve # not a function either cursor.value = get_return_annotation(cursor.value) cursor.location = fully_qualified_name(cursor.value) cursor.instance = True def get_return_annotation(func: Callable) -> type | None: """Determine the target of a function return type hint.""" try: annotation = get_type_hints(func).get("return") except (NameError, TypeError) as e: msg = f"Unable to follow return annotation of {get_name_for_debugging(func)}." raise CouldNotResolve(msg) from e # Inner type from typing.Optional or Union[None, T] origin = getattr(annotation, "__origin__", None) args = getattr(annotation, "__args__", None) if (origin is Union or isinstance(annotation, UnionType)) and len(args) == 2: # noqa: PLR2004 nonetype = type(None) if args[0] is nonetype: annotation = args[1] elif args[1] is nonetype: annotation = args[0] if ( not annotation or not isinstance(annotation, type) or hasattr(annotation, "__origin__") ): msg = f"Unable to follow return annotation of {get_name_for_debugging(func)}." raise CouldNotResolve(msg) return annotation def fully_qualified_name(thing: type | Callable) -> str: """Construct the fully qualified name of a type.""" return thing.__module__ + "." + thing.__qualname__ def get_name_for_debugging(thing: type | Callable) -> str: """Construct the fully qualified name or some readable information of a type.""" try: return fully_qualified_name(thing) except (AttributeError, TypeError): return repr(thing) @cache def closest_module(components: tuple[str, ...]) -> tuple[Any, int]: """Find closest importable module.""" try: mod = import_module(components[0]) except ImportError as e: msg = f"Could not import {components[0]}." raise CouldNotResolve(msg) from e for i in range(1, len(components)): try: mod = import_module(".".join(components[: i + 1])) except ImportError: # noqa: PERF203 # import failed, exclude previously added item return mod, i # imports succeeded, include all items return mod, len(components) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/extension/translation.py0000644000076600000240000000024114762511057026502 0ustar00felixstaff"""Translation utilities.""" from sphinx.locale import get_translation MESSAGE_CATALOG_NAME = "sphinx-codeautolink" tr = get_translation(MESSAGE_CATALOG_NAME) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2407248 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/0000755000076600000240000000000014762512105023013 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2314327 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/de/0000755000076600000240000000000014762512105023403 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1741329477.241057 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/de/LC_MESSAGES/0000755000076600000240000000000014762512105025170 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/de/LC_MESSAGES/sphinx-codeautolink.mo0000644000076600000240000000111614762511057031521 0ustar00felixstaff4LM ft ?Expand for references toReferences toProject-Id-Version: 1 Report-Msgid-Bugs-To: cmarqu42@gmail.com POT-Creation-Date: 2025-02-23 17:30+0200 PO-Revision-Date: 2025-02-23 18:42+0100 Last-Translator: Colin Marquardt Language: de_DE Language-Team: Colin Marquardt Plural-Forms: nplurals=2; plural=(n != 1); MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.17.0 Aufklappen für Referenzen aufReferenzen auf././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/de/LC_MESSAGES/sphinx-codeautolink.po0000644000076600000240000000147214762511057031531 0ustar00felixstaff# German translations for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink project. # Colin Marquardt , 2025. # msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: cmarqu42@gmail.com\n" "POT-Creation-Date: 2025-02-23 17:30+0200\n" "PO-Revision-Date: 2025-02-23 18:42+0100\n" "Last-Translator: Colin Marquardt \n" "Language: de\n" "Language-Team: Colin Marquardt \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "Referenzen auf" msgid "Expand for references to" msgstr "Aufklappen für Referenzen auf" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2317626 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/es/0000755000076600000240000000000014762512105023422 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2413895 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/es/LC_MESSAGES/0000755000076600000240000000000014762512105025207 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/es/LC_MESSAGES/sphinx-codeautolink.mo0000644000076600000240000000112414762511057031537 0ustar00felixstaff4LM ft(CExpand for references toReferences toProject-Id-Version: 1 Report-Msgid-Bugs-To: felix.hilden@gmail.com POT-Creation-Date: 2025-02-23 17:30+0200 PO-Revision-Date: 2025-02-23 18:05+0200 Last-Translator: Felix Hildén Language: es_ES Language-Team: Felix Hildén Plural-Forms: nplurals=2; plural=(n != 1); MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.17.0 Amplíe para referencias aReferencias para././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/es/LC_MESSAGES/sphinx-codeautolink.po0000644000076600000240000000150014762511057031540 0ustar00felixstaff# Spanish translations for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink project. # Felix Hildén , 2025. # msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: felix.hilden@gmail.com\n" "POT-Creation-Date: 2025-02-23 17:30+0200\n" "PO-Revision-Date: 2025-02-23 18:05+0200\n" "Last-Translator: Felix Hildén \n" "Language: es\n" "Language-Team: Felix Hildén \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "Referencias a" msgid "Expand for references to" msgstr "Amplíe para referencias a" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1741329477.231961 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/fi/0000755000076600000240000000000014762512105023411 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2417216 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/fi/LC_MESSAGES/0000755000076600000240000000000014762512105025176 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/fi/LC_MESSAGES/sphinx-codeautolink.mo0000644000076600000240000000114214762511057031526 0ustar00felixstaff4LM ft%(NExpand for references toReferences toProject-Id-Version: 1 Report-Msgid-Bugs-To: felix.hilden@gmail.com POT-Creation-Date: 2025-02-23 17:30+0200 PO-Revision-Date: 2025-02-23 17:32+0200 Last-Translator: Felix Hildén Language: fi_FI Language-Team: Felix Hildén Plural-Forms: nplurals=2; plural=(n != 1); MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.17.0 Avaa nähdäksesi viitteet kohteeseenViitteet kohteeseen././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/fi/LC_MESSAGES/sphinx-codeautolink.po0000644000076600000240000000152114762511057031532 0ustar00felixstaff# Finnish translations for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink project. # Felix Hildén , 2025. # msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: felix.hilden@gmail.com\n" "POT-Creation-Date: 2025-02-23 17:30+0200\n" "PO-Revision-Date: 2025-02-23 17:32+0200\n" "Last-Translator: Felix Hildén \n" "Language: fi\n" "Language-Team: Felix Hildén \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "Viitteet kohteeseen" msgid "Expand for references to" msgstr "Avaa nähdäksesi viitteet kohteeseen" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1741329477.232112 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/fr/0000755000076600000240000000000014762512105023422 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2420692 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/fr/LC_MESSAGES/0000755000076600000240000000000014762512105025207 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/fr/LC_MESSAGES/sphinx-codeautolink.mo0000644000076600000240000000111714762511057031541 0ustar00felixstaff4LM ft!?Expand for references toReferences toProject-Id-Version: 1 Report-Msgid-Bugs-To: cmarqu42@gmail.com POT-Creation-Date: 2025-02-23 17:30+0200 PO-Revision-Date: 2025-02-25 22:23+0100 Last-Translator: Colin Marquardt Language: fr Language-Team: Colin Marquardt Plural-Forms: nplurals=2; plural=(n != 1); MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.17.0 Étendre pour les références àRéférences à././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/fr/LC_MESSAGES/sphinx-codeautolink.po0000644000076600000240000000147614762511057031554 0ustar00felixstaff# French translations for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink project. # Colin Marquardt , 2025. # msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: cmarqu42@gmail.com\n" "POT-Creation-Date: 2025-02-23 17:30+0200\n" "PO-Revision-Date: 2025-02-25 22:23+0100\n" "Last-Translator: Colin Marquardt \n" "Language: fr\n" "Language-Team: Colin Marquardt \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "Références à" msgid "Expand for references to" msgstr "Étendre pour les références à" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2322555 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/it/0000755000076600000240000000000014762512105023427 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2423925 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/it/LC_MESSAGES/0000755000076600000240000000000014762512105025214 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/it/LC_MESSAGES/sphinx-codeautolink.mo0000644000076600000240000000110514762511057031543 0ustar00felixstaff4LM ft 7Expand for references toReferences toProject-Id-Version: 1 Report-Msgid-Bugs-To: cmarqu42@gmail.com POT-Creation-Date: 2025-02-23 17:30+0200 PO-Revision-Date: 2025-02-24 20:26+0100 Last-Translator: Colin Marquardt Language: it Language-Team: Colin Marquardt Plural-Forms: nplurals=2; plural=(n != 1); MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.17.0 Espandi per riferimenti aRiferimenti a././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/it/LC_MESSAGES/sphinx-codeautolink.po0000644000076600000240000000146514762511057031557 0ustar00felixstaff# Italian translations for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink project. # Colin Marquardt , 2025. # msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: cmarqu42@gmail.com\n" "POT-Creation-Date: 2025-02-23 17:30+0200\n" "PO-Revision-Date: 2025-02-24 20:26+0100\n" "Last-Translator: Colin Marquardt \n" "Language: it\n" "Language-Team: Colin Marquardt \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "Riferimenti a" msgid "Expand for references to" msgstr "Espandi per riferimenti a" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2323942 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/nl/0000755000076600000240000000000014762512105023424 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2427406 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/nl/LC_MESSAGES/0000755000076600000240000000000014762512105025211 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/nl/LC_MESSAGES/sphinx-codeautolink.mo0000644000076600000240000000114714762511057031546 0ustar00felixstaff4LM ft 5VExpand for references toReferences toProject-Id-Version: 1 Report-Msgid-Bugs-To: smith.trevorj@ouranos.ca POT-Creation-Date: 2025-02-23 17:30+0200 PO-Revision-Date: 2025-02-26 09:04-0500 Last-Translator: Trevor James Smith Language: nl Language-Team: Trevor James Smith Plural-Forms: nplurals=2; plural=(n != 1); MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Generated-By: Babel 2.17.0 Uitklappen voor referenties naarReferenties naar././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/nl/LC_MESSAGES/sphinx-codeautolink.po0000644000076600000240000000156014762511057031550 0ustar00felixstaff# Dutch (Netherlands) translations for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink # project. # Trevor James Smith , 2025. # msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: smith.trevorj@ouranos.ca\n" "POT-Creation-Date: 2025-02-23 17:30+0200\n" "PO-Revision-Date: 2025-02-26 09:04-0500\n" "Last-Translator: Trevor James Smith \n" "Language: nl\n" "Language-Team: Trevor James Smith \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "Referenties naar" msgid "Expand for references to" msgstr "Uitklappen voor referenties naar" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2325366 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/ru/0000755000076600000240000000000014762512105023441 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2429233 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/ru/LC_MESSAGES/0000755000076600000240000000000014762512105025226 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/ru/LC_MESSAGES/sphinx-codeautolink.po0000644000076600000240000000151114762511057031561 0ustar00felixstaff# Russian translations for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink project. # Yuriy Skalko , 2025. # msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: yuriy.skalko@gmail.com\n" "POT-Creation-Date: 2025-02-24 17:30+0200\n" "PO-Revision-Date: 2025-02-24 18:42+0100\n" "Last-Translator: Yuriy Skalko \n" "Language: ru\n" "Language-Team: Yuriy Skalko \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "Ссылки на" msgid "Expand for references to" msgstr "Раскрыть ссылки на" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/sphinx-codeautolink.pot0000644000076600000240000000132614762511057027536 0ustar00felixstaff# Translations template for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink project. # Felix Hildén , 2025. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: felix.hilden@gmail.com\n" "POT-Creation-Date: 2025-02-23 17:30+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: Felix Hildén \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "" msgid "Expand for references to" msgstr "" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2327726 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/uk/0000755000076600000240000000000014762512105023432 5ustar00felixstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2430942 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/uk/LC_MESSAGES/0000755000076600000240000000000014762512105025217 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/locale/uk/LC_MESSAGES/sphinx-codeautolink.po0000644000076600000240000000153314762511057031556 0ustar00felixstaff# Ukrainian translations for sphinx-codeautolink. # Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the sphinx-codeautolink project. # Yuriy Skalko , 2025. # msgid "" msgstr "" "Project-Id-Version: 1\n" "Report-Msgid-Bugs-To: yuriy.skalko@gmail.com\n" "POT-Creation-Date: 2025-02-24 17:30+0200\n" "PO-Revision-Date: 2025-02-24 18:42+0100\n" "Last-Translator: Yuriy Skalko \n" "Language: uk\n" "Language-Team: Yuriy Skalko \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" msgid "References to" msgstr "Посилання на" msgid "Expand for references to" msgstr "Розгорнути посилання на" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/parse.py0000644000076600000240000005412414762511057023253 0ustar00felixstaff"""Analyse AST of code blocks to determine used names and their sources.""" from __future__ import annotations import ast import builtins import sys from dataclasses import dataclass from enum import Enum from importlib import import_module from .warn import logger, warn_type HAS_MATCH = sys.version_info >= (3, 10) def parse_names(source: str, doctree_node) -> list[Name]: """Parse names from source.""" tree = ast.parse(source) visitor = ImportTrackerVisitor(doctree_node) visitor.visit(tree) return visitor.accessed def linenos(node: ast.AST) -> tuple[int, int]: """Return lineno and end_lineno safely.""" return node.lineno, getattr(node, "end_lineno", node.lineno) @dataclass class Component: """Name access component.""" name: str lineno: int end_lineno: int context: str # as in ast.Load / Store / Del @classmethod def from_ast(cls, node: ast.AST) -> Component: """Generate a Component from an AST node.""" context = "load" if isinstance(node, ast.Name): name = node.id context = node.ctx.__class__.__name__.lower() elif isinstance(node, ast.Attribute): name = node.attr context = node.ctx.__class__.__name__.lower() elif isinstance(node, ast.arg): name = node.arg elif isinstance(node, ast.Call): name = NameBreak.call elif isinstance(node, ast.NamedExpr): name = NameBreak.namedexpr elif HAS_MATCH and isinstance(node, ast.MatchAs): name = node.name context = "store" else: msg = f"Invalid AST for component: {node.__class__.__name__}" raise ValueError(msg) return cls(name, *linenos(node), context) @dataclass class PendingAccess: """Name access chain pending to be resolved in the scope and recorded.""" components: list[Component] @dataclass class Assignment: """ Representation of an assignment statement. - ordinarily one value to a single target - multiple targets when chain assigning (a = b = c) Target or value may be None if not trackable. """ targets: list[PendingAccess | None] value: PendingAccess | None class NameBreak(str, Enum): """ Elements that break name access chains. The symbols are arbitrary, only avoiding being valid Python names in order to not clash with actual named components in the chain. """ call = "()" namedexpr = ":=" import_from = ">" class LinkContext(str, Enum): """Context in which a link appears to help HTML matching.""" none = "none" after_call = "after_call" import_from = "import_from" # from *mod.sub* import foo import_target = "import_target" # from mod.sub import *foo* @dataclass class Name: """A name accessed in the source traced back to an import.""" import_components: list[str] code_str: str lineno: int end_lineno: int context: LinkContext | None = None resolved_location: str | None = None @dataclass class Access: """ Accessed import, whose tail is about to be recorded as a Name. :attr:`prior_components` are components that are implicitly used via the base name in :attr:`components`, which is the part that shows on the line. The base component that connects an import to the name that was used to access it is automatically removed from the components in :attr:`full_components`. """ context: LinkContext prior_components: list[Component] components: list[Component] @property def full_components(self) -> list[Component]: """All components from import base to used components.""" if not self.prior_components: # Import statement itself return self.components return self.prior_components + self.components[1:] @property def lineno_span(self) -> tuple[int, int]: """Estimate the lineno span of components.""" min_ = min(c.lineno for c in self.components) max_ = max(c.end_lineno for c in self.components) return min_, max_ def to_name(self) -> Name: """Convert access tail (from a break) into a Name.""" components = [ c.name for c in self.full_components if c.name not in (NameBreak.import_from, NameBreak.namedexpr) ] breaks = [i for i, c in enumerate(self.components) if c.name in list(NameBreak)] start_ix = breaks[-1] + 1 if breaks else 0 code_str = ".".join(c.name for c in self.components[start_ix:]) if breaks and self.components[breaks[-1]].name == NameBreak.call: context = LinkContext.after_call else: context = self.context return Name(components, code_str, *self.lineno_span, context=context) builtin_components: dict[str, list[Component]] = { b: [Component(b, -1, -1, LinkContext.none)] for b in dir(builtins) } class ImportTrackerVisitor(ast.NodeVisitor): """Track imports and their use through source code.""" def __init__(self, doctree_node) -> None: super().__init__() self.doctree_node = doctree_node self.accessed: list[Name] = [] self.in_augassign = False self._is_chaining = 0 # Stack for dealing with class body pseudo scopes # which are completely bypassed by inner scopes (func, lambda). # Current values are copied to the next class body level. self.pseudo_scopes_stack: list[dict[str, list[Component]]] = [ builtin_components.copy() ] # Stack for dealing with nested scopes. # Holds references to the values of previous nesting levels. self.outer_scopes_stack: list[dict[str, list[Component]]] = [] @property def current_locals(self): """Get the current local variables and their component chains.""" return self.pseudo_scopes_stack[-1] def save_accessed_names(self, access: Access) -> None: """Convert Access to Names to store in the visitor for aggregation.""" self.accessed.append(access.to_name()) def visit(self, node: ast.AST) -> PendingAccess | None: """ Override default visit to track name access and assignments. Only ast.Name and ast.Attribute form unbroken access chains. We make sure those are processed unbroken first. After reaching the end of the chain (or when encountering an assignment), the access chains and assignments are resolved. """ self._is_chaining, was_chaining = ( isinstance(node, ast.Name | ast.Attribute), self._is_chaining, ) result: Assignment | PendingAccess | None = super().visit(node) if isinstance(result, PendingAccess) and self._is_chaining and not was_chaining: result = self.resolve_pending_access(result) elif isinstance(result, Assignment): self.resolve_assignment(result) result = None self._is_chaining = was_chaining return result def overwrite_name(self, name: str) -> None: """Overwrite name in current scope.""" # Technically dotted values could now be bricked, # but we can't prevent the earlier values in the chain from being used. # There is a chance that the value which was assigned is a something # that we could follow, but for now it's not really worth the effort. # With a dotted value, the following condition will never hold as long # as the dotted components of imports are discarded on creating the import. self.current_locals.pop(name, None) def assign_name(self, name: str, components: list[Component]) -> None: """Import or assign a name to current scope.""" # Overwriting technically unnecessary until it properly follows dots self.overwrite_name(name) for c in components: c.context = "load" prev_comps = self.current_locals.get(components[0].name) if prev_comps: components = prev_comps + components[1:] self.current_locals[name] = components def _create_access(self, components: list[Component]) -> PendingAccess | None: """Create access from scope.""" scope_key = components[0].name prior = self.current_locals.get(scope_key) if prior is None: return None access = Access(LinkContext.none, prior, components) self.save_accessed_names(access) return PendingAccess(components=components) def resolve_pending_access(self, pending: PendingAccess) -> PendingAccess | None: """Resolve and save pending access.""" components = pending.components context = components[0].context if context == "store" and not self.in_augassign: self.overwrite_name(components[0].name) return pending access = self._create_access(components) if context == "del": self.overwrite_name(components[0].name) return access def resolve_assignment(self, assignment: Assignment) -> None: """Resolve access for assignment values and targets.""" for target in assignment.targets: self.resolve_single_assign_target(target, assignment.value) def resolve_single_assign_target( self, target: PendingAccess | None, value: PendingAccess | None ) -> None: """Assign value to a single assignment target.""" if target is None: return if len(target.components) == 1: comp = target.components[0] if value is None: self.overwrite_name(comp.name) else: self.assign_name(comp.name, value.components) self._create_access(target.components) else: self.resolve_pending_access(target) def create_simple_access(self, name: str, lineno: int) -> None: """Create single-component access to scope.""" component = Component(name, lineno, lineno, "load") self._create_access([component]) def visit_Global(self, node: ast.Global) -> None: """Import from top scope.""" if not self.outer_scopes_stack: return # in outermost scope already, no-op for imports imports = self.outer_scopes_stack[0] for name in node.names: self.overwrite_name(name) if name in imports: self.assign_name(name, imports[name]) self.create_simple_access(name, node.lineno) def visit_Nonlocal(self, node: ast.Nonlocal) -> None: """Import from intermediate scopes.""" imports_stack = self.outer_scopes_stack[1:] for name in node.names: self.overwrite_name(name) for imports in imports_stack[::-1]: if name in imports: self.assign_name(name, imports[name]) self.create_simple_access(name, node.lineno) break def visit_Import(self, node: ast.Import | ast.ImportFrom, prefix: str = "") -> None: """Register import source.""" import_star = node.names[0].name == "*" if import_star: try: mod = import_module(node.module) import_names = [ name for name in mod.__dict__ if not name.startswith("_") ] aliases = [None] * len(import_names) except ImportError: logger.warning( f"Could not import module `{node.module}` for parsing!", type=warn_type, subtype="import_star", location=self.doctree_node, ) import_names = [] aliases = [] else: import_names = [name.name for name in node.names] aliases = [name.asname for name in node.names] prefix_parts = prefix.rstrip(".").split(".") if prefix else [] prefix_components = [Component(n, *linenos(node), "load") for n in prefix_parts] if prefix_components: self.save_accessed_names( Access(LinkContext.import_from, [], prefix_components) ) prefix_components.append( Component(NameBreak.import_from, *linenos(node), "load") ) for import_name, alias in zip(import_names, aliases, strict=True): if not import_star: components = [ Component(n, *linenos(node), "load") for n in import_name.split(".") ] self.save_accessed_names( Access( LinkContext.import_target, [], prefix_components + components ) ) if not alias and "." in import_name: # equivalent to only import top level module since we don't # follow assignments and the outer modules also get imported import_name = import_name.split(".")[0] # noqa: PLW2901 full_components = [ Component(n, *linenos(node), "store") for n in (prefix + import_name).split(".") ] self.assign_name(alias or import_name, full_components) def visit_ImportFrom(self, node: ast.ImportFrom) -> None: """Register import source.""" if node.level: # relative import for name in node.names: self.overwrite_name(name.asname or name.name) else: self.visit_Import(node, prefix=node.module + ".") def visit_Name(self, node: ast.Name) -> PendingAccess: """Create the initial pending access chain.""" return PendingAccess([Component.from_ast(node)]) def visit_Attribute(self, node: ast.Attribute) -> PendingAccess | None: """Add attribute access to an existing access chain.""" inner: PendingAccess | None = self.visit(node.value) if inner is not None: inner.components.append(Component.from_ast(node)) return inner def visit_Call(self, node: ast.Call) -> PendingAccess | None: """Add call to an existing access chain and separately visit args.""" inner: PendingAccess | None = self.visit(node.func) if inner is not None: inner.components.append(Component.from_ast(node)) for arg in node.args + node.keywords: self.visit(arg) return inner def visit_Tuple(self, node: ast.Tuple) -> None: """Override stored values as tuple assignment is not supported.""" for element in node.elts: result = self.visit(element) if isinstance(node.ctx, ast.Store) and isinstance(result, PendingAccess): self.resolve_single_assign_target(result, None) def visit_Assign(self, node: ast.Assign) -> Assignment: """Visit an Assign node.""" value = self.visit(node.value) targets = [] for n in node.targets[::-1]: target = self.visit(n) targets.append(target) return Assignment(targets, value) def visit_AnnAssign(self, node: ast.AnnAssign) -> Assignment: """Visit an AnnAssign node.""" value = self.visit(node.value) if node.value is not None else None annot = self.visit(node.annotation) if annot is not None: annot.components.append( Component(NameBreak.call, *linenos(node.annotation), "load") ) value = annot target = self.visit(node.target) return Assignment([target], value) def visit_AugAssign(self, node: ast.AugAssign) -> None: """Visit an AugAssign node.""" self.visit(node.value) self.in_augassign, temp = (True, self.in_augassign) self.visit(node.target) self.in_augassign = temp def visit_NamedExpr(self, node: ast.NamedExpr) -> PendingAccess: """Visit a NamedExpr node.""" value = self.visit(node.value) target = self.visit(node.target) assign = Assignment([target], value) self.resolve_assignment(assign) if value is not None: value.components.append(Component.from_ast(node)) return value def visit_MatchClass(self, node: ast.AST) -> None: """Visit a match case class as a series of assignments.""" cls = self.visit(node.cls) pattern_accesses = [] for n in node.patterns: access = self.visit(n) if access is not None: pattern_accesses.append(access) kwd_accesses = [] for attr in node.kwd_attrs: if cls is None: kwd_accesses.append(None) continue attr_comps = [ Component(NameBreak.call, *linenos(node), "load"), Component(attr, *linenos(node), "load"), ] access = PendingAccess(cls.components + attr_comps) kwd_accesses.append(access) for access in pattern_accesses + kwd_accesses: if access is not None: self.resolve_pending_access(access) assigns = [] for access, pattern in zip(kwd_accesses, node.kwd_patterns, strict=True): target = self.visit(pattern) if cls is not None and target is not None: assigns.append(Assignment([target], access)) for assign in assigns: self.resolve_assignment(assign) def visit_MatchAs(self, node: ast.AST) -> PendingAccess | None: """Track match alias names.""" if node.name is None: return None return PendingAccess([Component.from_ast(node)]) def visit_AsyncFor(self, node: ast.AsyncFor) -> None: """Delegate to sync for.""" self.visit_For(node) def visit_For(self, node: ast.For | ast.AsyncFor) -> None: """Swap node order.""" self.visit(node.iter) self.visit(node.target) for n in node.body: self.visit(n) for n in node.orelse: self.visit(n) def visit_ClassDef(self, node: ast.ClassDef) -> None: """Handle pseudo scope of class body.""" for dec in node.decorator_list: self.visit(dec) for base in node.bases: self.visit(base) for kw in node.keywords: self.visit(kw) self.overwrite_name(node.name) self.pseudo_scopes_stack.append(self.pseudo_scopes_stack[0].copy()) for b in node.body: self.visit(b) self.pseudo_scopes_stack.pop() def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: """Delegate to func def.""" self.visit_FunctionDef(node) @staticmethod def _get_args(node: ast.arguments) -> list[ast.arg]: return node.args + node.kwonlyargs + node.posonlyargs def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: """Swap node order and separate inner scope.""" self.overwrite_name(node.name) for dec in node.decorator_list: self.visit(dec) for d in node.args.defaults + node.args.kw_defaults: if d is None: continue self.visit(d) args = self._get_args(node.args) args += [node.args.vararg, node.args.kwarg] inner = self.__class__(self.doctree_node) inner.pseudo_scopes_stack[0] = self.pseudo_scopes_stack[0].copy() inner.outer_scopes_stack = list(self.outer_scopes_stack) inner.outer_scopes_stack.append(self.pseudo_scopes_stack[0]) for arg in args: if arg is None: continue inner.visit(arg) if node.returns is not None: self.visit(node.returns) for n in node.body: inner.visit(n) self.accessed.extend(inner.accessed) def visit_arg(self, arg: ast.arg) -> Assignment: """Handle function argument and its annotation.""" target = PendingAccess([Component.from_ast(arg)]) if arg.annotation is not None: value = self.visit(arg.annotation) if value is not None: value.components.append( Component(NameBreak.call, *linenos(arg), "load") ) else: value = None return Assignment([target], value) def visit_Lambda(self, node: ast.Lambda) -> None: """Swap node order and separate inner scope.""" for d in node.args.defaults + node.args.kw_defaults: if d is None: continue self.visit(d) args = self._get_args(node.args) args += [node.args.vararg, node.args.kwarg] inner = self.__class__(self.doctree_node) inner.pseudo_scopes_stack[0] = self.pseudo_scopes_stack[0].copy() for arg in args: if arg is None: continue inner.overwrite_name(arg.arg) inner.visit(node.body) self.accessed.extend(inner.accessed) def visit_ListComp(self, node: ast.ListComp) -> None: """Delegate to generic comp.""" self.visit_generic_comp([node.elt], node.generators) def visit_SetComp(self, node: ast.SetComp) -> None: """Delegate to generic comp.""" self.visit_generic_comp([node.elt], node.generators) def visit_DictComp(self, node: ast.DictComp) -> None: """Delegate to generic comp.""" self.visit_generic_comp([node.key, node.value], node.generators) def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None: """Delegate to generic comp.""" self.visit_generic_comp([node.elt], node.generators) def visit_comprehension(self, node: ast.comprehension) -> None: """Swap node order.""" self.visit(node.iter) self.visit(node.target) for f in node.ifs: self.visit(f) def visit_generic_comp( self, values: list[ast.AST], generators: list[ast.comprehension] ) -> None: """Separate inner scope, respects class body scope.""" inner = self.__class__(self.doctree_node) inner.pseudo_scopes_stack[0] = self.current_locals.copy() for gen in generators: inner.visit(gen) for value in values: inner.visit(value) self.accessed.extend(inner.accessed) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2432582 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/static/0000755000076600000240000000000014762512105023043 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/static/sphinx-codeautolink.css0000644000076600000240000000035714762511057027557 0ustar00felixstaff.sphinx-codeautolink-a{ color: inherit; text-decoration: none; } .sphinx-codeautolink-a:link{ color: inherit; } .sphinx-codeautolink-a:visited{ color: inherit; } .sphinx-codeautolink-a:hover{ color: rgb(0, 139, 139); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink/warn.py0000644000076600000240000000021414740025011023061 0ustar00felixstaff"""Logging definitions.""" from sphinx.util.logging import getLogger logger = getLogger("sphinx_codeautolink") warn_type = "codeautolink" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1741329477.263767 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink.egg-info/0000755000076600000240000000000014762512105023246 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329477.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink.egg-info/PKG-INFO0000644000076600000240000001042714762512105024347 0ustar00felixstaffMetadata-Version: 2.2 Name: sphinx-codeautolink Version: 0.17.4 Summary: Automatic links from code examples to reference documentation. Author-email: Felix Hildén Maintainer-email: Felix Hildén License: MIT License Copyright (c) 2021-2025 Felix Hildén Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Project-URL: homepage, https://pypi.org/project/sphinx-codeautolink Project-URL: download, https://pypi.org/project/sphinx-codeautolink Project-URL: source, https://github.com/felix-hilden/sphinx-codeautolink Project-URL: issues, https://github.com/felix-hilden/sphinx-codeautolink/issues Project-URL: documentation, https://sphinx-codeautolink.rtfd.org Keywords: sphinx,extension,code,link Classifier: Development Status :: 4 - Beta Classifier: Framework :: Sphinx Classifier: Framework :: Sphinx :: Extension Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Classifier: Topic :: Software Development :: Documentation Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: sphinx>=3.2.0 Requires-Dist: beautifulsoup4>=4.8.1 Provides-Extra: ipython Requires-Dist: ipython!=8.7.0; extra == "ipython" sphinx-codeautolink =================== |pyversions| |downloads| |license| |readthedocs| sphinx-codeautolink makes code examples clickable by inserting links from individual code elements to the corresponding reference documentation. We aim for a minimal setup assuming your examples are already valid Python. For a live demo, see our online documentation on `Read The Docs `_. Installation ------------ sphinx-codeautolink can be installed from the following sources: .. code:: sh $ pip install sphinx-codeautolink $ conda install -c conda-forge sphinx-codeautolink Note that the library is in early development, so version pinning is advised. To enable sphinx-codeautolink, modify the extension list in ``conf.py``. Note that the extension name uses an underscore rather than a hyphen. .. code:: python extensions = [ ..., "sphinx_codeautolink", ] That's it! Now your code examples are linked. For ways of concatenating examples, setting default import statements, or customising link style among other things, see the `online documentation `_. .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sphinx-codeautolink :alt: Python versions .. |downloads| image:: https://img.shields.io/pypi/dm/sphinx-codeautolink :alt: Monthly downloads .. |license| image:: https://img.shields.io/badge/License-MIT-blue.svg :target: https://choosealicense.com/licenses/mit :alt: License: MIT .. |readthedocs| image:: https://rtfd.org/projects/sphinx-codeautolink/badge/?version=stable :target: https://sphinx-codeautolink.rtfd.org/en/stable/ :alt: Documentation ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329477.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink.egg-info/SOURCES.txt0000644000076600000240000001605714762512105025143 0ustar00felixstaffLICENSE MANIFEST.in contributing.rst pyproject.toml readme_pypi.rst tox.ini docs/requirements.txt docs/src/404.rst docs/src/about.rst docs/src/conf.py docs/src/example_library.rst docs/src/examples.rst docs/src/index.rst docs/src/lib.py docs/src/reference.rst docs/src/release_notes.rst docs/src/static/custom.css requirements/build requirements/dev requirements/docs requirements/extras requirements/tests src/sphinx_codeautolink/__init__.py src/sphinx_codeautolink/parse.py src/sphinx_codeautolink/warn.py src/sphinx_codeautolink.egg-info/PKG-INFO src/sphinx_codeautolink.egg-info/SOURCES.txt src/sphinx_codeautolink.egg-info/dependency_links.txt src/sphinx_codeautolink.egg-info/requires.txt src/sphinx_codeautolink.egg-info/top_level.txt src/sphinx_codeautolink/extension/__init__.py src/sphinx_codeautolink/extension/backref.py src/sphinx_codeautolink/extension/block.py src/sphinx_codeautolink/extension/cache.py src/sphinx_codeautolink/extension/directive.py src/sphinx_codeautolink/extension/resolve.py src/sphinx_codeautolink/extension/translation.py src/sphinx_codeautolink/locale/sphinx-codeautolink.pot src/sphinx_codeautolink/locale/de/LC_MESSAGES/sphinx-codeautolink.mo src/sphinx_codeautolink/locale/de/LC_MESSAGES/sphinx-codeautolink.po src/sphinx_codeautolink/locale/es/LC_MESSAGES/sphinx-codeautolink.mo src/sphinx_codeautolink/locale/es/LC_MESSAGES/sphinx-codeautolink.po src/sphinx_codeautolink/locale/fi/LC_MESSAGES/sphinx-codeautolink.mo src/sphinx_codeautolink/locale/fi/LC_MESSAGES/sphinx-codeautolink.po src/sphinx_codeautolink/locale/fr/LC_MESSAGES/sphinx-codeautolink.mo src/sphinx_codeautolink/locale/fr/LC_MESSAGES/sphinx-codeautolink.po src/sphinx_codeautolink/locale/it/LC_MESSAGES/sphinx-codeautolink.mo src/sphinx_codeautolink/locale/it/LC_MESSAGES/sphinx-codeautolink.po src/sphinx_codeautolink/locale/nl/LC_MESSAGES/sphinx-codeautolink.mo src/sphinx_codeautolink/locale/nl/LC_MESSAGES/sphinx-codeautolink.po src/sphinx_codeautolink/locale/ru/LC_MESSAGES/sphinx-codeautolink.po src/sphinx_codeautolink/locale/uk/LC_MESSAGES/sphinx-codeautolink.po src/sphinx_codeautolink/static/sphinx-codeautolink.css tests/__init__.py tests/extension/__init__.py tests/extension/_check.py tests/extension/fail/concat_invalid.txt tests/extension/fail/custom_block_import_invalid.txt tests/extension/fail/custom_block_syntax_error.txt tests/extension/fail/failed_resolve_invalid_location.txt tests/extension/fail/failed_resolve_no_module.txt tests/extension/fail/failed_resolve_return_type.txt tests/extension/fail/match_block_warning.txt tests/extension/fail/missing_inventory.txt tests/extension/fail/no_backreference.txt tests/extension/fail/no_module_docs.txt tests/extension/fail/preface_invalid_arg.txt tests/extension/fail/ref_import_star_invalid.txt tests/extension/fail/ref_invalid_block_type.txt tests/extension/fail/ref_invalid_syntax.txt tests/extension/fail/ref_invalid_syntax_complex.txt tests/extension/fail/ref_ipython_non-python.txt tests/extension/fail/skip_invalid.txt tests/extension/ref/class_mro_fix.txt tests/extension/ref/concat_default.txt tests/extension/ref/concat_off.txt tests/extension/ref/concat_on_across_sections.txt tests/extension/ref/concat_on_breaks.txt tests/extension/ref/concat_section_breaks.txt tests/extension/ref/concat_section_returns_to_global.txt tests/extension/ref/custom_block.txt tests/extension/ref/custom_block_imported.txt tests/extension/ref/empty_project.txt tests/extension/ref/inventory_map_is_needed.txt tests/extension/ref/inventory_map_not_needed.txt tests/extension/ref/lexer_bazel.txt tests/extension/ref/lexer_default.txt tests/extension/ref/lexer_default_fail.txt tests/extension/ref/lexer_doctest.txt tests/extension/ref/lexer_doctest_empty_input.txt tests/extension/ref/lexer_extdoctest_doctest.txt tests/extension/ref/lexer_extdoctest_setup.txt tests/extension/ref/lexer_extdoctest_setup_wrong.txt tests/extension/ref/lexer_extdoctest_testcode.txt tests/extension/ref/lexer_highlight_language.txt tests/extension/ref/lexer_highlight_none.txt tests/extension/ref/lexer_ipython.txt tests/extension/ref/lexer_ipython3.txt tests/extension/ref/lexer_ipython_directive.txt tests/extension/ref/lexer_ipython_directive_comment.txt tests/extension/ref/lexer_ipython_syntax.txt tests/extension/ref/lexer_literal.txt tests/extension/ref/lexer_non_python_block.txt tests/extension/ref/lexer_py.txt tests/extension/ref/lexer_py3.txt tests/extension/ref/lexer_pycon.txt tests/extension/ref/lexer_pycon_implicit.txt tests/extension/ref/lexer_pyi.txt tests/extension/ref/lexer_python3.txt tests/extension/ref/lexer_sage.txt tests/extension/ref/lexer_starlark.txt tests/extension/ref/line_numbers.txt tests/extension/ref/preface_concatenated.txt tests/extension/ref/preface_consumed_non_python.txt tests/extension/ref/preface_document.txt tests/extension/ref/preface_global.txt tests/extension/ref/preface_global_multiline.txt tests/extension/ref/preface_global_overwritten.txt tests/extension/ref/preface_multiline.txt tests/extension/ref/preface_multiline_and_arg.txt tests/extension/ref/preface_multiple.txt tests/extension/ref/preface_single.txt tests/extension/ref/ref_assign_targets.txt tests/extension/ref/ref_builtins.txt tests/extension/ref/ref_chain.txt tests/extension/ref/ref_chain_call.txt tests/extension/ref/ref_class_attr.txt tests/extension/ref/ref_class_meth_returns_self.txt tests/extension/ref/ref_class_type_attr.txt tests/extension/ref/ref_decorator.txt tests/extension/ref/ref_fluent_attrs.txt tests/extension/ref/ref_fluent_call.txt tests/extension/ref/ref_func_args.txt tests/extension/ref/ref_func_no_module.txt tests/extension/ref/ref_import_as.txt tests/extension/ref/ref_import_from.txt tests/extension/ref/ref_import_from_complex.txt tests/extension/ref/ref_import_multiple.txt tests/extension/ref/ref_import_multiple_libs.txt tests/extension/ref/ref_import_star.txt tests/extension/ref/ref_import_submodule.txt tests/extension/ref/ref_imported_func.txt tests/extension/ref/ref_imported_func_relocated.txt tests/extension/ref/ref_inherited.txt tests/extension/ref/ref_intersphinx_only.txt tests/extension/ref/ref_invalid_func.txt tests/extension/ref/ref_invalid_typehint.txt tests/extension/ref/ref_optional.txt tests/extension/ref/ref_optional_future.txt tests/extension/ref/ref_shadow_builtin.txt tests/extension/ref/ref_simple.txt tests/extension/ref/search_css_duplicates_removed.txt tests/extension/ref/skip_file.txt tests/extension/ref/skip_next.txt tests/extension/ref/skip_next_consumed_non_python.txt tests/extension/ref/skip_off.txt tests/extension/ref/skip_section.txt tests/extension/src/class_mro_fix.py tests/extension/src/future_project.py tests/extension/src/parser_func.py tests/extension/src/test_project/__init__.py tests/extension/src/test_project/sub.py tests/extension/table/tab_autodoc_empty.txt tests/extension/table/tab_autodoc_links.txt tests/extension/table/tab_manual_collapse.txt tests/extension/table/tab_manual_empty.txt tests/extension/table/tab_manual_links.txt tests/extension/table/tab_manual_links_subsection.txt tests/extension/table/tab_no_table.txt tests/parse/__init__.py tests/parse/_util.py tests/parse/assign.py tests/parse/chain.py tests/parse/match.py tests/parse/scope.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329477.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink.egg-info/dependency_links.txt0000644000076600000240000000000114762512105027314 0ustar00felixstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329477.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink.egg-info/requires.txt0000644000076600000240000000007614762512105025651 0ustar00felixstaffsphinx>=3.2.0 beautifulsoup4>=4.8.1 [ipython] ipython!=8.7.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329477.0 sphinx_codeautolink-0.17.4/src/sphinx_codeautolink.egg-info/top_level.txt0000644000076600000240000000002414762512105025774 0ustar00felixstaffsphinx_codeautolink ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2434444 sphinx_codeautolink-0.17.4/tests/0000755000076600000240000000000014762512105016055 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/__init__.py0000644000076600000240000000045514303102654020165 0ustar00felixstaffimport sphinx_codeautolink class TestPackage: def test_version(self): assert sphinx_codeautolink.__version__ def test_clean_pycon_public(self): assert sphinx_codeautolink.clean_pycon def test_clean_ipython_public(self): assert sphinx_codeautolink.clean_ipython ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2438138 sphinx_codeautolink-0.17.4/tests/extension/0000755000076600000240000000000014762512105020071 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329086.0 sphinx_codeautolink-0.17.4/tests/extension/__init__.py0000644000076600000240000002504614762511276022221 0ustar00felixstafffrom __future__ import annotations import re import sys from pathlib import Path from unittest.mock import patch import pytest from bs4 import BeautifulSoup from sphinx.cmd.build import main as sphinx_main from ._check import check_link_targets, check_reference_targets_exist # Insert test package root to path for all tests sys.path.insert(0, str(Path(__file__).parent / "src")) default_conf = """ extensions = [ "sphinx.ext.autodoc", "sphinx_codeautolink", ] autodoc_default_options = { "members": True, "undoc-members": True, } codeautolink_warn_on_missing_inventory = True codeautolink_warn_on_failed_resolve = True codeautolink_warn_on_no_backreference = False """ any_whitespace = re.compile(r"\s*") ref_tests = [(p.name, p) for p in Path(__file__).with_name("ref").glob("*.txt")] ref_xfails = {} def assert_links(file: Path, links: list): text = file.read_text("utf-8") soup = BeautifulSoup(text, "html.parser") blocks = list(soup.find_all("a", attrs={"class": "sphinx-codeautolink-a"})) strings = [any_whitespace.sub("", "".join(b.strings)) for b in blocks] assert len(strings) == len(links) for s, link in zip(strings, links, strict=False): assert s == link @pytest.mark.parametrize(("name", "file"), ref_tests) def test_references(name: str, file: Path, tmp_path: Path): """ Basic extension tests for reference building. The tests are structured as .txt files, parsed and executed here. The structure of the file is:: expected autolink link.targets # split lines to add to the default conf.py # split index.html content """ if ref_xfails.get(file.name, False): pytest.xfail("Expected to fail.") links, conf, index = file.read_text("utf-8").split("# split") links = links.strip().split("\n") if len(links) == 1 and not links[0]: links = [] files = {"conf.py": default_conf + conf, "index.rst": index} print(f"Building file {name}.") result_dir = _sphinx_build(tmp_path, "html", files) assert_links(result_dir / "index.html", links) assert check_link_targets(result_dir) == len(links) table_tests = list(Path(__file__).with_name("table").glob("*.txt")) @pytest.mark.parametrize("file", table_tests) def test_tables(file: Path, tmp_path: Path): """ Tests for backreference tables. The tests are structured as .txt files, parsed and executed here. The structure of the file is:: expected table link.targets # split lines to add to the default conf.py # split index.html content Note that the header of the table is also considered a link target. However, if the table is collapsible, the header is not a part of the table, so it should be omitted from the expected links. The processing also removes any whitespace, which should be taken into account. """ links, conf, index = file.read_text("utf-8").split("# split") links = links.strip().split("\n") if len(links) == 1 and not links[0]: links = [] files = {"conf.py": default_conf + conf, "index.rst": index} result_dir = _sphinx_build(tmp_path, "html", files) index_html = result_dir / "index.html" text = index_html.read_text("utf-8") soup = BeautifulSoup(text, "html.parser") blocks = list(soup.select("table a")) strings = [any_whitespace.sub("", "".join(b.strings)) for b in blocks] assert len(strings) == len(links) for s, link in zip(strings, links, strict=False): assert s == link fail_tests = list(Path(__file__).with_name("fail").glob("*.txt")) @pytest.mark.parametrize("file", fail_tests) def test_fails(file: Path, tmp_path: Path): """ Tests for failing builds. The tests are structured as .txt files, parsed and executed here. The structure of the file is:: lines to add to the default conf.py # split index.html content """ conf, index = file.read_text("utf-8").split("# split") files = {"conf.py": default_conf + conf, "index.rst": index} with pytest.raises(RuntimeError): _sphinx_build(tmp_path, "html", files) def test_non_html_build(tmp_path: Path): index = """ Test project ------------ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. autolink-examples:: test_project.bar """ files = {"conf.py": default_conf, "index.rst": index} _sphinx_build(tmp_path, "man", files) def test_build_twice_and_modify_one_file(tmp_path: Path): index = """ Test project ------------ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. toctree:: another """ another = """ Another ------- .. autolink-examples:: test_package.bar """ another2 = """ Another ------- But edited. .. autolink-examples:: test_package.bar """ files = {"conf.py": default_conf, "index.rst": index, "another.rst": another} _sphinx_build(tmp_path, "html", files) _sphinx_build(tmp_path, "html", {"another.rst": another2}) def test_build_twice_and_delete_one_file(tmp_path: Path): conf = default_conf + "\nsuppress_warnings = ['toc.not_readable']" index = """ Test project ------------ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. toctree:: another """ another = """ Another ------- .. autolink-examples:: test_project.bar """ files = {"conf.py": conf, "index.rst": index, "another.rst": another} _sphinx_build(tmp_path, "html", files) (tmp_path / "src" / "another.rst").unlink() _sphinx_build(tmp_path, "html", {}) def test_raise_unexpected(tmp_path: Path): index = """ Test project ------------ .. code:: python import test_project test_project.bar() .. automodule:: test_project """ files = {"conf.py": default_conf, "index.rst": index} def raise_msg(*_, **__): msg = "ValueError" raise ValueError(msg) def raise_nomsg(*_, **__): raise ValueError target = "sphinx_codeautolink.parse.ImportTrackerVisitor" with pytest.raises(RuntimeError), patch(target, raise_msg): _sphinx_build(tmp_path, "html", files) with pytest.raises(RuntimeError), patch(target, raise_nomsg): _sphinx_build(tmp_path, "html", files) def test_parallel_build(tmp_path: Path): index = """ Test project ------------ .. automodule:: test_project .. toctree:: """ template = """ {header} --- .. code:: python import test_project test_project.bar() """ links = ["test_project", "test_project.bar"] n_subfiles = 20 subfiles = { name: template.format(header=name) for name in (f"F{x}" for x in range(n_subfiles)) } index = index + "\n ".join(["", *list(subfiles)]) files = {"conf.py": default_conf, "index.rst": index} files.update({k + ".rst": v for k, v in subfiles.items()}) result_dir = _sphinx_build(tmp_path, "html", files, n_processes=4) for file in subfiles: assert_links(result_dir / (file + ".html"), links) assert check_link_targets(result_dir) == n_subfiles * len(links) def test_skip_identical_code(tmp_path: Path): """Code skipped and then linked in an identical block after.""" index = """ Test project ============ .. autolink-skip:: .. code:: python import test_project test_project.bar() .. code:: python import test_project test_project.bar() .. automodule:: test_project """ files = {"conf.py": default_conf, "index.rst": index} result_dir = _sphinx_build(tmp_path, "html", files) index_html = result_dir / "index.html" text = index_html.read_text("utf-8") soup = BeautifulSoup(text, "html.parser") blocks = list(soup.select(".highlight-python")) assert len(blocks) == 2 assert "sphinx-codeautolink-a" not in str(blocks[0]) assert "sphinx-codeautolink-a" in str(blocks[1]) def test_dirhtml_builder(tmp_path: Path): index = """ Test project ============ .. toctree:: :maxdepth: 2 page1/index page2 subdir/page3 Index Page Code --------------- .. code:: python import test_project test_project.bar() .. automodule:: test_project """ page = """ Page {idx} =========== .. code:: python import test_project test_project.bar() .. autolink-examples:: test_project.bar """ files = { "conf.py": default_conf, "index.rst": index, "page1/index.rst": page.format(idx=1), "page2.rst": page.format(idx=2), "subdir/page3.rst": page.format(idx=3), } links = ["test_project", "test_project.bar"] result_dir = _sphinx_build(tmp_path, "dirhtml", files) assert_links(result_dir / "index.html", links) assert_links(result_dir / "page1/index.html", links) assert_links(result_dir / "page2/index.html", links) assert_links(result_dir / "subdir/page3/index.html", links) assert check_link_targets(result_dir) == len(links) * 4 check_reference_targets_exist(result_dir) def test_html_subdir_reference(tmp_path: Path): index = """ Test project ============ .. toctree:: subdir/page1 subdir/subdir2/page2 Index Page ---------- .. code:: python import test_project test_project.bar() .. automodule:: test_project """ page = """ Page {idx} =========== .. code:: python import test_project test_project.bar() .. autolink-examples:: test_project.bar """ files = { "conf.py": default_conf, "index.rst": index, "subdir/page1.rst": page.format(idx=1), "subdir/subdir2/page2.rst": page.format(idx=2), } links = ["test_project", "test_project.bar"] result_dir = _sphinx_build(tmp_path, "html", files) assert_links(result_dir / "index.html", links) assert_links(result_dir / "subdir/page1.html", links) assert_links(result_dir / "subdir/subdir2/page2.html", links) assert check_link_targets(result_dir) == len(links) * 3 check_reference_targets_exist(result_dir) def _sphinx_build( folder: Path, builder: str, files: dict[str, str], n_processes: int | None = None ) -> Path: """Build Sphinx documentation and return result folder.""" src_dir = folder / "src" src_dir.mkdir(exist_ok=True) for name, content in files.items(): path = src_dir / name path.parent.mkdir(exist_ok=True, parents=True) path.write_text(content, "utf-8") build_dir = folder / "build" args = ["-M", builder, str(src_dir), str(build_dir), "-W"] if n_processes: args.extend(["-j", str(n_processes)]) ret_val = sphinx_main(args) if ret_val: msg = "Sphinx build failed!" raise RuntimeError(msg) return build_dir / builder ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741329086.0 sphinx_codeautolink-0.17.4/tests/extension/_check.py0000644000076600000240000000515314762511276021673 0ustar00felixstafffrom pathlib import Path import requests from bs4 import BeautifulSoup # Cache external pages for the duration of the runtime, # so that we don't request them multiple times needlessly sess = requests.Session() external_site_ids = {} def check_link_targets(root: Path) -> int: """Validate links in HTML site at root, return number of links found.""" site_docs = { p: BeautifulSoup(p.read_text("utf-8"), "html.parser") for p in root.glob("**/*.html") } site_ids = {k: gather_ids(v) for k, v in site_docs.items()} total = 0 for doc, soup in site_docs.items(): for link in soup.find_all("a", attrs={"class": "sphinx-codeautolink-a"}): base, id_ = link["href"].split("#") if any(base.startswith(s) for s in ("http://", "https://")): if base not in external_site_ids: sub_soup = BeautifulSoup(sess.get(base).text, "html.parser") external_site_ids[base] = gather_ids(sub_soup) ids = external_site_ids[base] else: target_path = (doc.parent / base).resolve() if target_path.is_dir(): target_path /= "index.html" assert target_path.exists(), ( f"Target path {target_path!s} not found while validating" f" link for `{link.string}` in {doc.relative_to(root)!s}!" ) ids = site_ids[target_path] assert id_ in ids, ( f"ID {id_} not found in {base} while validating link" f" for `{link.string}` in {doc.relative_to(root)!s}!" ) total += 1 return total def check_reference_targets_exist(root: Path): site_docs = { p: BeautifulSoup(p.read_text("utf-8"), "html.parser") for p in root.glob("**/*.html") } for doc, soup in site_docs.items(): for link in soup.find_all("a", attrs={"class": "reference internal"}): base = link["href"].split("#")[0] if any(base.startswith(s) for s in ("http://", "https://")): continue target_path = doc if base == "" else (doc.parent / base).resolve() if target_path.is_dir(): target_path /= "index.html" assert target_path.exists(), ( f"Target path {target_path!s} not found while validating" f" link for `{link.string}` in {doc.relative_to(root)!s}!" ) def gather_ids(soup: BeautifulSoup) -> set: """Gather all HTML IDs from a given page.""" return {tag["id"] for tag in soup.find_all(id=True)} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1741329477.246633 sphinx_codeautolink-0.17.4/tests/extension/fail/0000755000076600000240000000000014762512105021004 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/fail/concat_invalid.txt0000644000076600000240000000023714303102654024517 0ustar00felixstaff# split Test project ============ .. autolink-concat:: incorrect .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/fail/custom_block_import_invalid.txt0000644000076600000240000000032114303102654027320 0ustar00felixstaffcodeautolink_custom_blocks = {"python": "parser_func.not_a_function"} # split Test project ============ .. code:: python import test_project -*- test_project.bar() -*- .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/fail/custom_block_syntax_error.txt0000644000076600000240000000025614740025011027042 0ustar00felixstaffcodeautolink_custom_blocks = {"python": "parser_func.syntax_error"} # split Test project ============ .. code:: python import test_project .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/fail/failed_resolve_invalid_location.txt0000644000076600000240000000026714740025011030122 0ustar00felixstaffcodeautolink_warn_on_failed_resolve = True # split Test project ============ .. code:: python import test_project test_project.bar().not_an_attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/fail/failed_resolve_no_module.txt0000644000076600000240000000027114740025011026560 0ustar00felixstaffcodeautolink_warn_on_failed_resolve = True # split Test project ============ .. code:: python import test_project test_project.Bar().__eq__().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/fail/failed_resolve_return_type.txt0000644000076600000240000000027514740025011027163 0ustar00felixstaffcodeautolink_warn_on_failed_resolve = True # split Test project ============ .. code:: python import test_project test_project.optional_counter().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/fail/match_block_warning.txt0000644000076600000240000000027414740025011025532 0ustar00felixstaffcodeautolink_custom_blocks = {"python": "parser_func.manipulate_original_source"} # split Test project ============ .. code:: python import test_project .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/fail/missing_inventory.txt0000644000076600000240000000022014303102654025320 0ustar00felixstaffcodeautolink_warn_on_missing_inventory = True # split Test project ============ .. code:: python import test_project test_project.bar() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739622272.0 sphinx_codeautolink-0.17.4/tests/extension/fail/no_backreference.txt0000644000076600000240000000021714754103600025015 0ustar00felixstaffcodeautolink_warn_on_no_backreference = True # split Test project ============ .. code:: python import test_project test_project.bar() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/fail/no_module_docs.txt0000644000076600000240000000014214740025011024522 0ustar00felixstaff# split Test project ============ .. code:: python import test_project test_project.bar() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/fail/preface_invalid_arg.txt0000644000076600000240000000016514762511057025520 0ustar00felixstaff# split Test project ============ .. autolink-preface:: import lib :level: whatevs .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/fail/ref_import_star_invalid.txt0000644000076600000240000000016014740025011026436 0ustar00felixstaff# split Test project ============ .. code:: python from non_project import * .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/fail/ref_invalid_block_type.txt0000644000076600000240000000021614762511057026246 0ustar00felixstaff# split Test project ============ .. code:: python In[1]: import test_project ... test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/fail/ref_invalid_syntax.txt0000644000076600000240000000033414740025011025424 0ustar00felixstaff# split Test project ============ .. autolink-concat:: .. code:: python import a .. autolink-preface:: import b .. code:: python import test_project this is not python though .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/fail/ref_invalid_syntax_complex.txt0000644000076600000240000000050614762511057027172 0ustar00felixstaffcodeautolink_global_preface = "import test_project" # split Test project ============ .. autolink-preface:: import test_project :level: file .. autolink-concat:: .. code:: python test_project.bar() .. autolink-preface:: test_project.Foo() .. code:: python this is not python though .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/fail/ref_ipython_non-python.txt0000644000076600000240000000040214303102654026253 0ustar00felixstaffextensions.append('IPython.sphinxext.ipython_directive') # split Test project ============ .. ipython:: In [2]: alias bracket echo "Input in brackets <%l>" In [3]: bracket hello world Input in brackets: .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/fail/skip_invalid.txt0000644000076600000240000000023514303102654024214 0ustar00felixstaff# split Test project ============ .. autolink-skip:: incorrect .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2606392 sphinx_codeautolink-0.17.4/tests/extension/ref/0000755000076600000240000000000014762512105020645 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739624402.0 sphinx_codeautolink-0.17.4/tests/extension/ref/class_mro_fix.txt0000644000076600000240000000033614754107722024246 0ustar00felixstaffclass_mro_fix Parameter # split codeautolink_warn_on_missing_inventory = False # split Test project ============ .. code:: python from class_mro_fix import Parameter Parameter.empty .. automodule:: class_mro_fix ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/concat_default.txt0000644000076600000240000000052114303102654024352 0ustar00felixstafftest_project test_project.bar test_project.Foo test_project.Baz # split codeautolink_concat_default = True # split Test project ============ .. code:: python import test_project test_project.bar() .. code:: python test_project.Foo Subsection ---------- .. code:: python test_project.Baz .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/concat_off.txt0000644000076600000240000000037414303102654023506 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. autolink-concat:: .. code:: python import test_project test_project.bar() .. autolink-concat:: off .. code:: python test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/concat_on_across_sections.txt0000644000076600000240000000050614303102654026626 0ustar00felixstafftest_project test_project.bar test_project.Foo test_project.Baz # split # split Test project ============ .. autolink-concat:: on .. code:: python import test_project test_project.bar() .. code:: python test_project.Foo Subsection ---------- .. code:: python test_project.Baz .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/concat_on_breaks.txt0000644000076600000240000000037714303102654024702 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. autolink-concat:: on .. code:: python import test_project test_project.bar() .. autolink-concat:: on .. code:: python test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/concat_section_breaks.txt0000644000076600000240000000047314303102654025727 0ustar00felixstafftest_project test_project.bar test_project.Foo # split # split Test project ============ .. autolink-concat:: section .. code:: python import test_project test_project.bar() .. code:: python test_project.Foo Subsection ---------- .. code:: python test_project.Baz .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/concat_section_returns_to_global.txt0000644000076600000240000000063014303102654030177 0ustar00felixstafftest_project test_project.bar test_project.Foo test_project test_project.Baz # split # split Test project ============ .. autolink-concat:: .. autolink-concat:: section .. code:: python import test_project test_project.bar() .. code:: python test_project.Foo Subsection ---------- .. code:: python import test_project .. code:: python test_project.Baz .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/ref/custom_block.txt0000644000076600000240000000046714740025011024070 0ustar00felixstafftest_project test_project.bar # split def clean(s): return s, s.replace("-*-", "") codeautolink_custom_blocks = {"python": clean} suppress_warnings = ["config.cache"] # split Test project ============ .. code:: python import test_project -*- test_project.bar() -*- .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/ref/custom_block_imported.txt0000644000076600000240000000042314740025011025763 0ustar00felixstafftest_project test_project.bar # split codeautolink_custom_blocks = {"python": "parser_func.clean"} suppress_warnings = ["config.cache"] # split Test project ============ .. code:: python import test_project -*- test_project.bar() -*- .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/empty_project.txt0000644000076600000240000000006514303102654024266 0ustar00felixstaff# split # split Test project ============ Some text. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/ref/inventory_map_is_needed.txt0000644000076600000240000000057314740025011026273 0ustar00felixstafftest_project test_project.sub_return sub_attr # split codeautolink_inventory_map = { "test_project.sub.SubBar": "test_project.SubBar", "test_project.sub.SubBar.sub_attr": "test_project.SubBar.sub_attr", } # split Test project ============ .. code:: python import test_project test_project.sub_return().sub_attr .. automodule:: test_project :imported-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/ref/inventory_map_not_needed.txt0000644000076600000240000000041114740025011026447 0ustar00felixstafftest_project test_project.SubBar sub_bar test_project.sub_return # split # split Test project ============ .. code:: python import test_project test_project.SubBar() sub_bar = test_project.sub_return() .. automodule:: test_project :imported-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_bazel.txt0000644000076600000240000000025314754341711023706 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code-block:: bazel import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_default.txt0000644000076600000240000000024014754341711024231 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code:: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_default_fail.txt0000644000076600000240000000021214754341711025223 0ustar00felixstaff# split # split Test project ============ .. code:: invalid python in a default block should not crash .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_doctest.txt0000644000076600000240000000052414754341711024257 0ustar00felixstafftest_project test_project.bar attr test_project.Foo test_project.Foo # split # split Test project ============ >>> import test_project >>> test_project.bar().attr >>> def foo(): ... test_project.Foo this is an output line test_project.Baz ... test_project.Baz >>> test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_doctest_empty_input.txt0000644000076600000240000000032414762511057026713 0ustar00felixstaff# split # split Test project ============ >>> ast.dump(ast.parse("""\ ... for a in b: ... if a > 5: ... break ... else: ... continue ... ... """), indent=4) .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_extdoctest_doctest.txt0000644000076600000240000000066214762511057026531 0ustar00felixstafftest_project test_project.bar attr test_project.Foo test_project.Foo # split extensions.append("sphinx.ext.doctest") # split Test project ============ .. doctest:: >>> import test_project >>> test_project.bar().attr >>> def foo(): ... test_project.Foo this is an output line test_project.Baz ... test_project.Baz >>> test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_extdoctest_setup.txt0000644000076600000240000000066514762511057026227 0ustar00felixstafftest_project.bar tp.bar test.Foo # split extensions.append("sphinx.ext.doctest") # split Test project ============ .. testsetup:: import test_project .. doctest:: >>> test_project.bar() .. testsetup:: * .. doctest:: >>> test_project.Foo() .. testsetup:: thisgroup import test_project as tp import test_project as test .. doctest:: thisgroup >>> tp.bar() >>> test.Foo() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_extdoctest_setup_wrong.txt0000644000076600000240000000032114762511057027430 0ustar00felixstaff# split extensions.append("sphinx.ext.doctest") # split Test project ============ .. testsetup:: specificgroup import test_project .. doctest:: >>> test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_extdoctest_testcode.txt0000644000076600000240000000046614762511057026700 0ustar00felixstafftest_project test_project.bar attr test_project.Foo test_project.Foo # split extensions.append("sphinx.ext.doctest") # split Test project ============ .. testcode:: import test_project test_project.bar().attr def foo(): test_project.Foo test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_highlight_language.txt0000644000076600000240000000024714754341711026426 0ustar00felixstaff# split highlight_language = "none" # split Test project ============ Here's an example:: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_highlight_none.txt0000644000076600000240000000024014754341711025573 0ustar00felixstaff# split # split Test project ============ .. highlight:: none Here's an example:: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_ipython.txt0000644000076600000240000000077414754341711024313 0ustar00felixstafftest_project b test_project.bar test_project # split # split Test project ============ .. code:: ipython In [1]: %cd subdir In [2]: import test_project In [3]: b = test_project.bar() In [4]: 2 + 2 Out[4]: 4 In [5]: class A: ...: pass In [6]: In [7]: test_project Out[7]: multiline msg In [8]: "asd asd\n ...: this is output" asd asd ...: this is output In [9]: 2 + 2 Out[9]: asd asd ...: this is output .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_ipython3.txt0000644000076600000240000000026614754341711024372 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code:: ipython3 %cd subdir import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_ipython_directive.txt0000644000076600000240000000047214754341711026344 0ustar00felixstafftest_project b test_project.bar # split extensions.append('IPython.sphinxext.ipython_directive') # split Test project ============ .. ipython:: In [2]: import test_project In [3]: b = test_project.bar() In [4]: 2 + 2 Out[4]: 4 In [5]: class A: ...: pass .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_ipython_directive_comment.txt0000644000076600000240000000043614754341711030066 0ustar00felixstafftest_project b test_project.bar # split extensions.append('IPython.sphinxext.ipython_directive') # split Test project ============ .. ipython:: python # leading comment and empty line import test_project # comment b = test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_ipython_syntax.txt0000644000076600000240000000105414754341711025711 0ustar00felixstaff# split extensions.append('IPython.sphinxext.ipython_directive') # split Test project ============ .. ipython:: :verbatim: In [15]: !wc * 2 12 77 README.txt 40 97 884 buttons.py 26 90 712 check_buttons.py 19 52 416 cursor.py 180 404 4882 menu.py 16 45 337 multicursor.py 36 106 916 radio_buttons.py 48 226 2082 rectangle_selector.py 43 118 1063 slider_demo.py 40 124 1088 span_selector.py 450 1274 12457 total .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_literal.txt0000644000076600000240000000025114754341711024243 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ Here's an example:: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_non_python_block.txt0000644000076600000240000000020414754341711026152 0ustar00felixstaff# split # split Test project ============ .. code:: c import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_py.txt0000644000076600000240000000024214754341711023237 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code:: py import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_py3.txt0000644000076600000240000000025114754341711023322 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code-block:: py3 import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_pycon.txt0000644000076600000240000000060314754341711023740 0ustar00felixstafftest_project test_project.bar attr test_project.Foo test_project.Foo # split # split Test project ============ .. code:: pycon >>> import test_project >>> test_project.bar().attr >>> def foo(): ... test_project.Foo this is an output line test_project.Baz ... test_project.Baz >>> test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_pycon_implicit.txt0000644000076600000240000000060414762511057025634 0ustar00felixstafftest_project test_project.bar attr test_project.Foo test_project.Foo # split # split Test project ============ .. code:: python >>> import test_project >>> test_project.bar().attr >>> def foo(): ... test_project.Foo this is an output line test_project.Baz ... test_project.Baz >>> test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_pyi.txt0000644000076600000240000000025114754341711023410 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code-block:: pyi import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_python3.txt0000644000076600000240000000025514754341711024217 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code-block:: python3 import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_sage.txt0000644000076600000240000000025214754341711023527 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code-block:: sage import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/lexer_starlark.txt0000644000076600000240000000025614754341711024437 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code-block:: starlark import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/ref/line_numbers.txt0000644000076600000240000000027114740025011024057 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code-block:: python :linenos: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_concatenated.txt0000644000076600000240000000037114303102654025517 0ustar00felixstafftest_project.bar test_project.Foo # split # split Test project ============ .. autolink-concat:: .. autolink-preface:: import test_project .. code:: python test_project.bar() .. code:: python test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739703241.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_consumed_non_python.txt0000644000076600000240000000027514754341711027173 0ustar00felixstaff# split # split Test project ============ .. autolink-preface:: import test_project .. code:: c test_project.bar() .. code:: python test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_document.txt0000644000076600000240000000050714762511057024720 0ustar00felixstafftest_project.Foo test_project.bar # split # split Test project ============ .. autolink-preface:: import test_project :level: file .. code:: python test_project.Foo() .. code:: python test_project.bar() .. autolink-preface:: :level: file .. code:: python test_project.baz() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_global.txt0000644000076600000240000000033314303102654024325 0ustar00felixstafftest_project.bar test_project.Foo # split codeautolink_global_preface = "import test_project" # split Test project ============ .. code:: python test_project.bar() test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_global_multiline.txt0000644000076600000240000000033714303102654026413 0ustar00felixstafftest_project.bar Foo # split codeautolink_global_preface = "import test_project\nfrom test_project import Foo" # split Test project ============ .. code:: python test_project.bar() Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_global_overwritten.txt0000644000076600000240000000036514303102654027002 0ustar00felixstaff# split codeautolink_global_preface = "import test_project" # split Test project ============ .. autolink-concat:: .. code:: python test_project = 0 .. code:: python test_project.bar() test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_multiline.txt0000644000076600000240000000034114303102654025066 0ustar00felixstafftest_project.bar tp.bar # split # split Test project ============ .. autolink-preface:: import test_project import test_project as tp .. code:: python test_project.bar() tp.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_multiline_and_arg.txt0000644000076600000240000000033614303102654026545 0ustar00felixstafftest_project.bar tp.bar # split # split Test project ============ .. autolink-preface:: import test_project import test_project as tp .. code:: python test_project.bar() tp.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_multiple.txt0000644000076600000240000000035714303102654024726 0ustar00felixstafftest_project.bar tp.bar # split # split Test project ============ .. autolink-preface:: import test_project .. autolink-preface:: import test_project as tp .. code:: python test_project.bar() tp.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/preface_single.txt0000644000076600000240000000025414303102654024350 0ustar00felixstafftest_project.bar # split # split Test project ============ .. autolink-preface:: import test_project .. code:: python test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736583619.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_assign_targets.txt0000644000076600000240000000027314740424703025263 0ustar00felixstafftest_project bar target bar target # split # split Test project ============ .. code:: python from test_project import bar target = bar() target .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_builtins.txt0000644000076600000240000000041514303102654024066 0ustar00felixstaffprint RuntimeError # split extensions = [ "sphinx.ext.intersphinx", "sphinx_codeautolink", ] intersphinx_mapping = {"python": ('https://docs.python.org/3/', None)} # split Test project ============ .. code:: python print("Hi") raise RuntimeError("Bye") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_chain.txt0000644000076600000240000000026114303102654023316 0ustar00felixstafftest_project test_project.bar attr # split # split Test project ============ .. code:: python import test_project test_project.bar().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_chain_call.txt0000644000076600000240000000026214303102654024312 0ustar00felixstafftest_project test_project.Foo bute # split # split Test project ============ .. code:: python import test_project test_project.Foo()().bute .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_class_attr.txt0000644000076600000240000000050014303102654024367 0ustar00felixstafftest_project test_project.Foo attr test_project.Foo.attr test_project.Baz bute test_project.Baz.bute # split # split Test project ============ .. code:: python import test_project test_project.Foo().attr test_project.Foo.attr test_project.Baz().bute test_project.Baz.bute .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_class_meth_returns_self.txt0000644000076600000240000000031314303102654027147 0ustar00felixstafftest_project test_project.Foo selfref selfref # split # split Test project ============ .. code:: python import test_project test_project.Foo().selfref().selfref() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_class_type_attr.txt0000644000076600000240000000030614303102654025434 0ustar00felixstafftest_project test_project.Foo type_attr bute # split # split Test project ============ .. code:: python import test_project test_project.Foo().type_attr().bute .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_decorator.txt0000644000076600000240000000035314303102654024220 0ustar00felixstafftest_project @test_project.bar @test_project.bar # split # split Test project ============ .. code:: python import test_project @test_project.bar @test_project.bar() def test(): pass .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_fluent_attrs.txt0000644000076600000240000000043214303102654024746 0ustar00felixstafftest_project test_project.Foo.attr test_project.Foo.attr # split # split Test project ============ .. code:: python import test_project ( test_project .Foo .attr ) ( test_project. Foo. attr ) .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_fluent_call.txt0000644000076600000240000000051414303102654024525 0ustar00felixstafftest_project test_project.bar attr test_project.bar attr test_project.bar attr # split # split Test project ============ .. code:: python import test_project test_project.bar( ).attr ( test_project.bar() .attr ) ( test_project.bar() . attr ) .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736583758.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_func_args.txt0000644000076600000240000000051714740425116024215 0ustar00felixstafftest_project Foo arg Foo Foo test_project arg test_project.Foo test_project.Foo # split # split Test project ============ .. code:: python from test_project import Foo def f(arg: Foo = Foo()): pass import test_project def f(arg: test_project.Foo = test_project.Foo()): pass .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_func_no_module.txt0000644000076600000240000000043514740025011025227 0ustar00felixstaffdatetime datetime datetime.fromisoformat # split extensions.append("sphinx.ext.intersphinx") intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), } # split Test project ============ .. code:: python from datetime import datetime datetime.fromisoformat() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_import_as.txt0000644000076600000240000000027314303102654024234 0ustar00felixstafftest_project test_project Foo # split # split Test project ============ .. code:: python import test_project as tp from test_project import Foo as F .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_import_from.txt0000644000076600000240000000021414303102654024567 0ustar00felixstafftest_project bar # split # split Test project ============ .. code:: python from test_project import bar .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_import_from_complex.txt0000644000076600000240000000055314303102654026324 0ustar00felixstafftest_project Foo bar test_project Foo bar test_project Foo bar # split # split Test project ============ .. code:: python from test_project import ( Foo as F, bar , ) from test_project import ( Foo, bar as b, ) from test_project import ( Foo as F, bar) .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_import_multiple.txt0000644000076600000240000000033114303102654025457 0ustar00felixstafftest_project Foo bar test_project Foo bar # split # split Test project ============ .. code:: python from test_project import Foo, bar from test_project import Foo as F, bar as b .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_import_multiple_libs.txt0000644000076600000240000000041314740025011026465 0ustar00felixstaffjson math # split extensions = [ "sphinx.ext.intersphinx", "sphinx_codeautolink", ] intersphinx_mapping = {"python": ('https://docs.python.org/3/', None)} # split Test project ============ .. code:: python import json, math .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_import_star.txt0000644000076600000240000000020614303102654024576 0ustar00felixstafftest_project # split # split Test project ============ .. code:: python from test_project import * .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_import_submodule.txt0000644000076600000240000000034714303102654025632 0ustar00felixstafftest_project.sub test_project.sub subfoo # split # split Test project ============ .. code:: python import test_project.sub from test_project.sub import subfoo .. automodule:: test_project .. automodule:: test_project.sub ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_imported_func.txt0000644000076600000240000000026414303102654025075 0ustar00felixstafftest_project.subfoo # split # split Test project ============ .. autolink-preface:: import test_project .. code:: python test_project.subfoo .. automodule:: test_project.sub ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_imported_func_relocated.txt0000644000076600000240000000030014303102654027106 0ustar00felixstafftest_project test_project.subfoo # split # split Test project ============ .. code:: python import test_project test_project.subfoo .. automodule:: test_project :imported-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_inherited.txt0000644000076600000240000000055514303102654024215 0ustar00felixstafftest_project test_project.Child.meth test_project.Child meth test_project.Child.attr test_project.Child attr # split # split Test project ============ .. code:: python import test_project test_project.Child.meth() test_project.Child().meth() test_project.Child.attr test_project.Child().attr .. automodule:: test_project :no-inherited-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_intersphinx_only.txt0000644000076600000240000000040614303102654025651 0ustar00felixstaffnumpy np.arange # split extensions = [ "sphinx.ext.intersphinx", "sphinx_codeautolink", ] intersphinx_mapping = {"numpy": ("https://numpy.org/doc/stable/", None)} # split Test project ============ .. code:: python import numpy as np np.arange() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739622272.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_invalid_func.txt0000644000076600000240000000050314754103600024676 0ustar00felixstaffnumpy np.concatenate # split extensions = [ "sphinx.ext.intersphinx", "sphinx_codeautolink", ] intersphinx_mapping = {"numpy": ("https://numpy.org/doc/stable/", None)} codeautolink_warn_on_failed_resolve = False # split Test project ============ .. code:: python import numpy as np np.concatenate().mean() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739622272.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_invalid_typehint.txt0000644000076600000240000000036514754103600025615 0ustar00felixstafffuture_project future_project.invalid_ref # split codeautolink_warn_on_failed_resolve = False # split Test project ============ .. code:: python import future_project future_project.invalid_ref().whatever .. automodule:: future_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736589772.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_optional.txt0000644000076600000240000000040314740440714024066 0ustar00felixstafftest_project test_project.optional attr test_project.optional_manual attr # split # split Test project ============ .. code:: python import test_project test_project.optional().attr test_project.optional_manual().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736589772.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_optional_future.txt0000644000076600000240000000042114740440714025460 0ustar00felixstafffuture_project future_project.optional attr future_project.optional_manual attr # split # split Test project ============ .. code:: python import future_project future_project.optional().attr future_project.optional_manual().attr .. automodule:: future_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_shadow_builtin.txt0000644000076600000240000000037114303102654025251 0ustar00felixstafftest_project test_project compile test_project.compile compile # split # split Test project ============ .. code:: python import test_project from test_project import compile test_project.compile compile .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/ref_simple.txt0000644000076600000240000000024714303102654023531 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/search_css_duplicates_removed.txt0000644000076600000240000000035414303102654027456 0ustar00felixstafftest_project test_project.bar # split codeautolink_search_css_classes = ["highlight-python", "notranslate"] # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/skip_file.txt0000644000076600000240000000046714303102654023355 0ustar00felixstaff# split # split Test project ============ .. autolink-skip:: file .. code:: python import test_project test_project.bar() .. code:: python import test_project test_project.bar() Subsection ---------- .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/skip_next.txt0000644000076600000240000000037114303102654023406 0ustar00felixstafftest_project test_project.Foo # split # split Test project ============ .. autolink-skip:: .. code:: python import test_project test_project.bar() .. code:: python import test_project test_project.Foo() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/extension/ref/skip_next_consumed_non_python.txt0000644000076600000240000000036414762511057027572 0ustar00felixstafftest_project test_project.Foo # split # split Test project ============ .. autolink-skip:: .. code:: c import test_project test_project.bar() .. code:: python import test_project test_project.Foo() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/skip_off.txt0000644000076600000240000000061314303102654023201 0ustar00felixstafftest_project test_project.bar test_project test_project.bar # split # split Test project ============ .. autolink-skip:: file .. code:: python import test_project test_project.bar() .. autolink-skip:: off .. code:: python import test_project test_project.bar() Subsection ---------- .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/ref/skip_section.txt0000644000076600000240000000053014303102654024071 0ustar00felixstafftest_project test_project.bar # split # split Test project ============ .. autolink-skip:: section .. code:: python import test_project test_project.bar() .. code:: python import test_project test_project.bar() Subsection ---------- .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2610781 sphinx_codeautolink-0.17.4/tests/extension/src/0000755000076600000240000000000014762512105020660 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739624500.0 sphinx_codeautolink-0.17.4/tests/extension/src/class_mro_fix.py0000644000076600000240000000025114754110064024057 0ustar00felixstaff# noqa: INP001 # Test an edge case that happens in the inspect module, see #165 for details class _empty: # noqa: N801 pass class Parameter: empty = _empty ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739622272.0 sphinx_codeautolink-0.17.4/tests/extension/src/future_project.py0000644000076600000240000000063014754103600024266 0ustar00felixstaff# noqa: INP001 from __future__ import annotations from typing import Optional class Foo: """Foo test class.""" attr: str = "test" def optional() -> Optional[Foo]: # noqa: UP007 """Return optional type.""" def optional_manual() -> None | Foo: """Return manually constructed optional type.""" def invalid_ref() -> NotAClass: # noqa: F821 """Reference to a nonexistent class.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736498541.0 sphinx_codeautolink-0.17.4/tests/extension/src/parser_func.py0000644000076600000240000000032114740156555023546 0ustar00felixstaff# noqa: INP001 def clean(s): """Custom parser for tests.""" return s, s.replace("-*-", "") def syntax_error(_): raise SyntaxError def manipulate_original_source(s): return s + "\na = 1", s ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1741329477.261395 sphinx_codeautolink-0.17.4/tests/extension/src/test_project/0000755000076600000240000000000014762512105023365 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736589772.0 sphinx_codeautolink-0.17.4/tests/extension/src/test_project/__init__.py0000644000076600000240000000154714740440714025507 0ustar00felixstaff"""Docstring.""" from .sub import SubBar, subfoo # noqa: F401 class Baz: """Baz test class.""" bute = 1 class Foo: """Foo test class.""" attr: str = "test" type_attr = Baz def meth(self) -> Baz: """Test method.""" def selfref(self) -> "Foo": """Return self.""" def __call__(self) -> Baz: """Test call.""" def bar() -> Foo: """Bar test function.""" def optional() -> Foo | None: """Return optional type.""" def optional_manual() -> None | Foo: """Return manually constructed optional type.""" def optional_counter() -> Foo | Baz: """Failing case for incorrect optional type handling.""" def compile(): # noqa: A001 """Shadows built in compile function.""" class Child(Foo): """Foo child class.""" def sub_return() -> SubBar: """Returns a type in a submodule.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/src/test_project/sub.py0000644000076600000240000000025014740025011024514 0ustar00felixstaffdef subfoo(): """Function in submodule.""" class SubBar: """Class in submodule to be imported and documented in the main module.""" sub_attr: str = "yo" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2624798 sphinx_codeautolink-0.17.4/tests/extension/table/0000755000076600000240000000000014762512105021160 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/table/tab_autodoc_empty.txt0000644000076600000240000000032414740025011025411 0ustar00felixstaffTestproject # split codeautolink_autodoc_inject = True # split Test project ============ .. code:: python import test_project .. automodule:: test_project :no-members: .. autofunction:: test_project.bar ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/extension/table/tab_autodoc_links.txt0000644000076600000240000000030114740025011025366 0ustar00felixstaffTestproject Testproject # split codeautolink_autodoc_inject = True # split Test project ============ .. code:: python import test_project test_project.bar .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/table/tab_manual_collapse.txt0000644000076600000240000000031214303102654025675 0ustar00felixstaffTestproject # split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. autolink-examples:: test_project.bar :collapse: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/table/tab_manual_empty.txt0000644000076600000240000000026014303102654025233 0ustar00felixstaff# split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. autolink-examples:: test_project.Foo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/table/tab_manual_links.txt0000644000076600000240000000031514303102654025216 0ustar00felixstafftest_project.bar Testproject # split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. autolink-examples:: test_project.bar ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/table/tab_manual_links_subsection.txt0000644000076600000240000000040214303102654027451 0ustar00felixstafftest_project.bar Testproject/Subsection # split # split Test project ============ Subsection ---------- .. code:: python import test_project test_project.bar() Reference --------- .. automodule:: test_project .. autolink-examples:: test_project.bar ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1661765036.0 sphinx_codeautolink-0.17.4/tests/extension/table/tab_no_table.txt0000644000076600000240000000021014303102654024316 0ustar00felixstaff# split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741329477.2635095 sphinx_codeautolink-0.17.4/tests/parse/0000755000076600000240000000000014762512105017167 5ustar00felixstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/parse/__init__.py0000644000076600000240000001140414762511057021305 0ustar00felixstaffimport pytest from sphinx_codeautolink.parse import Component from ._util import refs_equal class TestUnit: def test_component_from_unrecognised_ast(self): with pytest.raises(ValueError, match="Invalid AST"): Component.from_ast("not ast") class TestSimple: @refs_equal def test_empty_source(self): return "", [] @refs_equal def test_no_imports(self): return "1\na = 2\nb()", [] @refs_equal def test_builtins(self): s = "print()" refs = [("print", "print")] return s, refs @pytest.mark.xfail(reason="Magics are currently not tracked.") @refs_equal def test_magics(self): s = "__file__" refs = [("__file__", "__file__")] return s, refs @refs_equal def test_import_from(self): s = "from lib import a" refs = [("lib", "lib"), ("lib.a", "a")] return s, refs @refs_equal def test_import_from_as(self): s = "from lib import a as b" refs = [("lib", "lib"), ("lib.a", "a")] return s, refs @refs_equal def test_import_from_multiline(self): s = "from lib import (\n a,\n b,\n)" refs = [("lib", "lib"), ("lib.a", "a"), ("lib.b", "b")] return s, refs @refs_equal def test_import_from_as_multiline(self): s = "from lib import (\n a as b,\n c as d,\n)" refs = [("lib", "lib"), ("lib.a", "a"), ("lib.c", "c")] return s, refs @refs_equal def test_simple_import_then_access(self): s = "import lib\nlib" refs = [("lib", "lib"), ("lib", "lib")] return s, refs @refs_equal def test_inside_list_literal(self): s = "import lib\n[lib]" refs = [("lib", "lib"), ("lib", "lib")] return s, refs @refs_equal def test_inside_subscript(self): s = "import lib\n0[lib]" refs = [("lib", "lib"), ("lib", "lib")] return s, refs @refs_equal def test_outside_subscript(self): s = "import lib\nlib[0]" refs = [("lib", "lib"), ("lib", "lib")] return s, refs @refs_equal def test_simple_import_then_attrib(self): s = "import lib\nlib.attr" refs = [("lib", "lib"), ("lib.attr", "lib.attr")] return s, refs @refs_equal def test_subscript_then_attrib_not_linked(self): s = "import a\na[b].c" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_subscript_then_attrib_then_call_not_linked(self): s = "import a\na[b].c()" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_import_as_then_attrib(self): s = "import lib as b\nb.attr" refs = [("lib", "lib"), ("lib.attr", "b.attr")] return s, refs @refs_equal def test_import_from_then_attrib(self): s = "from lib import a\na.attr" refs = [("lib", "lib"), ("lib.a", "a"), ("lib.a.attr", "a.attr")] return s, refs @refs_equal def test_import_from_as_then_attrib(self): s = "from lib import a as b\nb.attr" refs = [("lib", "lib"), ("lib.a", "a"), ("lib.a.attr", "b.attr")] return s, refs @refs_equal def test_dotted_import(self): s = "import a.b\na.b" refs = [("a.b", "a.b"), ("a.b", "a.b")] return s, refs @refs_equal def test_dotted_import_then_only_part(self): s = "import a.b\na" refs = [("a.b", "a.b"), ("a", "a")] return s, refs @refs_equal def test_dotted_import_then_attrib(self): s = "import a.b\na.b.c" refs = [("a.b", "a.b"), ("a.b.c", "a.b.c")] return s, refs @refs_equal def test_dotted_import_then_call_attrib(self): s = "import a.b\na.b().c" refs = [("a.b", "a.b"), ("a.b", "a.b"), ("a.b.().c", "c")] return s, refs @refs_equal def test_relative_import_is_noop(self): s = "from .a import b\nb" refs = [] return s, refs @refs_equal def test_del_removes_import(self): s = "import a\ndel a\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_del_dotted_removes_only_part(self): s = "import a.b\ndel a.b\na" refs = [("a.b", "a.b"), ("a.b", "a.b"), ("a", "a")] return s, refs @pytest.mark.xfail(reason="Assignments to imports are not tracked.") @refs_equal def test_overwrite_dotted_not_tracked(self): s = "import a.b\na.b = 1\na.b.c" refs = [("a.b", "a.b")] return s, refs @refs_equal def test_import_star(self): s = "from sphinx_codeautolink import *\nsetup" refs = [ ("sphinx_codeautolink", "sphinx_codeautolink"), ("sphinx_codeautolink.setup", "setup"), ] return s, refs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/parse/_util.py0000644000076600000240000000254714762511057020672 0ustar00felixstaffimport sys from functools import wraps import pytest from sphinx_codeautolink.parse import parse_names skip_type_union = pytest.mark.skipif( sys.version_info < (3, 10), reason="Type union introduced in Python 3.10." ) skip_match = pytest.mark.skipif( sys.version_info < (3, 10), reason="Match introduced in Python 3.10." ) def refs_equal(func): @wraps(func) def wrapper(self): source, expected = func(self) names = parse_names(source, doctree_node=None) names = sorted(names, key=lambda name: name.lineno) print("Source:\n" + source) print("\nExpected names:") for components, code_str in expected: print(f"components={components}, code_str={code_str}") print("\nParsed names:") for n in names: print( f"components={'.'.join(n.import_components)}, code_str={n.code_str}," f"lines=({n.lineno}, {n.end_lineno})" ) for n, e in zip(names, expected, strict=True): s = ".".join(c for c in n.import_components) assert s == e[0], f"Wrong import! Expected\n{e}\ngot\n{n}" assert n.code_str == e[1], f"Wrong code str! Expected\n{e}\ngot\n{n}" msg = f"Wrong number of nodes! Expected {len(expected)}, got {len(names)}" assert len(names) == len(expected), msg return wrapper ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tests/parse/assign.py0000644000076600000240000002321714762511057021037 0ustar00felixstaffimport pytest from ._util import refs_equal class TestAssign: @refs_equal def test_assign_before_import(self): s = "a = 1\nimport a\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_assign_after_import(self): s = "import a\na = 1\na" refs = [("a", "a")] return s, refs @refs_equal def test_assign_to_other_name_linked(self): s = "import a\nb = a" refs = [("a", "a"), ("a", "a"), ("a", "b")] return s, refs @refs_equal def test_assign_uses_and_assigns_imported(self): s = "import a\na = a\na" refs = [("a", "a"), ("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_unpack_assign_starred(self): s = "*a, b = c" refs = [] return s, refs @refs_equal def test_unpack_assign_uses_and_overwrites(self): s = "import a\na, b = a\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_multilevel_unpack_assign_uses_and_overwrites(self): s = "import a\n(a, b), c = a\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_multitarget_assign_to_different_names(self): s = "import a\nc = b = a" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a", "c")] return s, refs @refs_equal def test_multitarget_assign_uses_and_overwrites(self): s = "import a\na = b = a\na, b" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a", "a"), ("a", "a"), ("a", "b")] return s, refs @refs_equal def test_multitarget_assign_overwrites_twice(self): s = "import a\na = a = a" refs = [("a", "a"), ("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_multitarget_assign_overwrites_twice_with_attribute(self): s = "import a\na = a = a.b" refs = [("a", "a"), ("a.b", "a.b"), ("a.b", "a"), ("a.b", "a")] return s, refs @refs_equal def test_multitarget_assign_with_unpacking_first_assigns_to_other(self): s = "import a\n(c, d) = b = a" refs = [("a", "a"), ("a", "a"), ("a", "b")] return s, refs @refs_equal def test_multitarget_assign_with_unpacking_last_assigns_to_other(self): s = "import a\nd = (b, c) = a" refs = [("a", "a"), ("a", "a"), ("a", "d")] return s, refs @refs_equal def test_assign_uses_and_assigns_modified_imported(self): s = "import a\na = a + 1\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_assign_subscript_uses(self): s = "import a\na.b[0] = 1" refs = [("a", "a"), ("a.b", "a.b")] return s, refs @refs_equal def test_assign_call_subscript_uses(self): s = "import a\na.b()[0] = 1" refs = [("a", "a"), ("a.b", "a.b")] return s, refs @refs_equal def test_augassign_uses_imported(self): s = "import a\na += 1\na" refs = [("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_augassign_uses_and_assigns_imported(self): s = "import a\na += a\na" refs = [("a", "a"), ("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_annassign_no_links(self): s = "import a\na: 1 = 1\na" refs = [("a", "a")] return s, refs @refs_equal def test_annassign_overwrites_imported(self): s = "import a\na: b = 1\na" refs = [("a", "a")] return s, refs @refs_equal def test_annassign_uses_and_assigns_imported(self): s = "import a\nb: 1 = a\nb.c" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a.c", "b.c")] return s, refs @refs_equal def test_annassign_uses_and_annotates_imported(self): s = "import a\nb: a = 1\nb.c" refs = [("a", "a"), ("a", "a"), ("a.()", "b"), ("a.().c", "b.c")] return s, refs @refs_equal def test_annassign_prioritises_annotation(self): s = "import a, b\nc: a = b\nc.d" # note that AnnAssign is executed from value -> annot -> target refs = [ ("a", "a"), ("b", "b"), ("b", "b"), ("a", "a"), ("a.()", "c"), ("a.().d", "c.d"), ] return s, refs @refs_equal def test_annassign_why_would_anyone_do_this(self): s = "import a\na: a = a\na.b" refs = [("a", "a"), ("a", "a"), ("a", "a"), ("a.()", "a"), ("a.().b", "a.b")] return s, refs @refs_equal def test_annassign_without_value_overrides_annotation_but_not_linked(self): # note that this is different from runtime behavior # which does not overwrite the variable value s = "import a\na: b\na" refs = [("a", "a")] return s, refs @refs_equal def test_walrus_uses_imported(self): s = "import a\n(a := 1)\na" refs = [("a", "a")] return s, refs @refs_equal def test_walrus_uses_and_assigns_imported(self): s = "import a\n(a := a)\na" refs = [("a", "a"), ("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_walrus_uses_and_assigns_modified_imported(self): s = "import a\n(a := a + 1)\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_nested_walrus_statements(self): s = "import a\n(c := (b := a))" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a", "c")] return s, refs @refs_equal def test_walrus_result_assigned(self): s = "import a\nc = (b := a)" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a", "c")] return s, refs @refs_equal def test_walrus_expr_used_in_attribute(self): s = "import a\n(b := a).c" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a.c", "c")] return s, refs @refs_equal def test_walrus_expr_used_in_call(self): s = "import a\n(b := a)().c" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a.().c", "c")] return s, refs @refs_equal def test_dotted_import_overwrites_all_components(self): s = "class a:\n b = 1\nimport a.b\na.b" refs = [("a.b", "a.b"), ("a.b", "a.b")] return s, refs @pytest.mark.xfail(reason="Following assigns into imports would be a pain.") @refs_equal def test_partially_overwrite_dotted_import(self): s = "import a.b.c\na.b = 1\na\na.b\na.b.c" refs = [("a.b.c", "a.b.c"), ("a", "a")] return s, refs class TestFollowAssignment: @refs_equal def test_follow_simple_assign(self): s = "import a\nb = a\nb" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a", "b")] return s, refs @refs_equal def test_follow_double_assign(self): s = "import a\nb = a\nc = b\nc" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a", "b"), ("a", "c"), ("a", "c")] return s, refs @refs_equal def test_follow_simple_assign_attr(self): s = "import a\nb = a\nb.c" refs = [("a", "a"), ("a", "a"), ("a", "b"), ("a.c", "b.c")] return s, refs @refs_equal def test_follow_attr_assign(self): s = "import a\nc = a.b\nc" refs = [("a", "a"), ("a.b", "a.b"), ("a.b", "c"), ("a.b", "c")] return s, refs @refs_equal def test_follow_attr_assign_attr(self): s = "import a\nc = a.b\nc.d" refs = [("a", "a"), ("a.b", "a.b"), ("a.b", "c"), ("a.b.d", "c.d")] return s, refs @refs_equal def test_follow_attr_call_assign_attr(self): s = "import a\nc = a.b()\nc.d" refs = [("a", "a"), ("a.b", "a.b"), ("a.b.()", "c"), ("a.b.().d", "c.d")] return s, refs @refs_equal def test_follow_attr_call_assign_attr_call(self): s = "import a\nc = a.b()\nc.d()" refs = [("a", "a"), ("a.b", "a.b"), ("a.b.()", "c"), ("a.b.().d", "c.d")] return s, refs @refs_equal def test_follow_through_two_complex_assignments(self): s = "import a\nd = a.b().c\nf = d().e" refs = [ ("a", "a"), ("a.b", "a.b"), ("a.b.().c", "c"), ("a.b.().c", "d"), ("a.b.().c", "d"), ("a.b.().c.().e", "e"), ("a.b.().c.().e", "f"), ] return s, refs class TestAssignLike: @refs_equal def test_with_uses_imported(self): s = "import a\nwith a as b:\n a" refs = [("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_with_overwrites_imported(self): s = "import a\nwith 1 as a:\n a" refs = [("a", "a")] return s, refs @refs_equal def test_with_uses_and_overwrites_imported(self): s = "import a\nwith a as a:\n a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_for_uses_imported(self): s = "import a\nfor b in a:\n a" refs = [("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_for_overwrites_imported(self): s = "import a\nfor a in b:\n a" refs = [("a", "a")] return s, refs @refs_equal def test_for_uses_and_overwrites_imported(self): s = "import a\nfor a in a:\n a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_async_for_uses(self): s = "import a\nasync def f():\n async for b in a:\n a" refs = [("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_for_else_uses(self): s = "import a\nfor b in c:\n pass\nelse:\n a" refs = [("a", "a"), ("a", "a")] return s, refs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/parse/chain.py0000644000076600000240000000131114740025011020606 0ustar00felixstafffrom ._util import refs_equal class TestChain: @refs_equal def test_attr_call_doesnt_contain_call(self): s = "import a\na.attr()" refs = [("a", "a"), ("a.attr", "a.attr")] return s, refs @refs_equal def test_attr_call_attr_split_in_two(self): s = "import a\na.attr().b" refs = [("a", "a"), ("a.attr", "a.attr"), ("a.attr.().b", "b")] return s, refs @refs_equal def test_attr_call_attr_call_attr_split_in_three(self): s = "import a\na.attr().b().c" refs = [ ("a", "a"), ("a.attr", "a.attr"), ("a.attr.().b", "b"), ("a.attr.().b.().c", "c"), ] return s, refs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/parse/match.py0000644000076600000240000000771314740025011020634 0ustar00felixstaffimport pytest from ._util import refs_equal, skip_match @skip_match class TestMatch: @refs_equal def test_match_link_nothing(self): s = "match a:\n case b(c, d=e):\n pass" refs = [] return s, refs @refs_equal def test_match_target_linked(self): s = "import a\nmatch a:\n case _:\n pass" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_case_class_linked(self): s = "import a\nmatch _:\n case a.C():\n pass" refs = [("a", "a"), ("a.C", "a.C")] return s, refs @refs_equal def test_case_inner_attribute_linked(self): s = "import a\nmatch _:\n case a.C(attr=0):\n pass" refs = [("a", "a"), ("a.C", "a.C"), ("a.C.().attr", "attr")] return s, refs @refs_equal def test_case_inner_pattern_linked(self): s = "import a\nmatch _:\n case C(a.b):\n pass" refs = [("a", "a"), ("a.b", "a.b")] return s, refs @refs_equal def test_case_inner_kw_pattern_linked(self): s = "import a\nmatch _:\n case C(attr=a.b):\n pass" refs = [("a", "a"), ("a.b", "a.b")] return s, refs @refs_equal def test_case_inner_kw_pattern_class_linked(self): s = "import a\nmatch _:\n case C(attr=a.C()):\n pass" refs = [("a", "a"), ("a.C", "a.C")] return s, refs @refs_equal def test_case_inner_pattern_with_single_name_overrides(self): s = "import a\nmatch _:\n case C(a):\n a" refs = [("a", "a")] return s, refs @refs_equal def test_case_inner_pattern_overrides_but_able_to_use_simultaneously(self): s = "import a\nmatch _:\n case C(a, a.b):\n a" refs = [("a", "a"), ("a.b", "a.b")] return s, refs @refs_equal def test_case_nested_class_linked(self): s = "import a\nmatch _:\n case C(attr=[a.D()]):\n pass" refs = [("a", "a"), ("a.D", "a.D")] return s, refs @refs_equal def test_case_class_attr_target_linked(self): s = "import a\nmatch _:\n case a.C(attr=x):\n pass" refs = [ ("a", "a"), ("a.C", "a.C"), ("a.C.().attr", "attr"), ("a.C.().attr", "x"), ] return s, refs @refs_equal def test_case_kw_pattern_overrides(self): s = "import a\nmatch _:\n case a.C(attr=a):\n a" refs = [ ("a", "a"), ("a.C", "a.C"), ("a.C.().attr", "attr"), ("a.C.().attr", "a"), ("a.C.().attr", "a"), ] return s, refs @pytest.mark.xfail(reason="Match overriding not implemented.") @refs_equal def test_case_pattern_overrides_but_able_to_use_simultaneously(self): s = "import a\nmatch _:\n case a.C(a, attr=a.b):\n a" refs = [("a", "a"), ("a.C", "a.C"), ("a.C.().attr", "attr"), ("a.b", "a.b")] return s, refs @pytest.mark.xfail(reason="Match overriding not implemented.") @refs_equal def test_case_kw_pattern_overrides_but_able_to_use_simultaneously(self): s = "import a\nmatch _:\n case a.C(attr=a, bttr=a.b):\n pass" refs = [ ("a", "a"), ("a.C", "a.C"), ("a.C.().attr", "attr"), ("a.C.().attr", "a"), ("a.C.().bttr", "bttr"), ("a.b", "a.b"), ] return s, refs @refs_equal def test_case_nested_patterns_override_but_able_to_use_simultaneously(self): s = "import a\nmatch _:\n case a.B(a, a.C(a, a.d, a), a):\n a" refs = [("a", "a"), ("a.B", "a.B"), ("a.C", "a.C"), ("a.d", "a.d")] return s, refs @refs_equal def test_case_nested_class_attr_target_linked(self): s = "import a\nmatch _:\n case C(attr=[a.C(attr=x)]):\n pass" refs = [ ("a", "a"), ("a.C", "a.C"), ("a.C.().attr", "attr"), ("a.C.().attr", "x"), ] return s, refs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736452617.0 sphinx_codeautolink-0.17.4/tests/parse/scope.py0000644000076600000240000002405414740025011020646 0ustar00felixstaffimport pytest from ._util import refs_equal, skip_type_union class TestFunction: @refs_equal def test_func_uses(self): s = "import a\ndef f():\n a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_async_func_uses(self): s = "import a\nasync def f():\n a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_func_name_shadows(self): s = "import a\ndef a():\n pass\n a" refs = [("a", "a")] return s, refs @refs_equal def test_func_name_shadows_inside(self): s = "import a\ndef a():\n a" refs = [("a", "a")] return s, refs @refs_equal def test_func_assigns_then_uses(self): s = "import a\ndef f():\n a = 1\n a" refs = [("a", "a")] return s, refs @refs_equal def test_func_assigns_then_used_outside(self): s = "import a\ndef f():\n a = 1\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_func_annotates_then_uses(self): s = "import a\ndef f(arg: a):\n arg.b" refs = [("a", "a"), ("a", "a"), ("a.()", "arg"), ("a.().b", "arg.b")] return s, refs @refs_equal def test_func_annotates_then_assigns(self): # Note: inner nodes after outer ones s = "import a\ndef f(arg: a) -> a:\n a = 1" refs = [("a", "a"), ("a", "a"), ("a", "a"), ("a.()", "arg")] return s, refs @refs_equal def test_func_annotates_as_generic_then_uses(self): s = "import a\ndef f(arg: a[0]):\n arg.b" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_func_annotates_inside_generic_then_uses(self): s = "import a\ndef f(arg: b[a]):\n arg.b" refs = [("a", "a"), ("a", "a")] return s, refs @skip_type_union @refs_equal def test_func_annotates_union_then_uses(self): s = "import a\ndef f(arg: a | 1):\n arg.b" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_func_kw_default_uses_not_assigned(self): s = "import a\ndef f(*_, c, b=a):\n b" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_func_arg_shadows(self): s = "import a\ndef f(a):\n a" refs = [("a", "a")] return s, refs @refs_equal def test_func_decorator_uses(self): s = "import a\n@a\ndef f():\n pass" refs = [("a", "a"), ("a", "a")] return s, refs @pytest.mark.xfail(reason="Assignments are not tracked.") @refs_equal def test_func_uses_overrided_later(self): s = "import a\ndef f():\n a\na = 1\nf()" refs = [("a", "a")] return s, refs @refs_equal def test_lambda_uses(self): s = "import a\nlambda: a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_lambda_arg_shadows(self): s = "import a\nlambda a: a" refs = [("a", "a")] return s, refs @refs_equal def test_lambda_arg_default_uses_not_assigned(self): s = "import a\nlambda x=a: x" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_lambda_arg_default_uses_then_shadows(self): s = "import a\nlambda a=a: a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_lambda_arg_shadows_used_outside(self): s = "import a\nlambda a: a\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_lambda_kw_default_uses(self): s = "import a\nlambda *b, c, d=a: 1" refs = [("a", "a"), ("a", "a")] return s, refs @pytest.mark.xfail(reason="No reason to do this in the real world.") @refs_equal def test_global_in_outermost_scope(self): s = "import a\nglobal a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_global_hits_import_after_inner_shadow(self): s = "import a\ndef f():\n a = 1\n def g():\n global a\n a" refs = [("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_global_skips_inner_import(self): s = "a = 1\ndef f():\n import a\n def g():\n global a\n a" refs = [("a", "a")] return s, refs @refs_equal def test_global_overwritten_then_used_in_inner(self): s = "import a\ndef f():\n global a\n a = 1\n a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_global_overwrite_in_next_call_then_used_in_outer_before(self): s = "import a\ndef f():\n global a\n a = 1\na\nf()" refs = [("a", "a"), ("a", "a"), ("a", "a")] return s, refs @pytest.mark.xfail(reason="Global assigns not tracked to outer scopes.") @refs_equal def test_global_overwritten_then_used_in_outer(self): s = "import a\ndef f():\n global a\n a = 1\nf()\na" refs = [("a", "a"), ("a", "a")] # ref only in global statement return s, refs @refs_equal def test_nonlocal_hits_import_after_outer_assign(self): s = "a = 1\ndef f():\n import a\n def g():\n nonlocal a\n a" refs = [("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_nonlocal_skips_outer_import(self): s = "import a\ndef f():\n a = 1\n def g():\n nonlocal a\n a" refs = [("a", "a")] return s, refs @refs_equal def test_nonlocal_overwritten_then_used(self): s = "a = 1\ndef f():\n import a\n def g():\n nonlocal a\n a = 1\n a" refs = [("a", "a"), ("a", "a")] return s, refs @pytest.mark.xfail(reason="Global deletes not tracked to outer scopes.") @refs_equal def test_global_deleted_then_used_in_outer(self): s = "import a\ndef f():\n global a\n del a\nf()\na" refs = [("a", "a"), ("a", "a"), ("a", "a")] # refs in global and del return s, refs class TestComprehension: @refs_equal def test_comp_uses_in_value(self): s = "import a\n[a for b in c]" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_setcomp_uses_in_value(self): s = "import a\n{a for b in c}" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_dictcomp_uses_in_value(self): s = "import a\n{a: a for b in c}" refs = [("a", "a"), ("a", "a"), ("a", "a")] return s, refs @refs_equal def test_generator_uses_in_value(self): s = "import a\n(a for b in c)" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_comp_uses_in_ifs(self): s = "import a\n[_ for _ in b if a]" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_comp_uses_in_iter(self): s = "import a\n[_ for _ in b(a)]" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_comp_overrides(self): s = "import a\n[a for a in b if a]" refs = [("a", "a")] return s, refs @refs_equal def test_comp_overrides_used_after(self): s = "import a\n[a for a in b]\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_multicomp_overrides(self): s = "import a\n[a for a in b for b in a]" refs = [("a", "a")] return s, refs @refs_equal def test_multicomp_uses_then_overrides(self): s = "import a\n[a for b in c(a) for a in b]" refs = [("a", "a"), ("a", "a")] return s, refs @pytest.mark.xfail(reason='Assignments are not tracked outside of a "scope".') @refs_equal def test_comp_leaks_walrus(self): s = "import a\n[a := i for i in b]\na" refs = [("a", "a")] return s, refs class TestClass: @refs_equal def test_class_name_shadows(self): s = "import a\nclass a:\n pass\na" refs = [("a", "a")] return s, refs @refs_equal def test_class_bases_uses(self): s = "import a\nclass A(a):\n pass" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_keyword_uses(self): s = "import a\nclass A(kw=a):\n pass" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_starargs_uses(self): s = "import a\nclass A(*a):\n pass" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_kwargs_uses(self): s = "import a\nclass A(**a):\n pass" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_decorator_uses(self): s = "import a\n@a\nclass A:\n pass" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_body_pseudo_assigns(self): s = "import a\nclass A:\n a = 1\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_body_pseudo_shadows_for_method(self): s = "import a\nclass A:\n a = 1\n def f(s):\n a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_method_pseudo_shadows_inside(self): s = "import a\nclass A:\n def a(s):\n a" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_method_pseudo_shadows_after(self): s = "import a\nclass A:\n def a(s):\n pass\na" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_method_shadows_body(self): s = "import a\nclass A:\n def a(s):\n pass\n a" refs = [("a", "a")] return s, refs @refs_equal def test_class_lambda_uses_outer(self): s = "import a\nclass A:\n b = lambda: a\na = 1" refs = [("a", "a"), ("a", "a")] return s, refs @refs_equal def test_class_lambda_skips_body(self): s = "import a\nclass A:\n a = 2\n b = lambda: a" refs = [("a", "a"), ("a", "a")] return s, refs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741328943.0 sphinx_codeautolink-0.17.4/tox.ini0000644000076600000240000000335414762511057016240 0ustar00felixstaff[tox] min_version = 4 no_package = true envlist = check doc8 docs build-{lin,mac,win} coverage labels = check = check,doc8 format = format docs = docs build = build-{lin,mac,win} test = coverage publish = publish-{lin,mac,win} [doc8] ignore = D004 max-line-length = 80 [testenv] description = Run test suite with code coverage platform = lin: linux mac: darwin win: win32 allowlist_externals = coverage commands = coverage run coverage report [testenv:coverage] ; Inherit everything from testenv [testenv:doc8] description = Check documentation .rst files allowlist_externals = doc8 commands = doc8 docs/src --quiet [testenv:docs] description = Build Sphinx documentation allowlist_externals = sphinx-build change_dir = docs commands = sphinx-build -M html src build -W [testenv:check] description = Check code style allowlist_externals = ruff commands = ruff format --check ruff check [testenv:format] description = Format code allowlist_externals = ruff commands = ruff format ruff check --fix [testenv:build-{lin,mac,win}] description = Build and check package deps = -r requirements/build allowlist_externals = rm, cmd commands = python -m build -C--quiet twine check --strict dist/* commands_post = lin,mac: rm -r dist win: cmd /c rmdir /s /q dist [testenv:publish-{lin,mac,win}] description = Build, check and publish package deps = -r requirements/build allowlist_externals = rm, cmd commands_pre = lin,mac: rm -rf dist win: cmd /c if exist dist rmdir /s /q dist commands = python -m build -C--quiet twine check --strict dist/* twine upload dist/* --config-file .pypirc commands_post = lin,mac: rm -r dist win: cmd /c rmdir /s /q dist