Skip to content

Commit ed0b347

Browse files
committed
Autogenerate Python API docs
1 parent b04641a commit ed0b347

File tree

6 files changed

+190
-3
lines changed

6 files changed

+190
-3
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ pythreejs/static/
1515

1616
# Autogen files
1717
*_autogen.py
18+
*_autogen.rst
1819
*.autogen.js
1920
*.autogen.json
2021
js/src/**/index.js
2122
pythreejs/**/__init__.py
23+
docs/source/api/**/index.rst
2224

2325
# Test and coverage
2426
.coverage

docs/source/index.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ Contents
3232
installing
3333
introduction
3434

35+
.. toctree::
36+
:maxdepth: 1
37+
38+
api/index
39+
3540

3641
.. toctree::
3742
:maxdepth: 2
@@ -40,7 +45,6 @@ Contents
4045
develop-install
4146

4247

43-
4448
.. links
4549
4650
.. _`Jupyter widgets`: https://jupyter.org/widgets.html

js/scripts/clean-generated-files.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,17 @@ function cleanGeneratedFilesAsync() {
6969
var pyPromise = rmFileGlobAsync('../pythreejs/**/*_autogen.py');
7070
var pyIndexPromise = rmFileGlobAsync('../pythreejs/**/__init__.py');
7171

72+
var docPromise = rmFileGlobAsync('../docs/source/**/*_autogen.rst');
73+
var docIndexPromise = rmFileGlobAsync('../docs/source/api/**/index.rst');
74+
7275
return Promise.all([
7376
jsPromise,
7477
jsonPromise,
7578
jsIndexPromise,
7679
pyPromise,
7780
pyIndexPromise,
81+
docPromise,
82+
docIndexPromise,
7883
]);
7984
}
8085

js/scripts/generate-wrappers.js

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const path = require('path');
55
const fse = require('fs-extra');
66
const Glob = require('glob').Glob;
77
const Handlebars = require('handlebars');
8-
98
const classConfigs = require('./three-class-config');
109
const Types = require('./prop-types.js');
1110

@@ -14,6 +13,7 @@ const baseDir = path.resolve(scriptDir, '..');
1413

1514
const jsSrcDir = path.resolve(baseDir, 'src/');
1615
const pySrcDir = path.resolve(baseDir, '..', 'pythreejs');
16+
const docSrcDir = path.resolve(baseDir, '..', 'docs', 'source', 'api');
1717
const templateDir = path.resolve(scriptDir, 'templates');
1818

1919
const threeSrcDir = path.resolve(baseDir, 'node_modules', 'three', 'src');
@@ -99,9 +99,27 @@ const jsWrapperTemplate = compileTemplate('js_wrapper');
9999
const jsIndexTemplate = compileTemplate('js_index');
100100
const pyWrapperTemplate = compileTemplate('py_wrapper');
101101
const pyTopLevelInitTemplate = compileTemplate('py_top_level_init');
102+
const docTemplate = compileTemplate('autodoc');
103+
const docIndexTemplate = compileTemplate('autodoc_index');
102104

103105
const pathSep = /\\|\//;
104106

107+
Handlebars.registerHelper('indent', function (data, indent) {
108+
const out = data.replace(/\n/g, '\n' + indent);
109+
return new Handlebars.SafeString(out);
110+
});
111+
112+
Handlebars.registerHelper('rst', function (data, indent) {
113+
let out = data
114+
.replace(/\*/g, '\\*')
115+
.replace(/\[/g, '\\[')
116+
.replace(/\]/g, '\\]');
117+
if (indent) {
118+
out = out.replace(/\n/g, '\n' + indent);
119+
}
120+
return new Handlebars.SafeString(out);
121+
});
122+
105123
//
106124
// Helper Functions
107125
//
@@ -856,6 +874,14 @@ class PythonWrapper {
856874
return this.pyAutoDestPath;
857875
}
858876

877+
getDocFilename() {
878+
return path.resolve(docSrcDir, this.dirRelativePath, `${this.className}_autogen.rst`);
879+
}
880+
881+
getDocOutput() {
882+
return docTemplate(this.context);
883+
}
884+
859885
}
860886

861887
function createPythonWrapper(modulePath, className) {
@@ -871,14 +897,89 @@ function createPythonWrapper(modulePath, className) {
871897
let fname = wrapper.getOutputFilename();
872898
let pyPromise = fse.outputFile(fname, wrapper.output);
873899

874-
return pyPromise;
900+
// Also output documentation for the Python API
901+
let docfname = wrapper.getDocFilename();
902+
//console.log(docfname);
903+
//let docPromise = Promise.resolve();
904+
let docPromise = fse.outputFile(docfname, wrapper.getDocOutput());
905+
906+
return Promise.all([pyPromise, docPromise]);
875907
}
876908

877909
function createPythonModuleInitFile(modulePath) {
878910

879911
const dirname = path.dirname(modulePath);
880912
const pyInitFilePath = path.resolve(pySrcDir, dirname, '__init__.py');
881913
return fse.ensureFile(pyInitFilePath);
914+
}
915+
916+
917+
918+
919+
function writeDocModuleFiles() {
920+
921+
console.log('Writing document indices...');
922+
923+
const RE_AUTOGEN = /index.rst/g;
924+
925+
function writeIndexForDir(dirPath, isTopLevel) {
926+
927+
const dirAbsPath = path.resolve(docSrcDir, dirPath);
928+
let moduleName;
929+
if (dirPath === '.') {
930+
moduleName = 'pythreejs';
931+
} else {
932+
moduleName = path.basename(dirPath);
933+
}
934+
935+
// Generate list of files in dir to include in module as toc entries
936+
return fse.readdir(dirAbsPath).then(function(dirFiles) {
937+
938+
// sort directories first:
939+
dirFiles = _.sortBy(dirFiles, filePath => {
940+
return fse.statSync(path.join(dirAbsPath, filePath)).isDirectory() ? 0 : 1;
941+
});
942+
943+
dirFiles = dirFiles.filter(filePath => {
944+
return !filePath.match(RE_AUTOGEN);
945+
});
946+
947+
// convert file paths to paths relative to dirPath
948+
dirFiles = dirFiles.map(filePath => {
949+
if (fse.statSync(path.join(dirAbsPath, filePath)).isDirectory()) {
950+
return `./${filePath}/index`;
951+
}
952+
// Need to use forward slash for RST:
953+
return `./${path.basename(filePath)}`;
954+
});
955+
956+
// render template
957+
const context = {
958+
now: new Date(),
959+
generatorScriptName: path.basename(__filename),
960+
moduleName: moduleName,
961+
submodules: dirFiles,
962+
top_level: isTopLevel,
963+
};
964+
const output = docIndexTemplate(context);
965+
const outputPath = path.resolve(docSrcDir, dirPath, 'index.rst');
966+
967+
return fse.outputFile(outputPath, output);
968+
969+
});
970+
}
971+
972+
// map over all directories in js src dir
973+
return mapPromiseFnOverGlob(
974+
`**/`, // trailing slash globs for dirs only
975+
function(dirPath) {
976+
return writeIndexForDir(dirPath, false);
977+
},
978+
{ cwd: docSrcDir, }
979+
).then(function() {
980+
// write top-level index (not included in above glob)
981+
return writeIndexForDir('.', true);
982+
});
882983

883984
}
884985

@@ -890,6 +991,14 @@ function createTopLevelPythonModuleFile() {
890991
'sage.py'
891992
];
892993

994+
const ignoreDocFiles = [
995+
'enums',
996+
'pythreejs',
997+
'traits',
998+
'_package',
999+
'_version',
1000+
];
1001+
8931002
const modules = [];
8941003

8951004
return mapPromiseFnOverGlob('**/*.py', function(filePath) {
@@ -917,8 +1026,16 @@ function createTopLevelPythonModuleFile() {
9171026
importPath = '.' + moduleName;
9181027
}
9191028

1029+
let docPath;
1030+
if (ignoreDocFiles.indexOf(moduleName) === -1) {
1031+
docPath = filePath.replace('_autogen', '').replace('.py', '') + '_autogen';
1032+
} else {
1033+
docPath = '';
1034+
}
1035+
9201036
modules.push({
9211037
pyRelativePath: importPath,
1038+
docRelativePath: docPath,
9221039
});
9231040

9241041
}, {
@@ -979,6 +1096,9 @@ function createPythonFiles() {
9791096
// Manually ensure base init file is created
9801097
return createPythonModuleInitFile('_base/__init__');
9811098
})
1099+
.then(function() {
1100+
return writeDocModuleFiles();
1101+
})
9821102
.then(function() {
9831103
// top level __init__.py file imports *all* pythreejs modules into namespace
9841104
return createTopLevelPythonModuleFile();
@@ -987,6 +1107,7 @@ function createPythonFiles() {
9871107
}
9881108

9891109

1110+
9901111
function generateFiles() {
9911112

9921113
return Promise.all([

js/scripts/templates/autodoc.mustache

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
.. py:currentmodule:: pythreejs
3+
4+
{{ className }}
5+
====================================================
6+
7+
.. py:class:: {{ className }}({{#unless constructor.hasParameters}}{{#each constructor.args as |arg|}}{{{ arg.name }}}{{#if arg.prop.defaultJson}}={{{ arg.prop.defaultJson }}}{{/if}}, {{/each}}{{/unless}})
8+
9+
{{#if hasOverride}}
10+
This widget has some manual overrides on the Python side.
11+
{{/if}}
12+
13+
{{#if superClass.className }}
14+
Inherits :py:class:`~pythreejs.{{ superClass.className }}`.
15+
{{/if}}
16+
17+
Three.js docs: {{ threejs_docs_url }}
18+
19+
.. py:attribute:: _model_name
20+
21+
.. sourcecode:: python
22+
23+
Unicode('{{ modelName }}').tag(sync=True)
24+
25+
{{#each properties as |prop propName|}}
26+
.. py:attribute:: {{ propName }}
27+
28+
.. sourcecode:: python
29+
30+
{{{indent prop.trait_declaration ' '}}}
31+
32+
{{/each}}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
{{#if top_level}}
3+
4+
=============
5+
API Reference
6+
=============
7+
8+
.. py:module:: pythreejs
9+
10+
{{else}}
11+
12+
========================================================
13+
{{ moduleName }}
14+
========================================================
15+
16+
{{/if}}
17+
18+
.. toctree::
19+
:maxdepth: 10
20+
21+
{{#each submodules as |module|}}
22+
{{ module }}
23+
{{/each}}

0 commit comments

Comments
 (0)