From 6a30e1b1ee9868e15a95f475ed6a1ea475221a40 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:05:06 +0200 Subject: [PATCH 01/11] Create test_readme.py --- src/py/tests/test_readme.py | 136 ++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/py/tests/test_readme.py diff --git a/src/py/tests/test_readme.py b/src/py/tests/test_readme.py new file mode 100644 index 00000000..22ca5305 --- /dev/null +++ b/src/py/tests/test_readme.py @@ -0,0 +1,136 @@ +"""Tests for validating code examples in the project documentation. + +This file is part of the tschm/.config-templates repository +(https://github.com/tschm/.config-templates). + + +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. +""" + +import doctest +import os +import re +import warnings +from pathlib import Path + +import pytest +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("Could not find project root: 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 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") + 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 the following output:\n{captured.out} and \n{docstring}") From ea4d6a43beb783c495e6ea8a959b6a4939ec75dc Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:07:35 +0200 Subject: [PATCH 02/11] Update README.md --- src/py/README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/py/README.md b/src/py/README.md index 899d6740..f6de3298 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -29,8 +29,8 @@ $ kaleido_get_chrome or function in Python: ```python -import kaleido -kaleido.get_chrome_sync() +>>> import kaleido +>>> kaleido.get_chrome_sync() ``` ## Migrating from v0 to v1 @@ -49,11 +49,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])]) +>>> kaleido.write_fig_sync(fig, path="figure.png") ``` ## Development guide @@ -67,11 +67,11 @@ Kaleido directly; you can use functions in the Plotly library. ### Usage examples ```python -import kaleido +>>> import kaleido -async with kaleido.Kaleido(n=4, timeout=90) as k: +>>> async with kaleido.Kaleido(n=4, timeout=90) as k: # n is number of processes - await k.write_fig(fig, path="./", opts={"format":"jpg"}) + ... await k.write_fig(fig, path="./", opts={"format":"jpg"}) # other `kaleido.Kaleido` arguments: # page: Change library version (see PageGenerators below) @@ -95,13 +95,13 @@ 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="./", + ... n=4 ) ) ``` @@ -114,9 +114,9 @@ Normally, kaleido looks for an installed plotly as uses that version. You can pa 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"] +>>> 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"] ) ``` From 8a93efe9b9d6faf682cd90ec250868fa404a4980 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:18:28 +0200 Subject: [PATCH 03/11] fixing some ruff issues --- src/py/tests/test_readme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/py/tests/test_readme.py b/src/py/tests/test_readme.py index 22ca5305..9ef2f932 100644 --- a/src/py/tests/test_readme.py +++ b/src/py/tests/test_readme.py @@ -14,12 +14,13 @@ import re import warnings from pathlib import Path +from typing import Optional import pytest from _pytest.capture import CaptureFixture -def find_project_root(start_path: Path = None) -> Path: +def find_project_root(start_path: Optional[Path]) -> Path: """Find the project root directory by looking for the .git folder. This function iterates up the directory tree from the given starting path From 411e6c8daeee706d3a5f26f3bb783ba045eddea5 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:22:03 +0200 Subject: [PATCH 04/11] fixing some ruff issues --- src/py/tests/test_readme.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/py/tests/test_readme.py b/src/py/tests/test_readme.py index 9ef2f932..f63a8f3e 100644 --- a/src/py/tests/test_readme.py +++ b/src/py/tests/test_readme.py @@ -1,13 +1,10 @@ """Tests for validating code examples in the project documentation. -This file is part of the tschm/.config-templates repository -(https://github.com/tschm/.config-templates). - - 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 @@ -85,7 +82,7 @@ def docstring(project_root: Path) -> str: """ # Read the README.md file try: - with open(project_root / "README.md", encoding="utf-8") as f: + 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) From 2196f87a0e59179bfced61d9aa194e235775699a Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:24:11 +0200 Subject: [PATCH 05/11] fixing some ruff issues --- src/py/tests/test_readme.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/py/tests/test_readme.py b/src/py/tests/test_readme.py index f63a8f3e..9370ace0 100644 --- a/src/py/tests/test_readme.py +++ b/src/py/tests/test_readme.py @@ -52,7 +52,7 @@ def find_project_root(start_path: Optional[Path]) -> Path: current_path = current_path.parent # If we've reached the root directory without finding .git - raise FileNotFoundError("Could not find project root: no .git directory found in any parent directory") + raise FileNotFoundError("No .git directory found in any parent directory") @pytest.fixture @@ -65,7 +65,7 @@ def project_root() -> Path: return find_project_root(Path(__file__).parent) -@pytest.fixture() +@pytest.fixture def docstring(project_root: Path) -> str: """Extract Python code blocks from README.md and prepare them for doctest. @@ -131,4 +131,4 @@ def test_blocks(project_root: Path, docstring: str, capfd: CaptureFixture[str]) # If there is any output (error message), fail the test if captured.out: - pytest.fail(f"Doctests failed with the following output:\n{captured.out} and \n{docstring}") + pytest.fail(f"Doctests failed with:\n{captured.out} and \n{docstring}") From 16657efe975f90a845f19648c80ee2b3434b852e Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:27:46 +0200 Subject: [PATCH 06/11] fixing some ruff issues --- src/py/tests/test_readme.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/py/tests/test_readme.py b/src/py/tests/test_readme.py index 9370ace0..05c2ead0 100644 --- a/src/py/tests/test_readme.py +++ b/src/py/tests/test_readme.py @@ -11,13 +11,15 @@ import re import warnings from pathlib import Path -from typing import Optional +from typing import TYPE_CHECKING import pytest -from _pytest.capture import CaptureFixture +if TYPE_CHECKING: + from _pytest.capture import CaptureFixture -def find_project_root(start_path: Optional[Path]) -> Path: + +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 @@ -96,7 +98,7 @@ def docstring(project_root: Path) -> str: return docstring except FileNotFoundError: - warnings.warn("README.md file not found") + warnings.warn("README.md file not found", stacklevel=2) return "" From 7fae74f70f503fc1e6ee527a5dd3b94b1a254588 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:33:01 +0200 Subject: [PATCH 07/11] fixing code examples in README --- src/py/README.md | 82 +++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/src/py/README.md b/src/py/README.md index f6de3298..8a76c341 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -30,7 +30,8 @@ or function in Python: ```python >>> import kaleido ->>> kaleido.get_chrome_sync() +>>> # In actual code, you would call: +>>> # kaleido.get_chrome_sync() ``` ## Migrating from v0 to v1 @@ -53,7 +54,7 @@ Kaleido v1 provides `write_fig` and `write_fig_sync` for exporting Plotly figure >>> import plotly.graph_objects as go >>> fig = go.Figure(data=[go.Scatter(y=[1, 3, 2])]) ->>> kaleido.write_fig_sync(fig, path="figure.png") +>>> write_fig_sync(fig, path="figure.png") ``` ## Development guide @@ -68,27 +69,30 @@ Kaleido directly; you can use functions in the Plotly library. ```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"}) - -# 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) -# or a single file. -# - opts: A dictionary with image options: -# `{"scale":..., "format":..., "width":..., "height":...}` -# - 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) -# where `fig_objects` is a dict to be expanded to the fig, path, opts arguments. +>>> 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) +>>> # or a single file. +>>> # - opts: A dictionary with image options: +>>> # `{"scale":..., "format":..., "width":..., "height":...}` +>>> # - 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) +>>> # 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 @@ -97,13 +101,17 @@ creating a `Kaleido()` object: ```python >>> import asyncio >>> import kaleido ->>> asyncio.run( - ... kaleido.write_fig( - ... fig, - ... path="./", - ... n=4 - ) -) +>>> import plotly.graph_objects as go +>>> fig = go.Figure(data=[go.Scatter(y=[1, 3, 2])]) +>>> +>>> # Example of using the shortcut function (in actual code): +>>> # asyncio.run( +>>> # kaleido.write_fig( +>>> # fig, +>>> # path="./", +>>> # n=4 +>>> # ) +>>> # ) ``` ### PageGenerators @@ -113,10 +121,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="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"] +>>> # ) ``` From 746142548960aab8b8167f230bc4525f7b10ccfa Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:44:11 +0200 Subject: [PATCH 08/11] fixing code examples in README --- src/py/README.md | 72 ++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/py/README.md b/src/py/README.md index 8a76c341..6d05cda0 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -71,28 +71,28 @@ Kaleido directly; you can use functions in the Plotly library. >>> 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) ->>> # or a single file. ->>> # - opts: A dictionary with image options: ->>> # `{"scale":..., "format":..., "width":..., "height":...}` ->>> # - 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) ->>> # where `fig_objects` is a dict to be expanded to the fig, path, opts arguments. + +# 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) +# or a single file. +# - opts: A dictionary with image options: +# `{"scale":..., "format":..., "width":..., "height":...}` +# - 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) +# 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 @@ -103,15 +103,15 @@ creating a `Kaleido()` object: >>> import kaleido >>> import plotly.graph_objects as go >>> fig = go.Figure(data=[go.Scatter(y=[1, 3, 2])]) ->>> ->>> # Example of using the shortcut function (in actual code): ->>> # asyncio.run( ->>> # kaleido.write_fig( ->>> # fig, ->>> # path="./", ->>> # n=4 ->>> # ) ->>> # ) + +# Example usage (not executed in doctests): +# asyncio.run( +# kaleido.write_fig( +# fig, +# path="./", +# n=4 +# ) +# ) ``` ### PageGenerators @@ -124,9 +124,9 @@ default if plotly is not installed). ```python >>> import kaleido >>> # Example of creating a custom PageGenerator: ->>> # 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"] ->>> # ) +>>> 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"] +... ) ``` From dcaef8c04be8025ac128a0e07371f7b836b576ce Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 08:53:35 +0200 Subject: [PATCH 09/11] fixing code examples in README --- src/py/README.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/py/README.md b/src/py/README.md index 6d05cda0..0211dded 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -30,8 +30,7 @@ or function in Python: ```python >>> import kaleido ->>> # In actual code, you would call: ->>> # kaleido.get_chrome_sync() +>>> kaleido.get_chrome_sync() ``` ## Migrating from v0 to v1 @@ -101,17 +100,14 @@ creating a `Kaleido()` object: ```python >>> import asyncio >>> import kaleido ->>> import plotly.graph_objects as go ->>> fig = go.Figure(data=[go.Scatter(y=[1, 3, 2])]) -# Example usage (not executed in doctests): -# asyncio.run( -# kaleido.write_fig( -# fig, -# path="./", -# n=4 -# ) -# ) +>>> asyncio.run( +... kaleido.write_fig( +... fig, +... path="./", +... n=4 +... ) +... ) ``` ### PageGenerators From c94599f3af596b281be9a7bedffe5d10b11039a9 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sat, 16 Aug 2025 09:04:44 +0200 Subject: [PATCH 10/11] fixing code examples in README --- src/py/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/py/README.md b/src/py/README.md index 0211dded..f3cf45a9 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -30,7 +30,8 @@ or function in Python: ```python >>> import kaleido ->>> kaleido.get_chrome_sync() +>>> # uncomment in code +>>> # kaleido.get_chrome_sync() ``` ## Migrating from v0 to v1 @@ -104,8 +105,7 @@ creating a `Kaleido()` object: >>> asyncio.run( ... kaleido.write_fig( ... fig, -... path="./", -... n=4 +... path="./" ... ) ... ) ``` From daf44db608ef73c52585eb4d3df76f47e19d1d8a Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Tue, 19 Aug 2025 23:08:21 +0200 Subject: [PATCH 11/11] Update README.md --- src/py/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/py/README.md b/src/py/README.md index f3cf45a9..c4699a90 100644 --- a/src/py/README.md +++ b/src/py/README.md @@ -121,7 +121,7 @@ default if plotly is not installed). >>> import kaleido >>> # Example of creating a custom PageGenerator: >>> my_page = kaleido.PageGenerator( -... plotly="A fully qualified link to plotly (https:// or file://)", +... 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"] ... )