Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

Commit

Permalink
ENH: NodeSaver: suppress unhelpful vega CLI errors
Browse files Browse the repository at this point in the history
  • Loading branch information
jakevdp committed Mar 28, 2020
1 parent 2930857 commit 763288b
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 13 deletions.
64 changes: 51 additions & 13 deletions altair_saver/savers/_node.py
@@ -1,7 +1,7 @@
import functools
import json
import shutil
from typing import Any, Dict, List, Optional
from typing import Any, Callable, Dict, List, Optional

from altair_saver.types import JSONDict, MimebundleContent
from altair_saver._utils import check_output_with_stderr
Expand Down Expand Up @@ -33,34 +33,54 @@ def exec_path(name: str) -> str:
raise ExecutableNotFound(name)


def vl2vg(spec: JSONDict) -> JSONDict:
def vl2vg(
spec: JSONDict, stderr_filter: Optional[Callable[[str], bool]] = None
) -> JSONDict:
"""Compile a Vega-Lite spec into a Vega spec."""
vl2vg = exec_path("vl2vg")
vl_json = json.dumps(spec).encode()
vg_json = check_output_with_stderr([vl2vg], input=vl_json)
vg_json = check_output_with_stderr(
[vl2vg], input=vl_json, stderr_filter=stderr_filter
)
return json.loads(vg_json)


def vg2png(spec: JSONDict, vega_cli_options: Optional[List[str]] = None) -> bytes:
def vg2png(
spec: JSONDict,
vega_cli_options: Optional[List[str]] = None,
stderr_filter: Optional[Callable[[str], bool]] = None,
) -> bytes:
"""Generate a PNG image from a Vega spec."""
vg2png = exec_path("vg2png")
vg_json = json.dumps(spec).encode()
return check_output_with_stderr([vg2png, *(vega_cli_options or [])], input=vg_json)
return check_output_with_stderr(
[vg2png, *(vega_cli_options or [])], input=vg_json, stderr_filter=stderr_filter
)


def vg2pdf(spec: JSONDict, vega_cli_options: Optional[List[str]] = None) -> bytes:
def vg2pdf(
spec: JSONDict,
vega_cli_options: Optional[List[str]] = None,
stderr_filter: Optional[Callable[[str], bool]] = None,
) -> bytes:
"""Generate a PDF image from a Vega spec."""
vg2pdf = exec_path("vg2pdf")
vg_json = json.dumps(spec).encode()
return check_output_with_stderr([vg2pdf, *(vega_cli_options or [])], input=vg_json)
return check_output_with_stderr(
[vg2pdf, *(vega_cli_options or [])], input=vg_json, stderr_filter=stderr_filter
)


def vg2svg(spec: JSONDict, vega_cli_options: Optional[List[str]] = None) -> str:
def vg2svg(
spec: JSONDict,
vega_cli_options: Optional[List[str]] = None,
stderr_filter: Optional[Callable[[str], bool]] = None,
) -> str:
"""Generate an SVG image from a Vega spec."""
vg2svg = exec_path("vg2svg")
vg_json = json.dumps(spec).encode()
return check_output_with_stderr(
[vg2svg, *(vega_cli_options or [])], input=vg_json
[vg2svg, *(vega_cli_options or [])], input=vg_json, stderr_filter=stderr_filter
).decode()


Expand All @@ -82,6 +102,12 @@ def __init__(
self._vega_cli_options = vega_cli_options or []
super().__init__(spec=spec, mode=mode, **kwargs)

_stderr_ignore = ["WARN Can not resolve event source: window"]

@classmethod
def _stderr_filter(cls, line: str) -> bool:
return line not in cls._stderr_ignore

@classmethod
def enabled(cls) -> bool:
try:
Expand All @@ -96,15 +122,27 @@ def _serialize(self, fmt: str, content_type: str) -> MimebundleContent:
spec = self._spec

if self._mode == "vega-lite":
spec = vl2vg(spec)
spec = vl2vg(spec, stderr_filter=self._stderr_filter)

if fmt == "vega":
return spec
elif fmt == "png":
return vg2png(spec, vega_cli_options=self._vega_cli_options)
return vg2png(
spec,
vega_cli_options=self._vega_cli_options,
stderr_filter=self._stderr_filter,
)
elif fmt == "svg":
return vg2svg(spec, vega_cli_options=self._vega_cli_options)
return vg2svg(
spec,
vega_cli_options=self._vega_cli_options,
stderr_filter=self._stderr_filter,
)
elif fmt == "pdf":
return vg2pdf(spec, vega_cli_options=self._vega_cli_options)
return vg2pdf(
spec,
vega_cli_options=self._vega_cli_options,
stderr_filter=self._stderr_filter,
)
else:
raise ValueError(f"Unrecognized format: {fmt!r}")
37 changes: 37 additions & 0 deletions altair_saver/savers/tests/test_node.py
Expand Up @@ -6,11 +6,13 @@
from PIL import Image
from PyPDF2 import PdfFileReader
import pytest
from _pytest.capture import SysCapture
from _pytest.monkeypatch import MonkeyPatch

from altair_saver import NodeSaver
from altair_saver._utils import fmt_to_mimetype
from altair_saver.savers import _node
from altair_saver.types import JSONDict


def get_testcases() -> Iterator[Tuple[str, Dict[str, Any]]]:
Expand All @@ -31,6 +33,19 @@ def get_testcases() -> Iterator[Tuple[str, Dict[str, Any]]]:
yield case, {"vega-lite": vl, "vega": vg, "svg": svg, "png": png, "pdf": pdf}


@pytest.fixture
def interactive_spec() -> JSONDict:
return {
"data": {"values": [{"x": 1, "y": 1}]},
"mark": "point",
"encoding": {
"x": {"field": "x", "type": "quantitative"},
"y": {"field": "y", "type": "quantitative"},
},
"selection": {"zoon": {"type": "interval", "bind": "scales"}},
}


def get_modes_and_formats() -> Iterator[Tuple[str, str]]:
for mode in ["vega", "vega-lite"]:
for fmt in NodeSaver.valid_formats[mode]:
Expand Down Expand Up @@ -89,3 +104,25 @@ def exec_path(name: str) -> str:

monkeypatch.setattr(_node, "exec_path", exec_path)
assert NodeSaver.enabled() is enabled


@pytest.mark.parametrize("suppress_warnings", [True, False])
def test_stderr_suppression(
interactive_spec: JSONDict,
suppress_warnings: bool,
monkeypatch: MonkeyPatch,
capsys: SysCapture,
) -> None:
message = NodeSaver._stderr_ignore[0]

# Window resolve warnings are emitted by the vega CLI when an interactive chart
# is saved, and are suppressed by default.
if not suppress_warnings:
monkeypatch.setattr(NodeSaver, "_stderr_ignore", [])

NodeSaver(interactive_spec).save(fmt="png")
captured = capsys.readouterr()
if suppress_warnings:
assert message not in captured.err
else:
assert message in captured.err

0 comments on commit 763288b

Please sign in to comment.