1414
1515import os
1616import re
17+ import subprocess
18+
1719import requests
1820import semver
19- import subprocess
21+ import toml
2022
21- SETUP_PY_PATH = "setup.py"
23+ PYPROJECT_TOML_PATH = "pyproject.toml"
24+ PACKAGE_PYPROJECT_TOML_PATH = "streamlit_bokeh/pyproject.toml"
2225
2326
2427def get_latest_bokeh_version ():
@@ -31,29 +34,31 @@ def get_latest_bokeh_version():
3134
3235
3336def get_component_version ():
34- with open (SETUP_PY_PATH , "r" ) as f :
35- setup_content = f . read ( )
37+ with open (PYPROJECT_TOML_PATH , "r" ) as f :
38+ pyproject_data = toml . load ( f )
3639
37- # Extract version from setup.py
38- match = re . search ( r"version\s*=\s*['\"]([\d\.]+)(['\"])" , setup_content )
40+ # Extract version from pyproject.toml
41+ version = pyproject_data . get ( "project" , {}). get ( "version" )
3942
40- if match :
41- return match . group ( 1 )
43+ if version :
44+ return version
4245 else :
43- raise ValueError ("Bokeh version not found in the file " )
46+ raise ValueError ("Component version not found in pyproject.toml " )
4447
4548
4649def get_dependency_bokeh_version ():
47- with open (SETUP_PY_PATH , "r" ) as f :
48- setup_content = f . read ( )
50+ with open (PYPROJECT_TOML_PATH , "r" ) as f :
51+ pyproject_data = toml . load ( f )
4952
50- # Extract Bokeh version from dependency line (e.g., bokeh==2.4.3)
51- match = re . search ( r"bokeh\s*==\s*([\d\.]+)" , setup_content )
53+ # Extract Bokeh version from dependencies list
54+ dependencies = pyproject_data . get ( "project" , {}). get ( "dependencies" , [] )
5255
53- if match :
54- return match .group (1 )
55- else :
56- raise ValueError ("Bokeh version not found in the file" )
56+ for dep in dependencies :
57+ match = re .search (r"bokeh\s*==\s*([\d\.]+)" , dep )
58+ if match :
59+ return match .group (1 )
60+
61+ raise ValueError ("Bokeh version not found in dependencies" )
5762
5863
5964def download_files (new_version , destination ):
@@ -67,6 +72,9 @@ def download_files(new_version, destination):
6772 f"https://raw.githubusercontent.com/bokeh/bokeh/refs/tags/{ new_version } /LICENSE.txt" ,
6873 ]
6974
75+ # Ensure destination/bokeh directory exists
76+ os .makedirs (os .path .join (destination , "bokeh" ), exist_ok = True )
77+
7078 for url in files_to_download :
7179 filename = os .path .basename (url )
7280 print (f"Downloading { filename } " )
@@ -77,28 +85,48 @@ def download_files(new_version, destination):
7785 f .write (chunk )
7886
7987
80- def update_setup_py (new_version , old_bokeh_version , new_bokeh_version ):
81- with open (SETUP_PY_PATH , "r" ) as f :
82- setup_content = f .read ()
88+ def update_pyproject_toml (new_version , old_bokeh_version , new_bokeh_version ):
89+ """
90+ Update the project version and Bokeh dependency in the TOML files
91+ without disturbing comments, headers, or table ordering.
92+
93+ We intentionally operate on raw text instead of round-tripping through
94+ a TOML serializer (which would drop comments and reformat sections).
95+ """
96+ for path in (PYPROJECT_TOML_PATH , PACKAGE_PYPROJECT_TOML_PATH ):
97+ if not os .path .exists (path ):
98+ raise FileNotFoundError (f"File { path } not found" )
99+
100+ with open (path , "r" , encoding = "utf-8" ) as f :
101+ contents = f .read ()
102+
103+ # 1) Update the [project] version line.
104+ #
105+ # All pyproject.toml files in this repo have a single `version = "..."`
106+ # entry under `[project]`. Anchoring at line-start ensures we don't
107+ # accidentally rewrite a value in another table or in comments.
108+ contents , replaced_version_count = re .subn (
109+ r'(?m)^(version\s*=\s*")([^"]+)(")' ,
110+ lambda m : f"{ m .group (1 )} { new_version } { m .group (3 )} " ,
111+ contents ,
112+ count = 1 ,
113+ )
83114
84- # Replace package version in `version='...'`
85- # This pattern is naive; adapt as needed for your file structure.
86- setup_content = re .sub (
87- r"(version\s*=\s*['\"])([\d\.]+)(['\"])" ,
88- rf"\g<1>{ new_version } \g<3>" ,
89- setup_content ,
90- )
115+ if replaced_version_count == 0 :
116+ raise ValueError (f"Could not find project version line in { path } " )
91117
92- # Replace bokeh==old_version with bokeh==new_version
93- if old_bokeh_version :
94- setup_content = re .sub (
95- rf"(bokeh\s*==\s*){ old_bokeh_version } " ,
96- rf"\g<1>{ new_bokeh_version } " ,
97- setup_content ,
98- )
118+ # 2) Update the Bokeh dependency version. This only has an effect in the
119+ # root pyproject, since the package-local pyproject does not currently
120+ # declare dependencies.
121+ if old_bokeh_version :
122+ contents = re .sub (
123+ rf"(bokeh\s*==\s*){ re .escape (old_bokeh_version )} " ,
124+ rf"\g<1>{ new_bokeh_version } " ,
125+ contents ,
126+ )
99127
100- with open (SETUP_PY_PATH , "w" ) as f :
101- f .write (setup_content )
128+ with open (path , "w" , encoding = "utf-8 " ) as f :
129+ f .write (contents )
102130
103131
104132def update_test_requirements (
@@ -160,16 +188,56 @@ def update_init_py(old_bokeh_version, new_bokeh_version):
160188 f .write (init_py_contents )
161189
162190
163- def update_index_html (public_dir , old_version , new_version ):
164- index_html_path = os .path .join (public_dir , "index.html" )
165- if os .path .exists (index_html_path ):
166- with open (index_html_path , "r" , encoding = "utf-8" ) as f :
191+ def update_loader_imports (old_bokeh_version , new_bokeh_version ):
192+ """
193+ Update versioned Bokeh asset import paths in the TypeScript loader to the new version.
194+
195+ This replaces occurrences like `bokeh-3.8.0.min.js` with `bokeh-<new>.min.js`
196+ in `streamlit_bokeh/frontend/src/v2/loaders.ts`.
197+ """
198+ loader_path = "streamlit_bokeh/frontend/src/v2/loaders.ts"
199+ with open (loader_path , "r" , encoding = "utf-8" ) as f :
200+ contents = f .read ()
201+
202+ suffixes = ["mathjax" , "gl" , "api" , "tables" , "widgets" , "" ]
203+ for suffix in suffixes :
204+ old_str = (
205+ f"bokeh-{ suffix } -{ old_bokeh_version } .min.js"
206+ if suffix
207+ else f"bokeh-{ old_bokeh_version } .min.js"
208+ )
209+ new_str = (
210+ f"bokeh-{ suffix } -{ new_bokeh_version } .min.js"
211+ if suffix
212+ else f"bokeh-{ new_bokeh_version } .min.js"
213+ )
214+ contents = contents .replace (old_str , new_str )
215+
216+ with open (loader_path , "w" , encoding = "utf-8" ) as f :
217+ f .write (contents )
218+
219+
220+ def update_index_html (frontend_dir , old_version , new_version ):
221+ """
222+ Update the index.html files to reference the new Bokeh asset version.
223+ Only the source Vite entry point (`frontend/index.html`) needs to be updated,
224+ since build artifacts are regenerated.
225+ """
226+
227+ index_html_paths = [os .path .join (frontend_dir , "index.html" )]
228+
229+ cdn_suffixes = ["mathjax" , "gl" , "api" , "tables" , "widgets" , "" ]
230+ found_index = False
231+
232+ for path in index_html_paths :
233+ if not os .path .exists (path ):
234+ continue
235+
236+ found_index = True
237+ with open (path , "r" , encoding = "utf-8" ) as f :
167238 html_content = f .read ()
168239
169- # If old_version is known, do a direct replacement
170240 if old_version :
171- # Replace each script reference with the new version
172- cdn_suffixes = ["mathjax" , "gl" , "api" , "tables" , "widgets" , "" ]
173241 for suffix in cdn_suffixes :
174242 old_str = (
175243 f"bokeh-{ suffix } -{ old_version } .min.js"
@@ -183,10 +251,13 @@ def update_index_html(public_dir, old_version, new_version):
183251 )
184252 html_content = html_content .replace (old_str , new_str )
185253
186- with open (index_html_path , "w" , encoding = "utf-8" ) as f :
254+ with open (path , "w" , encoding = "utf-8" ) as f :
187255 f .write (html_content )
188- else :
189- print ("No index.html found in frontend/public. Skipping HTML update." )
256+
257+ if not found_index :
258+ print (
259+ "No index.html found under streamlit_bokeh/frontend. Skipping HTML update."
260+ )
190261
191262
192263def check_remote_branch_exists (remote : str , new_version : str ) -> bool :
@@ -243,20 +314,25 @@ def check_remote_branch_exists(remote: str, new_version: str) -> bool:
243314
244315 print ("New version available!" )
245316 public_dir = "streamlit_bokeh/frontend/public"
317+ frontend_dir = "streamlit_bokeh/frontend"
246318
247- # Remove original files
319+ # Remove original files from the public bokeh directory
248320 bokeh_dir = os .path .join (public_dir , "bokeh" )
321+ os .makedirs (bokeh_dir , exist_ok = True )
249322 for filename in os .listdir (bokeh_dir ):
250323 if "bokeh" in filename and filename .endswith (".js" ):
251324 os .remove (os .path .join (bokeh_dir , filename ))
252325
326+ # Download new Bokeh assets into the public directory so they are served
327+ # from `/bokeh` at runtime.
253328 download_files (new_bokeh_version , public_dir )
254- # Update the bokeh dependency version in index.html and __init__.py
255- update_index_html (public_dir , old_bokeh_version , new_bokeh_version )
329+ # Update the bokeh dependency version in index.html, TS loader and __init__.py
330+ update_index_html (frontend_dir , old_bokeh_version , new_bokeh_version )
331+ update_loader_imports (old_bokeh_version , new_bokeh_version )
256332 update_init_py (old_bokeh_version , new_bokeh_version )
257333
258- # Update the bokeh dependency version and component version in setup.py and test-requirements.txt
259- update_setup_py (new_version , old_bokeh_version , new_bokeh_version )
334+ # Update the bokeh dependency version in pyproject.toml and test-requirements.txt
335+ update_pyproject_toml (new_version , old_bokeh_version , new_bokeh_version )
260336 update_test_requirements (
261337 old_bokeh_version , new_bokeh_version , old_version , new_version
262338 )
0 commit comments