Skip to content

Commit 44d167d

Browse files
authored
fix missing tv inline mixin (#77)
Co-authored-by: deeleeramone <>
1 parent 31b99e0 commit 44d167d

4 files changed

Lines changed: 318 additions & 36 deletions

File tree

claude/scripts/build_distributions.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,7 @@ def _build_cowork_plugin() -> Path:
121121
plugin = json.loads(plugin_json_path.read_text(encoding="utf-8"))
122122
if not plugin["description"].endswith(COWORK_DESCRIPTION_SUFFIX):
123123
plugin["description"] += COWORK_DESCRIPTION_SUFFIX
124-
plugin_json_path.write_text(
125-
json.dumps(plugin, indent=2) + "\n", encoding="utf-8"
126-
)
124+
plugin_json_path.write_text(json.dumps(plugin, indent=2) + "\n", encoding="utf-8")
127125

128126
out = DIST / "pywry-cowork.plugin"
129127
_zip_directory(workdir, out)
@@ -140,9 +138,7 @@ def _build_desktop_extension() -> Path:
140138
def _summarize(path: Path) -> None:
141139
size = path.stat().st_size
142140
if size > SIZE_LIMIT_BYTES:
143-
raise RuntimeError(
144-
f"{path.name} is {size:,} bytes — exceeds the 50 MB limit"
145-
)
141+
raise RuntimeError(f"{path.name} is {size:,} bytes — exceeds the 50 MB limit")
146142
with zipfile.ZipFile(path) as zf:
147143
files = zf.namelist()
148144
rel = path.relative_to(REPO_ROOT)

pywry/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pywry"
3-
version = "2.0.2"
3+
version = "2.0.3"
44
description = "A lightweight and blazingly fast, cross-platform, WebView rendering engine and desktop UI toolkit for Python. Batteries included."
55
authors = [{ name = "PyWry", email = "pywry2@gmail.com" }]
66
license = { text = "Apache 2.0" }

pywry/pywry/inline.py

Lines changed: 207 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
_UNSET,
4242
_Unset,
4343
)
44+
from .tvchart.mixin import TVChartStateMixin
4445
from .toolbar import Toolbar, get_toolbar_script, wrap_content_with_toolbars
4546
from .widget_protocol import BaseWidget # noqa: TC001
4647

@@ -1667,7 +1668,7 @@ async def get_widget_html_async(widget_id: str) -> str | None:
16671668
return await _state.get_widget_html_async(widget_id)
16681669

16691670

1670-
class InlineWidget(GridStateMixin, PlotlyStateMixin, ToolbarStateMixin):
1671+
class InlineWidget(GridStateMixin, PlotlyStateMixin, TVChartStateMixin, ToolbarStateMixin):
16711672
"""Base inline widget that renders via FastAPI server and IFrame.
16721673
16731674
Implements BaseWidget protocol for unified API across rendering backends.
@@ -3773,6 +3774,186 @@ def _preload_chart_data(user_id: str = "default") -> dict[str, str]:
37733774
return preload
37743775

37753776

3777+
def generate_tvchart_html(
3778+
chart_html: str,
3779+
config_payload: str,
3780+
chart_id: str,
3781+
widget_id: str,
3782+
title: str = "Chart",
3783+
theme: ThemeLiteral | None = None,
3784+
toolbars: list[dict[str, Any] | Toolbar] | None = None,
3785+
modals: list[dict[str, Any] | Modal] | None = None,
3786+
inline_css: str = "",
3787+
full_document: bool = True,
3788+
token: str | None = None,
3789+
) -> str:
3790+
"""Generate HTML for a TradingView Lightweight Chart.
3791+
3792+
Parameters
3793+
----------
3794+
chart_html : str
3795+
The chart container ``<div>`` (and any toolbar/modal markup).
3796+
config_payload : str
3797+
JSON string with ``chartOptions``, ``series``, ``storage``, etc.
3798+
chart_id : str
3799+
DOM id of the chart container element.
3800+
widget_id : str
3801+
Unique widget identifier (used by the pywry bridge).
3802+
title : str
3803+
Page title.
3804+
theme : 'dark' or 'light', optional
3805+
Color theme.
3806+
toolbars : list, optional
3807+
Toolbar configurations.
3808+
modals : list, optional
3809+
Modal configurations.
3810+
inline_css : str
3811+
Extra CSS to inject.
3812+
full_document : bool
3813+
If True, return complete HTML document; if False, content fragment only.
3814+
token : str or None
3815+
Widget auth token for the pywry bridge.
3816+
3817+
Returns
3818+
-------
3819+
str
3820+
"""
3821+
from .assets import (
3822+
get_pywry_css,
3823+
get_scrollbar_js,
3824+
get_toast_css,
3825+
get_tvchart_defaults_js,
3826+
get_tvchart_js,
3827+
)
3828+
from .modal import wrap_content_with_modals
3829+
from .notebook import _wrap_content_with_toolbars
3830+
3831+
if theme is None:
3832+
theme = _get_default_theme()
3833+
3834+
tvchart_js = get_tvchart_js()
3835+
tvchart_script = f"<script>{tvchart_js}</script>" if tvchart_js else ""
3836+
tvchart_defaults = get_tvchart_defaults_js()
3837+
tvchart_defaults_script = f"<script>{tvchart_defaults}</script>" if tvchart_defaults else ""
3838+
3839+
# Chart init script — waits for LightweightCharts then renders
3840+
chart_init_script = f"""<script>
3841+
(function() {{
3842+
function initChart() {{
3843+
if (typeof LightweightCharts === 'undefined') {{
3844+
setTimeout(initChart, 50);
3845+
return;
3846+
}}
3847+
var payload = {config_payload};
3848+
var container = document.getElementById('{chart_id}');
3849+
if (!container) {{
3850+
setTimeout(initChart, 50);
3851+
return;
3852+
}}
3853+
if (window.PYWRY_TVCHART_RENDER) {{
3854+
window.PYWRY_TVCHART_RENDER('{chart_id}', container, payload);
3855+
}} else if (window.PYWRY_TVCHART_CREATE) {{
3856+
window.PYWRY_TVCHART_CREATE('{chart_id}', container, payload);
3857+
}}
3858+
}}
3859+
initChart();
3860+
}})();
3861+
</script>"""
3862+
3863+
if not full_document:
3864+
# Content fragment for anywidget — caller handles wrapping
3865+
wrapped = _wrap_content_with_toolbars(chart_html, toolbars)
3866+
if modals:
3867+
modal_html, modal_scripts = wrap_content_with_modals("", modals)
3868+
wrapped = f"{wrapped}{modal_html}{modal_scripts}"
3869+
return f"{wrapped}\n{chart_init_script}"
3870+
3871+
# Full document for IFrame / browser mode
3872+
pywry_css = get_pywry_css()
3873+
pywry_style = f"<style>{pywry_css}</style>" if pywry_css else ""
3874+
toast_css = get_toast_css()
3875+
toast_style = f"<style>{toast_css}</style>" if toast_css else ""
3876+
scrollbar_js = get_scrollbar_js()
3877+
scrollbar_script = f"<script>{scrollbar_js}</script>" if scrollbar_js else ""
3878+
inline_style = f"<style>{inline_css}</style>" if inline_css else ""
3879+
3880+
if theme == "dark":
3881+
widget_theme_class = "pywry-theme-dark"
3882+
elif theme == "system":
3883+
widget_theme_class = "pywry-theme-system"
3884+
else:
3885+
widget_theme_class = "pywry-theme-light"
3886+
3887+
# Build widget content with toolbars
3888+
widget_content = wrap_content_with_toolbars(chart_html, toolbars)
3889+
3890+
# Inject modals
3891+
modal_block = ""
3892+
if modals:
3893+
modal_html, modal_scripts = wrap_content_with_modals("", modals)
3894+
modal_block = f"{modal_html}{modal_scripts}"
3895+
3896+
return f"""<!DOCTYPE html>
3897+
<html class="{theme}">
3898+
<head>
3899+
<meta charset="utf-8">
3900+
<title>{title}</title>
3901+
{tvchart_script}
3902+
{tvchart_defaults_script}
3903+
{pywry_style}
3904+
{toast_style}
3905+
{inline_style}
3906+
{scrollbar_script}
3907+
<style>
3908+
html, body {{
3909+
margin: 0;
3910+
padding: 0;
3911+
width: 100%;
3912+
height: 100%;
3913+
overflow: hidden;
3914+
background: var(--pywry-bg-primary);
3915+
}}
3916+
.pywry-widget {{
3917+
--pywry-widget-width: 100%;
3918+
--pywry-widget-height: 100%;
3919+
width: 100%;
3920+
height: 100%;
3921+
display: flex;
3922+
flex-direction: column;
3923+
border: none;
3924+
border-radius: 0;
3925+
box-sizing: border-box;
3926+
background-color: var(--pywry-bg-primary);
3927+
}}
3928+
.pywry-toolbar {{
3929+
border: none;
3930+
}}
3931+
.pywry-content {{
3932+
flex: 1;
3933+
min-height: 0;
3934+
box-sizing: border-box;
3935+
overflow: hidden;
3936+
}}
3937+
.pywry-tvchart-container {{
3938+
flex: 1;
3939+
min-height: 0;
3940+
width: 100%;
3941+
height: 100%;
3942+
box-sizing: border-box;
3943+
}}
3944+
</style>
3945+
</head>
3946+
<body>
3947+
<div class="pywry-widget pywry-custom-scrollbar {widget_theme_class}">
3948+
{widget_content}
3949+
</div>
3950+
{modal_block}
3951+
{_get_pywry_bridge_js(widget_id, token)}
3952+
{chart_init_script}
3953+
</body>
3954+
</html>"""
3955+
3956+
37763957
def show_tvchart(
37773958
data: Any = None,
37783959
callbacks: dict[str, Callable[..., Any]] | None = None,
@@ -3845,10 +4026,8 @@ def show_tvchart(
38454026
import json as _json
38464027
import uuid as _uuid
38474028

3848-
from .modal import wrap_content_with_modals
3849-
from .notebook import _wrap_content_with_toolbars
4029+
from .notebook import create_tvchart_widget
38504030
from .runtime import is_headless
3851-
from .widget import HAS_ANYWIDGET, PyWryTVChartWidget
38524031

38534032
if theme is None:
38544033
theme = _get_default_theme()
@@ -3930,25 +4109,21 @@ def show_tvchart(
39304109

39314110
chart_html = f'<div id="{chart_id}" class="pywry-tvchart-container"></div>'
39324111

3933-
# Inject toolbars
3934-
chart_html = _wrap_content_with_toolbars(chart_html, toolbars)
3935-
3936-
# Inject modals
3937-
if modals:
3938-
modal_html, modal_scripts = wrap_content_with_modals("", modals)
3939-
chart_html = f"{chart_html}{modal_html}{modal_scripts}"
3940-
3941-
if HAS_ANYWIDGET and not open_browser and not is_headless():
3942-
widget = PyWryTVChartWidget(
3943-
content=chart_html,
3944-
chart_config=config_payload,
3945-
theme=theme,
3946-
width=width,
3947-
height=f"{height}px",
3948-
chart_id=chart_id,
3949-
)
3950-
else:
3951-
widget = PyWryTVChartWidget(content=chart_html)
4112+
# Create widget using auto-backend selection
4113+
# Force InlineWidget (IFrame) for BROWSER mode since it has open_in_browser()
4114+
widget = create_tvchart_widget(
4115+
chart_html=chart_html,
4116+
config_payload=config_payload,
4117+
chart_id=chart_id,
4118+
widget_id=widget_id,
4119+
title=title,
4120+
theme=theme,
4121+
width=width,
4122+
height=height,
4123+
toolbars=toolbars,
4124+
modals=modals,
4125+
force_iframe=open_browser,
4126+
)
39524127

39534128
if callbacks:
39544129
for event_type, callback in callbacks.items():
@@ -3960,14 +4135,17 @@ def show_tvchart(
39604135
wire_storage(user_id="default")
39614136

39624137
if provider is not None:
3963-
widget._wire_datafeed_provider(provider)
4138+
wire_datafeed = getattr(widget, "_wire_datafeed_provider", None)
4139+
if callable(wire_datafeed):
4140+
wire_datafeed(provider)
39644141

4142+
# Display
39654143
if is_headless():
39664144
pass
4145+
elif open_browser:
4146+
open_fn = getattr(widget, "open_in_browser", None)
4147+
if callable(open_fn):
4148+
open_fn()
39674149
else:
3968-
open_in_browser = getattr(widget, "open_in_browser", None)
3969-
if open_browser and callable(open_in_browser):
3970-
open_in_browser()
3971-
else:
3972-
widget.display()
4150+
widget.display()
39734151
return widget

0 commit comments

Comments
 (0)