././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.359394 sphinx_codeautolink-0.16.2/0000755000175000017500000000000014742122031014706 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736320446.0 sphinx_codeautolink-0.16.2/LICENSE0000644000175000017500000000206314737422676015742 0ustar00felixfelixMIT 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=1736279420.0 sphinx_codeautolink-0.16.2/MANIFEST.in0000644000175000017500000000022614737302574016464 0ustar00felixfelixgraft docs prune docs/build graft requirements graft tests include contributing.rst readme_pypi.rst tox.ini global-exclude *.py[cod] __pycache__ *.so ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.359394 sphinx_codeautolink-0.16.2/PKG-INFO0000644000175000017500000001040214742122031016000 0ustar00felixfelixMetadata-Version: 2.2 Name: sphinx-codeautolink Version: 0.16.2 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 # or, alternatively: $ 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 multiple examples and setting default import statements among other things, have a look at 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=1736279420.0 sphinx_codeautolink-0.16.2/contributing.rst0000644000175000017500000000631614737302574020175 0ustar00felixfelixContributing ============ |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 `_. 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 .. |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 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.343395 sphinx_codeautolink-0.16.2/docs/0000755000175000017500000000000014742122031015636 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/docs/requirements.txt0000644000175000017500000000020114737302574021133 0ustar00felixfelix# 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=1737008153.3473947 sphinx_codeautolink-0.16.2/docs/src/0000755000175000017500000000000014742122031016425 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/docs/src/404.rst0000644000175000017500000000040614737302574017506 0ustar00felixfelix: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=1736320446.0 sphinx_codeautolink-0.16.2/docs/src/about.rst0000644000175000017500000001507614737422676020330 0ustar00felixfelix.. _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 documentation**, disabled otherwise. If the extension is off, it silently removes directives that would produce output. - **Only processes literal 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. 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=1736596629.0 sphinx_codeautolink-0.16.2/docs/src/conf.py0000644000175000017500000000351514740456225017745 0ustar00felixfeliximport 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 def setup(app) -> None: app.add_object_type( "confval", "confval", objname="configuration value", indextemplate="pair: %s; configuration value", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/docs/src/example_library.rst0000644000175000017500000000043714737302574022362 0ustar00felixfelix.. _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=1736279420.0 sphinx_codeautolink-0.16.2/docs/src/examples.rst0000644000175000017500000002156014737302574021021 0ustar00felixfelix.. _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. `Doctest `_ and console blocks using :code:`.. code:: pycon` work too. Including code via :rst:dir:`literalinclude` requires using a :code:`:language: py` parameter. .. 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`:: .. 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`:: .. autolink-preface:: import lib .. code:: python lib.Knight().taunt() A multiline preface can be written in the content portion of the directive:: .. 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`:: .. 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`:: .. 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`:: .. autolink-examples:: numpy.linspace :type: func Doctest code blocks ------------------- Using the ``sphinx.ext.doctest`` extension for code examples requires setting up :confval:`codeautolink_custom_blocks`. To help in that, :func:`clean_pycon ` is provided as a ready-made transformer. .. code:: python extensions = [ ..., "sphinx.ext.doctest", ] codeautolink_custom_blocks = { "python3": None, "pycon3": "sphinx_codeautolink.clean_pycon", } ``doctest`` and ``testcode`` blocks now work as expected. However, any test setup and teardown code is not taken into account. .. 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. 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. All code block links use the ``sphinx-codeautolink-a`` class. For example, you can add dotted lines to all links and change the hover colour: .. code:: python # conf.py html_static_path = ['static'] html_css_files = ['custom.css'] .. code:: css /* static/custom.css */ .sphinx-codeautolink-a{ border-bottom-color: rgb(0, 0, 0); border-bottom-style: dotted; border-bottom-width: 1px; } .sphinx-codeautolink-a:hover{ color: rgb(255, 139, 139); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/docs/src/index.rst0000644000175000017500000000625114737302574020312 0ustar00felixfelixsphinx-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 names 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 # or, alternatively: $ 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 multiple examples and setting default import statements 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 documentation - Only processes literal 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=1736596629.0 sphinx_codeautolink-0.16.2/docs/src/lib.py0000644000175000017500000000130514740456225017561 0ustar00felixfelixclass 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=1736279420.0 sphinx_codeautolink-0.16.2/docs/src/reference.rst0000644000175000017500000001451014737302574021136 0ustar00felixfelix.. _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 a :rst:dir:`autolink-examples` table to the end of all autodoc definitions. Defaults to :code:`False`. .. confval:: codeautolink_global_preface Type: ``str``. Include a :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``. Issue warning when an object cannot be found in the inventory (autodoc or intersphinx). Defaults to :code:`False`. .. confval:: codeautolink_warn_on_failed_resolve Type: ``bool``. Issue warning when failing to resolve the canonical location of an object that a code element references. 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 literal 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 in the next code block. The next block consumes this directive even if it is not processed (e.g. non-Python blocks) to avoid placement confusion. A multiline preface can be written in the content portion of the directive. Prefaces are included in block concatenation. .. rst:directive:: .. autolink-skip:: [level] Skip sphinx-codeautolink functionality. ``level``, if specified, must be one of: - "next" - next 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. CSS class --------- The CSS class used in all code block links is ``sphinx-codeautolink-a``. 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=1737008005.0 sphinx_codeautolink-0.16.2/docs/src/release_notes.rst0000644000175000017500000001635414742121605022026 0ustar00felixfelix.. _release-notes: Release notes ============= These release notes are based on `Keep a Changelog `_. sphinx-codeautolink adheres to `Semantic Versioning `_. 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736596629.0 sphinx_codeautolink-0.16.2/pyproject.toml0000644000175000017500000000552114740456225017642 0ustar00felixfelix[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"] [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 ] 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 ] # 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=1736279420.0 sphinx_codeautolink-0.16.2/readme_pypi.rst0000644000175000017500000000321114737302574017753 0ustar00felixfelixsphinx-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 # or, alternatively: $ 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 multiple examples and setting default import statements among other things, have a look at 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=1737008153.3473947 sphinx_codeautolink-0.16.2/requirements/0000755000175000017500000000000014742122031017431 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/requirements/build0000644000175000017500000000001414737302574020466 0ustar00felixfelixbuild twine ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736596629.0 sphinx_codeautolink-0.16.2/requirements/dev0000644000175000017500000000010414740456225020142 0ustar00felixfelix-r extras -r docs -r tests -r build tox>=4 doc8>=0.9 ruff pygments ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/requirements/docs0000644000175000017500000000004714737302574020325 0ustar00felixfelix-r extras sphinx-rtd-theme matplotlib ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/requirements/extras0000644000175000017500000000001714737302574020700 0ustar00felixfelixipython!=8.7.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/requirements/tests0000644000175000017500000000004714737302574020537 0ustar00felixfelix-r extras pytest>=6 coverage[toml]>=5 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.359394 sphinx_codeautolink-0.16.2/setup.cfg0000644000175000017500000000004614742122031016527 0ustar00felixfelix[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.343395 sphinx_codeautolink-0.16.2/src/0000755000175000017500000000000014742122031015475 5ustar00felixfelix././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737008153.3473947 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/0000755000175000017500000000000014742122031021547 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737008029.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/__init__.py0000644000175000017500000000463714742121635023703 0ustar00felixfelix"""Sphinx extension for linking code examples to reference documentation.""" from sphinx.application import Sphinx from .extension import SphinxCodeAutoLink, backref, directive from .extension.block import clean_ipython, clean_pycon __version__ = "0.16.2" 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_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) 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) ) return {"version": __version__, "env_version": 1, "parallel_read_safe": True} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737008153.3473947 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/extension/0000755000175000017500000000000014742122031023563 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736596629.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/extension/__init__.py0000644000175000017500000002564614740456225025726 0ustar00felixfelix"""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.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 # 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 != "html": 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 # 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, ) 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)) 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) 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, ) self.cache.write() def transpose_inventory(inv: dict, relative_to: str) -> 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 """ transposed = {} for type_, items in inv.items(): if not type_.startswith("py:"): continue for item, info in items.items(): location = info[2] if not location.startswith("http"): location = str(Path(location).relative_to(relative_to)) transposed[item] = location return transposed ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736596629.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/extension/backref.py0000644000175000017500000000656614740456225025564 0ustar00felixfelix"""Backreference tables implementation.""" from dataclasses import dataclass from docutils import nodes from .directive import DeferredExamples @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]], **kwargs ) -> None: super().__init__(*args, **kwargs) self.code_refs = code_refs 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 = ref.document + ".html" 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: # 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("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("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=1736596629.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/extension/block.py0000644000175000017500000003561014740456225025251 0ustar00felixfelix"""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 BUILTIN_BLOCKS = {"python": None, "py": 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 literal 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, **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.transformers = BUILTIN_BLOCKS.copy() self.transformers.update(custom_blocks) self.valid_blocks = self.transformers.keys() self.title_stack = [] self.current_refid = None self.prefaces = [] self.concat_global = concat_default self.concat_section = False self.concat_sources = [] self.skip = None 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): self.prefaces.extend(node.content.split("\n")) 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_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): """Visit a Python doctest block.""" return self.parse_source(node, "pycon") def visit_literal_block(self, node: nodes.literal_block): """Visit a generic literal block.""" return self.parse_source(node, node.get("language", None)) def parse_source( self, node: nodes.literal_block | nodes.doctest_block, language: str | None ) -> None: """Analyse Python code blocks.""" prefaces = self.prefaces self.prefaces = [] skip = self.skip if skip == "next": self.skip = None if ( skip or 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() 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 example = CodeExample( self.current_document, self.current_refid, list(self.title_stack) ) transform = SourceTransform(source, [], example, node.line) self.source_transforms.append(transform) modified_source = "\n".join( self.global_preface + self.concat_sources + prefaces + [clean_source] ) try: names = parse_names(modified_source, node) except SyntaxError as e: show_source = self._format_source_for_error( self.global_preface, self.concat_sources, prefaces, source ) msg = self._parsing_error_msg(e, language, show_source) logger.warning(msg, type=warn_type, subtype="parse_block", location=node) return if prefaces or self.concat_sources or self.global_preface: concat_lens = [s.count("\n") + 1 for s in self.concat_sources] hidden_len = len(prefaces) + sum(concat_lens) + len(self.global_preface) 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], concat_sources: list[str], prefaces: list[str], source: str, ) -> str: lines = global_preface + concat_sources + prefaces + source.split("\n") guides = [""] * len(lines) ix = 0 if global_preface: guides[0] = "global preface:" ix += len(global_preface) 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, ) -> None: """Inject links to code blocks on disk.""" 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=1736596629.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/extension/cache.py0000644000175000017500000000265714740456225025227 0ustar00felixfelix"""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=1736596629.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/extension/directive.py0000644000175000017500000000733714740456225026142 0ustar00felixfelix"""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 literal 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) -> None: super().__init__() self.content = content 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 def run(self): """Insert :class:`PrefaceMarker`.""" lines = list(self.arguments) + list(self.content) return [PrefaceMarker("\n".join(lines))] 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=1737007622.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/extension/resolve.py0000644000175000017500000001333114742121006025616 0ustar00felixfelix"""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) if isclass(previous.value) and cursor.location not in inventory: for val in previous.value.__mro__: 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=1736596629.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/parse.py0000644000175000017500000006151314740456225023256 0ustar00felixfelix"""Analyse AST of code blocks to determine used names and their sources.""" from __future__ import annotations import ast import builtins import sys from collections.abc import Generator from contextlib import contextmanager from dataclasses import dataclass, field from enum import Enum from functools import wraps 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 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: """Pending name access.""" components: list[Component] @dataclass class AssignTarget: """ Assign target. `elements` represent the flattened assignment target elements. If a single PendingAccess is found, it should be used to store the value on the right hand side of the assignment. If multiple values are found, the assignment contained unpacking, and only overwriting of names should occur. """ elements: list[PendingAccess | None] @dataclass class Assignment: """ Representation of an assignment statement. - ordinarily one value to a single target - multiple targets when chain assigning (a = b = c) - nested assignments in walruses (a = b := c) """ targets: list[AssignTarget] value: PendingAccess | Assignment | None class NameBreak(str, Enum): """Elements that break name access chains.""" call = "()" class LinkContext(str, Enum): """Context in which a link appears.""" 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, to be broken down into suitable chunks. :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. :attr:`hidden_components` is an attribute of split Access, in which the proper components are not moved to prior components to track which were present on the line of the access. 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] hidden_components: list[Component] = field(default_factory=list) @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.hidden_components + self.components if self.hidden_components: proper_components = self.hidden_components[1:] + self.components else: proper_components = self.components[1:] return self.prior_components + proper_components @property def code_str(self) -> str: """Code representation of components.""" break_on = set(NameBreak) breaks = [i for i, c in enumerate(self.components) if c.name in break_on] start_ix = breaks[-1] + 1 if breaks else 0 return ".".join(c.name for c in self.components[start_ix:]) @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_ @staticmethod def to_name(instance: Access) -> Name: """Convert access to name.""" return Name( [c.name for c in instance.full_components], instance.code_str, *instance.lineno_span, context=instance.context, ) def split(self) -> list[Name]: """Split access into multiple names.""" # Copy to avoid modifying the instance in place items = [ Access( context=self.context, prior_components=self.prior_components[:], components=self.components[:], hidden_components=self.hidden_components[:], ) ] while True: current = items[-1] for i, comp in enumerate(current.components): if i and comp.name == NameBreak.call: hidden = current.hidden_components + current.components[:i] next_ = Access( LinkContext.after_call, current.prior_components, current.components[i:], hidden_components=hidden, ) current.components = current.components[:i] items.append(next_) break else: break if items[-1].components[-1].name == NameBreak.call: items.pop() return [self.to_name(i) for i in items] def track_parents(func): """ Track a stack of nodes to determine the position of the current node. Uses and increments the surrounding classes :attr:`_parents`. """ @wraps(func) def wrapper(self: ImportTrackerVisitor, *args, **kwargs): self._parents += 1 result = func(self, *args, **kwargs) self._parents -= 1 if not self._parents: self.dispatch_result(result) return result return wrapper 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.accessed: list[Name] = [] self.in_augassign = False self._parents = 0 self._no_split = False self.doctree_node = doctree_node # 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]]] = [] def save_access(self, access: Access) -> None: """Convert Access to Names to store in the visitor for aggregation.""" names = access.split() if not self._no_split else [Access.to_name(access)] self.accessed.extend(names) @contextmanager def no_split(self) -> Generator[None, None, None]: """Disable splitting Accesses.""" self._no_split, old = (True, self._no_split) yield self._no_split = old @contextmanager def reset_parents(self) -> Generator[None, None, None]: """Reset parents state for the duration of the context.""" self._parents, old = (0, self._parents) yield self._parents = old # Nodes that are excempt from resetting parents in default visit track_nodes = (ast.Name, ast.Attribute, ast.Call, ast.NamedExpr) if HAS_MATCH: track_nodes += (ast.MatchAs,) def visit(self, node: ast.AST): """Override default visit to track name access and assignments.""" if isinstance(node, self.track_nodes): return super().visit(node) with self.reset_parents(): return super().visit(node) 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.pseudo_scopes_stack[-1].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) self.pseudo_scopes_stack[-1][name] = components def create_access( self, scope_key: str, new_components: list[Component] ) -> Access | None: """Create access from scope.""" prior = self.pseudo_scopes_stack[-1].get(scope_key, None) if prior is None: return None access = Access(LinkContext.none, prior, new_components) self.save_access(access) return access def resolve_pending_access(self, pending: PendingAccess) -> Access | 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 None access = self.create_access(components[0].name, components) if context == "del": self.overwrite_name(components[0].name) return access def resolve_assignment(self, assignment: Assignment) -> Access | None: """Resolve access for assignment values and targets.""" access = self.dispatch_result(assignment.value) self._resolve_assign_targets(assignment, access) return access def _resolve_assign_targets(self, assignment: Assignment, access: Access) -> None: for assign in assignment.targets: if assign is None: continue # On multiple nested targets, only overwrite assigned names value = access if len(assign.elements) <= 1 else None for target in assign.elements: self._resolve_assign_target(target, value) def _resolve_assign_target( self, target: PendingAccess | None, value: Access | None ) -> None: 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.full_components) self.create_access(comp.name, 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.name, [component]) def dispatch_result( self, result: PendingAccess | Assignment | None ) -> Access | None: """Determine the appropriate processing after tracking an access chain.""" if isinstance(result, Assignment): return self.resolve_assignment(result) if isinstance(result, PendingAccess): return self.resolve_pending_access(result) return None 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: self.save_access(Access(LinkContext.import_from, [], prefix_components)) 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_access( Access(LinkContext.import_target, [], components, prefix_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 + ".") @track_parents def visit_Name(self, node: ast.Name) -> PendingAccess: """Visit a Name node.""" return PendingAccess([Component.from_ast(node)]) @track_parents def visit_Attribute(self, node: ast.Attribute) -> PendingAccess | None: """Visit an Attribute node.""" inner: PendingAccess | None = self.visit(node.value) if inner is not None: inner.components.append(Component.from_ast(node)) return inner @track_parents def visit_Call(self, node: ast.Call) -> PendingAccess | None: """Visit a Call node.""" inner: PendingAccess | None = self.visit(node.func) if inner is not None: inner.components.append(Component.from_ast(node)) with self.reset_parents(): for arg in node.args + node.keywords: self.visit(arg) if hasattr(node, "starargs"): self.visit(node.starargs) if hasattr(node, "kwargs"): self.visit(node.kwargs) return inner @track_parents def visit_Tuple(self, node: ast.Tuple) -> list[PendingAccess] | None: """Visit a Tuple node.""" if isinstance(node.ctx, ast.Store): accesses = [] for element in node.elts: ret = self.visit(element) if isinstance(ret, PendingAccess) or ret is None: accesses.append(ret) else: accesses.extend(ret) return accesses with self.reset_parents(): for element in node.elts: self.visit(element) return None @track_parents 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) if not isinstance(target, list): target = [target] targets.append(AssignTarget(target)) return Assignment(targets, value) @track_parents 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: if value is not None: self.resolve_pending_access(value) annot.components.append( Component(NameBreak.call, *linenos(node.annotation), "load") ) value = annot target = self.visit(node.target) return Assignment([AssignTarget([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 @track_parents def visit_NamedExpr(self, node: ast.NamedExpr) -> Assignment: """Visit a NamedExpr node.""" value = self.visit(node.value) target = self.visit(node.target) return Assignment([AssignTarget([target])], value) @track_parents def visit_MatchClass(self, node: ast.AST) -> None: """Visit a match case class as a series of assignments.""" with self.reset_parents(): cls = self.visit(node.cls) accesses = [] for n in node.patterns: access = self.visit(n) if access is not None: accesses.append(access) assigns = [] for attr, pattern in zip(node.kwd_attrs, node.kwd_patterns, strict=True): target = self.visit(pattern) attr_comps = [ Component(NameBreak.call, *linenos(node), "load"), Component(attr, *linenos(node), "load"), ] access = PendingAccess(cls.components + attr_comps) assigns.append(Assignment([AssignTarget([target])], access)) for access in accesses: self.resolve_pending_access(access) with self.no_split(): for assign in assigns: self.resolve_assignment(assign) @track_parents def visit_MatchAs(self, node: ast.AST) -> PendingAccess: """Track match alias names.""" 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) @track_parents 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([AssignTarget([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.pseudo_scopes_stack[-1].copy() for gen in generators: inner.visit(gen) for value in values: inner.visit(value) self.accessed.extend(inner.accessed) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink/warn.py0000644000175000017500000000021414737302574023105 0ustar00felixfelix"""Logging definitions.""" from sphinx.util.logging import getLogger logger = getLogger("sphinx_codeautolink") warn_type = "codeautolink" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.359394 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink.egg-info/0000755000175000017500000000000014742122031023241 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737008153.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink.egg-info/PKG-INFO0000644000175000017500000001040214742122031024333 0ustar00felixfelixMetadata-Version: 2.2 Name: sphinx-codeautolink Version: 0.16.2 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 # or, alternatively: $ 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 multiple examples and setting default import statements among other things, have a look at 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=1737008153.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink.egg-info/SOURCES.txt0000644000175000017500000001213214742122031025124 0ustar00felixfelixLICENSE 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 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 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_module_docs.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/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/line_numbers.txt tests/extension/ref/non_python_block.txt tests/extension/ref/preface_concatenated.txt tests/extension/ref/preface_consumed_non_python.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_doctest.txt tests/extension/ref/ref_extdoctest_doctest.txt tests/extension/ref/ref_extdoctest_testcode.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_ipython.txt tests/extension/ref/ref_ipython3.txt tests/extension/ref/ref_ipython_directive.txt tests/extension/ref/ref_ipython_directive_comment.txt tests/extension/ref/ref_ipython_syntax.txt tests/extension/ref/ref_optional.txt tests/extension/ref/ref_optional_future.txt tests/extension/ref/ref_py.txt tests/extension/ref/ref_pycon.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/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=1737008153.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink.egg-info/dependency_links.txt0000644000175000017500000000000114742122031027307 0ustar00felixfelix ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737008153.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink.egg-info/requires.txt0000644000175000017500000000007614742122031025644 0ustar00felixfelixsphinx>=3.2.0 beautifulsoup4>=4.8.1 [ipython] ipython!=8.7.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737008153.0 sphinx_codeautolink-0.16.2/src/sphinx_codeautolink.egg-info/top_level.txt0000644000175000017500000000002414742122031025767 0ustar00felixfelixsphinx_codeautolink ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737008153.3473947 sphinx_codeautolink-0.16.2/tests/0000755000175000017500000000000014742122031016050 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/__init__.py0000644000175000017500000000045514737302574020205 0ustar00felixfeliximport 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=1737008153.3473947 sphinx_codeautolink-0.16.2/tests/extension/0000755000175000017500000000000014742122031020064 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736596629.0 sphinx_codeautolink-0.16.2/tests/extension/__init__.py0000644000175000017500000001704014740456225022214 0ustar00felixfelixfrom __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 # 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 """ 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): 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": default_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.extension.CodeBlockAnalyser" 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 _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(): (src_dir / name).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=1736596629.0 sphinx_codeautolink-0.16.2/tests/extension/_check.py0000644000175000017500000000272214740456225021672 0ustar00felixfelixfrom 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.relative_to(root): 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: ids = site_ids[Path(base)] assert id_ in ids, ( f"ID {id_} not found in {base}" f" while validating link for `{link.string}` in {doc!s}!" ) total += 1 return total 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)} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737008153.3513944 sphinx_codeautolink-0.16.2/tests/extension/fail/0000755000175000017500000000000014742122031020777 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/concat_invalid.txt0000644000175000017500000000023714737302574024537 0ustar00felixfelix# split Test project ============ .. autolink-concat:: incorrect .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/custom_block_import_invalid.txt0000644000175000017500000000032114737302574027340 0ustar00felixfelixcodeautolink_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/custom_block_syntax_error.txt0000644000175000017500000000025614737302574027066 0ustar00felixfelixcodeautolink_custom_blocks = {"python": "parser_func.syntax_error"} # split Test project ============ .. code:: python import test_project .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/failed_resolve_invalid_location.txt0000644000175000017500000000026714737302574030146 0ustar00felixfelixcodeautolink_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/failed_resolve_no_module.txt0000644000175000017500000000027114737302574026604 0ustar00felixfelixcodeautolink_warn_on_failed_resolve = True # split Test project ============ .. code:: python import test_project test_project.Bar().__eq__().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/failed_resolve_return_type.txt0000644000175000017500000000027514737302574027207 0ustar00felixfelixcodeautolink_warn_on_failed_resolve = True # split Test project ============ .. code:: python import test_project test_project.optional_counter().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/match_block_warning.txt0000644000175000017500000000027414737302574025556 0ustar00felixfelixcodeautolink_custom_blocks = {"python": "parser_func.manipulate_original_source"} # split Test project ============ .. code:: python import test_project .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/missing_inventory.txt0000644000175000017500000000022014737302574025340 0ustar00felixfelixcodeautolink_warn_on_missing_inventory = True # split Test project ============ .. code:: python import test_project test_project.bar() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/no_module_docs.txt0000644000175000017500000000014214737302574024546 0ustar00felixfelix# split Test project ============ .. code:: python import test_project test_project.bar() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/ref_import_star_invalid.txt0000644000175000017500000000016014737302574026462 0ustar00felixfelix# split Test project ============ .. code:: python from non_project import * .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/ref_invalid_block_type.txt0000644000175000017500000000021014737302574026246 0ustar00felixfelix# split Test project ============ .. code:: python >>> import test_project >>> test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/ref_invalid_syntax.txt0000644000175000017500000000033414737302574025450 0ustar00felixfelix# 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/ref_invalid_syntax_complex.txt0000644000175000017500000000041314737302574027175 0ustar00felixfelixcodeautolink_global_preface = "import test_project" # split Test project ============ .. 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/ref_ipython_non-python.txt0000644000175000017500000000040214737302574026273 0ustar00felixfelixextensions.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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/fail/skip_invalid.txt0000644000175000017500000000023514737302574024234 0ustar00felixfelix# split Test project ============ .. autolink-skip:: incorrect .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.359394 sphinx_codeautolink-0.16.2/tests/extension/ref/0000755000175000017500000000000014742122031020640 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/concat_default.txt0000644000175000017500000000052114737302574024372 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/concat_off.txt0000644000175000017500000000037414737302574023526 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/concat_on_across_sections.txt0000644000175000017500000000050614737302574026646 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/concat_on_breaks.txt0000644000175000017500000000037714737302574024722 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/concat_section_breaks.txt0000644000175000017500000000047314737302574025747 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/concat_section_returns_to_global.txt0000644000175000017500000000063014737302574030217 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/custom_block.txt0000644000175000017500000000046714737302574024114 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/custom_block_imported.txt0000644000175000017500000000042314737302574026007 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/empty_project.txt0000644000175000017500000000006514737302574024306 0ustar00felixfelix# split # split Test project ============ Some text. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/inventory_map_is_needed.txt0000644000175000017500000000057314737302574026317 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/inventory_map_not_needed.txt0000644000175000017500000000041114737302574026473 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/line_numbers.txt0000644000175000017500000000027114737302574024103 0ustar00felixfelixtest_project test_project.bar # split # split Test project ============ .. code-block:: python :linenos: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/non_python_block.txt0000644000175000017500000000020214737302574024760 0ustar00felixfelix# split # split Test project ============ .. code:: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_concatenated.txt0000644000175000017500000000037114737302574025537 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_consumed_non_python.txt0000644000175000017500000000027314737302574027200 0ustar00felixfelix# split # split Test project ============ .. autolink-preface:: import test_project .. code:: test_project.bar() .. code:: python test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_global.txt0000644000175000017500000000033314737302574024345 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_global_multiline.txt0000644000175000017500000000033714737302574026433 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_global_overwritten.txt0000644000175000017500000000036514737302574027022 0ustar00felixfelix# 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_multiline.txt0000644000175000017500000000034114737302574025106 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_multiline_and_arg.txt0000644000175000017500000000033614737302574026565 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_multiple.txt0000644000175000017500000000035714737302574024746 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/preface_single.txt0000644000175000017500000000025414737302574024370 0ustar00felixfelixtest_project.bar # split # split Test project ============ .. autolink-preface:: import test_project .. code:: python test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736596629.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_assign_targets.txt0000644000175000017500000000027314740456225025271 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_builtins.txt0000644000175000017500000000041514737302574024106 0ustar00felixfelixprint 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_chain.txt0000644000175000017500000000026114737302574023336 0ustar00felixfelixtest_project test_project.bar attr # split # split Test project ============ .. code:: python import test_project test_project.bar().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_chain_call.txt0000644000175000017500000000026214737302574024332 0ustar00felixfelixtest_project test_project.Foo bute # split # split Test project ============ .. code:: python import test_project test_project.Foo()().bute .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_class_attr.txt0000644000175000017500000000050014737302574024407 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_class_meth_returns_self.txt0000644000175000017500000000031314737302574027167 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_class_type_attr.txt0000644000175000017500000000030614737302574025454 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_decorator.txt0000644000175000017500000000035314737302574024240 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_doctest.txt0000644000175000017500000000052414737302574023723 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_extdoctest_doctest.txt0000644000175000017500000000111014737302574026161 0ustar00felixfelixtest_project test_project.bar attr test_project.Foo test_project.Foo # split from sphinx_codeautolink.extension.block import clean_pycon extensions.append("sphinx.ext.doctest") codeautolink_custom_blocks = {"pycon3": clean_pycon} suppress_warnings = ["config.cache"] # 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_extdoctest_testcode.txt0000644000175000017500000000054514737302574026341 0ustar00felixfelixtest_project test_project.bar attr test_project.Foo test_project.Foo # split extensions.append("sphinx.ext.doctest") codeautolink_custom_blocks = {"python3": None} # 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_fluent_attrs.txt0000644000175000017500000000043214737302574024766 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_fluent_call.txt0000644000175000017500000000051414737302574024545 0ustar00felixfelixtest_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=1736596629.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_func_args.txt0000644000175000017500000000051714740456225024224 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_func_no_module.txt0000644000175000017500000000043514737302574025253 0ustar00felixfelixdatetime 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_import_as.txt0000644000175000017500000000027314737302574024254 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_import_from.txt0000644000175000017500000000021414737302574024607 0ustar00felixfelixtest_project bar # split # split Test project ============ .. code:: python from test_project import bar .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_import_from_complex.txt0000644000175000017500000000055314737302574026344 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_import_multiple.txt0000644000175000017500000000033114737302574025477 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_import_multiple_libs.txt0000644000175000017500000000041314737302574026511 0ustar00felixfelixjson 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_import_star.txt0000644000175000017500000000020614737302574024616 0ustar00felixfelixtest_project # split # split Test project ============ .. code:: python from test_project import * .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_import_submodule.txt0000644000175000017500000000034714737302574025652 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_imported_func.txt0000644000175000017500000000026414737302574025115 0ustar00felixfelixtest_project.subfoo # split # split Test project ============ .. autolink-preface:: import test_project .. code:: python test_project.subfoo .. automodule:: test_project.sub ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_imported_func_relocated.txt0000644000175000017500000000030014737302574027126 0ustar00felixfelixtest_project test_project.subfoo # split # split Test project ============ .. code:: python import test_project test_project.subfoo .. automodule:: test_project :imported-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_inherited.txt0000644000175000017500000000055514737302574024235 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_intersphinx_only.txt0000644000175000017500000000040614737302574025671 0ustar00felixfelixnumpy 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=1737007807.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_invalid_func.txt0000644000175000017500000000050314742121277024707 0ustar00felixfelixnumpy 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=1736837675.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_invalid_typehint.txt0000644000175000017500000000036514741405053025622 0ustar00felixfelixfuture_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_ipython.txt0000644000175000017500000000077414737302574023757 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_ipython3.txt0000644000175000017500000000026614737302574024036 0ustar00felixfelixtest_project test_project.bar # split # split Test project ============ .. code:: ipython3 %cd subdir import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_ipython_directive.txt0000644000175000017500000000047214737302574026010 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_ipython_directive_comment.txt0000644000175000017500000000043614737302574027532 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_ipython_syntax.txt0000644000175000017500000000105414737302574025355 0ustar00felixfelix# 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=1736596629.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_optional.txt0000644000175000017500000000040314740456225024074 0ustar00felixfelixtest_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=1736596629.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_optional_future.txt0000644000175000017500000000042114740456225025466 0ustar00felixfelixfuture_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_py.txt0000644000175000017500000000024214737302574022703 0ustar00felixfelixtest_project test_project.bar # split # split Test project ============ .. code:: py import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_pycon.txt0000644000175000017500000000060314737302574023404 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_shadow_builtin.txt0000644000175000017500000000037114737302574025271 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/ref_simple.txt0000644000175000017500000000024714737302574023551 0ustar00felixfelixtest_project test_project.bar # split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/search_css_duplicates_removed.txt0000644000175000017500000000035414737302574027476 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/skip_file.txt0000644000175000017500000000046714737302574023375 0ustar00felixfelix# 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/skip_next.txt0000644000175000017500000000037114737302574023426 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/skip_next_consumed_non_python.txt0000644000175000017500000000036214737302574027576 0ustar00felixfelixtest_project test_project.Foo # split # split Test project ============ .. autolink-skip:: .. code:: import test_project test_project.bar() .. code:: python import test_project test_project.Foo() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/skip_off.txt0000644000175000017500000000061314737302574023221 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/ref/skip_section.txt0000644000175000017500000000053014737302574024111 0ustar00felixfelixtest_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 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.359394 sphinx_codeautolink-0.16.2/tests/extension/src/0000755000175000017500000000000014742122031020653 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736837776.0 sphinx_codeautolink-0.16.2/tests/extension/src/future_project.py0000644000175000017500000000063014741405220024267 0ustar00felixfelix# 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=1736596629.0 sphinx_codeautolink-0.16.2/tests/extension/src/parser_func.py0000644000175000017500000000032114740456225023545 0ustar00felixfelix# 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=1737008153.359394 sphinx_codeautolink-0.16.2/tests/extension/src/test_project/0000755000175000017500000000000014742122031023360 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736596629.0 sphinx_codeautolink-0.16.2/tests/extension/src/test_project/__init__.py0000644000175000017500000000154714740456225025515 0ustar00felixfelix"""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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/src/test_project/sub.py0000644000175000017500000000025014737302574024540 0ustar00felixfelixdef subfoo(): """Function in submodule.""" class SubBar: """Class in submodule to be imported and documented in the main module.""" sub_attr: str = "yo" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.359394 sphinx_codeautolink-0.16.2/tests/extension/table/0000755000175000017500000000000014742122031021153 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/table/tab_autodoc_empty.txt0000644000175000017500000000032414737302574025435 0ustar00felixfelixTestproject # 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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/table/tab_autodoc_links.txt0000644000175000017500000000030114737302574025412 0ustar00felixfelixTestproject Testproject # split codeautolink_autodoc_inject = True # split Test project ============ .. code:: python import test_project test_project.bar .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/table/tab_manual_collapse.txt0000644000175000017500000000031214737302574025715 0ustar00felixfelixTestproject # split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. autolink-examples:: test_project.bar :collapse: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/table/tab_manual_empty.txt0000644000175000017500000000026014737302574025253 0ustar00felixfelix# split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. autolink-examples:: test_project.Foo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/table/tab_manual_links.txt0000644000175000017500000000031514737302574025236 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/table/tab_manual_links_subsection.txt0000644000175000017500000000040214737302574027471 0ustar00felixfelixtest_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/extension/table/tab_no_table.txt0000644000175000017500000000021014737302574024336 0ustar00felixfelix# split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1737008153.359394 sphinx_codeautolink-0.16.2/tests/parse/0000755000175000017500000000000014742122031017162 5ustar00felixfelix././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736596629.0 sphinx_codeautolink-0.16.2/tests/parse/__init__.py0000644000175000017500000001110414740456225021305 0ustar00felixfeliximport 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_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=1736596629.0 sphinx_codeautolink-0.16.2/tests/parse/_util.py0000644000175000017500000000230414740456225020664 0ustar00felixfeliximport 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:") [print(n) for n in names] 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=1736320446.0 sphinx_codeautolink-0.16.2/tests/parse/assign.py0000644000175000017500000002210014737422676021041 0ustar00felixfeliximport 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_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_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=1736279420.0 sphinx_codeautolink-0.16.2/tests/parse/chain.py0000644000175000017500000000131114737302574020632 0ustar00felixfelixfrom ._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=1736279420.0 sphinx_codeautolink-0.16.2/tests/parse/match.py0000644000175000017500000000771314737302574020660 0ustar00felixfeliximport 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=1736320446.0 sphinx_codeautolink-0.16.2/tests/parse/scope.py0000644000175000017500000002405414737422676020700 0ustar00felixfeliximport 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=1736596629.0 sphinx_codeautolink-0.16.2/tox.ini0000644000175000017500000000332014740456225016234 0ustar00felixfelix[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 [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 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 twine check --strict dist/* twine upload dist/* --config-file .pypirc commands_post = lin,mac: rm -r dist win: cmd /c rmdir /s /q dist