Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Sync dependencies
run: |
python -m pip install --upgrade pip
pip install .[dev]
uv sync --extra dev
- name: Test with pytest
run: |
pytest --pyargs --numprocesses=logical tests
uv run pytest --pyargs --numprocesses=logical tests
15 changes: 8 additions & 7 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v6
# Installing all dependencies and not just the linters as mypy needs them for type checking
- name: Install dependencies
- name: Sync dependencies
run: |
python -m pip install --upgrade pip
pip install hatch
uv sync --extra dev
- name: Lint with ruff
run: |
hatch run ruff check .
uv run hatch run ruff check .
- name: Check formatting with ruff
run: |
hatch run ruff format --diff .
hatch run ruff format --check .
uv run hatch run ruff format --diff .
uv run hatch run ruff format --check .
- name: Lint with mypy
run: |
hatch run mypy sphinxext_altair tests
uv run hatch run mypy sphinxext_altair tests
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,26 @@ pip install sphinxext-altair
```

# Contributing
It's recommended to use a virtual environment for development:
It's recommended to use `uv` for development:

```bash
python -m venv .venv
# Install the project in editable mode including development dependencies
pip install -e '.[dev]'
uv sync --extra dev
```

`sphinxext-altair` uses [ruff](https://github.com/astral-sh/ruff) for code formatting and linting rules, [mypy](https://github.com/python/mypy) for static type checking, and [pytest](https://github.com/pytest-dev/pytest) for testing.
All of these tools can be executed by running:

```bash
hatch test
uv run hatch test
```

As part of those tests, a Sphinx documentation is built at `tests/roots/test-altairplot`. You can manually build this documentation and view it which is very useful during development of a new feature.

For example, if you want to add a new option to the `altair-plot` directive, you can add another example in the file `tests/roots/test-altairplot/index.rst` and then build and view the documentation by running:

```bash
hatch run doc:clean-build-html
hatch run doc:serve
uv run hatch run doc:clean-build-html
uv run hatch run doc:serve
```

The test documentation can now be viewed at [http://localhost:8000](http://localhost:8000).
8 changes: 4 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

4. Run test suite again after commit above to make sure everything passes:

hatch run test
uv run hatch test

5. Tag the release:

Expand All @@ -22,12 +22,12 @@

6. Build source & wheel distributions:

hatch clean # clean old builds & distributions
hatch build # create a source distribution and universal wheel
uv run hatch clean # clean old builds & distributions
uv run hatch build # create a source distribution and universal wheel

7. publish to PyPI (Requires correct PyPI owner permissions):

hatch publish
uv run hatch publish

8. update version to e.g. 0.3.0dev in `sphinxext_altair/__init__.py`

Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ keywords = [
]
requires-python = ">=3.9"
dynamic = ["version"]
license-files = { paths = ["LICENSE"] }
license-files = ["LICENSE"]
classifiers= [
"Development Status :: 5 - Production/Stable",
"Environment :: Plugins",
Expand Down Expand Up @@ -297,6 +297,9 @@ split-on-trailing-comma = false
[tool.ruff.lint.per-file-ignores]
"!sphinxext_altair/altairplot.py" = ["ANN"]

[tool.mypy]
exclude = "^tests/roots/"

[[tool.mypy.overrides]]
module = ["altair.*"]
ignore_missing_imports = true
3 changes: 2 additions & 1 deletion sphinxext_altair/altairplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ def run(self) -> list[nodes.Element]:
links=self.options.get("links", app.builder.config.altairplot_links),
output=self.options.get("output", "plot"),
strict="strict" in self.options,
**{"chart-var-name": self.options.get("chart-var-name", None)},
)
if "chart-var-name" in self.options:
plot_node["chart-var-name"] = self.options["chart-var-name"]
if "alt" in self.options:
plot_node["alt"] = self.options["alt"]

Expand Down
234 changes: 59 additions & 175 deletions tests/test_altairplot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ruff: noqa: E501
# Tests are inspired by the test suite of sphinx itself
from __future__ import annotations

import json
import re
from typing import TYPE_CHECKING, cast

Expand Down Expand Up @@ -78,177 +78,61 @@ def test_altairplotdirective(app: Sphinx) -> None:
assert result.count(VEGAEMBED_JS_URL_DEFAULT)
assert result.count(VEGALITE_JS_URL_DEFAULT)
assert result.count(VEGA_JS_URL_DEFAULT)
current_url = SCHEMA_URL # noqa: F841
# TODO: use regex to cut down length & avoid hardcoded `SCHEMA_URL`

assert result.count('class="test-class"') == 1
block_no_output = """\
<div class="highlight-python notranslate" id="index-rst-altair-source-0"><div class="highlight"><pre><span></span><span class="kn">import</span> <span class="nn">altair</span> <span class="k">as</span> <span class="nn">alt</span>

<span class="n">data</span> <span class="o">=</span> <span class="n">alt</span><span class="o">.</span><span class="n">Data</span><span class="p">(</span>
<span class="n">values</span><span class="o">=</span><span class="p">[</span>
<span class="p">{</span><span class="s2">&quot;x&quot;</span><span class="p">:</span> <span class="s2">&quot;A&quot;</span><span class="p">,</span> <span class="s2">&quot;y&quot;</span><span class="p">:</span> <span class="mi">5</span><span class="p">},</span>
<span class="p">{</span><span class="s2">&quot;x&quot;</span><span class="p">:</span> <span class="s2">&quot;B&quot;</span><span class="p">,</span> <span class="s2">&quot;y&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">},</span>
<span class="p">{</span><span class="s2">&quot;x&quot;</span><span class="p">:</span> <span class="s2">&quot;C&quot;</span><span class="p">,</span> <span class="s2">&quot;y&quot;</span><span class="p">:</span> <span class="mi">6</span><span class="p">},</span>
<span class="p">{</span><span class="s2">&quot;x&quot;</span><span class="p">:</span> <span class="s2">&quot;D&quot;</span><span class="p">,</span> <span class="s2">&quot;y&quot;</span><span class="p">:</span> <span class="mi">7</span><span class="p">},</span>
<span class="p">{</span><span class="s2">&quot;x&quot;</span><span class="p">:</span> <span class="s2">&quot;E&quot;</span><span class="p">,</span> <span class="s2">&quot;y&quot;</span><span class="p">:</span> <span class="mi">2</span><span class="p">},</span>
<span class="p">]</span>
<span class="p">)</span>
</pre></div>
</div>"""
assert block_no_output in result

block_plot_1 = """\
<div class="highlight-python notranslate" id="index-rst-altair-source-1"><div class="highlight"><pre><span></span><span class="n">alt</span><span class="o">.</span><span class="n">Chart</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="o">.</span><span class="n">mark_bar</span><span class="p">()</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span>
<span class="n">x</span><span class="o">=</span><span class="s2">&quot;x:N&quot;</span><span class="p">,</span>
<span class="n">y</span><span class="o">=</span><span class="s2">&quot;y:Q&quot;</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>

<div id="index-rst-altair-plot-1">
<script>
// embed when document is loaded, to ensure vega library is available
// this works on all modern browsers, except IE8 and older
document.addEventListener("DOMContentLoaded", function(event) {
var spec = {"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}}, "data": {"values": [{"x": "A", "y": 5}, {"x": "B", "y": 3}, {"x": "C", "y": 6}, {"x": "D", "y": 7}, {"x": "E", "y": 2}]}, "mark": {"type": "bar"}, "encoding": {"x": {"field": "x", "type": "nominal"}, "y": {"field": "y", "type": "quantitative"}}, "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json"};
var opt = {
"mode": "vega-lite",
"renderer": "canvas",
"actions": {"editor": true, "source": true, "export": true}
};
vegaEmbed('#index-rst-altair-plot-1', spec, opt).catch(console.err);
});
</script>
</div>"""
assert block_plot_1 in result

block_plot_2 = """\
<div id="index-rst-altair-plot-2">
<script>
// embed when document is loaded, to ensure vega library is available
// this works on all modern browsers, except IE8 and older
document.addEventListener("DOMContentLoaded", function(event) {
var spec = {"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}}, "data": {"values": [{"x": "A", "y": 5}, {"x": "B", "y": 3}, {"x": "C", "y": 6}, {"x": "D", "y": 7}, {"x": "E", "y": 2}]}, "mark": {"type": "bar"}, "encoding": {"x": {"field": "x", "type": "nominal"}, "y": {"field": "y", "type": "quantitative"}}, "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json"};
var opt = {
"mode": "vega-lite",
"renderer": "canvas",
"actions": {"editor": true, "source": true, "export": true}
};
vegaEmbed('#index-rst-altair-plot-2', spec, opt).catch(console.err);
});
</script>
</div><div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="n">alt</span><span class="o">.</span><span class="n">Chart</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="o">.</span><span class="n">mark_bar</span><span class="p">()</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span>
<span class="n">x</span><span class="o">=</span><span class="s2">&quot;x:N&quot;</span><span class="p">,</span>
<span class="n">y</span><span class="o">=</span><span class="s2">&quot;y:Q&quot;</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>"""
assert block_plot_2 in result

block_3 = """\
<div class="highlight-python notranslate" id="index-rst-altair-source-3"><div class="highlight"><pre><span></span><span class="n">data</span>
</pre></div>
</div>
<div class="highlight-none notranslate"><div class="highlight"><pre><span></span> Data({
values: [{&#39;x&#39;: &#39;A&#39;, &#39;y&#39;: 5}, {&#39;x&#39;: &#39;B&#39;, &#39;y&#39;: 3}, {&#39;x&#39;: &#39;C&#39;, &#39;y&#39;: 6}, {&#39;x&#39;: &#39;D&#39;, &#39;y&#39;: 7}, {&#39;x&#39;: &#39;E&#39;, &#39;y&#39;: 2}]
})
</pre></div>
</div>"""
assert block_3 in result

block_plot_4 = """\
<p>No code should be shown, only the plot.</p>

<div id="index-rst-altair-plot-4">
<script>
// embed when document is loaded, to ensure vega library is available
// this works on all modern browsers, except IE8 and older
document.addEventListener("DOMContentLoaded", function(event) {
var spec = {"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}}, "data": {"values": [{"x": "A", "y": 5}, {"x": "B", "y": 3}, {"x": "C", "y": 6}, {"x": "D", "y": 7}, {"x": "E", "y": 2}]}, "mark": {"type": "bar"}, "encoding": {"x": {"field": "x", "type": "nominal"}, "y": {"field": "y", "type": "quantitative"}}, "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json"};
var opt = {
"mode": "vega-lite",
"renderer": "canvas",
"actions": {"editor": true, "source": true, "export": true}
};
vegaEmbed('#index-rst-altair-plot-4', spec, opt).catch(console.err);
});
</script>
</div>"""
assert block_plot_4 in result

block_plot_5 = """\
<p>The code should be hidden and can be expanded.</p>
<details><summary><a>Click to show code</a></summary><div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="n">alt</span><span class="o">.</span><span class="n">Chart</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="o">.</span><span class="n">mark_bar</span><span class="p">()</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span>
<span class="n">x</span><span class="o">=</span><span class="s2">&quot;x:N&quot;</span><span class="p">,</span>
<span class="n">y</span><span class="o">=</span><span class="s2">&quot;y:Q&quot;</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>
</details>
<div id="index-rst-altair-plot-5">
<script>
// embed when document is loaded, to ensure vega library is available
// this works on all modern browsers, except IE8 and older
document.addEventListener("DOMContentLoaded", function(event) {
var spec = {"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}}, "data": {"values": [{"x": "A", "y": 5}, {"x": "B", "y": 3}, {"x": "C", "y": 6}, {"x": "D", "y": 7}, {"x": "E", "y": 2}]}, "mark": {"type": "bar"}, "encoding": {"x": {"field": "x", "type": "nominal"}, "y": {"field": "y", "type": "quantitative"}}, "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json"};
var opt = {
"mode": "vega-lite",
"renderer": "canvas",
"actions": {"editor": true, "source": true, "export": true}
};
vegaEmbed('#index-rst-altair-plot-5', spec, opt).catch(console.err);
});
</script>
</div>"""
assert block_plot_5 in result

block_plot_6 = """\
<div class="highlight-python notranslate" id="index-rst-altair-source-6"><div class="highlight"><pre><span></span><span class="n">alt</span><span class="o">.</span><span class="n">Chart</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="o">.</span><span class="n">mark_bar</span><span class="p">()</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span>
<span class="n">x</span><span class="o">=</span><span class="s2">&quot;x:N&quot;</span><span class="p">,</span>
<span class="n">y</span><span class="o">=</span><span class="s2">&quot;y:Q&quot;</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>

<div id="index-rst-altair-plot-6">
<script>
// embed when document is loaded, to ensure vega library is available
// this works on all modern browsers, except IE8 and older
document.addEventListener("DOMContentLoaded", function(event) {
var spec = {"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}}, "data": {"values": [{"x": "A", "y": 5}, {"x": "B", "y": 3}, {"x": "C", "y": 6}, {"x": "D", "y": 7}, {"x": "E", "y": 2}]}, "mark": {"type": "bar"}, "encoding": {"x": {"field": "x", "type": "nominal"}, "y": {"field": "y", "type": "quantitative"}}, "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json"};
var opt = {
"mode": "vega-lite",
"renderer": "canvas",
"actions": {"editor": true, "source": false, "export": false}
};
vegaEmbed('#index-rst-altair-plot-6', spec, opt).catch(console.err);
});
</script>
</div>"""
assert block_plot_6 in result

block_plot_7 = """\
<div class="highlight-python notranslate" id="index-rst-altair-source-7"><div class="highlight"><pre><span></span><span class="n">alt</span><span class="o">.</span><span class="n">Chart</span><span class="p">(</span><span class="n">data</span><span class="p">)</span><span class="o">.</span><span class="n">mark_bar</span><span class="p">()</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span>
<span class="n">x</span><span class="o">=</span><span class="s2">&quot;x:N&quot;</span><span class="p">,</span>
<span class="n">y</span><span class="o">=</span><span class="s2">&quot;y:Q&quot;</span><span class="p">,</span>
<span class="p">)</span>
</pre></div>
</div>

<div id="index-rst-altair-plot-7" class="test-class">
<script>
// embed when document is loaded, to ensure vega library is available
// this works on all modern browsers, except IE8 and older
document.addEventListener("DOMContentLoaded", function(event) {
var spec = {"config": {"view": {"continuousWidth": 300, "continuousHeight": 300}}, "data": {"values": [{"x": "A", "y": 5}, {"x": "B", "y": 3}, {"x": "C", "y": 6}, {"x": "D", "y": 7}, {"x": "E", "y": 2}]}, "mark": {"type": "bar"}, "encoding": {"x": {"field": "x", "type": "nominal"}, "y": {"field": "y", "type": "quantitative"}}, "$schema": "https://vega.github.io/schema/vega-lite/v5.20.1.json"};
var opt = {
"mode": "vega-lite",
"renderer": "canvas",
"actions": {"editor": true, "source": true, "export": true}
};
vegaEmbed('#index-rst-altair-plot-7', spec, opt).catch(console.err);
});
</script>
</div>"""
assert block_plot_7 in result
assert SCHEMA_URL in result

def extract_embed_values(
plot_id: int,
) -> tuple[dict[str, object], dict[str, object], str | None]:
div_id = f"index-rst-altair-plot-{plot_id}"
match = re.search(
rf'<div id="{div_id}"(?: class="([^"]+)")?>\s*<script>.*?'
rf"var spec = (\{{.*?\}});\s*"
rf"var opt = (\{{.*?\}});\s*"
rf"vegaEmbed\('#{div_id}', spec, opt\)",
result,
re.DOTALL,
)
assert match is not None
class_name = match.group(1)
spec = json.loads(match.group(2))
opt = json.loads(match.group(3))
return spec, opt, class_name

for plot_id in (1, 2, 4, 5, 6, 7):
spec, opt, _ = extract_embed_values(plot_id)
assert spec["$schema"] == SCHEMA_URL
assert opt["mode"] == "vega-lite"
assert opt["renderer"] == "canvas"

assert 'id="index-rst-altair-source-0"' in result
assert '<div id="index-rst-altair-plot-0"' not in result

assert 'id="index-rst-altair-source-1"' in result
_, plot_1_opt, _ = extract_embed_values(1)
assert plot_1_opt["actions"] == {"editor": True, "source": True, "export": True}

code_below_section = re.search(
r'<section id="code-below-plot">.*?</section>', result, re.DOTALL
)
assert code_below_section is not None
assert re.search(
r'<div id="index-rst-altair-plot-2">.*?</div><div class="highlight-python notranslate">',
code_below_section.group(0),
re.DOTALL,
)

assert 'id="index-rst-altair-source-3"' in result
assert "Data({" in result

extract_embed_values(4)
assert 'id="index-rst-altair-source-4"' not in result

assert "Click to show code" in result
assert re.search(r"<details>.*?Click to show code.*?</details>", result, re.DOTALL)
extract_embed_values(5)

_, plot_6_opt, _ = extract_embed_values(6)
assert plot_6_opt["actions"] == {"editor": True, "source": False, "export": False}

_, _, plot_7_class = extract_embed_values(7)
assert plot_7_class == "test-class"
Loading