Skip to content

Automatically generate code examples for different Python versions in mkdocs or Sphinx based documentations

License

Notifications You must be signed in to change notification settings

provinzkraut/AutoPyTabs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

71 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AutoPyTabs

Automatically generate code examples for different Python versions in mkdocs or Sphinx based documentations, or a plain markdown workflow, making use of the pymdown "tabbed" markdown extension for markdown, and sphinx{design} tabs for Sphinx.

Rationale

The problem

Python project documentation typically include code examples. Given that most of the time, a project will support multiple versions of Python, it would be ideal to showcase the adjustments that can or need to be made for different Python versions. This can be achieved by including several versions of the example code, conveniently displayed using the pymdown "tabbed" extension for markdown, or sphinx{design} tabs for Sphinx.

This, however, raises several problems:

  1. Maintaining multiple versions of a single example is tedious and error-prone as they can easily become out of sync
  2. Figuring out which examples need to be changed for which specific Python version is a labour intensive task
  3. Dropping or adding support for Python versions requires revisiting every example in the documentation
  4. Checking potentially ~4 versions of a single example into VCS creates unnecessary noise

Given those, it's no surprise that the current standard is to only show examples for the lowest supported version of Python.

The solution

AutoPyTabs aims to solve all of these problems by automatically generating versions (using the awesome ruff project) of code examples, targeting different Python versions at build-time, based on a base version (the lowest supported Python version). This means that:

  1. There exists only one version of each example: The lowest supported version becomes the source of truth, therefore preventing out-of-sync examples and reducing maintenance burden
  2. Dropping or adding support for Python versions can be done via a simple change in a configuration file

Table of contents

  1. Usage with mkdocs / markdown
    1. Configuration
    2. Differences between the mkdocs plugin vs markdown extension
    3. Examples
    4. Selectively disable
    5. Compatibility with pymdownx.snippets
  2. Usage with Sphinx
    1. Configuration
    2. Directives
    3. Examples
    4. Compatibility with other extensions

Installation

For mkdocs: pip install auto-pytabs[mkdocs] For markdown: pip install auto-pytabs[markdown] For sphinx: pip install auto-pytabs[sphinx]

Usage with mkdocs / markdown

Configuration

Mkdocs plugin

site_name: My Docs
markdown_extensions:
  - pymdownx.tabbed:
plugins:
  - auto_pytabs:
      min_version: "3.7"  # optional
      max_version: "3.12" # optional
      tab_title_template: "Python {min_version}+"  # optional
      no_cache: false  # optional
      default_tab: "highest"  # optional
      reverse_order: false  # optional

Available configuration options

Name Default Description
min_version (3, 7) Minimum python version
max_version (3, 12) Maximum python version
tab_title_template "Python {min_version}+" Template for tab titles
no_cache False Disable file system caching
default_tab highest (highest or lowest) Version tab to preselect
reverse_order False Reverse the order of tabs. Default is to go from lowest to highest version

Markdown extension

import markdown

md = markdown.Markdown(
    extensions=["auto_pytabs"],
    extension_configs={
        "auto_pytabs": {
            "min_version": "3.7",  # optional
            "max_version": "3.12",  # optional
            "tab_title_template": "Python {min_version}+",  # optional
            "default_tab": "highest",  # optional
            "reverse_order": False,  # optional
        }
    },
)

Available configuration options

Name Default Description
min_version (3, 7) Minimum python version to generate code for
max_version (3, 7) Maximum python version to generate code for
tab_title_template "Python {min_version}+" Template for tab titles
default_tab highest (highest or lowest) Version tab to preselect
reverse_order False Reverse the order of tabs. Default is to go from lowest to highest version

Differences between the mkdocs plugin and markdown extension

AutoPyTabs ships as a markdown extension and an mkdocs plugin, both of which can be used in mkdocs. The only difference between them is that the mkdocs plugin supports caching, which can make subsequent builds faster (i.e. when using mkdocs serve). The reason why the markdown extension does not support caching is that markdown does not have clearly defined build steps with wich an extension could interact (like mkdocs plugin events), making it impossible to know when to persist cached items to disk / evict unused items.

If you are using mkdocs, the mkdocs plugin is recommended. If you have caching disabled, there will be no difference either way.

Should you wish to integrate the markdown extension into a build process where you can manually persist the cache after the build, you can explicitly pass it a cache:

import markdown
from auto_pytabs.core import Cache

cache = Cache()

md = markdown.Markdown(
    extensions=["auto_pytabs"],
    extension_configs={
        "auto_pytabs": {
           "cache": cache
        }
    },
)


def build_markdown() -> None:
    md.convertFile("document.md", "document.html")
    cache.persist()

Examples

Input

```python
from typing import Optional, Dict

def foo(bar: Optional[str]) -> Dict[str, str]:
    ...
```

Equivalent markdown

=== "Python 3.7+"
    ```python
    from typing import Optional, Dict

    def foo(bar: Optional[str]) -> Dict[str, str]:
        ...
    ```

=== "Python 3.9+"
    ```python
    from typing import Optional
    
    
    def foo(bar: Optional[str]) -> dict[str, str]:
        ...
    ```

==== "Python 3.10+"
    ```python
    def foo(bar: str | None) -> dict[str, str]:
        ...
    ```

Nested blocks

Nested tabs are supported as well:

Input

=== "Level 1-1"

    === "Level 2-1"

        ```python
        from typing import List
        x: List[str]
        ```

    === "Level 2-2"
    
        Hello, world!

=== "Level 1-2"

    Goodbye, world!

Equivalent markdown

=== "Level 1-1"

    === "Level 2-1"

        === "Python 3.7+"
            ```python
            from typing import List
            x: List[str]
            ```
        
        === "Python 3.9+"
            ```python
            x: list[str]
            ```

    === "Level 2-2"

        Goodbye, world!

=== "Level 1-2"
    Hello, world!
    

Selectively disable

You can disable conversion for a single code block:

<!-- autopytabs: disable-block -->
```python
from typing import Set, Optional

def bar(baz: Optional[str]) -> Set[str]:
    ...
```

Or for whole sections / files

<!-- autopytabs: disable -->
everything after this will be ignored
<!-- autopytabs: enable -->
re-enables conversion again

Compatibility with pymdownx.snippets

If the pymdownx.snippets extension is used, make sure that it runs before AutoPyTab


Usage with Sphinx

AutPyTabs provides a Sphinx extension auto_pytabs.sphinx_ext, enabling its functionality for the .. code-block and .. literalinclude directives.

Configuration

Example configuration

extensions = ["auto_pytabs.sphinx_ext", "sphinx_design"]

auto_pytabs_min_version = (3, 7)  # optional
auto_pytabs_max_version = (3, 11)  # optional
auto_pytabs_tab_title_template = "Python {min_version}+"  # optional 
# auto_pytabs_no_cache = True  # disabled file system caching
# auto_pytabs_compat_mode = True  # enable compatibility mode
# auto_pytabs_default_tab = "lowest"  # Pre-select the tab with the lowest version
# auto_pytabs_reverse_order = True  # reverse the order of tabs to highest > lowest

Available configuration options

Name Default Description
auto_pytabs_min_version (3, 7) Minimum python version to generate code for
auto_pytabs_max_version (3, 7) Maximum python version to generate code for
auto_pytabs_tab_title_template "Python {min_version}+" Template for tab titles
auto_pytabs_no_cache False Disable file system caching
auto_pytabs_compat_mode False Enable compatibility mode
auto_pytabs_default_tab highest Either highest or lowest. Version tab to preselect
auto_pytabs_reverse_order False Reverse the order of tabs. Default is to go from lowest to highest version

Examples

Input

.. code-block:: python

   from typing import Optional, Dict
   
   def foo(bar: Optional[str]) -> Dict[str, str]:
       ...

Equivalent ReST

.. tab-set::

   .. tab-item:: Python 3.7+
   
       .. code-block:: python
       
          from typing import Optional, Dict
      
          def foo(bar: Optional[str]) -> Dict[str, str]:
              ...

   .. tab-item:: Python 3.9+
   
      .. code-block:: python
      
          from typing import Optional
          
          
          def foo(bar: Optional[str]) -> dict[str, str]:
              ...

   .. tab-item:: Python 3.10+
   
      .. code-block:: python
      
          def foo(bar: str | None) -> dict[str, str]:
              ...

Directives

AutoPyTabs overrides the built-in code-block and literal-include directives, extending them with auto-upgrade and tabbing functionality, which means no special directives, and therefore changes to existing documents are needed.

Additionally, a :no-upgrade: option is added to the directives, which can be used to selectively fall back the default behaviour.

Two new directives are provided as well:

  • .. pytabs-code-block::
  • .. pytabs-literalinclude::

which by default act exactly like .. code-block and .. literalinclude respectively, and are mainly to provide AutoPyTab's functionality in compatibility mode.

Compatibility mode

If you don't want the default behaviour of directive overrides, and instead wish to use the .. pytabs- directives manually (e.g. because of compatibility issues with other extensions or because you only want to apply it to select code blocks) you can make use AutoPyTabs' compatibility mode. To enable it, simply use the auto_pytabs_compat_mode = True in conf.py. Now, only content within .. pytabs- directives will be upgraded.

Compatibility with other extensions

Normally the directive overrides don't cause any problems and are very convenient, since no changes to existing documents have to be made. However, if other extensions are included, which themselves override one of those directives, one of them will inadvertently override the other, depending on the order they're defined in extensions.

To combat this, you can use the compatibility mode extension instead, which only includes the new directives.

If you control the conflicting overrides, you can alternatively inherit from auto_py_tabs.sphinx_ext.CodeBlockOverride and auto_py_tabs.sphinx_ext.LiteralIncludeOverride instead of sphinx.directives.code.CodeBlock and sphinx.directives.code.LiteralInclude respectively.