├── .gitignore
├── .npmignore
├── LICENSE.txt
├── babel.config.js
├── gulpfile.js
├── index.css
├── index.html
├── lib
├── brython-runner.bundle.js
├── brython-runner.js
├── core
│ ├── brython-runner.js
│ └── brython-runner.worker.js
└── scripts
│ ├── fileio.py
│ ├── sleep.py
│ └── stdio.py
├── package.json
├── readme.md
├── src
├── browser.js
├── brython-runner.js
├── core
│ ├── brython-runner.js
│ └── brython-runner.worker.js
└── scripts
│ ├── fileio.py
│ ├── sleep.py
│ └── stdio.py
├── webpack.config.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Mac OS X
3 | .DS_Store
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # Diagnostic reports (https://nodejs.org/api/report.html)
14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
15 |
16 | # Runtime data
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 | lib-cov
24 |
25 | # Coverage directory used by tools like istanbul
26 | coverage
27 | *.lcov
28 |
29 | # nyc test coverage
30 | .nyc_output
31 |
32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
33 | .grunt
34 |
35 | # Bower dependency directory (https://bower.io/)
36 | bower_components
37 |
38 | # node-waf configuration
39 | .lock-wscript
40 |
41 | # Compiled binary addons (https://nodejs.org/api/addons.html)
42 | build/Release
43 |
44 | # Dependency directories
45 | node_modules/
46 | jspm_packages/
47 |
48 | # Snowpack dependency directory (https://snowpack.dev/)
49 | web_modules/
50 |
51 | # TypeScript cache
52 | *.tsbuildinfo
53 |
54 | # Optional npm cache directory
55 | .npm
56 |
57 | # Optional eslint cache
58 | .eslintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variables file
76 | .env
77 | .env.test
78 |
79 | # parcel-bundler cache (https://parceljs.org/)
80 | .cache
81 | .parcel-cache
82 |
83 | # Next.js build output
84 | .next
85 | out
86 |
87 | # Nuxt.js build / generate output
88 | .nuxt
89 | dist
90 |
91 | # Gatsby files
92 | .cache/
93 | # Comment in the public line in if your project uses Gatsby and not Next.js
94 | # https://nextjs.org/blog/next-9-1#public-directory-support
95 | # public
96 |
97 | # vuepress build output
98 | .vuepress/dist
99 |
100 | # Serverless directories
101 | .serverless/
102 |
103 | # FuseBox cache
104 | .fusebox/
105 |
106 | # DynamoDB Local files
107 | .dynamodb/
108 |
109 | # TernJS port file
110 | .tern-port
111 |
112 | # Stores VSCode versions used for testing VSCode extensions
113 | .vscode-test
114 |
115 | # yarn v2
116 | .yarn/cache
117 | .yarn/unplugged
118 | .yarn/build-state.yml
119 | .yarn/install-state.gz
120 | .pnp.*
121 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Ignore the source files
2 | src/
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 |
12 | # Diagnostic reports (https://nodejs.org/api/report.html)
13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14 |
15 | # Runtime data
16 | pids
17 | *.pid
18 | *.seed
19 | *.pid.lock
20 |
21 | # Directory for instrumented libs generated by jscoverage/JSCover
22 | lib-cov
23 |
24 | # Coverage directory used by tools like istanbul
25 | coverage
26 | *.lcov
27 |
28 | # nyc test coverage
29 | .nyc_output
30 |
31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32 | .grunt
33 |
34 | # Bower dependency directory (https://bower.io/)
35 | bower_components
36 |
37 | # node-waf configuration
38 | .lock-wscript
39 |
40 | # Compiled binary addons (https://nodejs.org/api/addons.html)
41 | build/Release
42 |
43 | # Dependency directories
44 | node_modules/
45 | jspm_packages/
46 |
47 | # Snowpack dependency directory (https://snowpack.dev/)
48 | web_modules/
49 |
50 | # TypeScript cache
51 | *.tsbuildinfo
52 |
53 | # Optional npm cache directory
54 | .npm
55 |
56 | # Optional eslint cache
57 | .eslintcache
58 |
59 | # Microbundle cache
60 | .rpt2_cache/
61 | .rts2_cache_cjs/
62 | .rts2_cache_es/
63 | .rts2_cache_umd/
64 |
65 | # Optional REPL history
66 | .node_repl_history
67 |
68 | # Output of 'npm pack'
69 | *.tgz
70 |
71 | # Yarn Integrity file
72 | .yarn-integrity
73 |
74 | # dotenv environment variables file
75 | .env
76 | .env.test
77 |
78 | # parcel-bundler cache (https://parceljs.org/)
79 | .cache
80 | .parcel-cache
81 |
82 | # Next.js build output
83 | .next
84 | out
85 |
86 | # Nuxt.js build / generate output
87 | .nuxt
88 | dist
89 |
90 | # Gatsby files
91 | .cache/
92 | # Comment in the public line in if your project uses Gatsby and not Next.js
93 | # https://nextjs.org/blog/next-9-1#public-directory-support
94 | # public
95 |
96 | # vuepress build output
97 | .vuepress/dist
98 |
99 | # Serverless directories
100 | .serverless/
101 |
102 | # FuseBox cache
103 | .fusebox/
104 |
105 | # DynamoDB Local files
106 | .dynamodb/
107 |
108 | # TernJS port file
109 | .tern-port
110 |
111 | # Stores VSCode versions used for testing VSCode extensions
112 | .vscode-test
113 |
114 | # yarn v2
115 | .yarn/cache
116 | .yarn/unplugged
117 | .yarn/build-state.yml
118 | .yarn/install-state.gz
119 | .pnp.*
120 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020, Jeongmin Byun, jmbyun91@gmail.com
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const presets = [
2 | '@babel/preset-env',
3 | ];
4 |
5 | const plugins = [
6 | '@babel/plugin-proposal-class-properties',
7 | '@babel/plugin-transform-runtime',
8 | ];
9 |
10 | module.exports = { presets, plugins };
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require('gulp');
2 | const babel = require('gulp-babel');
3 | const webpack = require('webpack');
4 | const DevServer = require('webpack-dev-server');
5 | const argv = require('yargs').argv;
6 | const webpackConfig = require('./webpack.config');
7 |
8 | const argHost = argv.host || 'localhost';
9 | const argPort = argv.port || 4000;
10 |
11 | gulp.task('build-webpack', callback => {
12 | webpack(webpackConfig('production'), (err, stats) => {
13 | if (err) {
14 | throw Error('build-webpack', err);
15 | }
16 | if (stats.hasErrors()) {
17 | throw Error('Compile errors have occurred.');
18 | }
19 | callback();
20 | });
21 | });
22 |
23 | gulp.task('compile-js-babel', () => {
24 | return gulp.src(['src/**/*', '!src/**/*.py', '!src/browser.js'])
25 | .pipe(babel())
26 | .pipe(gulp.dest('lib'));
27 | });
28 |
29 | gulp.task('copy-py', () => {
30 | return gulp.src(['src/**/*.py'])
31 | .pipe(gulp.dest('lib'));
32 | });
33 |
34 | gulp.task('build-babel', gulp.parallel('compile-js-babel', 'copy-py'));
35 |
36 | gulp.task('dev-webpack', () => {
37 | const config = webpackConfig('development');
38 | DevServer.addDevServerEntrypoints(config, {
39 | ...config.devServer,
40 | host: argHost,
41 | });
42 | const compiler = webpack(config);
43 | const server = new DevServer(compiler, config.devServer);
44 | server.listen(argPort, argHost, err => {
45 | if (err) {
46 | throw err;
47 | }
48 | console.log('Dev server is running.');
49 | });
50 | });
51 |
52 | gulp.task('dev', gulp.series('dev-webpack'));
53 | gulp.task('build', gulp.parallel('build-babel', 'build-webpack'));
--------------------------------------------------------------------------------
/index.css:
--------------------------------------------------------------------------------
1 | .dev-box {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100vw;
6 | height: 100vh;
7 | padding-top: 3rem;
8 | }
9 |
10 | .toolbar-row {
11 | position: absolute;
12 | background-color: #eee;
13 | top: 0;
14 | left: 0;
15 | width: 100%;
16 | height: 3rem;
17 | }
18 |
19 | .toolbar-button {
20 | border: 0;
21 | background-color: #444;
22 | padding: 0 1rem;
23 | height: 100%;
24 | font-size: 1rem;
25 | color: #fff;
26 | cursor: pointer;
27 | }
28 |
29 | .toolbar-button:hover {
30 | background-color: #777;
31 | }
32 |
33 | .workspace-row {
34 | width: 100%;
35 | height: 100%;
36 | }
37 |
38 | .editor-column {
39 | float: left;
40 | width: 50%;
41 | height: 100%;
42 | }
43 |
44 | .editor {
45 | width: 100%;
46 | height: 100%;
47 | }
48 |
49 | .editor .CodeMirror {
50 | width: 100%;
51 | height: 100%;
52 | }
53 |
54 | .run-column {
55 | float: right;
56 | width: 50%;
57 | height: 100%;
58 | background-color: #222;
59 | color: #fff;
60 | overflow-y: auto;
61 | }
62 |
63 | .output-box {
64 | padding: 1rem;
65 | font-size: 1rem;
66 | }
67 |
68 | .output-box code {
69 | display: inline;
70 | white-space: pre-wrap;
71 | word-wrap: break-word;
72 | }
73 |
74 | .output-box code.error {
75 | color: red;
76 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
30 |
31 |
109 |
110 |
--------------------------------------------------------------------------------
/lib/brython-runner.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports["default"] = void 0;
9 |
10 | var _brythonRunner = _interopRequireDefault(require("./core/brython-runner"));
11 |
12 | var _default = _brythonRunner["default"];
13 | exports["default"] = _default;
--------------------------------------------------------------------------------
/lib/core/brython-runner.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | Object.defineProperty(exports, "__esModule", {
6 | value: true
7 | });
8 | exports["default"] = void 0;
9 |
10 | var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
11 |
12 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
13 |
14 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
15 |
16 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
17 |
18 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
19 |
20 | var _brythonRunnerWorker = _interopRequireDefault(require("!!raw-loader!./brython-runner.worker.js"));
21 |
22 | var _brython = _interopRequireDefault(require("!!raw-loader!brython/brython.js"));
23 |
24 | var _brython_stdlib = _interopRequireDefault(require("!!raw-loader!brython/brython_stdlib.js"));
25 |
26 | var _stdio = _interopRequireDefault(require("!!raw-loader!../scripts/stdio.py"));
27 |
28 | var _sleep = _interopRequireDefault(require("!!raw-loader!../scripts/sleep.py"));
29 |
30 | var _fileio = _interopRequireDefault(require("!!raw-loader!../scripts/fileio.py"));
31 |
32 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
33 |
34 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2["default"])(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
35 |
36 | var DEFAULT_PARAMS = {
37 | codeName: 'main.py',
38 | codeCwd: '.',
39 | staticUrl: null,
40 | hangerUrl: 'https://www.pythonpad.co/hanger',
41 | paths: [],
42 | postInitModules: [],
43 | postInitScripts: [],
44 | files: {},
45 | debug: 0,
46 | stdout: {
47 | write: function write(content) {
48 | console.log(content);
49 | },
50 | flush: function flush() {}
51 | },
52 | stderr: {
53 | write: function write(content) {
54 | console.error(content);
55 | },
56 | flush: function flush() {}
57 | },
58 | stdin: {
59 | readline: function readline() {
60 | return (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee() {
61 | return _regenerator["default"].wrap(function _callee$(_context) {
62 | while (1) {
63 | switch (_context.prev = _context.next) {
64 | case 0:
65 | return _context.abrupt("return", prompt());
66 |
67 | case 1:
68 | case "end":
69 | return _context.stop();
70 | }
71 | }
72 | }, _callee);
73 | }))();
74 | }
75 | },
76 | onInit: function onInit() {
77 | console.log('Brython runner is ready.');
78 | },
79 | onFileUpdate: function onFileUpdate(filename, data) {
80 | console.log('Brython runner has an updated file:', filename, data);
81 | },
82 | onMsg: function onMsg(type, value) {
83 | console.log('Brython runner got a message:', type, value);
84 | }
85 | };
86 |
87 | var BrythonRunner = /*#__PURE__*/function () {
88 | function BrythonRunner(params) {
89 | (0, _classCallCheck2["default"])(this, BrythonRunner);
90 | this.setParamValues(params);
91 | this.initWorker();
92 | }
93 |
94 | (0, _createClass2["default"])(BrythonRunner, [{
95 | key: "setParamValues",
96 | value: function setParamValues(params) {
97 | var values = _objectSpread(_objectSpread({}, DEFAULT_PARAMS), params);
98 |
99 | for (var _i = 0, _Object$keys = Object.keys(values); _i < _Object$keys.length; _i++) {
100 | var key = _Object$keys[_i];
101 | this[key] = values[key];
102 | }
103 | }
104 | }, {
105 | key: "initWorker",
106 | value: function initWorker() {
107 | var _this = this;
108 |
109 | this.worker = this.createWorker();
110 | this.worker.postMessage({
111 | type: 'init',
112 | debug: this.debug,
113 | codeName: this.codeName,
114 | codeCwd: this.codeCwd,
115 | staticUrl: this.staticUrl,
116 | hangerUrl: this.hangerUrl,
117 | paths: this.paths,
118 | initModules: [_brython["default"], _brython_stdlib["default"]],
119 | postInitModules: this.postInitModules,
120 | initScripts: [_stdio["default"], _sleep["default"], _fileio["default"]],
121 | postInitScripts: this.postInitScripts
122 | });
123 |
124 | this.worker.onmessage = function (msg) {
125 | return _this.handleMessage(msg);
126 | };
127 | }
128 | }, {
129 | key: "createWorker",
130 | value: function createWorker() {
131 | window.URL = window.URL || window.webkitURL;
132 | var blob;
133 |
134 | try {
135 | blob = new Blob([_brythonRunnerWorker["default"]], {
136 | type: 'application/javascript'
137 | });
138 | } catch (e) {
139 | window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
140 | blob = new BlobBuilder();
141 | blob.append(_brythonRunnerWorker["default"]);
142 | blob = blob.getBlob();
143 | }
144 |
145 | return new Worker(URL.createObjectURL(blob));
146 | }
147 | }, {
148 | key: "handleMessage",
149 | value: function () {
150 | var _handleMessage = (0, _asyncToGenerator2["default"])( /*#__PURE__*/_regenerator["default"].mark(function _callee2(msg) {
151 | var data;
152 | return _regenerator["default"].wrap(function _callee2$(_context2) {
153 | while (1) {
154 | switch (_context2.prev = _context2.next) {
155 | case 0:
156 | _context2.t0 = msg.data.type;
157 | _context2.next = _context2.t0 === 'brython.init' ? 3 : _context2.t0 === 'done' ? 5 : _context2.t0 === 'stdout.write' ? 8 : _context2.t0 === 'stdout.flush' ? 10 : _context2.t0 === 'stderr.write' ? 12 : _context2.t0 === 'stderr.flush' ? 14 : _context2.t0 === 'stdin.readline' ? 16 : _context2.t0 === 'file.update' ? 23 : 26;
158 | break;
159 |
160 | case 3:
161 | this.onInit();
162 | return _context2.abrupt("break", 28);
163 |
164 | case 5:
165 | this.done(msg.data.exit);
166 | this.restartWorker();
167 | return _context2.abrupt("break", 28);
168 |
169 | case 8:
170 | this.stdout.write(msg.data.value);
171 | return _context2.abrupt("break", 28);
172 |
173 | case 10:
174 | this.stdout.flush();
175 | return _context2.abrupt("break", 28);
176 |
177 | case 12:
178 | this.stderr.write(msg.data.value);
179 | return _context2.abrupt("break", 28);
180 |
181 | case 14:
182 | this.stderr.flush();
183 | return _context2.abrupt("break", 28);
184 |
185 | case 16:
186 | this.hangerKey = msg.data.value;
187 | _context2.next = 19;
188 | return this.stdin.readline();
189 |
190 | case 19:
191 | data = _context2.sent;
192 | this.writeInputData(this.hangerKey, data);
193 | this.hangerKey = null;
194 | return _context2.abrupt("break", 28);
195 |
196 | case 23:
197 | this.files[msg.data.value.filename] = msg.data.value.data;
198 | this.onFileUpdate(msg.data.value.filename, msg.data.value.data);
199 | return _context2.abrupt("break", 28);
200 |
201 | case 26:
202 | this.onMsg(msg.data.type, msg.data.value);
203 | return _context2.abrupt("break", 28);
204 |
205 | case 28:
206 | case "end":
207 | return _context2.stop();
208 | }
209 | }
210 | }, _callee2, this);
211 | }));
212 |
213 | function handleMessage(_x) {
214 | return _handleMessage.apply(this, arguments);
215 | }
216 |
217 | return handleMessage;
218 | }()
219 | }, {
220 | key: "writeInputData",
221 | value: function writeInputData(key, data) {
222 | var xhr = new XMLHttpRequest();
223 | xhr.open('POST', "".concat(this.hangerUrl, "/").concat(key, "/write/"), true);
224 |
225 | xhr.onload = function (e) {
226 | if (xhr.readyState === 4) {
227 | if (xhr.status === 200) {// Done.
228 | } else {
229 | console.error('Failed to send input data via server tunnel.', xhr.statusText);
230 | }
231 | }
232 | };
233 |
234 | xhr.onerror = function (e) {
235 | console.error('Failed to send input data via server tunnel.', xhr.statusText);
236 | };
237 |
238 | xhr.send(data);
239 | }
240 | }, {
241 | key: "runCode",
242 | value: function runCode(code) {
243 | var _this2 = this;
244 |
245 | return new Promise(function (resolve) {
246 | _this2.done = function (exit) {
247 | return resolve(exit);
248 | };
249 |
250 | _this2.worker.postMessage({
251 | type: 'run.code',
252 | code: code
253 | });
254 | });
255 | }
256 | }, {
257 | key: "runCodeWithFiles",
258 | value: function runCodeWithFiles(code, files) {
259 | var _this3 = this;
260 |
261 | return new Promise(function (resolve) {
262 | _this3.done = function (exit) {
263 | return resolve(exit);
264 | };
265 |
266 | _this3.worker.postMessage({
267 | type: 'run.code-with-files',
268 | code: code,
269 | files: files
270 | });
271 | });
272 | }
273 | }, {
274 | key: "runUrl",
275 | value: function runUrl(url) {
276 | var _this4 = this;
277 |
278 | return new Promise(function (resolve) {
279 | _this4.done = function (exit) {
280 | return resolve(exit);
281 | };
282 |
283 | _this4.worker.postMessage({
284 | type: 'run.url',
285 | url: url
286 | });
287 | });
288 | }
289 | }, {
290 | key: "sendMsg",
291 | value: function sendMsg(type, value) {
292 | this.worker.postMessage({
293 | type: type,
294 | value: value
295 | });
296 | }
297 | }, {
298 | key: "stopRunning",
299 | value: function stopRunning() {
300 | if (this.hangerKey) {
301 | this.writeInputData(this.hangerKey, '');
302 | }
303 |
304 | this.restartWorker();
305 | }
306 | }, {
307 | key: "restartWorker",
308 | value: function restartWorker() {
309 | this.worker.terminate();
310 | this.initWorker();
311 | }
312 | }]);
313 | return BrythonRunner;
314 | }();
315 |
316 | exports["default"] = BrythonRunner;
--------------------------------------------------------------------------------
/lib/core/brython-runner.worker.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4 |
5 | var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
6 |
7 | var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
8 |
9 | var _get4 = _interopRequireDefault(require("@babel/runtime/helpers/get"));
10 |
11 | var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
12 |
13 | var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
14 |
15 | var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
16 |
17 | var _wrapNativeSuper2 = _interopRequireDefault(require("@babel/runtime/helpers/wrapNativeSuper"));
18 |
19 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = (0, _getPrototypeOf2["default"])(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2["default"])(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2["default"])(this, result); }; }
20 |
21 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
22 |
23 | function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
24 |
25 | function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
26 |
27 | function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
28 |
29 | function _brInitRunner(data) {
30 | _brSetValues(data);
31 |
32 | _brOverwrite();
33 |
34 | _brInitMsgSenders();
35 |
36 | _brInitMsgListeners();
37 |
38 | _brRunModuleScripts(data);
39 |
40 | _brInitBrython(data);
41 |
42 | _brRunInitPythonScripts(data);
43 |
44 | _brOverrideOpen();
45 |
46 | _brRunPostInitPythonScripts(data);
47 |
48 | _brInitRunnerCallback();
49 | }
50 |
51 | function _brSetValues(data) {
52 | self._brLocalPathPrefix = '/__pythonpad_local__';
53 | self._brRunType = 'code';
54 | self._brId = data.codeName;
55 | self._brCodeCwd = data.codeCwd;
56 | self._brCode = '';
57 | self._brHangerUrl = data.hangerUrl;
58 | self._brImportLocalFile = _brImportLocalFile;
59 | self._brFilesUpdated = _brFilesUpdated;
60 | self._brHangSleep = _brHangSleep;
61 | self._brPrevErrOut = null;
62 | }
63 |
64 | function _brOverwrite() {
65 | self.window = self;
66 | self.prompt = _brGetInput;
67 | self.document = _brCreateMockDocument();
68 | }
69 |
70 | function _brCreateMockDocument() {
71 | return {
72 | getElementsByTagName: _brGetElementsByTagName
73 | };
74 | }
75 |
76 | function _brRunModuleScripts(data) {
77 | var _iterator = _createForOfIteratorHelper(data.initModules),
78 | _step;
79 |
80 | try {
81 | for (_iterator.s(); !(_step = _iterator.n()).done;) {
82 | var rawModule = _step.value;
83 | eval.call(null, rawModule);
84 | }
85 | } catch (err) {
86 | _iterator.e(err);
87 | } finally {
88 | _iterator.f();
89 | }
90 |
91 | var _iterator2 = _createForOfIteratorHelper(data.postInitModules),
92 | _step2;
93 |
94 | try {
95 | for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
96 | var _rawModule = _step2.value;
97 | eval.call(null, _rawModule);
98 | }
99 | } catch (err) {
100 | _iterator2.e(err);
101 | } finally {
102 | _iterator2.f();
103 | }
104 | }
105 |
106 | function _brInitBrython(data) {
107 | self.RealXMLHttpRequest = self.XMLHttpRequest;
108 | self.XMLHttpRequest = _brXHR;
109 |
110 | self.__BRYTHON__.brython({
111 | pythonpath: [self._brLocalPathPrefix].concat(data.paths),
112 | debug: data.debug || 0
113 | });
114 | }
115 |
116 | function _brRunInitPythonScripts(data) {
117 | _brRun(data.initScripts.join('\n'));
118 | }
119 |
120 | function _brOverrideOpen() {
121 | self.__BRYTHON__.builtins.open = self._brOpenFile;
122 | }
123 |
124 | function _brRunPostInitPythonScripts(data) {
125 | for (var i = 0; i < data.postInitScripts.length; i++) {
126 | _brRun(data.postInitScripts[i]);
127 | }
128 | }
129 |
130 | function _brInitRunnerCallback() {
131 | self.postMessage({
132 | type: 'brython.init',
133 | value: ''
134 | });
135 | }
136 |
137 | function _brImportLocalFile(filename) {
138 | if (self._brFilesObj[filename] && self._brFilesObj[filename].type === 'text') {
139 | return self._brFilesObj[filename].body;
140 | } else {
141 | return null;
142 | }
143 | }
144 |
145 | function _brSetFiles(files) {
146 | self._brFilesObj = files;
147 |
148 | self._brSetFilesFromObj();
149 | }
150 |
151 | function _brFilesUpdated(filename, type, body) {
152 | if (!type && !body) {
153 | delete self._brFilesObj[filename];
154 | self.postMessage({
155 | type: 'file.delete',
156 | value: filename
157 | });
158 | } else {
159 | self._brFilesObj[filename] = {
160 | type: type,
161 | body: body
162 | };
163 | self.postMessage({
164 | type: 'file.update',
165 | value: {
166 | filename: filename,
167 | data: {
168 | type: type,
169 | body: body
170 | }
171 | }
172 | });
173 | }
174 | }
175 |
176 | function _brGetInput(message) {
177 | if (self._brHangerUrl === null) {
178 | self._brRaiseInputError();
179 |
180 | return '';
181 | }
182 |
183 | if (message) {
184 | self._brStdoutWrite(message + '');
185 |
186 | self._brStdoutFlush();
187 | }
188 |
189 | var req = new RealXMLHttpRequest();
190 | console.log('URL', self._brHangerUrl + '/open/');
191 | req.open('POST', self._brHangerUrl + '/open/', false);
192 | req.send('');
193 |
194 | if (req.status !== 200) {
195 | console.error('Failed to tunnel through the server to get input.');
196 | return '';
197 | }
198 |
199 | var key = req.responseText;
200 | self.postMessage({
201 | type: 'stdin.readline',
202 | value: key
203 | });
204 | req = new RealXMLHttpRequest();
205 | req.open('POST', self._brHangerUrl + '/' + key + '/read/', false);
206 | req.send('');
207 |
208 | if (req.status !== 200) {
209 | console.error('Failed to tunnel through the server to get input.');
210 | return '';
211 | }
212 |
213 | return req.responseText;
214 | }
215 |
216 | function _brHangSleep(duration) {
217 | var req = new RealXMLHttpRequest();
218 | req.open('GET', self._brHangerUrl + '/sleep/?duration=' + duration, false);
219 | req.send(null);
220 | }
221 |
222 | function _brGetElementsByTagName(tagName) {
223 | if (tagName === 'script') {
224 | if (self._brRunType === 'code') {
225 | return [{
226 | type: 'text/python',
227 | id: self._brId,
228 | innerHTML: self._brCode
229 | }];
230 | } else if (self._brRunType === 'url') {
231 | return [{
232 | type: 'text/python',
233 | id: getFilename(self._brUrl),
234 | src: self._brUrl
235 | }];
236 | }
237 | }
238 |
239 | return [];
240 | }
241 |
242 | function _brInitMsgSenders() {
243 | self._brStdoutWrite = function (data) {
244 | self._brPrevErrOut = null;
245 | self.postMessage({
246 | type: 'stdout.write',
247 | value: data
248 | });
249 | };
250 |
251 | self._brStdoutFlush = function () {
252 | self.postMessage({
253 | type: 'stdout.flush'
254 | });
255 | };
256 |
257 | self._brStderrWrite = function (data) {
258 | if ((data + '').startsWith('Traceback (most recent call last):') && data === self._brPrevErrOut) {
259 | return; // Skip duplicated error message.
260 | }
261 |
262 | self._brPrevErrOut = data;
263 | self.postMessage({
264 | type: 'stderr.write',
265 | value: data
266 | });
267 | };
268 |
269 | self._brStderrFlush = function () {
270 | self.postMessage({
271 | type: 'stderr.flush'
272 | });
273 | };
274 |
275 | self._brSendMsg = function (type, value) {
276 | self.postMessage({
277 | type: type,
278 | value: value
279 | });
280 | };
281 | }
282 |
283 | function _brInitMsgListeners() {
284 | self._brMsgListeners = {};
285 |
286 | self._brAddMsgListener = function (type, callback) {
287 | if (!(type in self._brMsgListeners)) {
288 | self._brMsgListeners[type] = [callback];
289 | } else {
290 | self._brMsgListeners[type].push(callback);
291 | }
292 | };
293 |
294 | self._brRemoveMsgListener = function (type, callback) {
295 | if (type in self._brMsgListeners) {
296 | var newMsgListeners = [];
297 |
298 | for (var i = 0; i < self._brMsgListeners[type].length; i++) {
299 | if (self._brMsgListeners[type][i] !== callback) {
300 | newMsgListeners.push(self._brMsgListeners[type][i]);
301 | }
302 | }
303 |
304 | self._brMsgListeners[type] = newMsgListeners;
305 | }
306 | };
307 |
308 | self.receiveMsg = function (type) {
309 | return new Promise(function (resolve, reject) {
310 | var callback = function callback(msg) {
311 | resolve(msg.value);
312 |
313 | self._brRemoveMsgListener(type, callback);
314 | };
315 |
316 | self._brAddMsgListener(type, callback);
317 | });
318 | };
319 | }
320 |
321 | function getFilename(url) {
322 | var splitUrl = url.split('/');
323 | return splitUrl[splitUrl.length - 1];
324 | }
325 |
326 | function getParentUrl(url) {
327 | var splitUrl = url.split('/');
328 |
329 | if (splitUrl.length === 1) {
330 | return './';
331 | } else {
332 | return splitUrl.slice(0, splitUrl.length - 1).join('/');
333 | }
334 | }
335 |
336 | function _brRun(src) {
337 | self._brPrevErrOut = null;
338 | self._brRunType = 'code';
339 | self._brCode = src;
340 | var pathBackup = self.__BRYTHON__.script_path;
341 | self.__BRYTHON__.script_path = self._brCodeCwd;
342 |
343 | try {
344 | self.__BRYTHON__.parser._run_scripts({});
345 | } catch (err) {} finally {
346 | self.__BRYTHON__.script_path = pathBackup;
347 | }
348 | }
349 |
350 | function _brRunUrl(url) {
351 | self._brPrevErrOut = null;
352 | self._brRunType = 'url';
353 | self._brUrl = url;
354 | var pathBackup = self.__BRYTHON__.script_path;
355 | self.__BRYTHON__.script_path = getParentUrl(url);
356 |
357 | try {
358 | self.__BRYTHON__.parser._run_scripts({});
359 | } catch (err) {} finally {
360 | self.__BRYTHON__.script_path = pathBackup;
361 | }
362 | }
363 |
364 | function _brRunCallback(exit) {
365 | self.postMessage({
366 | type: 'done',
367 | exit: exit
368 | });
369 | }
370 |
371 | var _brXHR = /*#__PURE__*/function (_XMLHttpRequest) {
372 | (0, _inherits2["default"])(_brXHR, _XMLHttpRequest);
373 |
374 | var _super = _createSuper(_brXHR);
375 |
376 | function _brXHR() {
377 | var _this;
378 |
379 | (0, _classCallCheck2["default"])(this, _brXHR);
380 | _this = _super.call(this);
381 | _this.localPrefix = self._brLocalPathPrefix + '/';
382 | _this.localRequestOpened = false;
383 | _this.localRequestSent = false;
384 | _this.localResponseText = null;
385 | return _this;
386 | }
387 |
388 | (0, _createClass2["default"])(_brXHR, [{
389 | key: "open",
390 | value: function open() {
391 | var _get2;
392 |
393 | for (var _len = arguments.length, params = new Array(_len), _key = 0; _key < _len; _key++) {
394 | params[_key] = arguments[_key];
395 | }
396 |
397 | if (params.length > 1) {
398 | var url = params[1];
399 |
400 | if (url.startsWith(this.localPrefix)) {
401 | var localPath = url.slice(this.localPrefix.length, url.indexOf('?'));
402 | this.localResponseText = _brImportLocalFile(localPath);
403 | this.localRequestOpened = true; // TODO: Call onreadystatechange.
404 |
405 | return;
406 | }
407 | }
408 |
409 | return (_get2 = (0, _get4["default"])((0, _getPrototypeOf2["default"])(_brXHR.prototype), "open", this)).call.apply(_get2, [this].concat(params));
410 | }
411 | }, {
412 | key: "send",
413 | value: function send() {
414 | if (this.localRequestOpened) {
415 | this.localRequestSent = true;
416 | } else {
417 | var _get3;
418 |
419 | for (var _len2 = arguments.length, params = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
420 | params[_key2] = arguments[_key2];
421 | }
422 |
423 | return (_get3 = (0, _get4["default"])((0, _getPrototypeOf2["default"])(_brXHR.prototype), "send", this)).call.apply(_get3, [this].concat(params));
424 | }
425 | }
426 | }, {
427 | key: "status",
428 | get: function get() {
429 | if (this.localRequestOpened) {
430 | if (this.localResponseText === null) {
431 | return 404;
432 | } else {
433 | return 200;
434 | }
435 | } else {
436 | return (0, _get4["default"])((0, _getPrototypeOf2["default"])(_brXHR.prototype), "status", this);
437 | }
438 | }
439 | }, {
440 | key: "readyState",
441 | get: function get() {
442 | if (this.localRequestOpened) {
443 | if (this.localRequestSent) {
444 | return 4;
445 | } else {
446 | return 1;
447 | }
448 | } else {
449 | return (0, _get4["default"])((0, _getPrototypeOf2["default"])(_brXHR.prototype), "readyState", this);
450 | }
451 | }
452 | }, {
453 | key: "responseText",
454 | get: function get() {
455 | if (this.localRequestOpened) {
456 | return this.localResponseText;
457 | } else {
458 | return (0, _get4["default"])((0, _getPrototypeOf2["default"])(_brXHR.prototype), "responseText", this);
459 | }
460 | }
461 | }]);
462 | return _brXHR;
463 | }( /*#__PURE__*/(0, _wrapNativeSuper2["default"])(XMLHttpRequest));
464 |
465 | self.onmessage = function (message) {
466 | var data = message.data;
467 |
468 | switch (data.type) {
469 | case 'init':
470 | _brInitRunner(data);
471 |
472 | break;
473 |
474 | case 'run.code':
475 | try {
476 | _brRun(data.code);
477 |
478 | _brRunCallback(0);
479 | } catch (err) {
480 | _brRunCallback(1);
481 | }
482 |
483 | break;
484 |
485 | case 'run.code-with-files':
486 | try {
487 | _brSetFiles(data.files);
488 |
489 | _brRun(data.code);
490 |
491 | _brRunCallback(0);
492 | } catch (err) {
493 | _brRunCallback(1);
494 | }
495 |
496 | break;
497 |
498 | case 'run.url':
499 | try {
500 | _brRunUrl(data.url);
501 |
502 | _brRunCallback(0);
503 | } catch (err) {
504 | _brRunCallback(1);
505 | }
506 |
507 | break;
508 |
509 | default:
510 | break;
511 | }
512 |
513 | if (data.type in self._brMsgListeners) {
514 | for (var i = 0; i < self._brMsgListeners[data.type].length; i++) {
515 | self._brMsgListeners[data.type][i](data);
516 | }
517 | }
518 | };
--------------------------------------------------------------------------------
/lib/scripts/fileio.py:
--------------------------------------------------------------------------------
1 | import browser
2 | import io
3 | import os
4 |
5 | def set_files_from_obj():
6 | try:
7 | browser.self._brFiles = browser.self._brFilesObj.to_dict()
8 | except AttributeError:
9 | pass
10 | set_files_from_obj()
11 | browser.self._brSetFilesFromObj = set_files_from_obj
12 |
13 | class PythonpadTextIOWrapper(io.IOBase):
14 | def __init__(self, filename, target_file, mode, newline=None):
15 | self.stream = io.StringIO(newline=newline)
16 | self.stream.write(target_file['body'])
17 | self.filename = filename
18 | self.target_file = target_file
19 | self.mode = mode
20 | if 'a' not in mode:
21 | self.stream.seek(0)
22 |
23 | def __enter__(self):
24 | return self
25 |
26 | def __exit__(self, exc_type, exc_value, traceback):
27 | self.close()
28 |
29 | def __str__(self):
30 | return '' % (self.filename, self.mode)
31 |
32 | def __repr__(self):
33 | return self.__str__()
34 |
35 | def __del__(self):
36 | return self.stream.__del__()
37 |
38 | def __iter__(self):
39 | return self.stream.__iter__()
40 |
41 | def __next__(self):
42 | return self.stream.__next__()
43 |
44 | def __dict__(self):
45 | return self.stream.__dict__()
46 |
47 | def __eq__(self, other):
48 | return self.stream.__eq__(other.stream)
49 |
50 | def __format__(self, format_spec):
51 | return self.stream.__format__(format_spec)
52 |
53 | def __ge__(self, other):
54 | return self.stream.__ge__(other.stream)
55 |
56 | def __gt__(self, other):
57 | return self.stream.__gt__(other.stream)
58 |
59 | def __le__(self, other):
60 | return self.stream.__le__(other.stream)
61 |
62 | def __lt__(self, other):
63 | return self.stream.__lt__(other.stream)
64 |
65 | def __ne__(self, other):
66 | return self.stream.__ne__(other.stream)
67 |
68 | def __sizeof__(self):
69 | return self.stream.__sizeof__()
70 |
71 | def detach(self):
72 | raise NotImplementedError('not available in Pythonpad')
73 |
74 | def readable(self):
75 | return 'r' in self.mode or '+' in self.mode
76 |
77 | def read(self, size=-1):
78 | if 'r' not in self.mode and '+' not in self.mode:
79 | raise io.UnsupportedOperation('not readable')
80 | return self.stream.read(size)
81 |
82 | def readline(self, size=-1):
83 | if 'r' not in self.mode and '+' not in self.mode:
84 | raise io.UnsupportedOperation('not readable')
85 | return self.stream.readline(size)
86 |
87 | def readlines(self, hint=-1):
88 | if 'r' not in self.mode and '+' not in self.mode:
89 | raise io.UnsupportedOperation('not readable')
90 | return self.stream.readlines(hint)
91 |
92 | def writable(self):
93 | return 'r' not in self.mode or '+' in self.mode
94 |
95 | def write(self, s):
96 | if 'r' in self.mode and '+' not in self.mode:
97 | raise io.UnsupportedOperation('not writable')
98 | return self.stream.write(s)
99 |
100 | def writelines(self, lines):
101 | if 'r' in self.mode and '+' not in self.mode:
102 | raise io.UnsupportedOperation('not writable')
103 | return self.stream.writelines(s)
104 |
105 | def fileno(self):
106 | raise OSError('no file descriptor is available in simulated in-memory file system')
107 |
108 | def tell(self):
109 | return self.stream.tell()
110 |
111 | def seekable(self):
112 | return True
113 |
114 | def seek(self, offset):
115 | return self.stream.seek(offset)
116 |
117 | def isatty(self):
118 | return False
119 |
120 | def truncate(self, size=None):
121 | return self.stream.truncate(size=size)
122 |
123 | def flush(self):
124 | if 'r' in self.mode or '+' in self.mode:
125 | return
126 | cursor = self.stream.tell()
127 | self.stream.seek(0) # Seek to the beginning of the stream.
128 | self.target_file['body'] = self.stream.read()
129 | files_updated(self.filename, )
130 | self.stream.seek(cursor)
131 |
132 | def close(self):
133 | if 'r' not in self.mode or '+' in self.mode:
134 | self.stream.seek(0) # Seek to the beginning of the stream.
135 | self.target_file['body'] = self.stream.read()
136 | files_updated(self.filename)
137 | self.stream.close()
138 |
139 | @property
140 | def name(self):
141 | return self.filename
142 |
143 | @property
144 | def closed(self):
145 | return self.stream.closed
146 |
147 | class PythonpadBytesIOWrapper(io.BufferedIOBase):
148 | def __init__(self, filename, target_file, mode):
149 | global base64
150 | import base64
151 | self.stream = io.BytesIO()
152 | self.stream.write(base64.b64decode(target_file['body']))
153 | self.filename = filename
154 | self.target_file = target_file
155 | self.mode = mode
156 | if 'a' not in mode:
157 | self.stream.seek(0)
158 |
159 | def __enter__(self):
160 | return self
161 |
162 | def __exit__(self, exc_type, exc_value, traceback):
163 | self.close()
164 |
165 | def __str__(self):
166 | return '' % (self.filename, self.mode)
167 |
168 | def __repr__(self):
169 | return self.__str__()
170 |
171 | def __del__(self):
172 | return self.stream.__del__()
173 |
174 | def __dict__(self):
175 | return self.stream.__dict__()
176 |
177 | def __dir__(self):
178 | return self.stream.__dir__()
179 |
180 | def __eq__(self, other):
181 | return self.stream.__eq__(other.stream)
182 |
183 | def __format__(self, format_spec):
184 | return self.stream.__format__(format_spec)
185 |
186 | def __ge__(self, other):
187 | return self.stream.__ge__(other.stream)
188 |
189 | def __gt__(self, other):
190 | return self.stream.__gt__(other.stream)
191 |
192 | def __iter__(self):
193 | return self.stream.__iter__()
194 |
195 | def __le__(self, other):
196 | return self.stream.__le__(other.stream)
197 |
198 | def __lt__(self, other):
199 | return self.stream.__lt__(other.stream)
200 |
201 | def __ne__(self, other):
202 | return self.stream.__ne__(other.stream)
203 |
204 | def __next__(self):
205 | return self.stream.__next__()
206 |
207 | def __sizeof__(self):
208 | return self.stream.__sizeof__()
209 |
210 | def detach(self):
211 | raise NotImplementedError('not available in Pythonpad')
212 |
213 | def readable(self):
214 | return 'r' in self.mode or '+' in self.mode
215 |
216 | def read(self, *args, **kwargs):
217 | if 'r' not in self.mode and '+' not in self.mode:
218 | raise io.UnsupportedOperation('not readable')
219 | return self.stream.read(*args, **kwargs)
220 |
221 | def readline(self, *args, **kwargs):
222 | if 'r' not in self.mode and '+' not in self.mode:
223 | raise io.UnsupportedOperation('not readable')
224 | return self.stream.readline(*args, **kwargs)
225 |
226 | def readlines(self, *args, **kwargs):
227 | if 'r' not in self.mode and '+' not in self.mode:
228 | raise io.UnsupportedOperation('not readable')
229 | return self.stream.readlines(*args, **kwargs)
230 |
231 | def read1(self, *args, **kwargs):
232 | if 'r' not in self.mode and '+' not in self.mode:
233 | raise io.UnsupportedOperation('not readable')
234 | return self.stream.read1(*args, **kwargs)
235 |
236 | def readinto(self, *args, **kwargs):
237 | if 'r' not in self.mode and '+' not in self.mode:
238 | raise io.UnsupportedOperation('not readable')
239 | return self.stream.readinto(*args, **kwargs)
240 |
241 | def readinto1(self, *args, **kwargs):
242 | if 'r' not in self.mode and '+' not in self.mode:
243 | raise io.UnsupportedOperation('not readable')
244 | return self.stream.readinto1(*args, **kwargs)
245 |
246 | def writable(self):
247 | return 'r' not in self.mode or '+' in self.mode
248 |
249 | def write(self, s):
250 | if 'r' in self.mode and '+' not in self.mode:
251 | raise io.UnsupportedOperation('not writable')
252 | return self.stream.write(s)
253 |
254 | def writelines(self, lines):
255 | if 'r' in self.mode and '+' not in self.mode:
256 | raise io.UnsupportedOperation('not writable')
257 | return self.stream.writelines(s)
258 |
259 | def fileno(self):
260 | raise OSError('no file descriptor is available in simulated in-memory file system')
261 |
262 | def tell(self):
263 | return self.stream.tell()
264 |
265 | def peek(self, *args, **kwargs):
266 | return self.stream.peek(*args, **kwargs)
267 |
268 | def raw(self, *args, **kwargs):
269 | return self.stream.raw(*args, **kwargs)
270 |
271 | def seekable(self):
272 | return True
273 |
274 | def seek(self, offset):
275 | return self.stream.seek(offset)
276 |
277 | def isatty(self):
278 | return False
279 |
280 | def truncate(self, size=None):
281 | return self.stream.truncate(size=size)
282 |
283 | def flush(self):
284 | if 'r' in self.mode or '+' in self.mode:
285 | return
286 | cursor = self.stream.tell()
287 | self.stream.seek(0) # Seek to the beginning of the stream.
288 | self.target_file['body'] = base64.b64encode(self.stream.read()).decode('utf-8')
289 | files_updated(self.filename)
290 | self.stream.seek(cursor)
291 |
292 | def close(self):
293 | if 'r' not in self.mode or '+' in self.mode:
294 | self.stream.seek(0) # Seek to the beginning of the stream.
295 | self.target_file['body'] = base64.b64encode(self.stream.read()).decode('utf-8')
296 | files_updated(self.filename)
297 | self.stream.close()
298 |
299 | @property
300 | def name(self):
301 | return self.filename
302 |
303 | @property
304 | def closed(self):
305 | return self.stream.closed()
306 |
307 | def files_updated(path):
308 | if path in browser.self._brFiles:
309 | browser.self._brFilesUpdated(path, browser.self._brFiles[path]['type'], browser.self._brFiles[path]['body'])
310 | else:
311 | browser.self._brFilesUpdated(path, None, None)
312 |
313 | def normalize_path(path):
314 | normalized_path = os.path.normpath(path)
315 | if normalized_path.startswith('/'):
316 | raise NotImplementedError('absolute path is not supported in Pythonpad')
317 | elif normalized_path.startswith('../'):
318 | raise NotImplementedError('accessing out of the project is not supported in Pythonpad')
319 | return normalized_path
320 |
321 | def exists(path):
322 | normalized_path = normalize_path(path)
323 | return (normalized_path in browser.self._brFiles) or ((normalized_path + '/') in browser.self._brFiles)
324 |
325 | def is_dir(path):
326 | dir_path = normalize_path(path) + '/'
327 | return dir_path in browser.self._brFiles
328 |
329 | def get_file(path):
330 | return browser.self._brFiles[normalize_path(path)]
331 |
332 | def create_file(path, file_type=None, body=None):
333 | normalized_path = normalize_path(path)
334 | if '/' in normalized_path:
335 | tokens = normalized_path.split('/')
336 | parent_path = '/'.join(tokens[:-1])
337 | if (parent_path + '/') not in browser.self._brFiles:
338 | # No parent directory.
339 | raise FileNotFoundError('No such file or directory: \'%s\'' % path)
340 | file = {
341 | 'type': 'text' if file_type is None else file_type,
342 | 'body': '' if body is None else body,
343 | }
344 | browser.self._brFiles[normalized_path] = file
345 | files_updated(normalized_path)
346 | return file
347 |
348 | def open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
349 | count = 0
350 | for m in 'rwxa':
351 | if m in mode:
352 | count += 1
353 | if count != 1:
354 | raise ValueError('must have exactly one of create/read/write/append mode')
355 |
356 | if is_dir(file):
357 | raise IsADirectoryError('Is a directory: \'%s\'' % file)
358 |
359 | if 'b' in mode:
360 | if 'r' in mode:
361 | if not exists(file):
362 | raise FileNotFoundError('No such file or directory: \'%s\'' % file)
363 | target_file = get_file(file)
364 | elif 'w' in mode:
365 | target_file = create_file(file, file_type='base64')
366 | elif 'x' in mode:
367 | if exists(file):
368 | raise FileExistsError('File exists: \'%s\'' % file)
369 | target_file = create_file(file, file_type='base64')
370 | elif 'a' in mode:
371 | if exists(file):
372 | target_file = get_file(file)
373 | else:
374 | target_file = create_file(file, file_type='base64')
375 | if target_file['type'] != 'base64':
376 | raise NotImplementedError('opening text file in bytes mode is not implemented in Pythonpad')
377 | return PythonpadBytesIOWrapper(file, target_file, mode)
378 | else:
379 | if 'r' in mode:
380 | if not exists(file):
381 | raise FileNotFoundError('No such file or directory: \'%s\'' % file)
382 | target_file = get_file(file)
383 | elif 'w' in mode:
384 | target_file = create_file(file)
385 | elif 'x' in mode:
386 | if exists(file):
387 | raise FileExistsError('File exists: \'%s\'' % file)
388 | target_file = create_file(file)
389 | elif 'a' in mode:
390 | if exists(file):
391 | target_file = get_file(file)
392 | else:
393 | target_file = create_file(file)
394 | if target_file['type'] != 'text':
395 | raise NotImplementedError('opening byte file in text mode is not implemented in Pythonpad')
396 | return PythonpadTextIOWrapper(file, target_file, mode, newline=newline)
397 |
398 | browser.self._brOpenFile = open
399 | browser.self._brIsFileExist = exists
400 | browser.self._brGetFileDict = get_file
401 |
--------------------------------------------------------------------------------
/lib/scripts/sleep.py:
--------------------------------------------------------------------------------
1 | import browser
2 | import time
3 |
4 | def __sleep__(duration):
5 | # Busy wait with server-aided wait.
6 | target_ts = time.time() + duration
7 | if duration > 3 and browser.self._brHangerUrl:
8 | # Server-aided wait.
9 | browser.self._brHangSleep(duration - 1)
10 | # Busy wait
11 | while time.time() < target_ts:
12 | pass
13 |
14 | time.sleep = __sleep__
--------------------------------------------------------------------------------
/lib/scripts/stdio.py:
--------------------------------------------------------------------------------
1 | import browser
2 | import sys
3 |
4 | class StdOutStream:
5 | def write(self, data=''):
6 | browser.self._brStdoutWrite(str(data))
7 |
8 | def flush(self):
9 | browser.self._brStdoutFlush()
10 |
11 |
12 | class StdErrStream:
13 | def write(self, data=''):
14 | browser.self._brStderrWrite(str(data))
15 |
16 | def flush(self):
17 | browser.self._brStderrFlush()
18 |
19 | def raise_input_error():
20 | raise NotImplementedError('Standard input support is turned off. Please contact the website administrator for further information.')
21 |
22 | sys.stdout = StdOutStream()
23 | sys.stderr = StdErrStream()
24 | browser.self._brRaiseInputError = raise_input_error
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "brython-runner",
3 | "version": "1.0.10",
4 | "description": "Brython based Python code runner for web clients.",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/pythonpad/brython-runner.git"
9 | },
10 | "homepage": "https://github.com/pythonpad/brython-runner#readme",
11 | "scripts": {
12 | "dev": "./node_modules/.bin/gulp dev",
13 | "build": "./node_modules/.bin/gulp build"
14 | },
15 | "author": "Jeongmin Byun",
16 | "license": "MIT",
17 | "devDependencies": {
18 | "3": "^2.1.0",
19 | "@babel/cli": "^7.10.1",
20 | "@babel/core": "^7.10.2",
21 | "@babel/plugin-proposal-class-properties": "^7.10.1",
22 | "@babel/plugin-transform-runtime": "^7.12.1",
23 | "@babel/preset-env": "^7.10.2",
24 | "babel-loader": "^8.1.0",
25 | "babel-plugin-transform-runtime": "^6.23.0",
26 | "babel-polyfill": "^6.26.0",
27 | "gulp": "^4.0.2",
28 | "gulp-babel": "^8.0.0",
29 | "webpack": "^4.43.0",
30 | "webpack-dev-server": "^3.11.0",
31 | "yargs": "^15.3.1"
32 | },
33 | "dependencies": {
34 | "brython": "3.8.10",
35 | "raw-loader": "^4.0.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Brython Runner
2 |
3 | A JavaScript library that runs Python 3 code on web browsers based on [Brython](https://brython.info/).
4 |
5 | Brython is designed to replace JavaScript in the Web; it allows you to use Python 3 instead of JavaScript as the scripting language for your web application. Brython does that by translating Python code into the equivalent JavaScript code.
6 |
7 | However, if you want to run *user-written Python code* in your web application, it's not so simple to do so. Use **Brython Runner** for that.
8 |
9 | ## Demo
10 |
11 | See how [Pythonpad](https://www.pythonpad.co/pads/new/) runs user-written Python 3 code on the browser with Brython Runner; it supports `input()`, `time.sleep(x)`, and file system with local `.py` import feature.
12 |
13 | If you need a simple example, see our [demo page](https://pythonpad.github.io/brython-runner/) and see `brython-runner` in action.
14 |
15 | ## Installation
16 |
17 | ### Node.js
18 |
19 | ```
20 | $ npm install brython-runner
21 | ```
22 |
23 | ## Usage
24 |
25 | ### Browser
26 |
27 | The simple way to use it in a browser:
28 |
29 | ```html
30 |
31 |
60 | ```
61 |
62 | ### Webpack
63 |
64 | You can directly require the module if you're using webpack to bundle your project.
65 | For example:
66 |
67 | ```javascript
68 | var BrythonRunner = require('brython-runner/lib/brython-runner.js').default;
69 | ```
70 |
71 | or with `import` syntax:
72 |
73 | ```javascript
74 | import BrythonRunner from 'brython-runner/lib/brython-runner.js';
75 | ```
76 |
77 | #### Note
78 |
79 | The core source of `BrythonRunner` uses [raw-loader](https://webpack.js.org/loaders/raw-loader/) for importing JavaScript and Python scripts as String values. If your working in non-webpack CommonJS environment, be sure to handle import statements with prefix `!!raw-loader!` in the source.
80 | For example:
81 |
82 | ```javascript
83 | import stdioSrc from '!!raw-loader!../scripts/stdio.py'
84 | import sleepSrc from '!!raw-loader!../scripts/sleep.py'
85 | import fileioSrc from '!!raw-loader!../scripts/fileio.py'
86 | ```
87 |
88 | ## Usage Examples
89 |
90 | ### Simple
91 |
92 | ```javascript
93 | const runner = new BrythonRunner({
94 | stdout: {
95 | write(content) {
96 | console.log('StdOut: ' + content);
97 | },
98 | flush() {},
99 | },
100 | stderr: {
101 | write(content) {
102 | console.error('StdErr: ' + content);
103 | },
104 | flush() {},
105 | },
106 | stdin: {
107 | async readline() {
108 | var userInput = prompt();
109 | console.log('Received StdIn: ' + userInput);
110 | return userInput;
111 | },
112 | }
113 | });
114 | await runner.runCode('print("hello world")');
115 | ```
116 |
117 | ### Debug Level
118 |
119 | Set `debug` option to explicitly set the debug level for Brython. See [this page](https://brython.info/static_doc/en/options.html) from the Brython website for more information.
120 |
121 | - 0 (default) : no debugging. Use this when the application is debugged, it slightly speeds up execution
122 | - 1 : error messages are printed in the browser console (or to the output stream specified by sys.stderr)
123 | - 2 : the translation of Python code into Javascript code is printed in the console
124 | - 10 : the translation of Python code and of the imported modules is printed in the console
125 |
126 | ```javascript
127 | const runner = new BrythonRunner({ debug: 10 });
128 | ```
129 |
130 | ### Init Callback
131 |
132 | Use `onInit` option to set a function that is called after the web worker initialization.
133 |
134 | ```javascript
135 | const runner = new BrythonRunner({
136 | onInit() {
137 | console.log('Runner web worker is ready!');
138 | },
139 | });
140 | ```
141 |
142 | ### Standard Input
143 |
144 | Brython Runner requires a *hanger* server to support Python's `input()` function in the web worker environment.
145 |
146 | A Brython Runner instance will use the *hanger* server instance served for the [Pythonpad](https://www.pythonpad.co/) service on default settings.
147 | However, you can serve your own *hanger* server and provide the URL to the server as `hangerUrl` option.
148 |
149 | ```javascript
150 | const runner = new BrythonRunner({
151 | hangerUrl: 'https://www.pythonpad.co/hanger',
152 | });
153 | ```
154 |
155 | An *aiohttp*-based implementation of a *hanger* server is available in this repository: [Brython Runner StdIn Hanger](https://github.com/pythonpad/brython-runner-stdin-hanger).
156 | Go check out detailed information on how standard input works in the Brython Runner.
157 |
158 | ### Files
159 |
160 | Use `runCodeWithFiles` method with `onFileUpdate` option to provide files and directories for the Python code.
161 |
162 | ```javascript
163 | const files = {
164 | 'hello.py': {
165 | 'type': 'text',
166 | 'body': 'print("hello world")',
167 | },
168 | 'data/': {
169 | 'type': 'dir',
170 | 'body': '',
171 | },
172 | 'data/image.gif': {
173 | 'type': 'base64',
174 | 'body': 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
175 | },
176 | 'main.py': {
177 | 'type': 'text',
178 | 'body': 'import hello\nf = open("data/image.gif", "rb")\nf.close()',
179 | }
180 | };
181 | const runner = new BrythonRunner({
182 | onFileUpdate(filename, data) {
183 | files[filename].type = data.type;
184 | files[filename].body = data.body;
185 | },
186 | });
187 | runner.runCodeWithFiles(files['main.py'].body, files);
188 | ```
189 |
190 | `BrythonRunner.runCodeWithFiles(code, files)` method runs Python `code` with given `files`. Files must be provided as a JavaScript object that contains each file or directory as a key-value pair.
191 |
192 | An object key for a file or a folder represents a path to the file from a virtual *current working directory*. A path for a file should be normalized (`os.path.normpath(path) === path`) and should be inside the current working directory (e.g., `../file.txt` is not allowed).
193 | If a path is for a **folder**, it should have a trailing slash added to a normalized path. See the `data/` folder in the example code.
194 |
195 | A value for a file or a folder should be a JavaScript object that contains values for the keys: `type` and `body`.
196 | If it is a folder, `type` should be `'dir'` and `body` should be an empty string (`''`). If it is a file, two types are available: `'text'` and `'base64'`.
197 |
198 | Files with `'text'` type should have string type content of the file as the `body` value. Files with `'base64'` type should have the content encoded in **base64** as the `body` value.
199 |
200 | If the files are edited while running the code, a function given as `onFileUpdate` option is called. The path of the edited file is given as the first parameter (`filename`) and the data object with `type` and `body` values is given as the second parameter (`data`).
201 |
202 | ## Development
203 |
204 | To serve the exmaple web page for development, run:
205 |
206 | ```
207 | $ npm dev
208 | ```
209 |
210 | Check out http://localhost:4000 on your web browser to see the example web page.
211 |
212 | To build the library, run:
213 |
214 | ```
215 | $ npm build
216 | ```
217 |
218 |
--------------------------------------------------------------------------------
/src/browser.js:
--------------------------------------------------------------------------------
1 | import BrythonRunner from './core/brython-runner';
2 |
3 | var globalRef = (typeof this !== "undefined") ? this : window;
4 |
5 | if (module.hot) {
6 | module.hot.accept('./core/brython-runner', () => {
7 | console.log('Accepting the updated Brython Runner module!')
8 | })
9 | }
10 |
11 | globalRef.BrythonRunner = BrythonRunner
12 |
--------------------------------------------------------------------------------
/src/brython-runner.js:
--------------------------------------------------------------------------------
1 | import BrythonRunner from './core/brython-runner'
2 |
3 | export default BrythonRunner
--------------------------------------------------------------------------------
/src/core/brython-runner.js:
--------------------------------------------------------------------------------
1 | import brythonRunnerWorkerSrc from '!!raw-loader!./brython-runner.worker.js'
2 | import brythonModule from '!!raw-loader!brython/brython.js'
3 | import brythonStdlibModule from '!!raw-loader!brython/brython_stdlib.js'
4 | import stdioSrc from '!!raw-loader!../scripts/stdio.py'
5 | import sleepSrc from '!!raw-loader!../scripts/sleep.py'
6 | import fileioSrc from '!!raw-loader!../scripts/fileio.py'
7 |
8 | const DEFAULT_PARAMS = {
9 | codeName: 'main.py',
10 | codeCwd: '.',
11 | staticUrl: null,
12 | hangerUrl: 'https://www.pythonpad.co/hanger',
13 | paths: [],
14 | postInitModules: [],
15 | postInitScripts: [],
16 | files: {},
17 | debug: 0,
18 | stdout: {
19 | write(content) {
20 | console.log(content)
21 | },
22 | flush() { },
23 | },
24 | stderr: {
25 | write(content) {
26 | console.error(content)
27 | },
28 | flush() { },
29 | },
30 | stdin: {
31 | async readline() {
32 | return prompt();
33 | },
34 | },
35 | onInit() {
36 | console.log('Brython runner is ready.')
37 | },
38 | onFileUpdate(filename, data) {
39 | console.log('Brython runner has an updated file:', filename, data)
40 | },
41 | onMsg(type, value) {
42 | console.log('Brython runner got a message:', type, value)
43 | },
44 | }
45 |
46 | export default class BrythonRunner {
47 | constructor(params) {
48 | this.setParamValues(params)
49 | this.initWorker()
50 | }
51 |
52 | setParamValues(params) {
53 | const values = {
54 | ...DEFAULT_PARAMS,
55 | ...params,
56 | }
57 | for (const key of Object.keys(values)) {
58 | this[key] = values[key]
59 | }
60 | }
61 |
62 | initWorker() {
63 | this.worker = this.createWorker()
64 | this.worker.postMessage({
65 | type: 'init',
66 | debug: this.debug,
67 | codeName: this.codeName,
68 | codeCwd: this.codeCwd,
69 | staticUrl: this.staticUrl,
70 | hangerUrl: this.hangerUrl,
71 | paths: this.paths,
72 | initModules: [
73 | brythonModule,
74 | brythonStdlibModule,
75 | ],
76 | postInitModules: this.postInitModules,
77 | initScripts: [
78 | stdioSrc,
79 | sleepSrc,
80 | fileioSrc,
81 | ],
82 | postInitScripts: this.postInitScripts,
83 | })
84 | this.worker.onmessage = msg => this.handleMessage(msg)
85 | }
86 |
87 | createWorker() {
88 | window.URL = window.URL || window.webkitURL
89 | let blob;
90 | try {
91 | blob = new Blob([brythonRunnerWorkerSrc], { type: 'application/javascript' })
92 | } catch (e) {
93 | window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder
94 | blob = new BlobBuilder()
95 | blob.append(brythonRunnerWorkerSrc)
96 | blob = blob.getBlob()
97 | }
98 | return new Worker(URL.createObjectURL(blob))
99 | }
100 |
101 | async handleMessage(msg) {
102 | switch (msg.data.type) {
103 | case 'brython.init':
104 | this.onInit()
105 | break
106 |
107 | case 'done':
108 | this.done(msg.data.exit)
109 | this.restartWorker()
110 | break
111 |
112 | case 'stdout.write':
113 | this.stdout.write(msg.data.value)
114 | break
115 |
116 | case 'stdout.flush':
117 | this.stdout.flush()
118 | break
119 |
120 | case 'stderr.write':
121 | this.stderr.write(msg.data.value)
122 | break
123 |
124 | case 'stderr.flush':
125 | this.stderr.flush()
126 | break
127 |
128 | case 'stdin.readline':
129 | this.hangerKey = msg.data.value
130 | const data = await this.stdin.readline()
131 | this.writeInputData(this.hangerKey, data)
132 | this.hangerKey = null
133 | break
134 |
135 | case 'file.update':
136 | this.files[msg.data.value.filename] = msg.data.value.data
137 | this.onFileUpdate(msg.data.value.filename, msg.data.value.data)
138 | break
139 |
140 | default:
141 | this.onMsg(msg.data.type, msg.data.value)
142 | break
143 | }
144 | }
145 |
146 | writeInputData(key, data) {
147 | var xhr = new XMLHttpRequest()
148 | xhr.open('POST', `${this.hangerUrl}/${key}/write/`, true)
149 | xhr.onload = e => {
150 | if (xhr.readyState === 4) {
151 | if (xhr.status === 200) {
152 | // Done.
153 | } else {
154 | console.error('Failed to send input data via server tunnel.', xhr.statusText)
155 | }
156 | }
157 | }
158 | xhr.onerror = e => {
159 | console.error('Failed to send input data via server tunnel.', xhr.statusText)
160 | }
161 | xhr.send(data)
162 | }
163 |
164 | runCode(code) {
165 | return new Promise(resolve => {
166 | this.done = exit => resolve(exit)
167 | this.worker.postMessage({
168 | type: 'run.code',
169 | code,
170 | })
171 | })
172 | }
173 |
174 | runCodeWithFiles(code, files) {
175 | return new Promise(resolve => {
176 | this.done = exit => resolve(exit)
177 | this.worker.postMessage({
178 | type: 'run.code-with-files',
179 | code,
180 | files,
181 | })
182 | })
183 | }
184 |
185 | runUrl(url) {
186 | return new Promise(resolve => {
187 | this.done = exit => resolve(exit)
188 | this.worker.postMessage({
189 | type: 'run.url',
190 | url,
191 | })
192 | })
193 | }
194 |
195 | sendMsg(type, value) {
196 | this.worker.postMessage({
197 | type,
198 | value,
199 | })
200 | }
201 |
202 | stopRunning() {
203 | if (this.hangerKey) {
204 | this.writeInputData(this.hangerKey, '')
205 | }
206 | this.restartWorker()
207 | }
208 |
209 | restartWorker() {
210 | this.worker.terminate()
211 | this.initWorker()
212 | }
213 | }
--------------------------------------------------------------------------------
/src/core/brython-runner.worker.js:
--------------------------------------------------------------------------------
1 | function _brInitRunner(data) {
2 | _brSetValues(data);
3 | _brOverwrite();
4 | _brInitMsgSenders();
5 | _brInitMsgListeners();
6 | _brRunModuleScripts(data);
7 | _brInitBrython(data);
8 | _brRunInitPythonScripts(data);
9 | _brOverrideOpen();
10 | _brRunPostInitPythonScripts(data);
11 | _brInitRunnerCallback();
12 | }
13 |
14 | function _brSetValues(data) {
15 | self._brLocalPathPrefix = '/__pythonpad_local__';
16 | self._brRunType = 'code';
17 | self._brId = data.codeName;
18 | self._brCodeCwd = data.codeCwd;
19 | self._brCode = '';
20 | self._brHangerUrl = data.hangerUrl;
21 | self._brImportLocalFile = _brImportLocalFile;
22 | self._brFilesUpdated = _brFilesUpdated;
23 | self._brHangSleep = _brHangSleep;
24 | self._brPrevErrOut = null;
25 | }
26 |
27 | function _brOverwrite() {
28 | self.window = self;
29 | self.prompt = _brGetInput;
30 | self.document = _brCreateMockDocument();
31 | }
32 |
33 | function _brCreateMockDocument() {
34 | return {
35 | getElementsByTagName: _brGetElementsByTagName,
36 | };
37 | }
38 |
39 | function _brRunModuleScripts(data) {
40 | for (const rawModule of data.initModules) {
41 | eval.call(null, rawModule);
42 | }
43 | for (const rawModule of data.postInitModules) {
44 | eval.call(null, rawModule);
45 | }
46 | }
47 |
48 | function _brInitBrython(data) {
49 | self.RealXMLHttpRequest = self.XMLHttpRequest
50 | self.XMLHttpRequest = _brXHR;
51 | self.__BRYTHON__.brython({
52 | pythonpath: [self._brLocalPathPrefix].concat(data.paths),
53 | debug: data.debug || 0,
54 | });
55 | }
56 |
57 | function _brRunInitPythonScripts(data) {
58 | _brRun(data.initScripts.join('\n'));
59 | }
60 |
61 | function _brOverrideOpen() {
62 | self.__BRYTHON__.builtins.open = self._brOpenFile;
63 | }
64 |
65 | function _brRunPostInitPythonScripts(data) {
66 | for (var i = 0; i < data.postInitScripts.length; i++) {
67 | _brRun(data.postInitScripts[i]);
68 | }
69 | }
70 |
71 | function _brInitRunnerCallback() {
72 | self.postMessage({
73 | type: 'brython.init',
74 | value: '',
75 | });
76 | }
77 |
78 | function _brImportLocalFile(filename) {
79 | if (self._brFilesObj[filename] && self._brFilesObj[filename].type === 'text') {
80 | return self._brFilesObj[filename].body;
81 | } else {
82 | return null;
83 | }
84 | }
85 |
86 | function _brSetFiles(files) {
87 | self._brFilesObj = files;
88 | self._brSetFilesFromObj();
89 | }
90 |
91 | function _brFilesUpdated(filename, type, body) {
92 | if (!type && !body) {
93 | delete self._brFilesObj[filename];
94 | self.postMessage({
95 | type: 'file.delete',
96 | value: filename,
97 | });
98 | } else {
99 | self._brFilesObj[filename] = {
100 | type: type,
101 | body: body,
102 | };
103 | self.postMessage({
104 | type: 'file.update',
105 | value: {
106 | filename: filename,
107 | data: {
108 | type: type,
109 | body: body,
110 | }
111 | },
112 | });
113 | }
114 | }
115 |
116 | function _brGetInput(message) {
117 | if (self._brHangerUrl === null) {
118 | self._brRaiseInputError();
119 | return '';
120 | }
121 | if (message) {
122 | self._brStdoutWrite(message + '');
123 | self._brStdoutFlush();
124 | }
125 | var req = new RealXMLHttpRequest();
126 | console.log('URL', self._brHangerUrl + '/open/');
127 | req.open('POST', self._brHangerUrl + '/open/', false);
128 | req.send('');
129 |
130 | if (req.status !== 200) {
131 | console.error('Failed to tunnel through the server to get input.');
132 | return '';
133 | }
134 |
135 | var key = req.responseText;
136 |
137 | self.postMessage({
138 | type: 'stdin.readline',
139 | value: key,
140 | });
141 |
142 | req = new RealXMLHttpRequest();
143 | req.open('POST', self._brHangerUrl + '/' + key + '/read/', false);
144 | req.send('');
145 |
146 | if (req.status !== 200) {
147 | console.error('Failed to tunnel through the server to get input.');
148 | return '';
149 | }
150 |
151 | return req.responseText;
152 | }
153 |
154 | function _brHangSleep(duration) {
155 | var req = new RealXMLHttpRequest();
156 | req.open('GET', self._brHangerUrl + '/sleep/?duration=' + duration, false);
157 | req.send(null);
158 | }
159 |
160 | function _brGetElementsByTagName(tagName) {
161 | if (tagName === 'script') {
162 | if (self._brRunType === 'code') {
163 | return [{
164 | type: 'text/python',
165 | id: self._brId,
166 | innerHTML: self._brCode,
167 | }];
168 | } else if (self._brRunType === 'url') {
169 | return [{
170 | type: 'text/python',
171 | id: getFilename(self._brUrl),
172 | src: self._brUrl,
173 | }];
174 | }
175 | }
176 | return [];
177 | }
178 |
179 | function _brInitMsgSenders() {
180 | self._brStdoutWrite = function (data) {
181 | self._brPrevErrOut = null
182 | self.postMessage({
183 | type: 'stdout.write',
184 | value: data,
185 | });
186 | };
187 | self._brStdoutFlush = function () {
188 | self.postMessage({
189 | type: 'stdout.flush',
190 | });
191 | };
192 | self._brStderrWrite = function (data) {
193 | if ((data + '').startsWith('Traceback (most recent call last):') && (data === self._brPrevErrOut)) {
194 | return; // Skip duplicated error message.
195 | }
196 | self._brPrevErrOut = data;
197 | self.postMessage({
198 | type: 'stderr.write',
199 | value: data,
200 | });
201 | };
202 | self._brStderrFlush = function () {
203 | self.postMessage({
204 | type: 'stderr.flush',
205 | });
206 | };
207 | self._brSendMsg = function (type, value) {
208 | self.postMessage({
209 | type: type,
210 | value: value,
211 | });
212 | };
213 | }
214 |
215 | function _brInitMsgListeners() {
216 | self._brMsgListeners = {};
217 | self._brAddMsgListener = function (type, callback) {
218 | if (!(type in self._brMsgListeners)) {
219 | self._brMsgListeners[type] = [callback];
220 | } else {
221 | self._brMsgListeners[type].push(callback);
222 | }
223 | }
224 | self._brRemoveMsgListener = function (type, callback) {
225 | if (type in self._brMsgListeners) {
226 | var newMsgListeners = [];
227 | for (var i = 0; i < self._brMsgListeners[type].length; i++) {
228 | if (self._brMsgListeners[type][i] !== callback) {
229 | newMsgListeners.push(self._brMsgListeners[type][i]);
230 | }
231 | }
232 | self._brMsgListeners[type] = newMsgListeners;
233 | }
234 | }
235 | self.receiveMsg = function (type) {
236 | return new Promise(function (resolve, reject) {
237 | var callback = function callback(msg) {
238 | resolve(msg.value);
239 | self._brRemoveMsgListener(type, callback);
240 | }
241 | self._brAddMsgListener(type, callback);
242 | })
243 | }
244 | }
245 |
246 | function getFilename(url) {
247 | var splitUrl = url.split('/');
248 | return splitUrl[splitUrl.length - 1];
249 | }
250 |
251 | function getParentUrl(url) {
252 | var splitUrl = url.split('/');
253 | if (splitUrl.length === 1) {
254 | return './';
255 | } else {
256 | return splitUrl.slice(0, splitUrl.length - 1).join('/');
257 | }
258 | }
259 |
260 | function _brRun(src) {
261 | self._brPrevErrOut = null;
262 | self._brRunType = 'code';
263 | self._brCode = src;
264 | var pathBackup = self.__BRYTHON__.script_path;
265 | self.__BRYTHON__.script_path = self._brCodeCwd;
266 | try {
267 | self.__BRYTHON__.parser._run_scripts({});
268 | } catch (err) {} finally {
269 | self.__BRYTHON__.script_path = pathBackup;
270 | }
271 | }
272 |
273 | function _brRunUrl(url) {
274 | self._brPrevErrOut = null;
275 | self._brRunType = 'url';
276 | self._brUrl = url;
277 | var pathBackup = self.__BRYTHON__.script_path;
278 | self.__BRYTHON__.script_path = getParentUrl(url);
279 | try {
280 | self.__BRYTHON__.parser._run_scripts({});
281 | } catch (err) { } finally {
282 | self.__BRYTHON__.script_path = pathBackup;
283 | }
284 | }
285 |
286 | function _brRunCallback(exit) {
287 | self.postMessage({
288 | type: 'done',
289 | exit,
290 | });
291 | }
292 |
293 | class _brXHR extends XMLHttpRequest {
294 | constructor() {
295 | super();
296 | this.localPrefix = self._brLocalPathPrefix + '/';
297 | this.localRequestOpened = false;
298 | this.localRequestSent = false;
299 | this.localResponseText = null;
300 | }
301 |
302 | open(...params) {
303 | if (params.length > 1) {
304 | const url = params[1];
305 | if (url.startsWith(this.localPrefix)) {
306 | const localPath = url.slice(this.localPrefix.length, url.indexOf('?'));
307 | this.localResponseText = _brImportLocalFile(localPath);
308 | this.localRequestOpened = true;
309 | // TODO: Call onreadystatechange.
310 | return;
311 | }
312 | }
313 | return super.open(...params);
314 | }
315 |
316 | send(...params) {
317 | if (this.localRequestOpened) {
318 | this.localRequestSent = true;
319 | } else {
320 | return super.send(...params);
321 | }
322 | }
323 |
324 | get status() {
325 | if (this.localRequestOpened) {
326 | if (this.localResponseText === null) {
327 | return 404;
328 | } else {
329 | return 200;
330 | }
331 | } else {
332 | return super.status;
333 | }
334 | }
335 |
336 | get readyState() {
337 | if (this.localRequestOpened) {
338 | if (this.localRequestSent) {
339 | return 4;
340 | } else {
341 | return 1;
342 | }
343 | } else {
344 | return super.readyState;
345 | }
346 | }
347 |
348 | get responseText() {
349 | if (this.localRequestOpened) {
350 | return this.localResponseText;
351 | } else {
352 | return super.responseText;
353 | }
354 | }
355 | }
356 |
357 | self.onmessage = function (message) {
358 | var data = message.data;
359 | switch (data.type) {
360 | case 'init':
361 | _brInitRunner(data);
362 | break;
363 | case 'run.code':
364 | try {
365 | _brRun(data.code);
366 | _brRunCallback(0);
367 | } catch (err) {
368 | _brRunCallback(1);
369 | }
370 | break;
371 | case 'run.code-with-files':
372 | try {
373 | _brSetFiles(data.files);
374 | _brRun(data.code);
375 | _brRunCallback(0);
376 | } catch (err) {
377 | _brRunCallback(1);
378 | }
379 | break;
380 | case 'run.url':
381 | try {
382 | _brRunUrl(data.url);
383 | _brRunCallback(0);
384 | } catch (err) {
385 | _brRunCallback(1);
386 | }
387 | break;
388 | default:
389 | break;
390 | }
391 | if (data.type in self._brMsgListeners) {
392 | for (var i = 0; i < self._brMsgListeners[data.type].length; i++) {
393 | self._brMsgListeners[data.type][i](data);
394 | }
395 | }
396 | }
--------------------------------------------------------------------------------
/src/scripts/fileio.py:
--------------------------------------------------------------------------------
1 | import browser
2 | import io
3 | import os
4 |
5 | def set_files_from_obj():
6 | try:
7 | browser.self._brFiles = browser.self._brFilesObj.to_dict()
8 | except AttributeError:
9 | pass
10 | set_files_from_obj()
11 | browser.self._brSetFilesFromObj = set_files_from_obj
12 |
13 | class PythonpadTextIOWrapper(io.IOBase):
14 | def __init__(self, filename, target_file, mode, newline=None):
15 | self.stream = io.StringIO(newline=newline)
16 | self.stream.write(target_file['body'])
17 | self.filename = filename
18 | self.target_file = target_file
19 | self.mode = mode
20 | if 'a' not in mode:
21 | self.stream.seek(0)
22 |
23 | def __enter__(self):
24 | return self
25 |
26 | def __exit__(self, exc_type, exc_value, traceback):
27 | self.close()
28 |
29 | def __str__(self):
30 | return '' % (self.filename, self.mode)
31 |
32 | def __repr__(self):
33 | return self.__str__()
34 |
35 | def __del__(self):
36 | return self.stream.__del__()
37 |
38 | def __iter__(self):
39 | return self.stream.__iter__()
40 |
41 | def __next__(self):
42 | return self.stream.__next__()
43 |
44 | def __dict__(self):
45 | return self.stream.__dict__()
46 |
47 | def __eq__(self, other):
48 | return self.stream.__eq__(other.stream)
49 |
50 | def __format__(self, format_spec):
51 | return self.stream.__format__(format_spec)
52 |
53 | def __ge__(self, other):
54 | return self.stream.__ge__(other.stream)
55 |
56 | def __gt__(self, other):
57 | return self.stream.__gt__(other.stream)
58 |
59 | def __le__(self, other):
60 | return self.stream.__le__(other.stream)
61 |
62 | def __lt__(self, other):
63 | return self.stream.__lt__(other.stream)
64 |
65 | def __ne__(self, other):
66 | return self.stream.__ne__(other.stream)
67 |
68 | def __sizeof__(self):
69 | return self.stream.__sizeof__()
70 |
71 | def detach(self):
72 | raise NotImplementedError('not available in Pythonpad')
73 |
74 | def readable(self):
75 | return 'r' in self.mode or '+' in self.mode
76 |
77 | def read(self, size=-1):
78 | if 'r' not in self.mode and '+' not in self.mode:
79 | raise io.UnsupportedOperation('not readable')
80 | return self.stream.read(size)
81 |
82 | def readline(self, size=-1):
83 | if 'r' not in self.mode and '+' not in self.mode:
84 | raise io.UnsupportedOperation('not readable')
85 | return self.stream.readline(size)
86 |
87 | def readlines(self, hint=-1):
88 | if 'r' not in self.mode and '+' not in self.mode:
89 | raise io.UnsupportedOperation('not readable')
90 | return self.stream.readlines(hint)
91 |
92 | def writable(self):
93 | return 'r' not in self.mode or '+' in self.mode
94 |
95 | def write(self, s):
96 | if 'r' in self.mode and '+' not in self.mode:
97 | raise io.UnsupportedOperation('not writable')
98 | return self.stream.write(s)
99 |
100 | def writelines(self, lines):
101 | if 'r' in self.mode and '+' not in self.mode:
102 | raise io.UnsupportedOperation('not writable')
103 | return self.stream.writelines(s)
104 |
105 | def fileno(self):
106 | raise OSError('no file descriptor is available in simulated in-memory file system')
107 |
108 | def tell(self):
109 | return self.stream.tell()
110 |
111 | def seekable(self):
112 | return True
113 |
114 | def seek(self, offset):
115 | return self.stream.seek(offset)
116 |
117 | def isatty(self):
118 | return False
119 |
120 | def truncate(self, size=None):
121 | return self.stream.truncate(size=size)
122 |
123 | def flush(self):
124 | if 'r' in self.mode or '+' in self.mode:
125 | return
126 | cursor = self.stream.tell()
127 | self.stream.seek(0) # Seek to the beginning of the stream.
128 | self.target_file['body'] = self.stream.read()
129 | files_updated(self.filename, )
130 | self.stream.seek(cursor)
131 |
132 | def close(self):
133 | if 'r' not in self.mode or '+' in self.mode:
134 | self.stream.seek(0) # Seek to the beginning of the stream.
135 | self.target_file['body'] = self.stream.read()
136 | files_updated(self.filename)
137 | self.stream.close()
138 |
139 | @property
140 | def name(self):
141 | return self.filename
142 |
143 | @property
144 | def closed(self):
145 | return self.stream.closed
146 |
147 | class PythonpadBytesIOWrapper(io.BufferedIOBase):
148 | def __init__(self, filename, target_file, mode):
149 | global base64
150 | import base64
151 | self.stream = io.BytesIO()
152 | self.stream.write(base64.b64decode(target_file['body']))
153 | self.filename = filename
154 | self.target_file = target_file
155 | self.mode = mode
156 | if 'a' not in mode:
157 | self.stream.seek(0)
158 |
159 | def __enter__(self):
160 | return self
161 |
162 | def __exit__(self, exc_type, exc_value, traceback):
163 | self.close()
164 |
165 | def __str__(self):
166 | return '' % (self.filename, self.mode)
167 |
168 | def __repr__(self):
169 | return self.__str__()
170 |
171 | def __del__(self):
172 | return self.stream.__del__()
173 |
174 | def __dict__(self):
175 | return self.stream.__dict__()
176 |
177 | def __dir__(self):
178 | return self.stream.__dir__()
179 |
180 | def __eq__(self, other):
181 | return self.stream.__eq__(other.stream)
182 |
183 | def __format__(self, format_spec):
184 | return self.stream.__format__(format_spec)
185 |
186 | def __ge__(self, other):
187 | return self.stream.__ge__(other.stream)
188 |
189 | def __gt__(self, other):
190 | return self.stream.__gt__(other.stream)
191 |
192 | def __iter__(self):
193 | return self.stream.__iter__()
194 |
195 | def __le__(self, other):
196 | return self.stream.__le__(other.stream)
197 |
198 | def __lt__(self, other):
199 | return self.stream.__lt__(other.stream)
200 |
201 | def __ne__(self, other):
202 | return self.stream.__ne__(other.stream)
203 |
204 | def __next__(self):
205 | return self.stream.__next__()
206 |
207 | def __sizeof__(self):
208 | return self.stream.__sizeof__()
209 |
210 | def detach(self):
211 | raise NotImplementedError('not available in Pythonpad')
212 |
213 | def readable(self):
214 | return 'r' in self.mode or '+' in self.mode
215 |
216 | def read(self, *args, **kwargs):
217 | if 'r' not in self.mode and '+' not in self.mode:
218 | raise io.UnsupportedOperation('not readable')
219 | return self.stream.read(*args, **kwargs)
220 |
221 | def readline(self, *args, **kwargs):
222 | if 'r' not in self.mode and '+' not in self.mode:
223 | raise io.UnsupportedOperation('not readable')
224 | return self.stream.readline(*args, **kwargs)
225 |
226 | def readlines(self, *args, **kwargs):
227 | if 'r' not in self.mode and '+' not in self.mode:
228 | raise io.UnsupportedOperation('not readable')
229 | return self.stream.readlines(*args, **kwargs)
230 |
231 | def read1(self, *args, **kwargs):
232 | if 'r' not in self.mode and '+' not in self.mode:
233 | raise io.UnsupportedOperation('not readable')
234 | return self.stream.read1(*args, **kwargs)
235 |
236 | def readinto(self, *args, **kwargs):
237 | if 'r' not in self.mode and '+' not in self.mode:
238 | raise io.UnsupportedOperation('not readable')
239 | return self.stream.readinto(*args, **kwargs)
240 |
241 | def readinto1(self, *args, **kwargs):
242 | if 'r' not in self.mode and '+' not in self.mode:
243 | raise io.UnsupportedOperation('not readable')
244 | return self.stream.readinto1(*args, **kwargs)
245 |
246 | def writable(self):
247 | return 'r' not in self.mode or '+' in self.mode
248 |
249 | def write(self, s):
250 | if 'r' in self.mode and '+' not in self.mode:
251 | raise io.UnsupportedOperation('not writable')
252 | return self.stream.write(s)
253 |
254 | def writelines(self, lines):
255 | if 'r' in self.mode and '+' not in self.mode:
256 | raise io.UnsupportedOperation('not writable')
257 | return self.stream.writelines(s)
258 |
259 | def fileno(self):
260 | raise OSError('no file descriptor is available in simulated in-memory file system')
261 |
262 | def tell(self):
263 | return self.stream.tell()
264 |
265 | def peek(self, *args, **kwargs):
266 | return self.stream.peek(*args, **kwargs)
267 |
268 | def raw(self, *args, **kwargs):
269 | return self.stream.raw(*args, **kwargs)
270 |
271 | def seekable(self):
272 | return True
273 |
274 | def seek(self, offset):
275 | return self.stream.seek(offset)
276 |
277 | def isatty(self):
278 | return False
279 |
280 | def truncate(self, size=None):
281 | return self.stream.truncate(size=size)
282 |
283 | def flush(self):
284 | if 'r' in self.mode or '+' in self.mode:
285 | return
286 | cursor = self.stream.tell()
287 | self.stream.seek(0) # Seek to the beginning of the stream.
288 | self.target_file['body'] = base64.b64encode(self.stream.read()).decode('utf-8')
289 | files_updated(self.filename)
290 | self.stream.seek(cursor)
291 |
292 | def close(self):
293 | if 'r' not in self.mode or '+' in self.mode:
294 | self.stream.seek(0) # Seek to the beginning of the stream.
295 | self.target_file['body'] = base64.b64encode(self.stream.read()).decode('utf-8')
296 | files_updated(self.filename)
297 | self.stream.close()
298 |
299 | @property
300 | def name(self):
301 | return self.filename
302 |
303 | @property
304 | def closed(self):
305 | return self.stream.closed()
306 |
307 | def files_updated(path):
308 | if path in browser.self._brFiles:
309 | browser.self._brFilesUpdated(path, browser.self._brFiles[path]['type'], browser.self._brFiles[path]['body'])
310 | else:
311 | browser.self._brFilesUpdated(path, None, None)
312 |
313 | def normalize_path(path):
314 | normalized_path = os.path.normpath(path)
315 | if normalized_path.startswith('/'):
316 | raise NotImplementedError('absolute path is not supported in Pythonpad')
317 | elif normalized_path.startswith('../'):
318 | raise NotImplementedError('accessing out of the project is not supported in Pythonpad')
319 | return normalized_path
320 |
321 | def exists(path):
322 | normalized_path = normalize_path(path)
323 | return (normalized_path in browser.self._brFiles) or ((normalized_path + '/') in browser.self._brFiles)
324 |
325 | def is_dir(path):
326 | dir_path = normalize_path(path) + '/'
327 | return dir_path in browser.self._brFiles
328 |
329 | def get_file(path):
330 | return browser.self._brFiles[normalize_path(path)]
331 |
332 | def create_file(path, file_type=None, body=None):
333 | normalized_path = normalize_path(path)
334 | if '/' in normalized_path:
335 | tokens = normalized_path.split('/')
336 | parent_path = '/'.join(tokens[:-1])
337 | if (parent_path + '/') not in browser.self._brFiles:
338 | # No parent directory.
339 | raise FileNotFoundError('No such file or directory: \'%s\'' % path)
340 | file = {
341 | 'type': 'text' if file_type is None else file_type,
342 | 'body': '' if body is None else body,
343 | }
344 | browser.self._brFiles[normalized_path] = file
345 | files_updated(normalized_path)
346 | return file
347 |
348 | def open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
349 | count = 0
350 | for m in 'rwxa':
351 | if m in mode:
352 | count += 1
353 | if count != 1:
354 | raise ValueError('must have exactly one of create/read/write/append mode')
355 |
356 | if is_dir(file):
357 | raise IsADirectoryError('Is a directory: \'%s\'' % file)
358 |
359 | if 'b' in mode:
360 | if 'r' in mode:
361 | if not exists(file):
362 | raise FileNotFoundError('No such file or directory: \'%s\'' % file)
363 | target_file = get_file(file)
364 | elif 'w' in mode:
365 | target_file = create_file(file, file_type='base64')
366 | elif 'x' in mode:
367 | if exists(file):
368 | raise FileExistsError('File exists: \'%s\'' % file)
369 | target_file = create_file(file, file_type='base64')
370 | elif 'a' in mode:
371 | if exists(file):
372 | target_file = get_file(file)
373 | else:
374 | target_file = create_file(file, file_type='base64')
375 | if target_file['type'] != 'base64':
376 | raise NotImplementedError('opening text file in bytes mode is not implemented in Pythonpad')
377 | return PythonpadBytesIOWrapper(file, target_file, mode)
378 | else:
379 | if 'r' in mode:
380 | if not exists(file):
381 | raise FileNotFoundError('No such file or directory: \'%s\'' % file)
382 | target_file = get_file(file)
383 | elif 'w' in mode:
384 | target_file = create_file(file)
385 | elif 'x' in mode:
386 | if exists(file):
387 | raise FileExistsError('File exists: \'%s\'' % file)
388 | target_file = create_file(file)
389 | elif 'a' in mode:
390 | if exists(file):
391 | target_file = get_file(file)
392 | else:
393 | target_file = create_file(file)
394 | if target_file['type'] != 'text':
395 | raise NotImplementedError('opening byte file in text mode is not implemented in Pythonpad')
396 | return PythonpadTextIOWrapper(file, target_file, mode, newline=newline)
397 |
398 | browser.self._brOpenFile = open
399 | browser.self._brIsFileExist = exists
400 | browser.self._brGetFileDict = get_file
401 |
--------------------------------------------------------------------------------
/src/scripts/sleep.py:
--------------------------------------------------------------------------------
1 | import browser
2 | import time
3 |
4 | def __sleep__(duration):
5 | # Busy wait with server-aided wait.
6 | target_ts = time.time() + duration
7 | if duration > 3 and browser.self._brHangerUrl:
8 | # Server-aided wait.
9 | browser.self._brHangSleep(duration - 1)
10 | # Busy wait
11 | while time.time() < target_ts:
12 | pass
13 |
14 | time.sleep = __sleep__
--------------------------------------------------------------------------------
/src/scripts/stdio.py:
--------------------------------------------------------------------------------
1 | import browser
2 | import sys
3 |
4 | class StdOutStream:
5 | def write(self, data=''):
6 | browser.self._brStdoutWrite(str(data))
7 |
8 | def flush(self):
9 | browser.self._brStdoutFlush()
10 |
11 |
12 | class StdErrStream:
13 | def write(self, data=''):
14 | browser.self._brStderrWrite(str(data))
15 |
16 | def flush(self):
17 | browser.self._brStderrFlush()
18 |
19 | def raise_input_error():
20 | raise NotImplementedError('Standard input support is turned off. Please contact the website administrator for further information.')
21 |
22 | sys.stdout = StdOutStream()
23 | sys.stderr = StdErrStream()
24 | browser.self._brRaiseInputError = raise_input_error
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = mode => ({
5 | cache: true,
6 | mode: 'development',
7 | entry: {
8 | 'brython-runner.bundle': mode === 'development' ? ['babel-polyfill', './src/browser.js'] : ['./src/browser.js'],
9 | },
10 | output: {
11 | path: path.join(__dirname, 'lib'),
12 | publicPath: '/lib/',
13 | filename: '[name].js',
14 | },
15 | resolve: {
16 | extensions: ['.js'],
17 | modules: ['node_modules'],
18 | },
19 | module: {
20 | rules: [
21 | {
22 | test: /\.jsx?$/,
23 | include: [path.resolve(__dirname, 'src')],
24 | use: ['babel-loader'],
25 | }
26 | ],
27 | },
28 | plugins: mode === 'development' ? [
29 | new webpack.HotModuleReplacementPlugin(),
30 | ] : [],
31 | devServer: {
32 | hot: true,
33 | historyApiFallback: true,
34 | contentBase: '.',
35 | publicPath: '/lib/',
36 | },
37 | });
--------------------------------------------------------------------------------