Skip to content

Commit 94e2fdf

Browse files
fix: anywidget and ipywidgets-bokeh js incompatibility issue
1 parent f130968 commit 94e2fdf

File tree

22 files changed

+474
-266
lines changed

22 files changed

+474
-266
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PYTHON_ENV=development
2+
VITE_PORT=5173

docs/CONTRIBUTING.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,24 @@ Thank you for your interest in contributing to PFund-Plot! This guide will help
1515
```bash
1616
git clone https://github.com/PFund-Software-Ltd/pfund-plot.git
1717
cd pfund-plot
18-
poetry install --with dev,test --all-extras
18+
uv sync --all-extras --all-groups
19+
cd ui
20+
pnpm install
1921
```
2022

23+
## How to develop a Svelte component
24+
1. create a .svelte file in ui/src/components/
25+
2. create a widget wrapper of it in ui/src/widgets/
26+
3. set PYTHON_ENV=development in .env
27+
4. run `pnpm dev`
28+
29+
Fallback: if the above doesn't work, you can watch and build the widget(s) automatically:
30+
```bash
31+
# {widget_name} is e.g. candlestick
32+
WIDGET_TARGET={widget_name} pnpm build:widget:watch
33+
# or build ALL widgets for convenience
34+
pnpm build:widgets:watch
35+
```
2136

2237
## Adding a new plot
2338
1. if the plot is supported by [hvplot], use hvplot to implement the plot

docs/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from bokeh.resources import INLINE
44
from IPython.display import HTML, display
55

6-
from pfund_plot.types.core import tFigure
6+
from pfund_plot._typing import tFigure
77

88

99
def display_html(fig: tFigure):
1010
html_buffer = StringIO()
1111
fig.save(html_buffer, resources=INLINE)
1212
html_buffer.seek(0) # Go to the beginning of the buffer
13-
display(HTML(html_buffer.read()))
13+
display(HTML(html_buffer.read()))

pfund_plot/__init__.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@
1616
from pfund_plot.layout import layout
1717

1818

19+
config = get_config()
20+
21+
22+
1923
hvplot.extension('bokeh', 'plotly')
20-
pn.extension('tabulator', 'perspective', 'gridstack')
24+
pn.extension('tabulator', 'perspective', 'gridstack', 'ipywidgets')
2125
# used to throttle updates in panel plots
22-
# NOTE: without it, e.g. dragging a slider will cause the plot to update rapidly and lead to an error
23-
pn.config.throttled = True
26+
pn.config.throttled = False
27+
# NOTE: /assets can only recognized when setting pn.serve(static_dirs=pfund_plot.config.static_dirs)
28+
# see static_dirs in config.py
29+
pn.config.js_files = {
30+
"widgets_amd_config": "/assets/widgets-amd-config.js",
31+
}
2432

2533

2634
Matplotlib = pn.pane.Matplotlib
@@ -32,12 +40,12 @@
3240
__version__ = version("pfund_plot")
3341
__all__ = (
3442
"__version__",
43+
"config",
44+
"configure",
3545
"Matplotlib",
3646
"Bokeh",
3747
"Plotly",
3848
"Vega", "Altair",
39-
"get_config",
40-
"configure",
4149
"candlestick",
4250
"ohlc",
4351
"kline",

pfund_plot/cli/commands/config.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import click
22

3-
from pfeed.enums import DataTool
43
from pfund_plot.const.paths import PROJ_NAME
54

65

@@ -30,8 +29,6 @@ def reset(ctx):
3029

3130

3231
@config.command()
33-
@click.option('--data-tool', '--dt', type=click.Choice(DataTool, case_sensitive=False), help='Set the data tool')
34-
@click.option('--max-points', '--mp', type=int, help='Set the maximum number of points to display in the plot')
3532
@click.option('--data-path', '--dp', type=click.Path(resolve_path=True), help='Set the data path')
3633
@click.option('--cache-path', '--cp', type=click.Path(resolve_path=True), help='Set the cache path')
3734
def set(**kwargs):

pfund_plot/config.py

Lines changed: 84 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import os
2-
import shutil
3-
from dataclasses import dataclass, asdict
1+
from __future__ import annotations
42

5-
import yaml
3+
import os
4+
from pathlib import Path
5+
from dataclasses import dataclass, asdict, field, MISSING
66

7+
from pfund.utils.utils import load_yaml_file, dump_yaml_file
78
from pfund_plot.const.paths import (
89
PROJ_NAME,
10+
MAIN_PATH,
911
DATA_PATH,
1012
CACHE_PATH,
11-
CONFIG_PATH,
1213
CONFIG_FILE_PATH
1314
)
1415

@@ -23,76 +24,104 @@
2324
class Configuration:
2425
data_path: str = str(DATA_PATH)
2526
cache_path: str = str(CACHE_PATH)
27+
static_dirs: dict[str, str] = field(default_factory=dict)
2628

2729
_instance = None
2830
_verbose = False
31+
32+
# REVIEW: this won't be needed if we use pydantic.BaseModel instead of dataclass
33+
def _enforce_types(self):
34+
config_dict = asdict(self)
35+
for k, v in config_dict.items():
36+
_field = self.__dataclass_fields__[k]
37+
if _field.type == 'Path' and isinstance(v, str):
38+
setattr(self, k, Path(v))
2939

3040
@classmethod
3141
def get_instance(cls):
3242
if cls._instance is None:
43+
cls._load_env_file()
3344
cls._instance = cls.load()
3445
return cls._instance
3546

3647
@classmethod
3748
def set_verbose(cls, verbose: bool):
3849
cls._verbose = verbose
3950

51+
@classmethod
52+
def _load_env_file(cls):
53+
from dotenv import find_dotenv, load_dotenv
54+
env_file_path = find_dotenv(usecwd=True, raise_error_if_not_found=False)
55+
if env_file_path:
56+
load_dotenv(env_file_path, override=True)
57+
if cls._verbose:
58+
print(f'{PROJ_NAME} .env file loaded from {env_file_path}')
59+
else:
60+
if cls._verbose:
61+
print(f'{PROJ_NAME} .env file is not found')
62+
4063
@classmethod
41-
def load(cls):
64+
def load(cls) -> Configuration:
4265
'''Loads user's config file and returns a Configuration object'''
4366
CONFIG_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
4467
# Create default config from dataclass fields
45-
default_config = {
46-
field.name: field.default
47-
for field in cls.__dataclass_fields__.values()
48-
if not field.name.startswith('_') # Skip private fields
49-
}
68+
default_config = {}
69+
for _field in cls.__dataclass_fields__.values():
70+
if _field.name.startswith('_'): # Skip private fields
71+
continue
72+
if _field.default_factory is not MISSING:
73+
default_config[_field.name] = _field.default_factory()
74+
else:
75+
default_config[_field.name] = _field.default
5076
needs_update = False
5177
if CONFIG_FILE_PATH.is_file():
52-
with open(CONFIG_FILE_PATH, 'r') as f:
53-
saved_config = yaml.safe_load(f) or {}
54-
if cls._verbose:
55-
print(f"{PROJ_NAME} config loaded from {CONFIG_FILE_PATH}.")
56-
# Check for new or removed fields
57-
new_fields = set(default_config.keys()) - set(saved_config.keys())
58-
removed_fields = set(saved_config.keys()) - set(default_config.keys())
59-
needs_update = bool(new_fields or removed_fields)
60-
61-
if cls._verbose and needs_update:
62-
if new_fields:
63-
print(f"New config fields detected: {new_fields}")
64-
if removed_fields:
65-
print(f"Removed config fields detected: {removed_fields}")
66-
67-
# Filter out removed fields and merge with defaults
68-
saved_config = {k: v for k, v in saved_config.items() if k in default_config}
69-
config = {**default_config, **saved_config}
78+
current_config = load_yaml_file(CONFIG_FILE_PATH) or {}
79+
if cls._verbose:
80+
print(f"Loaded {CONFIG_FILE_PATH}")
81+
# Check for new or removed fields
82+
new_fields = set(default_config.keys()) - set(current_config.keys())
83+
removed_fields = set(current_config.keys()) - set(default_config.keys())
84+
needs_update = bool(new_fields or removed_fields)
85+
86+
if cls._verbose and needs_update:
87+
if new_fields:
88+
print(f"New config fields detected: {new_fields}")
89+
if removed_fields:
90+
print(f"Removed config fields detected: {removed_fields}")
91+
92+
# Filter out removed fields and merge with defaults
93+
current_config = {k: v for k, v in current_config.items() if k in default_config}
94+
config = {**default_config, **current_config}
7095
else:
7196
config = default_config
7297
needs_update = True
73-
config_handler = cls(**config)
98+
config = cls(**config)
7499
if needs_update:
75-
config_handler.dump()
76-
return config_handler
77-
78-
@classmethod
79-
def reset(cls):
80-
'''Resets the config by deleting the user config directory and reloading the config'''
81-
shutil.rmtree(CONFIG_PATH)
82-
if cls._verbose:
83-
print(f"{PROJ_NAME} config successfully reset.")
84-
return cls.load()
100+
config.dump()
101+
return config
85102

86103
def dump(self):
87-
with open(CONFIG_FILE_PATH, 'w') as f:
88-
yaml.dump(asdict(self), f, default_flow_style=False)
89-
if self._verbose:
90-
print(f"{PROJ_NAME} config saved to {CONFIG_FILE_PATH}.")
104+
dump_yaml_file(CONFIG_FILE_PATH, asdict(self))
105+
if self._verbose:
106+
print(f"Created {CONFIG_FILE_PATH}")
107+
108+
@property
109+
def file_path(self):
110+
return CONFIG_FILE_PATH
91111

92112
def __post_init__(self):
93-
self._initialize_configs()
113+
self._initialize()
94114

95-
def _initialize_configs(self):
115+
def _initialize(self):
116+
self._enforce_types()
117+
self._initialize_static_dirs()
118+
self._initialize_file_paths()
119+
120+
def _initialize_static_dirs(self):
121+
if 'assets' not in self.static_dirs:
122+
self.static_dirs['assets'] = str((Path(MAIN_PATH) / "ui" / "static"))
123+
124+
def _initialize_file_paths(self):
96125
for path in [self.data_path, self.cache_path]:
97126
if not os.path.exists(path):
98127
os.makedirs(path)
@@ -103,12 +132,14 @@ def _initialize_configs(self):
103132
def configure(
104133
data_path: str | None = None,
105134
cache_path: str | None = None,
135+
static_dirs: dict[str, str] | None = None,
106136
verbose: bool = False,
107137
write: bool = False,
108138
):
109139
'''Configures the global config object.
110140
It will override the existing config values from the existing config file or the default values.
111141
Args:
142+
static_dirs: a dict of static directories to be used in pn.serve(static_dirs=...)
112143
write: If True, the config will be saved to the config file.
113144
'''
114145
NON_CONFIG_KEYS = ['verbose', 'write']
@@ -117,7 +148,11 @@ def configure(
117148
config_updates.pop(k)
118149
config_updates.pop('NON_CONFIG_KEYS')
119150

120-
config = get_config(verbose=verbose)
151+
static_dirs = static_dirs or {}
152+
assert 'assets' not in static_dirs, "'assets' is a reserved key in static_dirs"
153+
154+
Configuration.set_verbose(verbose)
155+
config = get_config()
121156

122157
# Apply updates for non-None values
123158
for k, v in config_updates.items():
@@ -127,10 +162,9 @@ def configure(
127162
if write:
128163
config.dump()
129164

130-
config._initialize_configs()
165+
config._initialize()
131166
return config
132167

133168

134-
def get_config(verbose: bool = False) -> Configuration:
135-
Configuration.set_verbose(verbose)
169+
def get_config() -> Configuration:
136170
return Configuration.get_instance()

0 commit comments

Comments
 (0)