1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
|
# AutoPyTabs
Automatically generate code examples for different Python versions in
[mkdocs](https://www.mkdocs.org) or [Sphinx](https://www.sphinx-doc.org) based documentations, or a plain
[markdown](https://python-markdown.github.io/) workflow, making use of the
[pymdown "tabbed"](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/) markdown extension for markdown,
and [sphinx{design} tabs](https://sphinx-design.readthedocs.io/en/latest/tabs.html) 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"](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/) extension for markdown, or
[sphinx{design} tabs](https://sphinx-design.readthedocs.io/en/latest/tabs.html) 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 awsome
[ruff](https://github.com/charliermarsh/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
<hr>
## Table of contents
1. [Usage with mkdocs / markdown](#usage-markdown)
1. [Configuration](#markdown-config)
2. [Differences between the mkdocs plugin vs markdown extension](#differences-between-the-mkdocs-plugin-and-markdown-extension)
3. [Examples](#markdown-examples)
4. [Selectively disable](#selectively-disable)
5. [Compatibility with `pymdownx.snippets`](#compatibility-with-pymdownxsnippets)
2. [Usage with Sphinx](#usage-with-sphinx)
1. [Configuration](#sphinx-config)
2. [Directives](#directives)
3. [Examples](#sphinx-examples)
4. [Compatibility with other extensions](#compatibility-with-other-extensions)
<hr>
## Installation
For mkdocs: `pip install auto-pytabs[mkdocs]`
For markdown: `pip install auto-pytabs[markdown]`
For sphinx: `pip install auto-pytabs[sphinx]`
<h2 id="usage-markdown">Usage with mkdocs / markdown</h2>
<h3 id="markdown-config">Configuration</h3>
#### Mkdocs plugin
```yaml
site_name: My Docs
markdown_extensions:
- pymdownx.tabbed:
plugins:
- auto_pytabs:
min_version: "3.7" # optional
max_version: "3.11" # 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, 7)` | 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
```python
import markdown
md = markdown.Markdown(
extensions=["auto_pytabs"],
extension_configs={
"auto_pytabs": {
"min_version": "3.7", # optional
"max_version": "3.11", # 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](https://www.mkdocs.org/dev-guide/plugins/#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:
```python
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()
```
<h3 id="markdown-examples">Examples</h3>
**Input**
<pre>
```python
from typing import Optional, Dict
def foo(bar: Optional[str]) -> Dict[str, str]:
...
```
</pre>
**Equivalent markdown**
<pre>
=== "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]:
...
```
</pre>
#### Nested blocks
Nested tabs are supported as well:
**Input**
<pre>
=== "Level 1-1"
=== "Level 2-1"
```python
from typing import List
x: List[str]
```
=== "Level 2-2"
Hello, world!
=== "Level 1-2"
Goodbye, world!
</pre>
**Equivalent markdown**
<pre>
=== "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!
</pre>
### 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
<hr>
## Usage with Sphinx
AutPyTabs provides a Sphinx extension `auto_pytabs.sphinx_ext`, enabling its functionality
for the `.. code-block` and `.. literalinclude` directives.
<h3 id="sphinx-config">Configuration</h3>
#### Example configuration
```python
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](#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 |
<h3 id="sphinx-examples">Examples</h3>
**Input**
```rst
.. code-block:: python
from typing import Optional, Dict
def foo(bar: Optional[str]) -> Dict[str, str]:
...
```
**Equivalent ReST**
```rst
.. 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).
### 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](#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.
|