Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
41133f2
Make tabs_ready public
ayjayt Aug 21, 2025
d4a5bd4
Add some types to kaleido.py
ayjayt Aug 22, 2025
0cbd7c0
Merge branch 'master' into andrew/test_coverage
ayjayt Aug 22, 2025
196aca7
Merge branch 'master' into andrew/sometypes
ayjayt Aug 22, 2025
4b1ea3c
Merge branch 'andrew/sometypes' into andrew/test_coverage
ayjayt Aug 22, 2025
583b8c8
Upgrade choreographer for typing.
ayjayt Aug 22, 2025
d36dbab
Add types to __init__
ayjayt Aug 22, 2025
9838ac4
Fix some bad typing in __init__.py
ayjayt Aug 22, 2025
7d4d297
Improve page logic.
ayjayt Aug 22, 2025
5719383
Merge branch 'andrew/sometypes' into andrew/test_coverage
ayjayt Aug 22, 2025
cbd925b
Add claude run on init and public api
ayjayt Aug 22, 2025
d99bb88
Move pytest async detection to auto
ayjayt Aug 23, 2025
6d3cf13
Iterate with Claude on test_public_api
ayjayt Aug 23, 2025
e4ba49c
Make corrections to public api tests.
ayjayt Aug 23, 2025
1898bd9
Fix up test_init
ayjayt Aug 23, 2025
4147850
Add comments.
ayjayt Aug 23, 2025
7a808b4
Reorganize a bit.
ayjayt Aug 23, 2025
abcc184
Make error_log/profiler 100% optional in __init__
ayjayt Aug 23, 2025
24d0dab
Iterate with claude on public_api test.
ayjayt Aug 25, 2025
f5cc483
Parameterize public api tests for fig/to_dict()
ayjayt Aug 25, 2025
9489b71
Change to using args fixtures.
ayjayt Aug 25, 2025
824230c
Remove regex for html parsing.
ayjayt Aug 25, 2025
d152afa
Fix non-existent function
ayjayt Aug 25, 2025
ec62908
Fix whitespace errors.
ayjayt Aug 25, 2025
930f338
Add types for page_generaetor.
ayjayt Aug 25, 2025
3ddb876
Merge branch 'andrew/sometypes' into andrew/test_coverage
ayjayt Aug 25, 2025
f9175fb
Add hypothesis tests generated by Claude.
ayjayt Aug 25, 2025
2b20a3e
Fix file path ensuring error.
ayjayt Aug 26, 2025
a71a202
Add hypothesis.
ayjayt Aug 26, 2025
63332ca
Allow _ensure_path to deal with str/encoding combos
ayjayt Aug 26, 2025
d495e34
Tweak manually hypothesis tests in PageGenerator
ayjayt Aug 26, 2025
e92fca8
Rename test file
ayjayt Aug 26, 2025
6711e61
Fix bad mathjax conditional
ayjayt Aug 26, 2025
76ad595
Add more types to Kaleido
ayjayt Aug 26, 2025
cfdacc1
Add check for None value.
ayjayt Aug 26, 2025
9fd331c
Fix kaleido typing errors.
ayjayt Aug 26, 2025
e3d5a87
Iterate on kaleido.py tests claude
ayjayt Aug 26, 2025
c8afc88
Mark test skipped for after refactor.
ayjayt Aug 26, 2025
30eecca
Add further kaleido tests.
ayjayt Aug 26, 2025
8ca2ae7
Split context/noncontext tests into two
ayjayt Aug 27, 2025
84629f3
Close browser before cancelling kaleido tasks.
ayjayt Aug 27, 2025
74c8765
Reorganize so __init__ creates no tmp dir
ayjayt Aug 27, 2025
d2dd530
Fix test_kaleido tests.
ayjayt Aug 27, 2025
cc97c41
Remove forcefail
ayjayt Aug 27, 2025
fd1bb62
Type fig_tools.
ayjayt Aug 28, 2025
0adba8f
Shore up current file detecting for testing.
ayjayt Aug 28, 2025
a115723
Use tmp_path not __file__ for valid file.
ayjayt Aug 28, 2025
6f281f8
Supress unhelpful hypo health checks.
ayjayt Aug 28, 2025
be01fdb
Add faster assert to prove file existence
ayjayt Aug 28, 2025
0bca6f7
Add additional url parsing tool.
ayjayt Aug 28, 2025
a76fbad
Make special fixture for non existent file URI:
ayjayt Aug 28, 2025
719a7fd
Fix bad path definition
ayjayt Aug 28, 2025
02a1f80
Change function to str, so don't call
ayjayt Aug 28, 2025
cfb4eff
Be more explicit in path parsing.
ayjayt Aug 28, 2025
f9e7f64
Add missing function.
ayjayt Aug 28, 2025
69294ed
Add logging.
ayjayt Aug 28, 2025
5407965
Add yet more logging.
ayjayt Aug 28, 2025
33ead6d
Fix bad logic.
ayjayt Aug 28, 2025
449282b
Tone down unreasonable 20 processor test.
ayjayt Aug 28, 2025
62fcefe
Remove hypo deadlines for slow CI runners.
ayjayt Aug 28, 2025
501fb80
Organize a bit conftest.py
ayjayt Aug 28, 2025
8a15a12
Merge branch 'andrew/test_coverage' into andrew/test-utils
ayjayt Aug 28, 2025
01952f4
Organize fig_tools a bit.
ayjayt Aug 29, 2025
d69d1b2
Fix order
ayjayt Aug 29, 2025
f65f0ec
Readd figtools tests.
ayjayt Aug 29, 2025
336f479
Iterate on claude tests.
ayjayt Aug 29, 2025
c1a2f99
All tests pass
ayjayt Aug 29, 2025
b8f7185
Clear up mathjax logic.
ayjayt Aug 29, 2025
970f8b8
Test Path() as well as str() in filenotfound tests.
ayjayt Aug 29, 2025
7c70737
Fix to properly validate Path() types
ayjayt Aug 29, 2025
95e9b4b
Merge branch 'andrew/test_coverage' into andrew/test-utils
ayjayt Aug 29, 2025
9e9b9b3
Add tests for test_sync_server.
ayjayt Aug 29, 2025
88533f1
Fix CHANGELOG.txt
ayjayt Aug 29, 2025
a937881
Merge branch 'master' into andrew/test-utils
ayjayt Sep 10, 2025
56843d6
Ruff lint tests.
ayjayt Sep 10, 2025
7dfff3e
Silence ruff on one line.
ayjayt Sep 10, 2025
9d57e1b
Add timeouts to test.
ayjayt Sep 10, 2025
0d6af42
Fix bad GHA logic in conftest.py
ayjayt Sep 10, 2025
40db2b8
Lower tab count for tab count test.
ayjayt Sep 10, 2025
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
9 changes: 9 additions & 0 deletions src/py/CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
- Add testing
- Fix a variety of type bugs
- Change order of browser closer to fix hang
- Explicitly handle certain argument options better
- Move temp file creation to .open() out of __init__()
- Reduce mathjax version to plotly.py
- Fix hang and add automatic close with stop_sync_server
- Add option to silence warnings in start/stop_sync_server
- Fix bug where attribute was inconsistently named

v1.1.0rc0
- Improve verbosity of errors when starting kaleido improperly
- Add new api functions start/stop_sync_server
Expand Down
135 changes: 84 additions & 51 deletions src/py/kaleido/_fig_tools.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,53 @@
"""
Adapted from old code, it 1. validates, 2. write defaults, 3. packages object.

Its a bit complicated and mixed in order.
"""

from __future__ import annotations

import glob
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, TypedDict
from typing import TYPE_CHECKING, Literal, TypedDict

import logistro

if TYPE_CHECKING:
from typing import Any

from typing_extensions import TypeGuard

Figurish = Any # Be nice to make it more specific, dictionary or something
FormatString = Literal["png", "jpg", "jpeg", "webp", "svg", "json", "pdf"]

_logger = logistro.getLogger(__name__)

# constants
DEFAULT_EXT = "png"
DEFAULT_SCALE = 1
DEFAULT_WIDTH = 700
DEFAULT_HEIGHT = 500
SUPPORTED_FORMATS = ("png", "jpg", "jpeg", "webp", "svg", "json", "pdf")
FormatString = Literal["png", "jpg", "jpeg", "webp", "svg", "json", "pdf"]
SUPPORTED_FORMATS: tuple[FormatString, ...] = (
"png",
"jpg",
"jpeg",
"webp",
"svg",
"json",
"pdf",
)


def _assert_format(ext: str) -> TypeGuard[FormatString]:
if ext not in SUPPORTED_FORMATS:
raise ValueError(f"File format {ext} is not supported.")
raise ValueError(
f"Invalid format '{ext}'.\n Supported formats: {SUPPORTED_FORMATS!s}",
)
return True


Figurish = Any # Be nice to make it more specific, dictionary or something


def _is_figurish(o) -> TypeGuard[Figurish]:
def _is_figurish(o: Any) -> TypeGuard[Figurish]:
valid = hasattr(o, "to_dict") or (isinstance(o, dict) and "data" in o)
if not valid:
_logger.debug(
Expand All @@ -41,7 +58,11 @@ def _is_figurish(o) -> TypeGuard[Figurish]:
return valid


def _get_figure_dimensions(layout, width, height):
def _get_figure_dimensions(
layout: dict,
width: float | None,
height: float | None,
) -> tuple[float, float]:
# Compute image width / height with fallbacks
width = (
width
Expand All @@ -58,29 +79,25 @@ def _get_figure_dimensions(layout, width, height):
return width, height


def _get_format(extension):
original_format = extension
extension = extension.lower()
if extension == "jpg":
def _get_format(extension: str) -> FormatString:
formatted_extension = extension.lower()
if formatted_extension == "jpg":
return "jpeg"

if extension not in SUPPORTED_FORMATS:
raise ValueError(
f"Invalid format '{original_format}'.\n"
f" Supported formats: {SUPPORTED_FORMATS!s}",
)
return extension
if not _assert_format(formatted_extension):
raise ValueError # this line will never be reached its for typer
Comment on lines +86 to +87
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be just

Suggested change
if not _assert_format(formatted_extension):
raise ValueError # this line will never be reached its for typer
_assert_format(formatted_extension)

?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but the typechecker doesn't realize whats happened in that case (and things the format is not asserted)

return formatted_extension


# Input of to_spec
# Input of to_spec (user gives us this)
class LayoutOpts(TypedDict, total=False):
format: FormatString | None
scale: int | float
height: int | float
width: int | float


# Output of to_spec
# Output of to_spec (we give kaleido_scopes.js this)
# refactor note: this could easily be right before send
class Spec(TypedDict):
format: FormatString
width: int | float
Expand All @@ -89,7 +106,8 @@ class Spec(TypedDict):
data: Figurish


def to_spec(figure, layout_opts: LayoutOpts) -> Spec:
# validate configuration options for kaleido.js and package like its wants
def to_spec(figure: Figurish, layout_opts: LayoutOpts) -> Spec:
# Get figure layout
layout = figure.get("layout", {})

Expand All @@ -107,6 +125,7 @@ def to_spec(figure, layout_opts: LayoutOpts) -> Spec:

# Extract info
extension = _get_format(layout_opts.get("format") or DEFAULT_EXT)

width, height = _get_figure_dimensions(
layout,
layout_opts.get("width"),
Expand All @@ -123,7 +142,9 @@ def to_spec(figure, layout_opts: LayoutOpts) -> Spec:
}


def _next_filename(path, prefix, ext) -> str:
# if we need to suffix the filename automatically:
def _next_filename(path: Path | str, prefix: str, ext: str) -> str:
path = path if isinstance(path, Path) else Path(path)
default = 1 if (path / f"{prefix}.{ext}").exists() else 0
re_number = re.compile(
r"^" + re.escape(prefix) + r"\-(\d+)\." + re.escape(ext) + r"$",
Expand All @@ -139,36 +160,16 @@ def _next_filename(path, prefix, ext) -> str:
return f"{prefix}.{ext}" if n == 1 else f"{prefix}-{n}.{ext}"


def build_fig_spec( # noqa: C901, PLR0912
# validate and build full route if needed:
def _build_full_path(
path: Path | None,
fig: Figurish,
path: Path | str | None,
opts: LayoutOpts | None,
) -> tuple[Spec, Path]:
if not opts:
opts = {}

if not _is_figurish(fig):
raise TypeError("Figure supplied doesn't seem to be a valid plotly figure.")

if hasattr(fig, "to_dict"):
fig = fig.to_dict()

if isinstance(path, str):
path = Path(path)
elif path and not isinstance(path, Path):
raise TypeError("Path should be a string or `pathlib.Path` object (or None)")

if path and path.suffix and not opts.get("format"):
ext = path.suffix.lstrip(".")
if _assert_format(ext): # not strict necessary if but helps typeguard
opts["format"] = ext

spec = to_spec(fig, opts)

ext = spec["format"]

ext: FormatString,
) -> Path:
full_path: Path | None = None

directory: Path

if not path:
directory = Path() # use current Path
elif path and (not path.suffix or path.is_dir()):
Expand All @@ -181,6 +182,7 @@ def build_fig_spec( # noqa: C901, PLR0912
raise RuntimeError(
f"Cannot reach path {path.parent}. Are all directories created?",
)

if not full_path:
_logger.debug("Looking for title")
prefix = fig.get("layout", {}).get("title", {}).get("text", "fig")
Expand All @@ -190,5 +192,36 @@ def build_fig_spec( # noqa: C901, PLR0912
_logger.debug(f"Found: {prefix}")
name = _next_filename(directory, prefix, ext)
full_path = directory / name
return full_path


# call all validators/automatic config fill-in/packaging in expected format
def build_fig_spec(
fig: Figurish,
path: Path | str | None,
opts: LayoutOpts | None,
) -> tuple[Spec, Path]:
if not opts:
opts = {}

if not _is_figurish(fig):
raise TypeError("Figure supplied doesn't seem to be a valid plotly figure.")

if hasattr(fig, "to_dict"):
fig = fig.to_dict()

if isinstance(path, str):
path = Path(path)
elif path and not isinstance(path, Path):
raise TypeError("Path should be a string or `pathlib.Path` object (or None)")

if not opts.get("format") and path and path.suffix:
ext = path.suffix.lstrip(".")
if _assert_format(ext): # not strict necessary if but helps typeguard
opts["format"] = ext

spec = to_spec(fig, opts)

full_path = _build_full_path(path, fig, spec["format"])

return spec, full_path
3 changes: 2 additions & 1 deletion src/py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"logistro>=1.0.8",
"orjson>=3.10.15",
"packaging",
"pytest-timeout>=2.4.0",
]

[project.urls]
Expand Down Expand Up @@ -94,7 +95,7 @@ log_cli = false
# name = cmd

[tool.poe.tasks.test]
cmd = "pytest --log-level=1 -W error -n auto -v -rfE --capture=fd"
cmd = "pytest --timeout=90 --log-level=1 -W error -n auto -v -rfE --capture=fd"
help = "Run all tests quickly"

[tool.poe.tasks.debug-test]
Expand Down
2 changes: 1 addition & 1 deletion src/py/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)

is_ci = os.getenv("GITHUB_ACTIONS") == "true" or os.getenv("CI") == "true"
if is_ci and platform.system in {"Windows", "Darwin"}:
if is_ci and platform.system() in {"Windows", "Darwin"}:
settings.load_profile("ci")


Expand Down
Loading
Loading