Skip to content

Commit ef2b15b

Browse files
rashidnhmCatalinaAlbornozashishks0522
authored
Fix notebook converter (#1360)
Update notebook converter to remove setting author info. Fix cell_id ……issue. **Title:** Fix QML notebook converter **Summary:** The current QML converter is not working with newer jupyter notebooks due to them not having an id for each cell. In addition, the way demo author information is stored in QML today is completely different to how it was stored back when this converter was first written. This PR updates the notebook converter such that the author info is no longer needed, and the cell_id is used only if present, if missing it simply uses the cell number instead. **Relevant references:** [sc-90046] **Possible Drawbacks:** As per before, the generated demo can be passed to sphinx-build, which has been tested. But it likely will not be 100% up to standard of the Product team, but this converter should give the author a quick conversion base to work off of. **Related GitHub Issues:** None. --------- Co-authored-by: Catalina Albornoz <albornoz.catalina@hotmail.com> Co-authored-by: Ashish Kanwar Singh <104938869+ashishks0522@users.noreply.github.com>
1 parent defabc9 commit ef2b15b

2 files changed

Lines changed: 18 additions & 198 deletions

File tree

notebook_converter/README.md

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ conda install -c conda-forge pandoc
2121

2222
## Running the Converter
2323
```bash
24-
python3 notebook_converter/notebook_to_demo.py \
25-
path/to/my/notebook.ipynb \
26-
--author "John Doe" "Some info about author" "some/path/to/profile/picture/john_doe.png"
24+
python3 notebook_converter/notebook_to_demo.py path/to/my/notebook.ipynb
2725
```
2826

2927
### If the notebook is not going to be executable by sphinx-build
@@ -35,7 +33,6 @@ After that, you can indicate if the notebook is not executable by setting `--is-
3533
```bash
3634
python3 notebook_converter/notebook_to_demo.py \
3735
path/to/my/notebook.ipynb \
38-
--author "John Doe" "Some info about author" "some/path/to/profile/picture/john_doe.png"
3936
--is-executable=False
4037
```
4138

@@ -49,37 +46,6 @@ If it is omitted, then this information is determined based on the notebook name
4946
The path to the notebook, if absolute path is passed, it will be used verbatim. If relative path is passed,
5047
it must be relative to the script's location.
5148

52-
#### `--author` (optional)
53-
Information about the author, in the syntax of `--author "Full Name" "Bio" "path/to/profile_picture.png"`
54-
55-
For demos with multiple authors, then this flag can be passed multiple times:
56-
```bash
57-
python3 notebook_converter/notebook_to_demo.py \
58-
path/to/my/notebook.ipynb \
59-
--author "John Doe" "Some info about author" "some/path/to/profile/picture/john_doe.png" \
60-
--author "Jane Doe" "Some info about author" "some/path/to/profile/picture/jane_doe.png" \
61-
--is-executable=False
62-
```
63-
64-
#### `--author-file` (optional)
65-
If the notebook being converted is being used by an existing author, this option can be used to pass the location of the existing author-file.
66-
67-
```bash
68-
--author-file "qml/_static/authors/john_doe.txt"
69-
```
70-
71-
Similar to `--author`, this option can be passed multiple times. It can also be passed alongside `--author`
72-
73-
```bash
74-
python3 notebook_converter/notebook_to_demo.py \
75-
path/to/my/notebook.ipynb \
76-
--author "John Doe" "Some info about author" "some/path/to/profile/picture/john_doe.png" \
77-
--author "Jane Doe" "Some info about author" "some/path/to/profile/picture/jane_doe.png" \
78-
--author-file "path/to/bob_doe.txt" \
79-
--author-file "path/to/rob_doe.txt" \
80-
--is-executable=False
81-
```
82-
8349
#### `--is-executable` (True|False) (optional)
8450
Indicate if the notebook is intended to be an executable demo or non-executable. If this is not passed,
8551
the information is inferred from the notebook name. If the notebook name startswith `tutorial_` then it is
Lines changed: 17 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,22 @@
11
#!/usr/bin/env python3
22

3+
import json
34
import os
45
import re
5-
import json
6-
import shutil
7-
from itertools import chain
8-
from pathlib import Path, PurePosixPath
96
from base64 import b64decode
10-
from typing import Dict, List, Union, Optional
7+
from pathlib import Path
8+
from typing import Dict, List, Optional, Union
119

1210
import pypandoc
1311

1412
CWD = Path(os.path.dirname(os.path.realpath(__file__)))
1513
REPO_ROOT = CWD.parent
1614

17-
MATCH_AUTHOR_FILE = re.compile(r"\.{2} +bio:{2} +(?P<name>[\w '\-]+)\n+ *(:photo:)? *(?P<profile_picture>.*\.[a-zA-Z0-9]+)?\n+ *(?P<bio>.*)",
18-
flags=re.M | re.S)
1915

20-
AUTHORS = {
21-
"link-dir": PurePosixPath("../_static/authors"),
22-
"save-dir": REPO_ROOT / "_static" / "authors"
16+
DIRS = {
17+
"demo_images": REPO_ROOT / "_static" / "demonstration_assets",
18+
"demo": REPO_ROOT / "demonstrations_v2"
2319
}
24-
DEMO = {
25-
"link-dir": PurePosixPath("../demonstrations"),
26-
"save-dir": REPO_ROOT / "demonstrations"
27-
}
28-
29-
30-
def format_author_name(name: str) -> str:
31-
return re.sub(r"[ \-'\u0080-\uFFFF]+", "_", name).lower()
32-
33-
def parse_author_file(author_file_path: Union[Path, str]) -> Optional[Dict]:
34-
author_file_loc = Path(author_file_path)
35-
author_file_name = author_file_loc.stem
36-
with author_file_loc.open() as fh:
37-
content = fh.read()
38-
39-
m = MATCH_AUTHOR_FILE.match(content)
40-
if not m:
41-
return None
42-
43-
profile_picture_path = m.group("profile_picture")
44-
if profile_picture_path:
45-
profile_picture_path = Path(profile_picture_path)
46-
if not profile_picture_path.is_absolute():
47-
if profile_picture_path.is_relative_to(AUTHORS["link-dir"]):
48-
profile_picture_path = (REPO_ROOT / "_static" / profile_picture_path).resolve()
49-
else:
50-
profile_picture_path = (author_file_loc.parent / profile_picture_path).resolve()
51-
else:
52-
profile_picture_path = None
53-
return {
54-
"name": m.group("name"),
55-
"bio": m.group("bio"),
56-
"profile_picture": profile_picture_path,
57-
"formatted_name": format_author_name(author_file_name)
58-
}
59-
60-
61-
def set_author_info(author: Dict) -> Path:
62-
"""
63-
64-
:param author: A dictionary of author info with the following syntax
65-
{
66-
"name": Author Name,
67-
"bio": Author Info,
68-
"profile_picture": /path/to/profile_picture.png,
69-
"formatted_name": "<Optional> Author name"
70-
}
71-
:return: Path object of the file that was saved and author info txt
72-
"""
73-
name = author["name"]
74-
bio = author.get("bio", "").strip()
75-
profile_picture_loc = author.get("profile_picture")
76-
if profile_picture_loc:
77-
profile_picture_loc = Path(profile_picture_loc)
78-
name_formatted = author.get("name_formatted", profile_picture_loc.stem if profile_picture_loc else format_author_name(name)).lower()
79-
80-
if profile_picture_loc:
81-
new_profile_picture_save_loc = AUTHORS["save-dir"] / profile_picture_loc.name
82-
else:
83-
new_profile_picture_save_loc = None
84-
85-
info_file_name = f"{name_formatted}.txt"
86-
info_file_save_loc = AUTHORS["save-dir"] / info_file_name
87-
88-
if profile_picture_loc:
89-
try:
90-
shutil.copy(profile_picture_loc, new_profile_picture_save_loc)
91-
except shutil.SameFileError:
92-
pass
93-
94-
if profile_picture_loc:
95-
author_link = (AUTHORS["link-dir"] / profile_picture_loc.name).as_posix()
96-
photo_text = f":photo: {author_link}"
97-
else:
98-
photo_text = ""
99-
author_txts = [f".. bio:: {name}"]
100-
if photo_text:
101-
author_txts.append(f" {photo_text}")
102-
if bio:
103-
if photo_text:
104-
author_txts.append("")
105-
author_txts.append(f" {bio}")
106-
author_txt = "\n".join(author_txts)
107-
108-
with info_file_save_loc.open("w") as fh:
109-
fh.write(author_txt)
110-
111-
return info_file_save_loc
112-
113-
def set_authors(*authors: Dict) -> str:
114-
"""
115-
:param authors: An unpacked list of dictionaries, each one having the following schema:
116-
{
117-
"name": Author Name,
118-
"bio": Author Info,
119-
"profile_picture": /path/to/profile_picture.png,
120-
"formatted_name": "<Optional> Author name"
121-
}
122-
:return:
123-
"""
124-
author_files = [
125-
AUTHORS['link-dir'] / set_author_info(author).name
126-
for author in authors
127-
]
128-
129-
header = [
130-
"About the author",
131-
"----------------"
132-
]
133-
author_txts = [
134-
f"# .. include:: {author_txt_link}\n"
135-
for author_txt_link in author_files
136-
]
137-
138-
author_sphinx_txt = "\n".join([f"# {line}" for line in chain(header, author_txts)])
139-
return f"\n\n{'#' * 70}\n{author_sphinx_txt}"
14020

14121
def str_to_bool(s: str) -> Optional[bool]:
14222
if isinstance(s, bool):
@@ -177,7 +57,7 @@ def add_property_newline(rst: str) -> str:
17757
return re.sub(r"(\w+) (:property=)", r"\1\n \2", rst)
17858

17959

180-
def generate_code_output_block(output_source: List[str] = None, only_header: bool = False) -> str:
60+
def generate_code_output_block(output_source: Optional[List[str]] = None, only_header: bool = False) -> str:
18161
output_header = "\n".join(
18262
[
18363
f"# {line}"
@@ -215,6 +95,7 @@ def fix_image_alt_tag_as_text(rst: str) -> str:
21595
def convert_notebook_to_python(
21696
notebook: Dict,
21797
notebook_name: str,
98+
assets_folder_name: str,
21899
is_executable: bool
219100
) -> str:
220101
# Initial validations
@@ -259,7 +140,7 @@ def convert_notebook_to_python(
259140
output_data["text/plain"], only_header=j != 0
260141
)
261142
elif output["output_type"] == "display_data":
262-
cell_id = cell["id"]
143+
cell_id = cell.get("id", f"c_{i}")
263144
if "text/plain" in output_data and "image/png" not in output_data:
264145
if j == 0:
265146
ret_python_str += generate_code_output_block()
@@ -272,11 +153,11 @@ def convert_notebook_to_python(
272153
ret_python_str += f"\n\n{'#' * 70}"
273154
num_images += 1
274155
image_filename = f"{notebook_name}_{cell_id}_{num_images}.png"
275-
image_file_dir = DEMO["save-dir"] / notebook_assets_folder_name
156+
image_file_dir = DIRS["demo_images"] / assets_folder_name
276157
image_file_path = (
277158
image_file_dir / image_filename
278159
)
279-
image_file_link_path = DEMO["link-dir"] / notebook_assets_folder_name / image_filename
160+
image_file_link_path = Path("../_static/demonstration_assets") / assets_folder_name / image_filename
280161
role_text = generate_sphinx_role_comment(
281162
"figure", image_file_link_path.as_posix(), align="center", width="80%"
282163
)
@@ -315,14 +196,6 @@ def convert_notebook_to_python(
315196
default=None
316197
)
317198

318-
parser.add_argument("--author",
319-
help="Information about Demo Author, must be in format \"Name\" \"Bio\" \"/path/to/profile_picture.png\"",
320-
action="append",
321-
nargs=3)
322-
parser.add_argument("--author-file",
323-
help="Path to an existing author file that is formatted with sphinx roles",
324-
action="append")
325-
326199
results = parser.parse_args()
327200

328201
notebook_file = Path(results.notebook)
@@ -337,36 +210,17 @@ def convert_notebook_to_python(
337210
with notebook_file.open() as fh:
338211
nb = json.load(fh)
339212

340-
authors = []
341-
cli_authors = results.author or []
342-
cli_authors_files = results.author_file or []
343-
for author_file in cli_authors_files:
344-
author_info = parse_author_file(author_file)
345-
if author_info is None:
346-
raise ValueError(f"Unable to parse author file {author_file}")
347-
authors.append(author_info)
348-
for author_info in cli_authors:
349-
name = author_info[0]
350-
bio = author_info[1]
351-
profile_picture = author_info[2]
352-
formatted_name = format_author_name(name)
353-
authors.append({
354-
"name": name,
355-
"bio": bio,
356-
"profile_picture": profile_picture,
357-
"formatted_name": formatted_name
358-
})
359-
360213
nb_py = convert_notebook_to_python(
361214
nb,
362215
notebook_file_name,
216+
notebook_assets_folder_name,
363217
notebook_is_executable
364218
)
219+
220+
demo_dir = DIRS["demo"] / notebook_file_name
221+
if not demo_dir.exists():
222+
demo_dir.mkdir(parents=True)
365223

366-
if authors:
367-
author_sphinx = set_authors(*authors)
368-
nb_py += author_sphinx
369-
370-
with (DEMO["save-dir"] / f"{notebook_file_name}.py").open("w") as fh:
224+
with (demo_dir / "demo.py").open("w") as fh:
371225
fh.write(nb_py)
372226

0 commit comments

Comments
 (0)