├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── bin └── livepython ├── dist ├── images │ ├── fastforward.png │ ├── pause.png │ └── play.png ├── index.html ├── main.js ├── monaco.ttf ├── style.css ├── variable_inspector.html └── variable_inspector.js ├── livepython.icns ├── livepython.png ├── main.js ├── package.json ├── src ├── components │ ├── CodeView.js │ ├── MainView.js │ └── VariableInspector.js ├── index.js └── variable_inspector.js ├── tracer.py └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | .npm 4 | *.lock 5 | package-lock.json 6 | .DS_Store 7 | .vs-code 8 | tags 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anastasis Germanidis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://i.imgur.com/GM8vrCz.png) 2 | ![](https://i.imgur.com/36oEh3R.gif) 3 | 4 | ## Livepython 5 | ### Watch your Python run like a movie. 6 | 7 | ##### NOTE: Livepython is alpha software. It doesn't handle a lot of edge cases and features may change. 8 | 9 | Livepython is a desktop app that lets you visually trace, in real-time, the execution of a Python program. In addition, it can track changes in global and local variables as your program is running. Livepython is meant to give you a quick grasp of a program's execution flow. It's less messy than sprinkling print statements throughout your code and simpler to use than debuggers/profilers. 10 | 11 | Livepython can be launched from the command-line as easily as: 12 | 13 | livepython [program] [args...] 14 | 15 | **Controls:** 16 | 17 | SPACE: Play/Pause the program. 18 | 19 | Left/Right Arrow: Change speed of execution. 20 | 21 | V: Open/Close Variable Inspector. 22 | 23 | ### Compatibility 24 | 25 | | **Python Version** | **Compatible?** | 26 | |-----------|---------------| 27 | | 3.6 | ✅ | 28 | | 3.5 | ✅ | 29 | | 2.7 | ✅ | 30 | | 2.6 | ❌ | 31 | 32 | ### Installation 33 | 34 | npm install livepython -g 35 | 36 | ### Development 37 | 38 | 39 | 40 | Livepython has 3 main components: 41 | 42 | * a Python [tracer](https://github.com/agermanidis/livepython/blob/master/tracer.py) that uses `sys.settrace()` to intercept every line of your program as it's being evaluated 43 | * an [Electron app](https://github.com/agermanidis/livepython/blob/master/main.js) that is responsible for the rendering the Livepython frontend 44 | * a node.js [gateway script](https://github.com/agermanidis/livepython/blob/master/bin/livepython) that manages communication between the frontend and the tracer 45 | 46 | If you want to make changes to Livepython, you will need to run [webpack](https://webpack.js.org/): 47 | 48 | webpack 49 | 50 | Then you can test your built version of livepython by running: 51 | 52 | bin/livepython [your python program] 53 | 54 | ### License 55 | 56 | MIT 57 | -------------------------------------------------------------------------------- /bin/livepython: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require("fs"); 4 | var electronPath = require("electron"); 5 | 6 | const net = require("net"); 7 | 8 | var buffer = []; 9 | var electronWindowOpened = false; 10 | var socket; 11 | 12 | net.createServer((s) => { 13 | socket = s; 14 | 15 | const pythonLineStream = byline.createStream(socket); 16 | pythonLineStream.on("data", line => { 17 | line = line.toString(); 18 | if (!line.length) return; 19 | if (electronWindowOpened) { 20 | electronProcess.send(line); 21 | } else { 22 | buffer.push(line); 23 | } 24 | }); 25 | }).listen(4387) 26 | 27 | const { spawn } = require("child_process"); 28 | const byline = require("byline") 29 | 30 | var args = process.argv.slice(2) 31 | 32 | if (!args.length) { 33 | console.log("Usage: livepython [program] [..args]") 34 | process.exit() 35 | } 36 | 37 | args.unshift(__dirname + "/../tracer.py") 38 | 39 | const electronProcess = spawn(electronPath, [__dirname + "/../"], { 40 | stdio: ["pipe", "pipe", "pipe", "ipc"] 41 | }) 42 | 43 | const pythonProcess = spawn("python", args); 44 | 45 | pythonProcess.stdout.on("data", data => { 46 | process.stdout.write(data.toString()); 47 | }); 48 | 49 | pythonProcess.stderr.on("data", data => { 50 | process.stdout.write(data.toString()) 51 | }) 52 | 53 | pythonProcess.on("exit", (code) => { 54 | electronProcess.kill('SIGINT') 55 | process.exit(); 56 | }) 57 | 58 | electronProcess.on("message", msg => { 59 | if (msg.type === 'connected') { 60 | electronWindowOpened = true; 61 | buffer.forEach(msg => { 62 | electronProcess.send(msg); 63 | }); 64 | } else if (msg.type === "toggle_running_state") { 65 | if (msg.value) { 66 | pythonProcess.kill("SIGSTOP") 67 | } else { 68 | pythonProcess.kill("SIGCONT"); 69 | } 70 | } else { 71 | socket.write(JSON.stringify(msg)) 72 | } 73 | }) 74 | 75 | function killSubprocesses (e) { 76 | electronProcess.kill("SIGINT"); 77 | pythonProcess.kill("SIGINT"); 78 | process.exit(); 79 | } 80 | 81 | process.on('exit', killSubprocesses) 82 | process.on('SIGINT', killSubprocesses) 83 | process.on("SIGUSR1", killSubprocesses) 84 | process.on("SIGUSR2", killSubprocesses) 85 | process.on("uncaughtException", killSubprocesses) -------------------------------------------------------------------------------- /dist/images/fastforward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/dist/images/fastforward.png -------------------------------------------------------------------------------- /dist/images/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/dist/images/pause.png -------------------------------------------------------------------------------- /dist/images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/dist/images/play.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | livepython 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /dist/monaco.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/dist/monaco.ttf -------------------------------------------------------------------------------- /dist/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Monaco; 3 | src: url(monaco.ttf) 4 | } 5 | 6 | body { 7 | font-family: Monaco; 8 | color: white; 9 | background: #08091D; 10 | margin: 0; 11 | } 12 | 13 | #content { 14 | position: fixed; 15 | height: 100%; 16 | width: 100%; 17 | } 18 | 19 | #main { 20 | display: flex; 21 | flex-direction: column; 22 | flex-wrap: nowrap; 23 | height: 100%; 24 | } 25 | 26 | #header-view { 27 | flex: 0 0 50px; 28 | } 29 | 30 | #main-area { 31 | overflow: scroll; 32 | } 33 | 34 | .line { 35 | background: rgba(0, 0, 0, 0); 36 | 37 | } 38 | 39 | .selected-line { 40 | background: #278865; 41 | 42 | } 43 | 44 | .selected-paused { 45 | background: #717171; 46 | } 47 | 48 | .error { 49 | background: #AD4343; 50 | } 51 | 52 | .error-msg { 53 | display: inline; 54 | font-size: 20px; 55 | padding: 10px; 56 | } 57 | 58 | #program-title { 59 | padding: 10px; 60 | text-align: center; 61 | margin: 0px; 62 | color: white; 63 | background-color: #278865; 64 | } 65 | 66 | #code-view { 67 | background: none; 68 | font-size: 18px; 69 | outline: none; 70 | flex: auto; 71 | } 72 | 73 | #source pre { 74 | margin: 0; 75 | /* margin-left: 35px; */ 76 | } 77 | 78 | #source pre code { 79 | background: none; 80 | color: #c5c8c6; 81 | margin-left: 10px; 82 | } 83 | 84 | #line-numbers { 85 | margin-top: 10px; 86 | width: 50px; 87 | font-size: 15px; 88 | float: left; 89 | } 90 | 91 | .line { 92 | padding: 5px; 93 | margin: 0; 94 | /* transition-duration: 0.05s; */ 95 | font-size: 15px; 96 | font-family: Monaco; 97 | } 98 | 99 | .line-number { 100 | color: #AAA; 101 | padding: 0 5px; 102 | margin: 0; 103 | float: left; 104 | } 105 | 106 | #main-area { 107 | height: 100%; 108 | transition-duration: 1s; 109 | display: flex; 110 | flex-direction: row; 111 | } 112 | 113 | .finished { 114 | background-color: #131F1D; 115 | } 116 | 117 | .failed { 118 | background-color: #4C0F19; 119 | } 120 | 121 | .finished #program-title { 122 | background-color: #278865; 123 | } 124 | 125 | .paused #program-title { 126 | background-color: #6B6B6B; 127 | } 128 | 129 | .failed #program-title { 130 | background-color: red; 131 | } 132 | 133 | pre#exception-message { 134 | font-size: 13px; 135 | padding-left: 20px; 136 | color: white; 137 | margin: 15px; 138 | font-family: Monaco; 139 | } 140 | 141 | .exception { 142 | background: #820c0c; 143 | } 144 | 145 | .exception-message .hljs-string { 146 | color: white; 147 | } 148 | 149 | .exception-message .hljs-keyword { 150 | color: white; 151 | } 152 | 153 | #program-state { 154 | font-size: 13px; 155 | padding: 5px; 156 | border-radius: 5px; 157 | margin: 15px; 158 | height: 15px; 159 | } 160 | 161 | #program-state.running { 162 | background: green; 163 | } 164 | 165 | #program-state.paused { 166 | background: gray; 167 | } 168 | 169 | code { 170 | font-family: Monaco; 171 | } 172 | 173 | #code-area { 174 | padding-top: 10px; 175 | padding-left: 5px; 176 | } 177 | 178 | .status-indicator { 179 | position: fixed; 180 | top: 50%; 181 | left: 50%; 182 | width: 150px; 183 | height: 150px; 184 | margin-top: -75px; 185 | margin-left: -75px; 186 | } 187 | 188 | #variables-view { 189 | font-family: Helvetica Neue; 190 | } 191 | 192 | #variable-search { 193 | background: none; 194 | outline: none; 195 | font-size: 20px; 196 | padding: 10px; 197 | border: none; 198 | border-left: 5px solid white; 199 | margin-left: 20px; 200 | margin: 10px; 201 | color: white; 202 | } 203 | 204 | table { 205 | 206 | width: 100%; 207 | margin-top: 10px; 208 | font-size: 16px; 209 | padding: 0 5px; 210 | 211 | } 212 | 213 | thead { 214 | background: #353434; 215 | font-weight: bold; 216 | } 217 | 218 | /* tbody tr:nth-child(even) { 219 | background: rgba(255, 255, 255, 0.2); 220 | } 221 | */ 222 | td { 223 | padding: 10px; 224 | } -------------------------------------------------------------------------------- /dist/variable_inspector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Variable Inspector 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /livepython.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/livepython.icns -------------------------------------------------------------------------------- /livepython.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agermanidis/livepython/9178a59b6cf86ac4279fe77e27617cd7f3ced6cd/livepython.png -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const {app, BrowserWindow, ipcMain} = require('electron') 2 | const path = require('path') 3 | const url = require('url') 4 | 5 | let win 6 | let varInspector; 7 | 8 | function openVariableInspector () { 9 | varInspector = new BrowserWindow({ 10 | x: 800, 11 | y: 0, 12 | width: 500, 13 | height: 800, 14 | title: "Variable Inspector" 15 | }); 16 | 17 | varInspector.loadURL(url.format({ 18 | pathname: path.join(__dirname, "dist", "variable_inspector.html"), 19 | protocol: "file:", 20 | slashes: true 21 | })); 22 | 23 | varInspector.on('close', () => { 24 | varInspector = null; 25 | }) 26 | } 27 | 28 | function createWindow () { 29 | win = new BrowserWindow({ 30 | x: 20, 31 | y: 0, 32 | width: 750, 33 | height: 1000, 34 | icon: path.join(__dirname, 'livepython.png'), 35 | title: "Livepython" 36 | }) 37 | 38 | win.loadURL(url.format({ 39 | pathname: path.join(__dirname, 'dist', 'index.html'), 40 | protocol: 'file:', 41 | slashes: true, 42 | })) 43 | 44 | ipcMain.on("command", (evt, msg) => { 45 | process.send(msg) 46 | }) 47 | 48 | ipcMain.on("toggle_variable_inspector", (evt, msg) => { 49 | if (varInspector) { 50 | varInspector.close(); 51 | varInspector = null; 52 | } else { 53 | openVariableInspector(); 54 | } 55 | }); 56 | 57 | process.on('message', message => { 58 | const parsed = JSON.parse(message) 59 | if (parsed.type === 'finish') { 60 | app.quit() 61 | } 62 | if (win) win.webContents.send('trace', { msg: message }) 63 | if (varInspector) varInspector.webContents.send('trace', { msg: message }); 64 | }) 65 | 66 | // win.webContents.openDevTools() 67 | 68 | win.on('closed', () => { 69 | win = null 70 | }) 71 | } 72 | 73 | app.on('ready', createWindow) 74 | 75 | app.on('window-all-closed', () => { 76 | app.quit() 77 | }) 78 | 79 | app.on('activate', () => { 80 | if (win === null) { 81 | createWindow() 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livepython", 3 | "description": "Visually trace your Python program as it's running", 4 | "version": "0.0.6", 5 | "main": "main.js", 6 | "bin": { 7 | "livepython": "./bin/livepython" 8 | }, 9 | "icon": "livepython.png", 10 | "license": "MIT", 11 | "dependencies": { 12 | "byline": "^5.0.0", 13 | "chroma-js": "^1.3.4", 14 | "electron": "1.7.8", 15 | "electron-packager": "^9.1.0", 16 | "jquery": "^3.2.1", 17 | "mousetrap": "^1.6.1", 18 | "process-nextick-args": "^1.0.7", 19 | "react": "^16.0.0", 20 | "react-dom": "^16.0.0", 21 | "react-syntax-highlighter": "^5.7.0", 22 | "util-deprecate": "^1.0.2" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "^6.26.0", 26 | "babel-core": "^6.26.0", 27 | "babel-loader": "^7.1.2", 28 | "babel-preset-env": "^1.6.0", 29 | "babel-preset-es2015": "^6.24.1", 30 | "babel-preset-react": "^6.24.1", 31 | "webpack": "^3.6.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/CodeView.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import jq from 'jquery' 3 | 4 | import SyntaxHighlighter from 'react-syntax-highlighter' 5 | import { tomorrowNight } from 'react-syntax-highlighter/dist/styles' 6 | 7 | tomorrowNight.hljs.background = 'none' 8 | tomorrowNight.hljs.padding = 0 9 | 10 | class CodeView extends Component { 11 | constructor() { 12 | super(); 13 | this.state = { 14 | activity: null, 15 | intervalId: null 16 | }; 17 | } 18 | 19 | spaceFixedLineNumber(curLine, totLines) { 20 | return ( 21 | "\u00a0".repeat(totLines.toString().length - curLine.toString().length) + 22 | curLine.toString() 23 | ); 24 | } 25 | 26 | componentDidMount() { 27 | this.tick(); 28 | } 29 | 30 | tick() { 31 | try { 32 | const currentOffset = document.scrollingElement.scrollTop; 33 | const targetOffset = jq(".selected").offset().top - 400; 34 | const change = (targetOffset - currentOffset) / 30; 35 | document.scrollingElement.scrollTop += change; 36 | } catch (e) {} 37 | requestAnimationFrame(this.tick.bind(this)); 38 | } 39 | 40 | componentWillReceiveProps(props) { 41 | var activity = JSON.parse(JSON.stringify(this.state.activity)) || {}; 42 | 43 | if (!hasOwnProperty.call(activity, props.filename)) { 44 | activity[props.filename] = {}; 45 | } 46 | 47 | activity[props.filename][props.lineno - 1] = Date.now(); 48 | 49 | this.setState({ activity }); 50 | } 51 | 52 | render() { 53 | if (!this.state.activity) return ""; 54 | if (!this.state.activity[this.props.filename]) return ""; 55 | if (!this.props.source) return ""; 56 | var lines = this.props.source.split("\n"); 57 | var lineEls = []; 58 | for (var i = 0; i < lines.length; i++) { 59 | var line = lines[i]; 60 | if (!line.length) line = "\n"; 61 | var isExceptionLine = false; 62 | var cs = "line"; 63 | var exceptionMessage; 64 | if (this.props.exception) { 65 | var exception = this.props.exception; 66 | if (i === exception.lineno - 1) { 67 | isExceptionLine = true; 68 | cs += " exception"; 69 | exceptionMessage = " " + exception.type + ": " + exception.message; 70 | } 71 | } else if ( 72 | i === this.props.lineno - 1 && 73 | this.props.state !== "finished" 74 | ) { 75 | cs += " selected"; 76 | } 77 | const lastActive = this.state.activity[this.props.filename][i] || 0; 78 | const opacity = 1 - Math.min(1, (Date.now() - lastActive) / 800); 79 | var el = ( 80 |
87 |

88 | {this.spaceFixedLineNumber(i + 1, lines.length)} 89 |

90 | 91 | {line} 92 | 93 | {isExceptionLine && ( 94 |
{exceptionMessage}
95 | )} 96 | {"\n"} 97 |
98 | ); 99 | lineEls.push(el); 100 | } 101 | return ( 102 |
103 |
104 |
{lineEls}
105 |
106 |
107 | ); 108 | } 109 | } 110 | 111 | export default CodeView 112 | -------------------------------------------------------------------------------- /src/components/MainView.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { ipcRenderer } from 'electron' 3 | import jq from 'jquery' 4 | import mousetrap from 'mousetrap' 5 | 6 | import CodeView from './CodeView' 7 | 8 | function showIndicator(name) { 9 | jq(".status-indicator").remove(); 10 | const src = `images/${name}.png` 11 | var $img = jq(""); 12 | $img.attr("src", src); 13 | $img.addClass("status-indicator"); 14 | jq("body").append($img); 15 | $img.fadeOut(1000, function() { 16 | $img.remove(); 17 | }); 18 | } 19 | 20 | class MainView extends Component { 21 | constructor () { 22 | super() 23 | this.state = { 24 | paused: false, 25 | state: "running", 26 | filename: null, 27 | function_name: null, 28 | lineno: 0, 29 | locals: {}, 30 | source: "", 31 | exception: null, 32 | time: null, 33 | fastForward: false 34 | } 35 | } 36 | 37 | componentDidUpdate () { 38 | document.title = 'Livepython - ' + this.state.filename; 39 | } 40 | 41 | componentWillMount () { 42 | ipcRenderer.send('command', { 43 | type: 'connected' 44 | }) 45 | ipcRenderer.on('trace', (event, data) => { 46 | const msg = JSON.parse(data.msg) 47 | if (msg.type === 'call') { 48 | this.setState(Object.assign(this.state, msg)) 49 | } else if (msg.type === 'switch') { 50 | this.setState(Object.assign(this.state, msg)); 51 | } else if (msg.type === 'finish') { 52 | this.setState({state: 'finished'}) 53 | } else if (msg.type === 'exception') { 54 | this.setState({ 55 | state: 'failed', 56 | exception: { 57 | message: msg.exception_message, 58 | type: msg.exception_type, 59 | lineno: msg.lineno 60 | } 61 | }) 62 | } 63 | }) 64 | 65 | mousetrap.bind("space", (evt) => { 66 | const {paused} = this.state; 67 | showIndicator(paused ? 'play' : 'pause'); 68 | ipcRenderer.send('command', { 69 | type: "toggle_running_state", 70 | value: !paused 71 | }); 72 | this.setState({paused: !this.state.paused}) 73 | return false; 74 | }); 75 | 76 | mousetrap.bind("right", evt => { 77 | showIndicator('fastforward'); 78 | ipcRenderer.send('command', { 79 | type: 'change_speed', 80 | speed: 'fast' 81 | }); 82 | this.setState({ fastForward: true }); 83 | return false; 84 | }); 85 | 86 | mousetrap.bind("left", evt => { 87 | showIndicator("play"); 88 | ipcRenderer.send('command', { 89 | type: "change_speed", 90 | speed: "slow" 91 | }); 92 | this.setState({ fastForward: false }); 93 | return false; 94 | }); 95 | 96 | mousetrap.bind("v", evt => { 97 | ipcRenderer.send("toggle_variable_inspector"); 98 | return false; 99 | }); 100 | } 101 | 102 | 103 | render () { 104 | if (!this.state.source) return

Loading

105 | return ( 106 |
107 | 108 |
109 | ) 110 | } 111 | } 112 | 113 | export default MainView -------------------------------------------------------------------------------- /src/components/VariableInspector.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { ipcRenderer } from "electron"; 3 | 4 | function truncateString(str, num) { 5 | if (num < str.length) { 6 | return str.slice(0, num) + "..."; 7 | } else { 8 | return str; 9 | } 10 | } 11 | 12 | function arrDiff (a1, a2) { 13 | var a = [], diff = []; 14 | 15 | for (var i = 0; i < a1.length; i++) { 16 | a[a1[i]] = true; 17 | } 18 | for (var i = 0; i < a2.length; i++) { 19 | if (a[a2[i]]) { 20 | delete a[a2[i]]; 21 | } else { 22 | a[a2[i]] = true; 23 | } 24 | } 25 | for (var k in a) { 26 | diff.push(k); 27 | } 28 | return diff; 29 | }; 30 | 31 | class VariableInspector extends Component { 32 | constructor() { 33 | super(); 34 | this.state = { 35 | filter: "", 36 | hideModules: true, 37 | variables: {}, 38 | recentlyChanged: {} 39 | }; 40 | } 41 | 42 | componentWillMount () { 43 | ipcRenderer.send('command', { 44 | type: 'connected' 45 | }) 46 | ipcRenderer.on('trace', (event, data) => { 47 | const msg = JSON.parse(data.msg) 48 | if (msg.type === 'call') { 49 | this.setState({ 50 | variables: Object.assign( 51 | msg.frame_locals, 52 | msg.frame_globals 53 | ), 54 | prevVariables: Object.assign(this.state.variables), 55 | recentlyChanged: arrDiff( 56 | Object.keys(msg.frame_locals).concat(Object.keys(msg.frame_globals)), 57 | Object.keys(this.state.variables) 58 | ) 59 | }); 60 | } 61 | }) 62 | } 63 | 64 | getKeys() { 65 | var keys = Object.keys(this.state.variables); 66 | var filtered = []; 67 | for (var i = 0; i < keys.length; i++) { 68 | var name = keys[i]; 69 | var type = this.state.variables[name].type; 70 | if (this.state.hideModules && (type === "module" || 71 | type === "function" || 72 | type === "classobj" || 73 | type === '_Feature' || 74 | type === 'type')) { 75 | continue; 76 | } 77 | if (!this.state.filter || keys[i].startsWith(this.state.filter)) { 78 | filtered.push(keys[i]); 79 | } 80 | } 81 | return filtered; 82 | } 83 | 84 | filterChange (evt) { 85 | this.setState({ filter: evt.target.value || null }); 86 | } 87 | 88 | toggleModuleHide() { 89 | this.setState({ hideModules: !this.state.hideModules }); 90 | } 91 | 92 | componentDidMount() { 93 | this.searchInput.focus(); 94 | } 95 | 96 | render() { 97 | var keys = this.getKeys(); 98 | var rows = []; 99 | for (var i = 0; i < keys.length; i++) { 100 | var name = keys[i]; 101 | var type = this.state.variables[name].type; 102 | var value = this.state.variables[name].value; 103 | var cs = 'variable-line' 104 | console.log(this.state.prevVariables[name], this.state.variables[name]); 105 | if (!this.state.prevVariables[name] || this.state.prevVariables[name].value !== this.state.variables[name].value) { 106 | cs += " selected-line"; 107 | } 108 | console.log(value, typeof value) 109 | rows.push( 110 | {name} 111 | {type} 112 | {truncateString(value.toString(), 30)} 113 | ); 114 | } 115 | return
116 | { 117 | this.searchInput = input; 118 | }} /> 119 |
120 | this.setState( 121 | { hideModules: !this.state.hideModules } 122 | )} /> 123 | Hide modules, functions, and classes 124 |
125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | {rows} 134 |
NameTypeValue
135 |
; 136 | } 137 | } 138 | 139 | export default VariableInspector; 140 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | 4 | import MainView from './components/MainView' 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById('root') 9 | ) 10 | -------------------------------------------------------------------------------- /src/variable_inspector.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import VariableInspector from "./components/VariableInspector"; 5 | 6 | ReactDOM.render( 7 | , 8 | document.getElementById("root") 9 | ); 10 | -------------------------------------------------------------------------------- /tracer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import linecache 5 | import os 6 | import sys 7 | import threading 8 | import inspect 9 | import time 10 | import socket 11 | 12 | state = { 13 | 'speed': 'slow' 14 | } 15 | 16 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 | s.connect(("localhost", 4387)) 18 | s.setblocking(False) 19 | 20 | def debounce(wait): 21 | def decorator(fn): 22 | class context: 23 | last_call = None 24 | def debounced(*args, **kwargs): 25 | def call_it(): 26 | args, kwargs = context.last_call 27 | fn(*args, **kwargs) 28 | context.last_call = None 29 | if context.last_call is None: 30 | debounced.t = threading.Timer(wait, call_it) 31 | debounced.t.start() 32 | context.last_call = (args, kwargs) 33 | return debounced 34 | return decorator 35 | 36 | 37 | def log(msg): 38 | try: 39 | s.send(bytes(msg+'\n', 'utf8')) 40 | except: 41 | s.send(msg+'\n') 42 | 43 | 44 | @debounce(0.1) 45 | def log_frame(frame): 46 | log(json.dumps(generate_call_event(frame))) 47 | 48 | 49 | starting_filename = os.path.abspath(sys.argv[1]) 50 | starting_dir = os.path.dirname(starting_filename) 51 | 52 | os.chdir(starting_dir) 53 | sys.path.insert(0, starting_dir) 54 | 55 | current_filename = None 56 | current_line = None 57 | current_locals = {} 58 | failed = False 59 | 60 | 61 | def should_ignore_variable(name): 62 | return name.startswith('__') and name.endswith('__') 63 | 64 | 65 | def truncate_list(l): 66 | if len(l) > 3: 67 | ret = ', '.join(map(process_variable, l[:2])) 68 | ret += ", ..., " 69 | ret += process_variable(l[-1]) 70 | return ret 71 | else: 72 | return ', '.join(map(process_variable, l)) 73 | 74 | 75 | def format_function(f): 76 | args = inspect.getargspec(f).args 77 | return "function(%s)" % truncate_list(args) 78 | 79 | 80 | def format_list(l): 81 | return "[%s]" % truncate_list(l) 82 | 83 | 84 | def process_variable(var): 85 | type_name = type(var).__name__ 86 | if type_name == 'list': 87 | return format_list(var) 88 | elif type_name == 'module': 89 | return "" % var.__name__ 90 | else: 91 | return str(var) 92 | 93 | 94 | def get_module_name(full_path): 95 | global starting_filename 96 | return os.path.relpath( 97 | os.path.abspath(full_path), 98 | os.path.dirname(os.path.abspath(starting_filename)) 99 | ) 100 | 101 | 102 | def generate_call_event(frame): 103 | frame_locals = {k: 104 | {'value': process_variable(v), 'type': type(v).__name__} 105 | for k, v in frame.f_locals.items() if not should_ignore_variable(k) 106 | } 107 | frame_globals = {k: 108 | {'value': process_variable(v), 'type': type(v).__name__} 109 | for k, v in frame.f_globals.items() if not should_ignore_variable(k) 110 | } 111 | obj = { 112 | 'type': 'call', 113 | 'frame_locals': frame_locals, 114 | 'frame_globals': frame_globals, 115 | 'filename': get_module_name(frame.f_code.co_filename), 116 | 'lineno': frame.f_lineno, 117 | 'source': ''.join(linecache.getlines(frame.f_code.co_filename)) 118 | } 119 | return obj 120 | 121 | 122 | def generate_exception_event(e): 123 | return { 124 | 'type': 'exception', 125 | 'exception_type': type(e).__name__, 126 | 'exception_message': str(e), 127 | 'filename': current_filename, 128 | 'lineno': current_line, 129 | 'time': time.time() 130 | } 131 | 132 | 133 | def process_msg(msg): 134 | global state 135 | if type(msg) == bytes: 136 | msg = msg.decode('utf8') 137 | msg = json.loads(msg) 138 | if msg['type'] == 'change_speed': 139 | print('changed speed') 140 | state['speed'] = msg['speed'] 141 | 142 | 143 | def local_trace(frame, why, arg): 144 | try: 145 | received_msg = s.recv(1024) 146 | process_msg(received_msg) 147 | except: 148 | pass 149 | 150 | global current_line 151 | global current_filename 152 | 153 | if failed: 154 | return 155 | 156 | if why == 'exception': 157 | exc_type = arg[0].__name__ 158 | exc_msg = arg[1] 159 | return 160 | 161 | current_filename = frame.f_code.co_filename 162 | current_line = frame.f_lineno 163 | 164 | if not current_filename.startswith(starting_dir): 165 | return 166 | 167 | if 'livepython' in current_filename: 168 | return 169 | 170 | if 'site-packages' in current_filename: 171 | return 172 | 173 | if 'lib/python' in current_filename: 174 | return 175 | 176 | log_frame(frame) 177 | 178 | if state['speed'] == 'slow': 179 | time.sleep(1) 180 | 181 | return local_trace 182 | 183 | 184 | def global_trace(frame, why, arg): 185 | return local_trace 186 | 187 | 188 | with open(starting_filename, 'rb') as fp: 189 | code = compile(fp.read(), starting_filename, 'exec') 190 | 191 | 192 | namespace = { 193 | '__file__': starting_filename, 194 | '__name__': '__main__', 195 | } 196 | 197 | 198 | log(json.dumps({ 199 | 'type': 'start', 200 | 'startmodule': starting_filename 201 | })) 202 | 203 | sys.settrace(global_trace) 204 | threading.settrace(global_trace) 205 | 206 | try: 207 | sys.argv = sys.argv[1:] 208 | exec(code, namespace) 209 | log(json.dumps({'type': 'finish'})) 210 | except Exception as err: 211 | failed = True 212 | log(json.dumps(generate_exception_event(err))) 213 | finally: 214 | sys.settrace(None) 215 | threading.settrace(None) 216 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: { 5 | main: './src/index.js', 6 | variable_inspector: './src/variable_inspector.js' 7 | }, 8 | target: 'electron', 9 | module: { 10 | loaders: [ 11 | { test: /\.js$/, loader: 'babel-loader', exclude: /(node_modules|main)/ } 12 | ] 13 | }, 14 | output: { 15 | filename: '[name].js', 16 | path: path.resolve(__dirname, 'dist') 17 | }, 18 | } 19 | --------------------------------------------------------------------------------