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
66 changes: 36 additions & 30 deletions src/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ $ kaleido_get_chrome
or function in Python:

```python
import kaleido
kaleido.get_chrome_sync()
>>> import kaleido
>>> # uncomment in code
>>> # kaleido.get_chrome_sync()
```

## Migrating from v0 to v1
Expand All @@ -49,11 +50,11 @@ removed in v1.
Kaleido v1 provides `write_fig` and `write_fig_sync` for exporting Plotly figures.

```python
from kaleido import write_fig_sync
import plotly.graph_objects as go
>>> from kaleido import write_fig_sync
>>> import plotly.graph_objects as go

fig = go.Figure(data=[go.Scatter(y=[1, 3, 2])])
kaleido.write_fig_sync(fig, path="figure.png")
>>> fig = go.Figure(data=[go.Scatter(y=[1, 3, 2])])
>>> write_fig_sync(fig, path="figure.png")
```

## Development guide
Expand All @@ -67,15 +68,18 @@ Kaleido directly; you can use functions in the Plotly library.
### Usage examples

```python
import kaleido

async with kaleido.Kaleido(n=4, timeout=90) as k:
# n is number of processes
await k.write_fig(fig, path="./", opts={"format":"jpg"})

>>> import kaleido
>>> import plotly.graph_objects as go
>>> fig = go.Figure(data=[go.Scatter(y=[1, 3, 2])])

# Example of using Kaleido with async context manager
# In an async function, you would do:
# async with kaleido.Kaleido(n=4, timeout=90) as k:
# await k.write_fig(fig, path="./", opts={"format":"jpg"})

# other `kaleido.Kaleido` arguments:
# page: Change library version (see PageGenerators below)

# `Kaleido.write_fig()` arguments:
# - fig: A single plotly figure or an iterable.
# - path: A directory (names auto-generated based on title)
Expand All @@ -85,25 +89,25 @@ async with kaleido.Kaleido(n=4, timeout=90) as k:
# - error_log: If you pass a list here, image-generation errors will be appended
# to the list and generation continues. If left as `None`, the
# first error will cause failure.

# You can also use Kaleido.write_fig_from_object:
await k.write_fig_from_object(fig_objects, error_log)
# await k.write_fig_from_object(fig_objects, error_log)
# where `fig_objects` is a dict to be expanded to the fig, path, opts arguments.
```

There are shortcut functions which can be used to generate images without
creating a `Kaleido()` object:

```python
import asyncio
import kaleido
asyncio.run(
kaleido.write_fig(
fig,
path="./",
n=4
)
)
>>> import asyncio
>>> import kaleido

>>> asyncio.run(
... kaleido.write_fig(
... fig,
... path="./"
... )
... )
```

### PageGenerators
Expand All @@ -113,10 +117,12 @@ Normally, kaleido looks for an installed plotly as uses that version. You can pa
`kaleido.PageGenerator(force_cdn=True)` to force use of a CDN version of plotly (the
default if plotly is not installed).

```
my_page = kaleido.PageGenerator(
plotly="A fully qualified link to plotly (https:// or file://)",
mathjax=False # no mathjax, or another fully quality link
others=["a list of other script links to include"]
)
```python
>>> import kaleido
>>> # Example of creating a custom PageGenerator:
>>> my_page = kaleido.PageGenerator(
... plotly="https://cdn.plot.ly/plotly-latest.min.js",
... mathjax=False, # no mathjax, or another fully quality link
... others=["a list of other script links to include"]
... )
```
136 changes: 136 additions & 0 deletions src/py/tests/test_readme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Tests for validating code examples in the project documentation.

This module contains tests that extract Python code blocks from the README.md file
and run them through doctest to ensure they are valid and working as expected.
This helps maintain accurate and working examples in the documentation.
"""
from __future__ import annotations

import doctest
import os
import re
import warnings
from pathlib import Path
from typing import TYPE_CHECKING

import pytest

if TYPE_CHECKING:
from _pytest.capture import CaptureFixture


def find_project_root(start_path: Path | None) -> Path:
"""Find the project root directory by looking for the .git folder.

This function iterates up the directory tree from the given starting path
until it finds a directory containing a .git folder, which is assumed to be
the project root.

Args:
start_path (Path, optional): The path to start searching from.
If None, uses the directory of the file calling this function.

Returns:
Path: The path to the project root directory.

Raises:
FileNotFoundError: If no .git directory is found in any parent directory.
"""
if start_path is None:
# If no start_path is provided, use the current file's directory
start_path = Path(__file__).parent

# Convert to absolute path to handle relative paths
current_path = start_path.absolute()

# Iterate up the directory tree
while current_path != current_path.parent: # Stop at the root directory
# Check if .git directory exists
git_dir = current_path / ".git"
if git_dir.exists() and git_dir.is_dir():
return current_path

# Move up to the parent directory
current_path = current_path.parent

# If we've reached the root directory without finding .git
raise FileNotFoundError("No .git directory found in any parent directory")


@pytest.fixture
def project_root() -> Path:
"""Fixture that provides the project root directory.

Returns:
Path: The path to the project root directory.
"""
return find_project_root(Path(__file__).parent)


@pytest.fixture
def docstring(project_root: Path) -> str:
"""Extract Python code blocks from README.md and prepare them for doctest.

This fixture reads the README.md file, extracts all Python code blocks
(enclosed in triple backticks with 'python' language identifier), and
combines them into a single docstring that can be processed by doctest.

Args:
project_root: Path to the project root directory

Returns:
str: A docstring containing all Python code examples from README.md

"""
# Read the README.md file
try:
with Path.open(project_root / "README.md", encoding="utf-8") as f:
content = f.read()

# Extract Python code blocks (assuming they are in triple backticks)
blocks = re.findall(r"```python(.*?)```", content, re.DOTALL)

code = "\n".join(blocks).strip()

# Add a docstring wrapper for doctest to process the code
docstring = f"\n{code}\n"

return docstring

except FileNotFoundError:
warnings.warn("README.md file not found", stacklevel=2)
return ""


def test_blocks(project_root: Path, docstring: str, capfd: CaptureFixture[str]) -> None:
"""Test that all Python code blocks in README.md execute without errors.

This test runs all the Python code examples from the README.md file
through doctest to ensure they execute correctly. It captures any
output or errors and fails the test if any issues are detected.

Args:
project_root: Path to the project root directory
docstring: String containing all Python code examples from README.md
capfd: Pytest fixture for capturing stdout/stderr output

Raises:
pytest.fail: If any doctest fails or produces unexpected output

"""
# Change to the root directory to ensure imports work correctly
os.chdir(project_root)

try:
# Run the code examples through doctest
doctest.run_docstring_examples(docstring, globals())
except doctest.DocTestFailure as e:
# If a DocTestFailure occurs, capture it and manually fail the test
pytest.fail(f"Doctests failed: {e}")

# Capture the output after running doctests
captured = capfd.readouterr()

# If there is any output (error message), fail the test
if captured.out:
pytest.fail(f"Doctests failed with:\n{captured.out} and \n{docstring}")
Loading