diff --git a/src/py/README.md b/src/py/README.md index 899d6740..c4699a90 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -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 @@ -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 @@ -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) @@ -85,9 +89,9 @@ 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. ``` @@ -95,15 +99,15 @@ 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 @@ -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"] +... ) ``` diff --git a/src/py/tests/test_readme.py b/src/py/tests/test_readme.py new file mode 100644 index 00000000..05c2ead0 --- /dev/null +++ b/src/py/tests/test_readme.py @@ -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}")