Skip to content

Commit

Permalink
Add webdriver kwarg to Bokeh export functions (contourpy#261)
Browse files Browse the repository at this point in the history
* Add webdriver kwarg to Bokeh export functions

* Use webdriver in bokeh renderer tests

* Pin sphinx version
  • Loading branch information
ianthomas23 authored Aug 18, 2023
1 parent 7132cf4 commit 0bfd918
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 53 deletions.
4 changes: 2 additions & 2 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
version: 2

build:
os: ubuntu-20.04
os: ubuntu-22.04
tools:
python: "3.9"
python: "3.11"

python:
install:
Expand Down
21 changes: 16 additions & 5 deletions lib/contourpy/util/bokeh_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from bokeh.models import GridPlot
from bokeh.palettes import Palette
from numpy.typing import ArrayLike
from selenium.webdriver.remote.webdriver import WebDriver

from contourpy._contourpy import FillReturn, LineReturn

Expand Down Expand Up @@ -220,13 +221,20 @@ def mask(
x, y = self._grid_as_2d(x, y)
fig.circle(x[mask], y[mask], fill_color=color, size=10)

def save(self, filename: str, transparent: bool = False) -> None:
def save(
self,
filename: str,
transparent: bool = False,
*,
webdriver: WebDriver | None = None,
) -> None:
"""Save plots to SVG or PNG file.
Args:
filename (str): Filename to save to.
transparent (bool, optional): Whether background should be transparent, default
``False``.
webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image.
Warning:
To output to SVG file, ``want_svg=True`` must have been passed to the constructor.
Expand All @@ -237,17 +245,20 @@ def save(self, filename: str, transparent: bool = False) -> None:
fig.border_fill_color = None # type: ignore[assignment]

if self._want_svg:
export_svg(self._layout, filename=filename)
export_svg(self._layout, filename=filename, webdriver=webdriver)
else:
export_png(self._layout, filename=filename)
export_png(self._layout, filename=filename, webdriver=webdriver)

def save_to_buffer(self) -> io.BytesIO:
def save_to_buffer(self, *, webdriver: WebDriver | None = None) -> io.BytesIO:
"""Save plots to an ``io.BytesIO`` buffer.
Args:
webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image.
Return:
BytesIO: PNG image buffer.
"""
image = get_screenshot_as_png(self._layout)
image = get_screenshot_as_png(self._layout, driver=webdriver)
buffer = io.BytesIO()
image.save(buffer, "png")
return buffer
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ requires-python = ">= 3.8"
[project.optional-dependencies]
docs = [
"furo",
"sphinx >= 7.2",
"sphinx-copybutton",
]
bokeh = [
Expand Down
File renamed without changes
File renamed without changes
116 changes: 116 additions & 0 deletions tests/test_bokeh_renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Iterator

import numpy as np
import pytest

from contourpy import FillType, LineType, contour_generator
from contourpy.util.data import random

if TYPE_CHECKING:
from selenium.webdriver.remote.webdriver import WebDriver

bokeh_renderer = pytest.importorskip("contourpy.util.bokeh_renderer")


@pytest.fixture(scope="session")
def driver() -> Iterator[WebDriver]:
# Based on Bokeh's tests/support/plugins/selenium.py
def chrome() -> WebDriver:
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.webdriver import WebDriver as Chrome
options = Options()
options.add_argument("--headless") # type: ignore[no-untyped-call]
options.add_argument("--no-sandbox") # type: ignore[no-untyped-call]
return Chrome(options=options)

driver = chrome()
driver.implicitly_wait(10)
yield driver
driver.quit()


def test_chrome_version(driver: WebDriver) -> None:
# Print version of chrome used for export to PNG by Bokeh as test images, particularly those
# containing text, are sensitive to chrome version.
capabilities = driver.capabilities
print("Browser used by Bokeh:", capabilities["browserName"], capabilities["browserVersion"])
if "chrome" in capabilities:
print("Chromedriver:", capabilities["chrome"]["chromedriverVersion"])


@pytest.mark.image
@pytest.mark.text
@pytest.mark.parametrize("show_text", [False, True])
@pytest.mark.parametrize("fill_type", FillType.__members__.values())
def test_renderer_filled_bokeh(show_text: bool, fill_type: FillType, driver: WebDriver) -> None:
from .image_comparison import compare_images

renderer = bokeh_renderer.BokehRenderer(ncols=2, figsize=(8, 3), show_frame=False)
x, y, z = random((3, 4), mask_fraction=0.35)
for ax, quad_as_tri in enumerate((False, True)):
cont_gen = contour_generator(x, y, z, fill_type=fill_type)

levels = np.linspace(0.0, 1.0, 11)
for i in range(len(levels)-1):
filled = cont_gen.filled(levels[i], levels[i+1])
if quad_as_tri:
renderer.filled(filled, fill_type, ax=ax, color=f"C{i}", alpha=0.4)
else:
renderer.filled(filled, fill_type, ax=ax, color=f"C{i}")

if quad_as_tri:
renderer.grid(x, y, ax=ax, alpha=0.5, quad_as_tri_alpha=0.5)
renderer.mask(x, y, z, ax=ax, color="red")
if show_text:
renderer.title("Title", ax=ax)
else:
renderer.grid(x, y, ax=ax, alpha=0.8, point_color="black")
if show_text:
renderer.title("Colored title", ax=ax, color="red")

image_buffer = renderer.save_to_buffer(webdriver=driver)
suffix = "" if show_text else "_no_text"
compare_images(
image_buffer, f"bokeh_renderer_filled{suffix}.png", f"{fill_type}", mean_threshold=0.03,
)


@pytest.mark.image
@pytest.mark.text
@pytest.mark.parametrize("show_text", [False, True])
@pytest.mark.parametrize("line_type", LineType.__members__.values())
def test_renderer_lines_bokeh(show_text: bool, line_type: LineType, driver: WebDriver) -> None:
from .image_comparison import compare_images

renderer = bokeh_renderer.BokehRenderer(ncols=2, figsize=(8, 3), show_frame=show_text)
x, y, z = random((3, 4), mask_fraction=0.35)
for ax, quad_as_tri in enumerate((False, True)):
cont_gen = contour_generator(x, y, z, line_type=line_type)

levels = np.linspace(0.1, 0.9, 9)
for i in range(len(levels)):
lines = cont_gen.lines(levels[i])
if quad_as_tri:
renderer.lines(lines, line_type, ax=ax, color=f"C{i}", alpha=0.5, linewidth=3)
else:
renderer.lines(lines, line_type, ax=ax, color=f"C{i}")

if quad_as_tri:
renderer.grid(x, y, ax=ax, alpha=0.2, quad_as_tri_alpha=0.2)
renderer.mask(x, y, z, ax=ax, color="red")
if show_text:
renderer.z_values(x, y, z, ax=ax, fmt=".2f", quad_as_tri=True)
renderer.title("Title", ax=ax)
else:
renderer.grid(x, y, ax=ax, point_color="black")
if show_text:
renderer.z_values(x, y, z, ax=ax, fmt=".2f", color="blue")
renderer.title("Colored title", ax=ax, color="red")

image_buffer = renderer.save_to_buffer(webdriver=driver)
suffix = "" if show_text else "_no_text"
compare_images(
image_buffer, f"bokeh_renderer_lines{suffix}.png", f"{line_type}", mean_threshold=0.03,
)
54 changes: 8 additions & 46 deletions tests/test_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,6 @@
from contourpy.util.data import random, simple


def test_chrome_version() -> None:
# Print version of chrome used for export to PNG by Bokeh as test images, particularly those
# containing text, are sensitive to chrome version.
try:
import contourpy.util.bokeh_renderer # noqa: F401
except ImportError:
pytest.skip("Optional bokeh dependencies not installed")

from bokeh.io.webdriver import webdriver_control
driver = webdriver_control.get()
capabilities = driver.capabilities
print("chrome version used by Bokeh:", capabilities["browserVersion"])
if "chrome" in capabilities:
print("chromedriver version used by Bokeh:", capabilities["chrome"]["chromedriverVersion"])


@pytest.mark.image
@pytest.mark.text
@pytest.mark.parametrize("show_text", [False, True])
Expand Down Expand Up @@ -82,22 +66,13 @@ def test_debug_renderer_lines(show_text: bool, line_type: LineType) -> None:
@pytest.mark.text
@pytest.mark.parametrize("show_text", [False, True])
@pytest.mark.parametrize("fill_type", FillType.__members__.values())
@pytest.mark.parametrize("renderer_type", ["mpl", "bokeh"])
def test_renderer_filled(show_text: bool, fill_type: FillType, renderer_type: str) -> None:
if renderer_type == "bokeh":
try:
from contourpy.util.bokeh_renderer import BokehRenderer as Renderer
except ImportError:
pytest.skip("Optional bokeh dependencies not installed")
elif renderer_type == "mpl":
from contourpy.util.mpl_renderer import MplRenderer as Renderer # type: ignore
else:
raise ValueError(f"Unrecognised renderer type {renderer_type}")
def test_renderer_filled(show_text: bool, fill_type: FillType) -> None:
from contourpy.util.mpl_renderer import MplRenderer

from .image_comparison import compare_images

renderer = MplRenderer(ncols=2, figsize=(8, 3), show_frame=False)
x, y, z = random((3, 4), mask_fraction=0.35)
renderer = Renderer(ncols=2, figsize=(8, 3), show_frame=False)
for ax, quad_as_tri in enumerate((False, True)):
cont_gen = contour_generator(x, y, z, fill_type=fill_type)

Expand All @@ -121,32 +96,20 @@ def test_renderer_filled(show_text: bool, fill_type: FillType, renderer_type: st

image_buffer = renderer.save_to_buffer()
suffix = "" if show_text else "_no_text"
compare_images(
image_buffer, f"renderer_filled_{renderer_type}{suffix}.png", f"{fill_type}",
mean_threshold=0.03 if renderer_type == "bokeh" else None,
)
compare_images(image_buffer, f"renderer_filled{suffix}.png", f"{fill_type}")


@pytest.mark.image
@pytest.mark.text
@pytest.mark.parametrize("show_text", [False, True])
@pytest.mark.parametrize("line_type", LineType.__members__.values())
@pytest.mark.parametrize("renderer_type", ["mpl", "bokeh"])
def test_renderer_lines(show_text: bool, line_type: LineType, renderer_type: str) -> None:
if renderer_type == "bokeh":
try:
from contourpy.util.bokeh_renderer import BokehRenderer as Renderer
except ImportError:
pytest.skip("Optional bokeh dependencies not installed")
elif renderer_type == "mpl":
from contourpy.util.mpl_renderer import MplRenderer as Renderer # type: ignore
else:
raise ValueError(f"Unrecognised renderer type {renderer_type}")
def test_renderer_lines(show_text: bool, line_type: LineType) -> None:
from contourpy.util.mpl_renderer import MplRenderer

from .image_comparison import compare_images

renderer = MplRenderer(ncols=2, figsize=(8, 3), show_frame=show_text)
x, y, z = random((3, 4), mask_fraction=0.35)
renderer = Renderer(ncols=2, figsize=(8, 3), show_frame=show_text)
for ax, quad_as_tri in enumerate((False, True)):
cont_gen = contour_generator(x, y, z, line_type=line_type)

Expand All @@ -173,6 +136,5 @@ def test_renderer_lines(show_text: bool, line_type: LineType, renderer_type: str
image_buffer = renderer.save_to_buffer()
suffix = "" if show_text else "_no_text"
compare_images(
image_buffer, f"renderer_lines_{renderer_type}{suffix}.png", f"{line_type}",
mean_threshold=0.03 if renderer_type == "bokeh" else None,
image_buffer, f"renderer_lines{suffix}.png", f"{line_type}",
)

0 comments on commit 0bfd918

Please sign in to comment.