├── .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 |
21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 |
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 | }); --------------------------------------------------------------------------------