././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.3387816 sphinx_codeautolink-0.15.2/0000777000000000000000000000000014627365715012623 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1672399081.0 sphinx_codeautolink-0.15.2/LICENSE0000666000000000000000000000211014353544351013611 0ustar00MIT License Copyright (c) 2021-2023 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=1672772488.0 sphinx_codeautolink-0.15.2/MANIFEST.in0000666000000000000000000000023414355075610014346 0ustar00graft docs prune docs/build graft requirements graft tests include contributing.rst readme_pypi.rst tox.ini global-exclude *.py[cod] __pycache__ *.so ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.3382814 sphinx_codeautolink-0.15.2/PKG-INFO0000666000000000000000000001063214627365715013722 0ustar00Metadata-Version: 2.1 Name: sphinx-codeautolink Version: 0.15.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-2023 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.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Classifier: Topic :: Software Development :: Documentation Requires-Python: >=3.7 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=1672772488.0 sphinx_codeautolink-0.15.2/contributing.rst0000666000000000000000000000645414355075610016063 0ustar00Contributing ============ |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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.2672691 sphinx_codeautolink-0.15.2/docs/0000777000000000000000000000000014627365715013553 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710402825.0 sphinx_codeautolink-0.15.2/docs/requirements.txt0000666000000000000000000000020714574526411017027 0ustar00# 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=1717431245.2717702 sphinx_codeautolink-0.15.2/docs/src/0000777000000000000000000000000014627365715014342 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717430834.0 sphinx_codeautolink-0.15.2/docs/src/404.rst0000666000000000000000000000042014627365062015372 0ustar00: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=1717430834.0 sphinx_codeautolink-0.15.2/docs/src/about.rst0000666000000000000000000001561214627365062016206 0ustar00.. _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: - Multiline statements cannot be matched on Python versions before 3.8. This is because the builtin AST parser does not supply the necessary line number information to construct the proper search range. 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=1717430834.0 sphinx_codeautolink-0.15.2/docs/src/conf.py0000666000000000000000000000360714627365062015642 0ustar00import 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-2023, 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): app.add_object_type( "confval", "confval", objname="configuration value", indextemplate="pair: %s; configuration value", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638544765.0 sphinx_codeautolink-0.15.2/docs/src/example_library.rst0000666000000000000000000000045114152432575020243 0ustar00.. _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=1674844475.0 sphinx_codeautolink-0.15.2/docs/src/examples.rst0000666000000000000000000002226014365014473016703 0ustar00.. _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=1675584211.0 sphinx_codeautolink-0.15.2/docs/src/index.rst0000666000000000000000000000643714367661323016210 0ustar00sphinx-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=1638544765.0 sphinx_codeautolink-0.15.2/docs/src/lib.py0000666000000000000000000000136614152432575015460 0ustar00from typing import List class 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): self.looks_nice = looks_nice self.too_expensive = too_expensive ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1675586235.0 sphinx_codeautolink-0.15.2/docs/src/reference.rst0000666000000000000000000001475014367665273017044 0ustar00.. _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=1717431144.0 sphinx_codeautolink-0.15.2/docs/src/release_notes.rst0000666000000000000000000001562414627365550017731 0ustar00.. _release-notes: Release notes ============= These release notes are based on `Keep a Changelog `_. sphinx-codeautolink adheres to `Semantic Versioning `_. 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=1673168602.0 sphinx_codeautolink-0.15.2/pyproject.toml0000666000000000000000000000414314356503332015525 0ustar00[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.7" 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.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "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.pydocstyle] ignore = "D107,D203,D212,D413,D416" [tool.black] skip-magic-trailing-comma = true [tool.isort] atomic = true profile = "black" line_length = 88 skip_gitignore = true ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1672389582.0 sphinx_codeautolink-0.15.2/readme_pypi.rst0000666000000000000000000000327314353521716015647 0ustar00sphinx-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=1717431245.2737708 sphinx_codeautolink-0.15.2/requirements/0000777000000000000000000000000014627365715015346 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1672772488.0 sphinx_codeautolink-0.15.2/requirements/build0000666000000000000000000000001614355075610016353 0ustar00build twine ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1672772488.0 sphinx_codeautolink-0.15.2/requirements/dev0000666000000000000000000000020414355075610016031 0ustar00-r extras -r docs -r tests -r build tox>=4 doc8>=0.9 flake8 flake8-bugbear pydocstyle[toml]>=6.1 pygments black isort ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889367.0 sphinx_codeautolink-0.15.2/requirements/docs0000666000000000000000000000005314167237127016211 0ustar00-r extras sphinx-rtd-theme matplotlib ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1672399081.0 sphinx_codeautolink-0.15.2/requirements/extras0000666000000000000000000000002014353544351016556 0ustar00ipython!=8.7.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889367.0 sphinx_codeautolink-0.15.2/requirements/tests0000666000000000000000000000005314167237127016423 0ustar00-r extras pytest>=6 coverage[toml]>=5 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.3392816 sphinx_codeautolink-0.15.2/setup.cfg0000666000000000000000000000005214627365715014441 0ustar00[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.2617683 sphinx_codeautolink-0.15.2/src/0000777000000000000000000000000014627365715013412 5ustar00././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1717431245.275771 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/0000777000000000000000000000000014627365715017464 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717431164.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/__init__.py0000666000000000000000000000432214627365574021601 0ustar00"""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 # NOQA __version__ = "0.15.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", False, "html", types=[bool]) app.add_config_value("codeautolink_global_preface", "", "html", types=[str]) app.add_config_value("codeautolink_custom_blocks", {}, "html", types=[dict]) app.add_config_value("codeautolink_concat_default", False, "html", types=[bool]) app.add_config_value("codeautolink_search_css_classes", [], "html", types=[list]) app.add_config_value("codeautolink_inventory_map", {}, "html", types=[dict]) app.add_config_value( "codeautolink_warn_on_missing_inventory", False, "html", types=[bool] ) app.add_config_value( "codeautolink_warn_on_failed_resolve", False, "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=1717431245.2867725 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/extension/0000777000000000000000000000000014627365715021500 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710402523.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/extension/__init__.py0000666000000000000000000002611114574525733023611 0ustar00"""Sphinx extension implementation.""" from dataclasses import dataclass from functools import wraps from pathlib import Path from traceback import print_exc from typing import Dict, List, Optional, Set from sphinx.ext.intersphinx import InventoryAdapter from sphinx.util import import_object from ..parse import Name from ..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): # 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: Optional[DataCache] = None @print_exceptions() def build_inited(self, app): """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): """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): """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): """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): """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): """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(set(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 ): """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{str(e)}" ) 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): 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) @print_exceptions() def apply_links(self, app, exception): """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): """ 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=1710402523.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/extension/backref.py0000666000000000000000000000671714574525733023461 0ustar00"""Backreference tables implementation.""" from dataclasses import dataclass from typing import Dict, List 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): """Insert a details tag.""" self.body.append("
") def depart_details(self, node: DetailsNode): """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): """Insert a summary tag.""" self.body.append("") def depart_summary(self, node: SummaryNode): """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): super().__init__(*args, **kwargs) self.code_refs = code_refs def unknown_departure(self, node): """Ignore unknown nodes.""" def unknown_visit(self, node): """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=1717428916.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/extension/block.py0000666000000000000000000003602514627361264023145 0ustar00"""Code block processing.""" import re from copy import copy from dataclasses import dataclass from pathlib import Path from typing import Callable, Dict, List, Optional, Tuple, Union from bs4 import BeautifulSoup from docutils import nodes from ..parse import LinkContext, Name, parse_names from ..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, ): 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): """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): """Ignore unknown nodes.""" def visit_title(self, node): """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): """Record first section ID.""" self.current_refid = node["ids"][0] def depart_section(self, node): """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: Union[nodes.literal_block, nodes.doctest_block], language: Optional[str], ): """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)]) 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, ): """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_prere = r'(?\.)()' # Potentially instead assert an initial closing parenthesis followed by a dot. call_dot_prere = r'(\)\s*\.\s*)' import_prere = ( r'((import\s+(\(\s*)?)' r'|(,\s*))' ) from_prere = r'(from\s+)' no_dot_postre = r'(?!(\.)|())' import_postre = ( r'(?=($)|(\s+)|(,)|(\)))(?!)' ) from_postre = r'(?=\s*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_prere + pattern + no_dot_postre elif 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_prere + pattern + no_dot_postre elif name.context == LinkContext.import_from: pattern = import_from_pattern.format(name=name.code_str) return from_prere + pattern + from_postre elif name.context == LinkContext.import_target: pattern = import_target_pattern.format(name=name.code_str) return import_prere + pattern + import_postre ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710402523.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/extension/cache.py0000666000000000000000000000273714574525733023125 0ustar00"""Extension data cache.""" import json from dataclasses import asdict from pathlib import Path from typing import Dict, List from .block import CodeExample, Name, SourceTransform class DataCache: """Data cache.""" cache_filename = "codeautolink-cache.json" def __init__(self, cache_dir: str, src_dir: str): self.cache_dir: Path = Path(cache_dir) self.src_dir: Path = Path(src_dir) self.transforms: Dict[str, List[SourceTransform]] = {} def read(self): """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): """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=1710402523.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/extension/directive.py0000666000000000000000000000735314574525733024037 0ustar00"""Directive implementations.""" 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): 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 = {"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): 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): 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): 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): """Ignore unknown nodes.""" def unknown_visit(self, node): """Remove nodes.""" if isinstance(node, DeferredExamples): # Remove surrounding paragraph too node.parent.parent.remove(node.parent) return elif isinstance(node, (ConcatMarker, PrefaceMarker, SkipMarker)): node.parent.remove(node) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710444733.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/extension/resolve.py0000666000000000000000000001356714574650275023544 0ustar00"""Resolve import locations and type hints.""" from dataclasses import dataclass from functools import lru_cache from importlib import import_module from inspect import isclass, isroutine from typing import Any, Callable, List, Optional, Tuple, Union from ..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): """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, 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: raise CouldNotResolve(f"{cursor.location} does not exist.") 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 try: cursor.location = fully_qualified_name(cursor.value) except (AttributeError, TypeError): # Odd construct encountered: don't try to be clever but continue pass 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): """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) -> Optional[type]: """Determine the target of a function return type hint.""" annotations = getattr(func, "__annotations__", {}) ret_annotation = annotations.get("return", None) # Inner type from typing.Optional or Union[None, T] origin = getattr(ret_annotation, "__origin__", None) args = getattr(ret_annotation, "__args__", None) if origin is Union and len(args) == 2: nonetype = type(None) if args[0] is nonetype: ret_annotation = args[1] elif args[1] is nonetype: ret_annotation = args[0] # Try to resolve a string annotation in the module scope if isinstance(ret_annotation, str): location = fully_qualified_name(func) mod, _ = closest_module(tuple(location.split("."))) ret_annotation = getattr(mod, ret_annotation, ret_annotation) if ( not ret_annotation or not isinstance(ret_annotation, type) or hasattr(ret_annotation, "__origin__") ): raise CouldNotResolve( f"Unable to follow return annotation of {get_name_for_debugging(func)}." ) return ret_annotation def fully_qualified_name(thing: Union[type, Callable]) -> str: """Construct the fully qualified name of a type.""" return thing.__module__ + "." + thing.__qualname__ def get_name_for_debugging(thing: Union[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) @lru_cache(maxsize=None) def closest_module(components: Tuple[str, ...]) -> Tuple[Any, int]: """Find closest importable module.""" try: mod = import_module(components[0]) except ImportError as e: raise CouldNotResolve(f"Could not import {components[0]}.") from e for i in range(1, len(components)): try: mod = import_module(".".join(components[: i + 1])) except ImportError: # import failed, exclude previously added item return mod, i # imports succeeded, include all items return mod, len(components) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710402524.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/parse.py0000666000000000000000000006217414574525734021162 0ustar00"""Analyse AST of code blocks to determine used names and their sources.""" import ast import builtins import sys from contextlib import contextmanager from dataclasses import dataclass, field from enum import Enum from functools import wraps from importlib import import_module from typing import Dict, List, Optional, Tuple, Union from .warn import logger, warn_type HAS_WALRUS = sys.version_info >= (3, 8) 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): """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: raise ValueError(f"Invalid AST for component: {node.__class__.__name__}") 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[Optional[PendingAccess]] @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: Union[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: Optional[LinkContext] = None resolved_location: Optional[str] = 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): """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): """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): 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): """Disable splitting Accesses.""" self._no_split, old = (True, self._no_split) yield self._no_split = old @contextmanager def reset_parents(self): """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) if HAS_WALRUS: track_nodes += (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): """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]): """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] ) -> Optional[Access]: """Create access from scope.""" prior = self.pseudo_scopes_stack[-1].get(scope_key, None) if prior is None: return access = Access(LinkContext.none, prior, new_components) self.save_access(access) return access def resolve_pending_access(self, pending: PendingAccess) -> Optional[Access]: """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 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) -> Optional[Access]: """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): 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: Optional[PendingAccess], value: Optional[Access] ): 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: Union[PendingAccess, Assignment, None] ) -> Optional[Access]: """Determine the appropriate processing after tracking an access chain.""" if isinstance(result, Assignment): return self.resolve_assignment(result) elif isinstance(result, PendingAccess): return self.resolve_pending_access(result) def visit_Global(self, node: ast.Global): """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): """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: Union[ast.Import, ast.ImportFrom], prefix: str = ""): """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): 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] 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): """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): """Visit a Name node.""" return PendingAccess([Component.from_ast(node)]) @track_parents def visit_Attribute(self, node: ast.Attribute): """Visit an Attribute node.""" inner: Optional[PendingAccess] = 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): """Visit a Call node.""" inner: Optional[PendingAccess] = 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): """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 else: with self.reset_parents(): for element in node.elts: self.visit(element) @track_parents def visit_Assign(self, node: ast.Assign): """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): """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): """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): """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): """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): 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): """Track match alias names.""" return PendingAccess([Component.from_ast(node)]) def visit_AsyncFor(self, node: ast.AsyncFor): """Delegate to sync for.""" self.visit_For(node) def visit_For(self, node: Union[ast.For, ast.AsyncFor]): """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): """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): """Delegate to func def.""" self.visit_FunctionDef(node) @staticmethod def _get_args(node: ast.arguments): posonly = getattr(node, "posonlyargs", []) # only on 3.8+ return node.args + node.kwonlyargs + posonly def visit_FunctionDef(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]): """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): """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): """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): """Delegate to generic comp.""" self.visit_generic_comp([node.elt], node.generators) def visit_SetComp(self, node: ast.SetComp): """Delegate to generic comp.""" self.visit_generic_comp([node.elt], node.generators) def visit_DictComp(self, node: ast.DictComp): """Delegate to generic comp.""" self.visit_generic_comp([node.key, node.value], node.generators) def visit_GeneratorExp(self, node: ast.GeneratorExp): """Delegate to generic comp.""" self.visit_generic_comp([node.elt], node.generators) def visit_comprehension(self, node: ast.comprehension): """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] ): """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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.2872727 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/static/0000777000000000000000000000000014627365715020753 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1638544765.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/static/sphinx-codeautolink.css0000666000000000000000000000052714152432575025451 0ustar00.sphinx-codeautolink-a:link{ color: inherit; text-decoration: none; } .sphinx-codeautolink-a:active{ color: inherit; text-decoration: none; } .sphinx-codeautolink-a:visited{ color: inherit; text-decoration: none; } .sphinx-codeautolink-a:hover{ color: rgb(0, 139, 139); text-decoration: none; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710402523.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink/warn.py0000666000000000000000000000022214574525733021000 0ustar00"""Logging definitions.""" from sphinx.util.logging import getLogger logger = getLogger("sphinx_codeautolink") warn_type = "codeautolink" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.3372817 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink.egg-info/0000777000000000000000000000000014627365715021156 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717431245.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink.egg-info/PKG-INFO0000666000000000000000000001063214627365715022255 0ustar00Metadata-Version: 2.1 Name: sphinx-codeautolink Version: 0.15.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-2023 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.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Documentation Classifier: Topic :: Documentation :: Sphinx Classifier: Topic :: Software Development :: Documentation Requires-Python: >=3.7 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=1717431245.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink.egg-info/SOURCES.txt0000666000000000000000000001175714627365715023055 0ustar00LICENSE 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 src/sphinx_codeautolink/static/sphinx-codeautolink.css tests/__init__.py tests/extension/__init__.py tests/extension/_check.py tests/extension/fail/concat_invalid.txt tests/extension/fail/custom_block_import_invalid.txt tests/extension/fail/custom_block_syntax_error.txt tests/extension/fail/failed_resolve_invalid_location.txt tests/extension/fail/failed_resolve_no_module.txt tests/extension/fail/failed_resolve_return_type.txt tests/extension/fail/match_block_warning.txt tests/extension/fail/missing_inventory.txt tests/extension/fail/no_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_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_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_manual.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/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=1717431245.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink.egg-info/dependency_links.txt0000666000000000000000000000000114627365715025224 0ustar00 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717431245.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink.egg-info/requires.txt0000666000000000000000000000007614627365715023561 0ustar00sphinx>=3.2.0 beautifulsoup4>=4.8.1 [ipython] ipython!=8.7.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717431245.0 sphinx_codeautolink-0.15.2/src/sphinx_codeautolink.egg-info/top_level.txt0000666000000000000000000000002414627365715023704 0ustar00sphinx_codeautolink ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.2877727 sphinx_codeautolink-0.15.2/tests/0000777000000000000000000000000014627365715013765 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889367.0 sphinx_codeautolink-0.15.2/tests/__init__.py0000666000000000000000000000047114167237127016072 0ustar00import 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=1717431245.2887728 sphinx_codeautolink-0.15.2/tests/extension/0000777000000000000000000000000014627365715016001 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713370807.0 sphinx_codeautolink-0.15.2/tests/extension/__init__.py0000666000000000000000000001767214607773267020132 0ustar00import re import sys from pathlib import Path from typing import Dict 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 = { "ref_fluent_attrs.txt": sys.version_info < (3, 8), "ref_fluent_call.txt": sys.version_info < (3, 8), "ref_import_from_complex.txt": sys.version_info < (3, 8), } 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): 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): 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(*args, **kwargs): raise ValueError("ValueError") def raise_nomsg(*args, **kwargs): 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 map(lambda x: f"F{x}", 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 ) -> 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: raise RuntimeError("Sphinx build failed!") return build_dir / builder ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1672772488.0 sphinx_codeautolink-0.15.2/tests/extension/_check.py0000666000000000000000000000301414355075610017553 0ustar00from 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(): doc = str(doc) 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: soup = BeautifulSoup(sess.get(base).text, "html.parser") external_site_ids[base] = gather_ids(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}!" ) total += 1 return total def gather_ids(soup: BeautifulSoup) -> set: """Gather all HTML IDs from a given page.""" return set(tag["id"] for tag in soup.find_all(id=True)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.2967746 sphinx_codeautolink-0.15.2/tests/extension/fail/0000777000000000000000000000000014627365715016714 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/fail/concat_invalid.txt0000666000000000000000000000025214167236671022426 0ustar00# split Test project ============ .. autolink-concat:: incorrect .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889367.0 sphinx_codeautolink-0.15.2/tests/extension/fail/custom_block_import_invalid.txt0000666000000000000000000000033514167237127025234 0ustar00codeautolink_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=1675584271.0 sphinx_codeautolink-0.15.2/tests/extension/fail/custom_block_syntax_error.txt0000666000000000000000000000027014367661417024756 0ustar00codeautolink_custom_blocks = {"python": "parser_func.syntax_error"} # split Test project ============ .. code:: python import test_project .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674905639.0 sphinx_codeautolink-0.15.2/tests/extension/fail/failed_resolve_invalid_location.txt0000666000000000000000000000030214365204047026016 0ustar00codeautolink_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=1675099278.0 sphinx_codeautolink-0.15.2/tests/extension/fail/failed_resolve_no_module.txt0000666000000000000000000000030414365776216024477 0ustar00codeautolink_warn_on_failed_resolve = True # split Test project ============ .. code:: python import test_project test_project.Bar().__eq__().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674905639.0 sphinx_codeautolink-0.15.2/tests/extension/fail/failed_resolve_return_type.txt0000666000000000000000000000031014365204047025057 0ustar00codeautolink_warn_on_failed_resolve = True # split Test project ============ .. code:: python import test_project test_project.optional_counter().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674905639.0 sphinx_codeautolink-0.15.2/tests/extension/fail/match_block_warning.txt0000666000000000000000000000030614365204047023434 0ustar00codeautolink_custom_blocks = {"python": "parser_func.manipulate_original_source"} # split Test project ============ .. code:: python import test_project .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1653403409.0 sphinx_codeautolink-0.15.2/tests/extension/fail/missing_inventory.txt0000666000000000000000000000023114243167421023223 0ustar00codeautolink_warn_on_missing_inventory = True # split Test project ============ .. code:: python import test_project test_project.bar() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674858705.0 sphinx_codeautolink-0.15.2/tests/extension/fail/no_module_docs.txt0000666000000000000000000000015214365050321022423 0ustar00# split Test project ============ .. code:: python import test_project test_project.bar() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674905639.0 sphinx_codeautolink-0.15.2/tests/extension/fail/ref_import_star_invalid.txt0000666000000000000000000000017114365204047024346 0ustar00# split Test project ============ .. code:: python from non_project import * .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/fail/ref_invalid_block_type.txt0000666000000000000000000000022214167236671024143 0ustar00# split Test project ============ .. code:: python >>> import test_project >>> test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674905639.0 sphinx_codeautolink-0.15.2/tests/extension/fail/ref_invalid_syntax.txt0000666000000000000000000000035614365204047023336 0ustar00# 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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/fail/ref_invalid_syntax_complex.txt0000666000000000000000000000043314167236671025071 0ustar00codeautolink_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=1641889367.0 sphinx_codeautolink-0.15.2/tests/extension/fail/ref_ipython_non-python.txt0000666000000000000000000000041614167237127024167 0ustar00extensions.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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/fail/skip_invalid.txt0000666000000000000000000000025014167236671022123 0ustar00# split Test project ============ .. autolink-skip:: incorrect .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1717431245.32928 sphinx_codeautolink-0.15.2/tests/extension/ref/0000777000000000000000000000000014627365715016555 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/concat_default.txt0000666000000000000000000000055514167236671022273 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/concat_off.txt0000666000000000000000000000042014167236671021410 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/concat_on_across_sections.txt0000666000000000000000000000054214167236671024540 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/concat_on_breaks.txt0000666000000000000000000000042414167236671022605 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/concat_section_breaks.txt0000666000000000000000000000052714167236671023641 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/concat_section_returns_to_global.txt0000666000000000000000000000067314167236671026120 0ustar00test_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=1713370439.0 sphinx_codeautolink-0.15.2/tests/extension/ref/custom_block.txt0000666000000000000000000000051214607772507021777 0ustar00test_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=1713370877.0 sphinx_codeautolink-0.15.2/tests/extension/ref/custom_block_imported.txt0000666000000000000000000000044314607773375023711 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/empty_project.txt0000666000000000000000000000007214167236671022176 0ustar00# split # split Test project ============ Some text. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674847415.0 sphinx_codeautolink-0.15.2/tests/extension/ref/inventory_map_is_needed.txt0000666000000000000000000000061614365022267024201 0ustar00test_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=1674846914.0 sphinx_codeautolink-0.15.2/tests/extension/ref/inventory_map_not_needed.txt0000666000000000000000000000043214365021302024347 0ustar00test_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=1710442074.0 sphinx_codeautolink-0.15.2/tests/extension/ref/line_numbers.txt0000666000000000000000000000030714574643132021771 0ustar00test_project test_project.bar # split # split Test project ============ .. code-block:: python :linenos: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/non_python_block.txt0000666000000000000000000000021614167236671022657 0ustar00# split # split Test project ============ .. code:: import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_concatenated.txt0000666000000000000000000000041314167236671023426 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_consumed_non_python.txt0000666000000000000000000000031214167236671025064 0ustar00# split # split Test project ============ .. autolink-preface:: import test_project .. code:: test_project.bar() .. code:: python test_project.Foo .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_global.txt0000666000000000000000000000035114167236671022237 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_global_multiline.txt0000666000000000000000000000035514167236671024325 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_global_overwritten.txt0000666000000000000000000000040614167236671024710 0ustar00# 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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_multiline.txt0000666000000000000000000000036314167236671023004 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_multiline_and_arg.txt0000666000000000000000000000035714167236671024462 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_multiple.txt0000666000000000000000000000037614167236671022641 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/preface_single.txt0000666000000000000000000000027014167236671022260 0ustar00test_project.bar # split # split Test project ============ .. autolink-preface:: import test_project .. code:: python test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1672389582.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_assign_targets.txt0000666000000000000000000000040514353521716023155 0ustar00test_project bar Foo target bar target arg Foo # split # split Test project ============ .. code:: python from test_project import bar, Foo target = bar() target def f(arg: Foo): pass .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1713369270.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_builtins.txt0000666000000000000000000000043514607770266022004 0ustar00print 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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_chain.txt0000666000000000000000000000030014167236671021222 0ustar00test_project test_project.bar attr # split # split Test project ============ .. code:: python import test_project test_project.bar().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1653403409.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_chain_call.txt0000666000000000000000000000030014243167421022204 0ustar00test_project test_project.Foo bute # split # split Test project ============ .. code:: python import test_project test_project.Foo()().bute .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1653403409.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_class_attr.txt0000666000000000000000000000052614243167421022300 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_class_meth_returns_self.txt0000666000000000000000000000033314167236671025063 0ustar00test_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=1653403409.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_class_type_attr.txt0000666000000000000000000000032514243167421023336 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_decorator.txt0000666000000000000000000000037514167236671022136 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_doctest.txt0000666000000000000000000000055214167236671021616 0ustar00test_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=1713370438.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_extdoctest_doctest.txt0000666000000000000000000000114414607772506024063 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_extdoctest_testcode.txt0000666000000000000000000000057214167236671024233 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_fluent_attrs.txt0000666000000000000000000000046214167236671022663 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_fluent_call.txt0000666000000000000000000000055214167236671022441 0ustar00test_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=1674858761.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_func_no_module.txt0000666000000000000000000000045514365050411023131 0ustar00datetime 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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_import_as.txt0000666000000000000000000000031114167236671022137 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_import_from.txt0000666000000000000000000000023014167236671022477 0ustar00test_project bar # split # split Test project ============ .. code:: python from test_project import bar .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1717428279.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_import_from_complex.txt0000666000000000000000000000061314627360067024231 0ustar00test_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=1717426980.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_import_multiple.txt0000666000000000000000000000035214627355444023375 0ustar00test_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=1717427167.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_import_multiple_libs.txt0000666000000000000000000000043414627355737024414 0ustar00json 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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_import_star.txt0000666000000000000000000000022114167236671022505 0ustar00test_project # split # split Test project ============ .. code:: python from test_project import * .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_import_submodule.txt0000666000000000000000000000036614167236671023545 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_imported_func.txt0000666000000000000000000000030014167236671022776 0ustar00test_project.subfoo # split # split Test project ============ .. autolink-preface:: import test_project .. code:: python test_project.subfoo .. automodule:: test_project.sub ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_imported_func_relocated.txt0000666000000000000000000000031614167236671025027 0ustar00test_project test_project.subfoo # split # split Test project ============ .. code:: python import test_project test_project.subfoo .. automodule:: test_project :imported-members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_inherited.txt0000666000000000000000000000060314167236671022121 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_intersphinx_only.txt0000666000000000000000000000042714167236671023566 0ustar00numpy 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=1674848051.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_ipython.txt0000666000000000000000000000103414365023463021630 0ustar00test_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=1641889367.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_ipython3.txt0000666000000000000000000000030414167237127021716 0ustar00test_project test_project.bar # split # split Test project ============ .. code:: ipython3 %cd subdir import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1672389582.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_ipython_directive.txt0000666000000000000000000000052014353521716023666 0ustar00test_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=1674756748.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_ipython_directive_comment.txt0000666000000000000000000000046014364541214025410 0ustar00test_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=1673817331.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_ipython_syntax.txt0000666000000000000000000000110314361066363023235 0ustar00# 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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_optional.txt0000666000000000000000000000031014167236671021766 0ustar00test_project test_project.optional attr # split # split Test project ============ .. code:: python import test_project test_project.optional().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_optional_manual.txt0000666000000000000000000000031014167236671023323 0ustar00test_project test_project.optional attr # split # split Test project ============ .. code:: python import test_project test_project.optional().attr .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889367.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_py.txt0000666000000000000000000000025714167237127020600 0ustar00test_project test_project.bar # split # split Test project ============ .. code:: py import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_pycon.txt0000666000000000000000000000063314167236671021301 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_shadow_builtin.txt0000666000000000000000000000041414167236671023161 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/ref_simple.txt0000666000000000000000000000026514167236671021443 0ustar00test_project test_project.bar # split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/search_css_duplicates_removed.txt0000666000000000000000000000037214167236671025370 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/skip_file.txt0000666000000000000000000000052014167236671021255 0ustar00# 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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/skip_next.txt0000666000000000000000000000041414167236671021316 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/skip_next_consumed_non_python.txt0000666000000000000000000000040514167236671025466 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/skip_off.txt0000666000000000000000000000065214167236671021116 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/ref/skip_section.txt0000666000000000000000000000056314167236671022011 0ustar00test_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 ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1717431245.32978 sphinx_codeautolink-0.15.2/tests/extension/src/0000777000000000000000000000000014627365715016570 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674905740.0 sphinx_codeautolink-0.15.2/tests/extension/src/parser_func.py0000666000000000000000000000031514365204214021431 0ustar00def 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 ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1717431245.33078 sphinx_codeautolink-0.15.2/tests/extension/src/test_project/0000777000000000000000000000000014627365715021275 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710402523.0 sphinx_codeautolink-0.15.2/tests/extension/src/test_project/__init__.py0000666000000000000000000000167514574525733023416 0ustar00"""Docstring.""" from typing import Optional, Union from .sub import SubBar, subfoo # NOQA 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() -> Optional[Foo]: """Return optional type.""" def optional_manual() -> Union[None, Foo]: """Return manually constructed optional type.""" def optional_counter() -> Union[Foo, Baz]: """Failing case for incorrect optional type handling.""" def compile(): """Shadows built in compile function.""" class Child(Foo): """Foo child class.""" def sub_return() -> SubBar: """Returns a type in a submodule.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674846914.0 sphinx_codeautolink-0.15.2/tests/extension/src/test_project/sub.py0000666000000000000000000000026014365021302022412 0ustar00def subfoo(): """Function in submodule.""" class SubBar: """Class in submodule to be imported and documented in the main module.""" sub_attr: str = "yo" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.3342807 sphinx_codeautolink-0.15.2/tests/extension/table/0000777000000000000000000000000014627365715017070 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674847851.0 sphinx_codeautolink-0.15.2/tests/extension/table/tab_autodoc_empty.txt0000666000000000000000000000034214365023153023314 0ustar00Testproject # 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=1674847757.0 sphinx_codeautolink-0.15.2/tests/extension/table/tab_autodoc_links.txt0000666000000000000000000000031714365023015023275 0ustar00Testproject Testproject # split codeautolink_autodoc_inject = True # split Test project ============ .. code:: python import test_project test_project.bar .. automodule:: test_project ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/table/tab_manual_collapse.txt0000666000000000000000000000033014167236671023607 0ustar00Testproject # split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. autolink-examples:: test_project.bar :collapse: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/table/tab_manual_empty.txt0000666000000000000000000000027414167236671023152 0ustar00# split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project .. autolink-examples:: test_project.Foo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/table/tab_manual_links.txt0000666000000000000000000000033314167236671023130 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/table/tab_manual_links_subsection.txt0000666000000000000000000000042414167236671025367 0ustar00test_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=1641889209.0 sphinx_codeautolink-0.15.2/tests/extension/table/tab_no_table.txt0000666000000000000000000000022314167236671022234 0ustar00# split # split Test project ============ .. code:: python import test_project test_project.bar() .. automodule:: test_project ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1717431245.3367813 sphinx_codeautolink-0.15.2/tests/parse/0000777000000000000000000000000014627365715015077 5ustar00././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1674905639.0 sphinx_codeautolink-0.15.2/tests/parse/__init__.py0000666000000000000000000001133114365204047017174 0ustar00import 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): 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=1672772488.0 sphinx_codeautolink-0.15.2/tests/parse/_util.py0000666000000000000000000000251614355075610016557 0ustar00import sys from functools import wraps import pytest from sphinx_codeautolink.parse import parse_names skip_walrus = pytest.mark.skipif( sys.version_info < (3, 8), reason="Walrus introduced in Python 3.8." ) 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): 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=1674905639.0 sphinx_codeautolink-0.15.2/tests/parse/assign.py0000666000000000000000000002273514365204047016733 0ustar00import pytest from ._util import refs_equal, skip_walrus 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 @skip_walrus @refs_equal def test_walrus_uses_imported(self): s = "import a\n(a := 1)\na" refs = [("a", "a")] return s, refs @skip_walrus @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 @skip_walrus @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 @skip_walrus @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 @skip_walrus @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=1672772488.0 sphinx_codeautolink-0.15.2/tests/parse/chain.py0000666000000000000000000000134314355075610016522 0ustar00from ._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=1672772488.0 sphinx_codeautolink-0.15.2/tests/parse/match.py0000666000000000000000000001011214355075610016526 0ustar00import 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=1672772488.0 sphinx_codeautolink-0.15.2/tests/parse/scope.py0000666000000000000000000002464514355075610016563 0ustar00import pytest from ._util import refs_equal, skip_type_union, skip_walrus 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 @skip_walrus @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=1717430834.0 sphinx_codeautolink-0.15.2/tox.ini0000666000000000000000000000471614627365062014141 0ustar00[tox] min_version = 4 envlist = flake8 doc8 pydocstyle docs black-check isort-check build-{lin,mac,win} coverage labels = lint = flake8, doc8, pydocstyle, black-check, isort-check format = black-format, isort-format docs = docs build = build-{lin,mac,win} test = coverage publish = publish-{lin,mac,win} no_package = true [flake8] max-line-length = 80 select = C,E,F,W,W504,B,B9 ignore = E203,E501,W503,B905,B906 [doc8] ; Ignore for Windows development 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:flake8] description = Check code style allowlist_externals = flake8 commands = flake8 src tests [testenv:pydocstyle] description = Check documentation string style allowlist_externals = pydocstyle commands = pydocstyle src [testenv:docs] description = Build Sphinx documentation allowlist_externals = sphinx-build change_dir = docs commands = sphinx-build -M html src build -W [testenv:black-check] description = Check code formatting allowlist_externals = black commands = black . --check [testenv:black-format] description = Format code allowlist_externals = black commands = black . [testenv:isort-check] description = Check import sorting allowlist_externals = isort commands = isort . --check-only [testenv:isort-format] description = Format imports allowlist_externals = isort commands = isort . [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