Skip to content

Commit 9e2f184

Browse files
Add script to export API Ids as JSON. (#53)
1 parent af03ac4 commit 9e2f184

File tree

5 files changed

+225
-112
lines changed

5 files changed

+225
-112
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/typeshed.json
22
/typeshed.*.json
3+
/api-ids.json
34
/node_modules/
45
*.rst
56
/crowdin/

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"description": "Temporary home for micro:bit MicroPython stubs.",
55
"scripts": {
66
"test": "pyright -p test-pyrightconfig.json",
7-
"i18n:typeshed-to-crowdin": "python3 scripts/crowdin-convert.py typeshed-to-crowdin",
8-
"i18n:crowdin-to-typeshed": "python3 scripts/crowdin-convert.py crowdin-to-typeshed"
7+
"i18n:typeshed-to-crowdin": "python3 scripts/crowdin_convert.py typeshed-to-crowdin",
8+
"i18n:crowdin-to-typeshed": "python3 scripts/crowdin_convert.py crowdin-to-typeshed",
9+
"export-api-ids": "python3 scripts/export_api_ids.py"
910
},
1011
"repository": {
1112
"type": "git",

scripts/common.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""
2+
Functions, types and variables
3+
shared with various scripts.
4+
Including:
5+
crowdin_convert.py
6+
export_api_ids.py
7+
"""
8+
9+
from dataclasses import dataclass
10+
import os
11+
import ast
12+
from typing import Any
13+
from typing import Optional
14+
15+
DIR = os.path.dirname(__file__)
16+
17+
18+
@dataclass
19+
class TypeshedFile:
20+
file_path: str
21+
module_name: str
22+
python_file: bool
23+
24+
25+
def get_source(file_path):
26+
with open(file_path, "r", encoding="utf-8") as file:
27+
return file.read()
28+
29+
30+
def module_name_for_path(file_path: str):
31+
"""Hacky determination of the module name."""
32+
name = os.path.basename(file_path)
33+
in_microbit_package = os.path.basename(os.path.dirname(file_path)) == "microbit"
34+
if in_microbit_package:
35+
if name == "__init__.pyi":
36+
return "microbit"
37+
return ".".join(["microbit", os.path.splitext(name)[0]])
38+
return os.path.splitext(name)[0]
39+
40+
41+
def get_stub_files() -> list[TypeshedFile]:
42+
top = os.path.join(DIR, "..", "lang/en/typeshed/stdlib")
43+
files_to_process: list[TypeshedFile] = []
44+
for root, dirs, files in os.walk(top):
45+
for name in files:
46+
file_path = os.path.join(root, name)
47+
# Skip audio stubs file that imports from microbit audio
48+
# (so we don't include its docstring)
49+
if (
50+
os.path.basename(os.path.dirname(file_path)) != "microbit"
51+
and name == "audio.pyi"
52+
):
53+
continue
54+
if name.endswith(".pyi"):
55+
files_to_process.append(
56+
TypeshedFile(
57+
file_path=file_path,
58+
module_name=module_name_for_path(file_path),
59+
python_file=True,
60+
)
61+
)
62+
else:
63+
files_to_process.append(
64+
TypeshedFile(
65+
file_path=file_path,
66+
module_name="",
67+
python_file=False,
68+
)
69+
)
70+
return sorted(files_to_process, key=lambda x: x.file_path)
71+
72+
73+
class DocStringVisitor(ast.NodeVisitor):
74+
def __init__(self, module_name):
75+
self.module_name = module_name
76+
self.key = []
77+
self.used_keys = set()
78+
self.preceding: Optional[str] = None
79+
80+
def visit_Module(self, node: ast.Module) -> Any:
81+
name = self.module_name
82+
self.handle_docstring(node, name)
83+
84+
self.key.append(name)
85+
self.generic_visit(node)
86+
self.key.pop()
87+
88+
def visit_ClassDef(self, node):
89+
name = node.name
90+
self.handle_docstring(node, name)
91+
92+
self.key.append(name)
93+
self.generic_visit(node)
94+
self.key.pop()
95+
96+
def visit_FunctionDef(self, node: ast.FunctionDef) -> Any:
97+
self.preceding = None
98+
self.handle_docstring(node, node.name)
99+
100+
def visit_AnnAssign(self, node: ast.AnnAssign) -> Any:
101+
self.preceding = node.target.id # type: ignore
102+
103+
def visit_Assign(self, node: ast.Assign) -> Any:
104+
if len(node.targets) != 1:
105+
raise AssertionError()
106+
self.preceding = node.targets[0].id # type: ignore
107+
108+
def visit_Expr(self, node: ast.Expr) -> Any:
109+
if self.preceding:
110+
self.handle_docstring(node, self.preceding)
111+
112+
def generic_visit(self, node: ast.AST) -> Any:
113+
self.preceding = None
114+
return super().generic_visit(node)
115+
116+
def handle_docstring(self, node: ast.AST, name: str) -> None:
117+
raise NotImplementedError()

scripts/crowdin-convert.py renamed to scripts/crowdin_convert.py

Lines changed: 9 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,14 @@
77
"""
88

99
import ast
10-
from dataclasses import dataclass
1110
import os
1211
import json
1312
import re
1413
import sys
15-
16-
from typing import Any
17-
18-
from typing import Optional
14+
from common import TypeshedFile, get_stub_files, DIR, get_source, DocStringVisitor
1915

2016

2117
NODE_TYPES_WITH_DOCSTRINGS = (ast.FunctionDef, ast.Module, ast.ClassDef)
22-
DIR = os.path.dirname(__file__)
2318
EN_JSON_PATH = os.path.join(DIR, "../crowdin/api.en.json")
2419
TRANSLATED_JSON_DIR = os.path.join(DIR, "../crowdin/translated")
2520

@@ -35,55 +30,6 @@ def typeshed_to_crowdin():
3530
save_docstrings_as_json(data)
3631

3732

38-
@dataclass
39-
class TypeshedFile:
40-
file_path: str
41-
module_name: str
42-
python_file: bool
43-
44-
45-
def get_stub_files() -> list[TypeshedFile]:
46-
top = os.path.join(DIR, "..", "lang/en/typeshed/stdlib")
47-
files_to_process: list[TypeshedFile] = []
48-
for root, dirs, files in os.walk(top):
49-
for name in files:
50-
file_path = os.path.join(root, name)
51-
# Skip audio stubs file that imports from microbit audio (so we don't include its docstring)
52-
if (
53-
os.path.basename(os.path.dirname(file_path)) != "microbit"
54-
and name == "audio.pyi"
55-
):
56-
continue
57-
if name.endswith(".pyi"):
58-
files_to_process.append(
59-
TypeshedFile(
60-
file_path=file_path,
61-
module_name=module_name_for_path(file_path),
62-
python_file=True,
63-
)
64-
)
65-
else:
66-
files_to_process.append(
67-
TypeshedFile(
68-
file_path=file_path,
69-
module_name="",
70-
python_file=False,
71-
)
72-
)
73-
return sorted(files_to_process, key=lambda x: x.file_path)
74-
75-
76-
def module_name_for_path(file_path: str):
77-
"""Hacky determination of the module name used as a translation key."""
78-
name = os.path.basename(file_path)
79-
in_microbit_package = os.path.basename(os.path.dirname(file_path)) == "microbit"
80-
if in_microbit_package:
81-
if name == "__init__.pyi":
82-
return "microbit"
83-
return ".".join(["microbit", os.path.splitext(name)[0]])
84-
return os.path.splitext(name)[0]
85-
86-
8733
# Translation key to dict with message/description fields.
8834
TranslationJSON = dict[str, dict[str, str]]
8935

@@ -101,9 +47,14 @@ def handle_docstring(self, node: ast.AST, name: str) -> None:
10147
key_root = ".".join([*self.key, name])
10248
key = key_root
10349
suffix = 1
104-
while key in self.used_keys:
105-
key = f"{key_root}-{suffix}"
106-
suffix += 1
50+
if isinstance(node, ast.FunctionDef): # ctx.id
51+
for decorator in node.decorator_list:
52+
if hasattr(decorator, "id"):
53+
if decorator.id == "overload":
54+
key = f"{key}-{suffix}"
55+
while key in self.used_keys:
56+
suffix += 1
57+
key = f"{key_root}-{suffix}"
10758
self.used_keys.add(key)
10859
self.data.update(get_entries(node, name, key))
10960

@@ -112,11 +63,6 @@ def handle_docstring(self, node: ast.AST, name: str) -> None:
11263
return collector.data
11364

11465

115-
def get_source(file_path):
116-
with open(file_path, "r", encoding="utf-8") as file:
117-
return file.read()
118-
119-
12066
def pretty_api_name(name):
12167
return name.replace("_", " ").strip().lower()
12268

@@ -458,53 +404,6 @@ def maybe_dir(maybe_path):
458404
os.mkdir(maybe_path)
459405

460406

461-
class DocStringVisitor(ast.NodeVisitor):
462-
def __init__(self, module_name):
463-
self.module_name = module_name
464-
self.key = []
465-
self.used_keys = set()
466-
self.preceding: Optional[str] = None
467-
468-
def visit_Module(self, node: ast.Module) -> Any:
469-
name = self.module_name
470-
self.handle_docstring(node, name)
471-
472-
self.key.append(name)
473-
self.generic_visit(node)
474-
self.key.pop()
475-
476-
def visit_ClassDef(self, node):
477-
name = node.name
478-
self.handle_docstring(node, name)
479-
480-
self.key.append(name)
481-
self.generic_visit(node)
482-
self.key.pop()
483-
484-
def visit_FunctionDef(self, node: ast.FunctionDef) -> Any:
485-
self.preceding = None
486-
self.handle_docstring(node, node.name)
487-
488-
def visit_AnnAssign(self, node: ast.AnnAssign) -> Any:
489-
self.preceding = node.target.id # type: ignore
490-
491-
def visit_Assign(self, node: ast.Assign) -> Any:
492-
if len(node.targets) != 1:
493-
raise AssertionError()
494-
self.preceding = node.targets[0].id # type: ignore
495-
496-
def visit_Expr(self, node: ast.Expr) -> Any:
497-
if self.preceding:
498-
self.handle_docstring(node, self.preceding)
499-
500-
def generic_visit(self, node: ast.AST) -> Any:
501-
self.preceding = None
502-
return super().generic_visit(node)
503-
504-
def handle_docstring(self, node: ast.AST, name: str) -> None:
505-
raise NotImplementedError()
506-
507-
508407
if __name__ == "__main__":
509408
operation = sys.argv[1]
510409
if operation == "typeshed-to-crowdin":

scripts/export_api_ids.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Creates api-ids.json file which
3+
contains all API calls that correspond
4+
to documentation shown in the
5+
Python Editor sidebar.
6+
"""
7+
8+
import ast
9+
import os
10+
import json
11+
from common import (
12+
get_stub_files,
13+
DIR,
14+
TypeshedFile,
15+
get_source,
16+
DocStringVisitor,
17+
)
18+
19+
modules = [
20+
"builtins",
21+
"gc",
22+
"log",
23+
"machine",
24+
"math",
25+
"microbit",
26+
"micropython",
27+
"music",
28+
"neopixel",
29+
"os",
30+
"radio",
31+
"random",
32+
"speech",
33+
"struct",
34+
"sys",
35+
"time",
36+
]
37+
38+
39+
def export_api_ids():
40+
data_list = []
41+
files_to_process = get_stub_files()
42+
for ts_file in files_to_process:
43+
if ts_file.python_file:
44+
data_list = data_list + get_api_ids(ts_file)
45+
data_list.sort()
46+
data = {"apiIds": data_list}
47+
save_api_ids(data)
48+
pass
49+
50+
51+
def save_api_ids(data):
52+
with open(os.path.join(DIR, "../", "api-ids.json"), "w") as file:
53+
file.write(json.dumps(data, indent=2))
54+
55+
56+
def checkModuleRequired(module_name):
57+
if module_name in modules:
58+
return True
59+
if "microbit" in module_name:
60+
return True
61+
return False
62+
63+
64+
def get_api_ids(ts_file: TypeshedFile):
65+
source = get_source(ts_file.file_path)
66+
tree = ast.parse(source)
67+
68+
class DocStringCollector(DocStringVisitor):
69+
def __init__(self):
70+
super().__init__(ts_file.module_name)
71+
self.data: list[str] = []
72+
73+
def handle_docstring(self, node: ast.AST, name: str) -> None:
74+
key_root = ".".join([*self.key, name])
75+
key = key_root
76+
suffix = 1
77+
if isinstance(node, ast.FunctionDef): # ctx.id
78+
for decorator in node.decorator_list:
79+
if hasattr(decorator, "id"):
80+
if decorator.id == "overload":
81+
key = f"{key}-{suffix}"
82+
while key in self.used_keys:
83+
suffix += 1
84+
key = f"{key_root}-{suffix}"
85+
self.used_keys.add(key)
86+
if checkModuleRequired(ts_file.module_name):
87+
self.data.append(key)
88+
89+
collector = DocStringCollector()
90+
collector.visit(tree)
91+
return collector.data
92+
93+
94+
if __name__ == "__main__":
95+
export_api_ids()

0 commit comments

Comments
 (0)