Skip to content

Commit db6aa61

Browse files
authored
Merge pull request #5540 from plotly/simplify-build-system
Clean up `pyproject.toml` and build pipeline
2 parents b009b25 + ac9e496 commit db6aa61

18 files changed

+303
-203
lines changed

.github/workflows/check-js-build.yml

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,60 @@ on: push
22

33
jobs:
44
check-js-build:
5-
name: Check JS build artifacts
5+
name: Check JS version number and build artifacts
66
runs-on: ubuntu-latest
77
steps:
88
- uses: actions/checkout@v4
99
- name: Set up Python
1010
uses: actions/setup-python@v5
1111
with:
1212
python-version: "3.x"
13-
13+
14+
- name: Check that version number for JS project matches version number for Python project
15+
run: |
16+
PYPROJECT_PATH="pyproject.toml"
17+
PKGJSON_PATH="js/package.json"
18+
PYPROJECT_VERSION=$(awk -F'"' '/^version/ {print $2; exit}' $PYPROJECT_PATH)
19+
JSPROJECT_VERSION=$(cat $PKGJSON_PATH | jq -r '.version')
20+
if [ "$PYPROJECT_VERSION" != "$JSPROJECT_VERSION" ]; then
21+
echo "❌ Version number $JSPROJECT_VERSION in $PKGJSON_PATH does not match version number $PYPROJECT_VERSION in $PYPROJECT_PATH"
22+
exit 1
23+
else
24+
echo "✅ Version number $JSPROJECT_VERSION in $PKGJSON_PATH matches version number $PYPROJECT_VERSION in $PYPROJECT_PATH"
25+
fi
1426
- name: Install Node
15-
uses: actions/setup-node@v2
27+
uses: actions/setup-node@v4
1628
with:
1729
node-version: '22'
1830

1931
- name: Copy current files to a temporary directory
2032
run: |
21-
cp -R plotly/labextension/ plotly/labextension-tmp/
33+
mv plotly/labextension/ plotly/labextension-tmp/
2234
2335
- name: Install dependencies and build
2436
run: |
2537
curl -LsSf https://astral.sh/uv/install.sh | sh
2638
uv venv
2739
source .venv/bin/activate
28-
uv pip install jupyter
40+
uv pip install jupyterlab
2941
cd js
3042
npm ci
3143
npm run build
3244
npm ls
3345
- name: Check JupyterLab build artifacts
3446
run: |
35-
# 1. Hash contents of all static files, sort by content hash
36-
find plotly/labextension/static -type f -exec sha256sum {} \; | awk '{print $1}' | sort > new_hashes.txt
37-
find plotly/labextension-tmp/static -type f -exec sha256sum {} \; | awk '{print $1}' | sort > old_hashes.txt
38-
39-
# 2. Compare the sorted content hashes
40-
diff old_hashes.txt new_hashes.txt > content_diff.txt
41-
42-
# Remove the "load" line from both package.json files before comparing
43-
grep -v '"load": "static/' plotly/labextension/package.json > pkg1.json
44-
grep -v '"load": "static/' plotly/labextension-tmp/package.json > pkg2.json
45-
46-
# Compare stripped versions
47-
diff pkg1.json pkg2.json > package_json_diff.txt
47+
# Compare the plotly/labextension and plotly/labextension-tmp directories
48+
diff -r --brief plotly/labextension/ plotly/labextension-tmp/ > labextension_diff.txt
4849
49-
# 5. Final check
50-
if [ -s content_diff.txt ] || [ -s package_json_diff.txt ]; then
50+
# Check for differences
51+
if [ -s labextension_diff.txt ]; then
5152
echo "❌ Build artifacts differ:"
5253
echo "--- Unexpected diffs ---"
53-
cat content_diff.txt
54-
echo "--- Unexpected package.json diffs ---"
55-
cat package_json_diff.txt
54+
cat labextension_diff.txt
5655
echo "Please replace the 'plotly/labextension' directory with the artifacts of this CI run."
5756
exit 1
5857
else
59-
echo "✅ Build artifacts match expected output (ignoring known 'load' hash in package.json)."
58+
echo "✅ Build artifacts match expected output"
6059
fi
6160
6261
- name: Store the build artifacts from plotly/labextension

CITATION.cff

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
cff-version: 1.2.0
2-
message: "If you use this software, please cite it as below."
3-
authors:
4-
- family-names: "Kruchten"
5-
given-names: "Nicolas"
6-
orcid: https://orcid.org/0000-0002-8416-789X
7-
- family-names: "Seier"
8-
given-names: "Andrew"
9-
- family-names: "Parmer"
10-
given-names: "Chris"
11-
title: "An interactive, open-source, and browser-based graphing library for Python"
12-
version: 6.6.0
13-
doi: 10.5281/zenodo.14503524
14-
date-released: 2026-03-02
15-
url: "https://github.com/plotly/plotly.py"
1+
cff-version: 1.2.0
2+
message: "If you use this software, please cite it as below."
3+
authors:
4+
- family-names: "Kruchten"
5+
given-names: "Nicolas"
6+
orcid: https://orcid.org/0000-0002-8416-789X
7+
- family-names: "Seier"
8+
given-names: "Andrew"
9+
- family-names: "Parmer"
10+
given-names: "Chris"
11+
title: "An interactive, open-source, and browser-based graphing library for Python"
12+
version: 6.6.0
13+
doi: 10.5281/zenodo.14503524
14+
date-released: 2026-03-02
15+
url: "https://github.com/plotly/plotly.py"

CONTRIBUTING.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,18 @@ You can then run the following command
307307
```bash
308308
python commands.py updateplotlyjsdev --local /path/to/your/plotly.js/
309309
```
310+
311+
## Documentation for `commands.py`
312+
313+
`commands.py` serves as an entry point for utilities to help with plotly.py development.
314+
315+
Usage: `python commands.py <subcommand> <args>`
316+
317+
| Subcommand | Purpose |
318+
|------------|---------|
319+
| `codegen [--noformat]` | Regenerate Python files according to `plot-schema.json`.`--noformat` skips formatter step. |
320+
| `lint` | Lint all Python code in `plotly/`. |
321+
| `format` | Format all Python code in `plotly/`. |
322+
| `updateplotlyjs` | Update `plotly.min.js` and `plot-schema.json` to match the `plotly.js` version specified in `js/package.json`. Then, run codegen to regenerate the Python files. |
323+
| `updateplotlyjsdev [--devrepo REPONAME --devbranch BRANCHNAME] \| [--local PATH]` | Update `plot-schema.json` and `plotly.min.js` to match the version in the provided plotly.js repo name and branch name, OR local path. Then, run codegen to regenerate the Python files. |
324+
| `bumpversion X.Y.Z` | Update the plotly.py version number to X.Y.Z across all files where it needs to be updated. |

MANIFEST.in

Lines changed: 0 additions & 3 deletions
This file was deleted.

RELEASE.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ This is the release process for releasing plotly.py version `X.Y.Z`, including c
77

88
### Finalize changelog
99

10-
Review the contents of `CHANGELOG.md`. We try to follow
10+
Review the contents of `CHANGELOG.md` under the **Unreleased** header. We try to follow
1111
the [keepachangelog](https://keepachangelog.com/en/1.0.0/) guidelines.
12-
Make sure the changelog includes the version being published at the top, along
13-
with the expected publication date.
12+
13+
**Note: You don't need to update the header itself with the new version number,
14+
as that will be done automatically as part of the next step.**
1415

1516
Use the `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security`
1617
labels for all changes to plotly.py. If the version of plotly.js has
@@ -22,16 +23,24 @@ a link to the plotly.js CHANGELOG.
2223

2324
**Create a release branch `git checkout -b release-X.Y.Z` _from the tip of `origin/main`_.**
2425

25-
- Manually update the versions to `X.Y.Z` in the files specified below:
26+
- Ensure that you have `npm` and `uv` installed in your environment
27+
28+
- Run the command `python commands.py bumpversion X.Y.Z`, which will update the version to X.Y.Z in the following places
2629
- `pyproject.toml`
27-
- update version
28-
- `CHANGELOG.md`
29-
- update version and release date
30-
- finalize changelog entries according to instructions above
30+
- `uv.lock`
31+
- `js/package.json`
32+
- `js/package-lock.json`
33+
- `CHANGELOG.md` (Adds a new header for X.Y.Z above the unreleased items)
3134
- `CITATION.cff`
32-
- update version and release date
33-
- Run `uv lock` to update the version number in the `uv.lock` file (do not update manually)
34-
- Commit and push your changes to the release branch:
35+
36+
- Run `git diff` and ensure the above files were all updated correctly.
37+
- Note: The current date is used as the release date in `CHANGELOG.md` and `CITATION.cff`. If you want to use a different date, edit these files manually afterward.
38+
- If the bumpversion command failed for any reason, you can update the versions yourself by doing the following:
39+
- Manually update the version number (and release date, as needed) in `pyproject.toml`, `CHANGELOG.md` and `CITATION.cff`
40+
- Run `npm version X.Y.Z` to update `js/package.json` and `js/package-lock.json`
41+
- Run `uv lock` to update `uv.lock`
42+
43+
- Commit and push the changed files to the release branch:
3544
```sh
3645
$ git add -u
3746
$ git commit -m "version changes for vX.Y.Z"

commands.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import json
66
import os
77
import platform
8+
import re
89
import requests
910
import shutil
11+
import subprocess
1012
from subprocess import check_call
1113
import sys
1214
import time
@@ -296,6 +298,177 @@ def update_plotlyjs_dev(args, outdir):
296298
perform_codegen(outdir)
297299

298300

301+
def bump_version_pyproject_toml(new_version):
302+
"""
303+
Bump the version in pyproject.toml to new_version
304+
Returns True on success, False on failure
305+
"""
306+
pyproject_toml_path = "pyproject.toml"
307+
pattern = r'(^\s*version\s*=\s*")([\w.]+)"(\s*$)'
308+
with open(pyproject_toml_path, "r") as f:
309+
content = f.read()
310+
new_content, n_subs = re.subn(
311+
pattern,
312+
rf'\g<1>{new_version}"\g<3>',
313+
content,
314+
count=1, # replace only the first match
315+
flags=re.MULTILINE,
316+
)
317+
if n_subs < 1:
318+
print(
319+
f"FAILED to update version in {pyproject_toml_path}.",
320+
"Please update manually and then run `uv lock`.",
321+
)
322+
return False
323+
324+
with open(pyproject_toml_path, "w") as f:
325+
f.write(new_content)
326+
327+
# Run `uv lock` to update the version number in the `uv.lock` file (do not update manually)
328+
subprocess.run(["uv", "lock"], check=True)
329+
330+
print(
331+
f"Updated version in {pyproject_toml_path} to {new_version},",
332+
"and updated uv lockfile",
333+
)
334+
return True
335+
336+
337+
def bump_version_package_json(new_version):
338+
"""
339+
Bump the version in package.json to new_version
340+
Returns True on success, False on failure
341+
"""
342+
js_dir = "js"
343+
subprocess.run(
344+
["npm", "version", new_version, "--no-git-tag-version", "--allow-same-version"],
345+
cwd=js_dir,
346+
check=True,
347+
)
348+
print(
349+
f"Updated version in {js_dir}/package.json and {js_dir}/package-lock.json to {new_version}"
350+
)
351+
return True
352+
353+
354+
def bump_version_citation_cff(new_version, new_date):
355+
"""
356+
Bump the version in CITATION.cff to new_version and date-released to new_date
357+
Returns True on success, False on failure
358+
"""
359+
citation_cff_path = "CITATION.cff"
360+
pattern_version = r"(^\s*version\s*:\s*)([\w.]+)(\s*$)"
361+
pattern_date = r"(^\s*date-released\s*:\s*)([0-9\-]+)(\s*$)"
362+
363+
with open(citation_cff_path, "r") as f:
364+
content = f.read()
365+
new_content, n_subs = re.subn(
366+
pattern_version,
367+
rf"\g<1>{new_version}\g<3>",
368+
content,
369+
count=1, # replace only the first match
370+
flags=re.MULTILINE,
371+
)
372+
if n_subs < 1:
373+
print(
374+
f"FAILED to update version in {citation_cff_path}.",
375+
"Please update manually.",
376+
)
377+
return False
378+
new_content, n_subs = re.subn(
379+
pattern_date,
380+
rf"\g<1>{new_date}\g<3>",
381+
new_content,
382+
count=1, # replace only the first match
383+
flags=re.MULTILINE,
384+
)
385+
if n_subs < 1:
386+
print(
387+
f"FAILED to update date-released in {citation_cff_path}.",
388+
"Please update manually.",
389+
)
390+
return False
391+
392+
with open(citation_cff_path, "w") as f:
393+
f.write(new_content)
394+
print(
395+
f"Updated version in {citation_cff_path} to {new_version}",
396+
f"and date-released to {new_date}",
397+
)
398+
return True
399+
400+
401+
def bump_version_changelog_md(new_version, new_date):
402+
"""
403+
Bump the version in CHANGELOG.md to new_version and date to new_date
404+
Returns True on success, False on failure
405+
"""
406+
changelog_md_path = "CHANGELOG.md"
407+
pattern = r"(^\s*##\s*Unreleased\s*$)"
408+
new_header = f"\n\n## [{new_version}] - {new_date}\n"
409+
410+
with open(changelog_md_path, "r") as f:
411+
content = f.read()
412+
413+
# Check if the header already exists, so that we don't add a double header
414+
already_exists_pattern = rf"(^\s*##\s*\[ *{re.escape(new_version)} *\])"
415+
if re.search(already_exists_pattern, content, flags=re.MULTILINE):
416+
print(
417+
f"Header for version {new_version} already exists ",
418+
f"in {changelog_md_path}.",
419+
)
420+
return True
421+
422+
new_content, n_subs = re.subn(
423+
pattern,
424+
rf"\g<1>{new_header}",
425+
content,
426+
count=1, # replace only the first match
427+
flags=re.MULTILINE,
428+
)
429+
if n_subs < 1:
430+
print(
431+
f"FAILED to update version in {changelog_md_path}.",
432+
"Please update manually.",
433+
)
434+
return False
435+
436+
with open(changelog_md_path, "w") as f:
437+
f.write(new_content)
438+
print(
439+
f"Added header in {changelog_md_path} with version {new_version}",
440+
f"and release date {new_date}",
441+
)
442+
return True
443+
444+
445+
def bump_version(args):
446+
"""Bump the version of plotly.py everywhere it needs to be updated."""
447+
new_version = args.version
448+
new_date = time.strftime("%Y-%m-%d")
449+
450+
success = True
451+
452+
success = success and bump_version_citation_cff(new_version, new_date)
453+
success = success and bump_version_changelog_md(new_version, new_date)
454+
success = success and bump_version_package_json(new_version)
455+
# Do this one last since it's the most visible,
456+
# so that if one of the above commands fails it will be easier to notice
457+
success = success and bump_version_pyproject_toml(new_version)
458+
459+
if not success:
460+
print(
461+
"\n\n*** FAILED to update version for at least one file.",
462+
"Please check the output above for details. ***\n\n",
463+
)
464+
exit(1)
465+
466+
print(
467+
f"\n\n*** SUCCESSFULLY updated version to {new_version}.",
468+
"Please verify the result using `git diff`. ***\n\n",
469+
)
470+
471+
299472
def make_parser():
300473
"""Make argument parser."""
301474

@@ -322,6 +495,10 @@ def make_parser():
322495

323496
subparsers.add_parser("updateplotlyjs", help="update plotly.js")
324497

498+
p_bump_version = subparsers.add_parser("bumpversion", help="bump plotly.py version")
499+
# Add a positional argument for the version
500+
p_bump_version.add_argument("version", help="version number")
501+
325502
return parser
326503

327504

@@ -356,6 +533,9 @@ def main():
356533
print(version)
357534
update_plotlyjs(version, outdir)
358535

536+
elif args.cmd == "bumpversion":
537+
bump_version(args)
538+
359539
elif args.cmd is None:
360540
parser.print_help()
361541
sys.exit(1)

0 commit comments

Comments
 (0)